ci: schedule guarded GitHub Pages publishing
Publish GitHub Pages output / publish (push) Has been cancelled
Publish GitHub Pages output / publish (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import { cp, mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import process from 'node:process';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const siteSource = path.join(projectRoot, 'pages-dist');
|
||||
const workflowSource = path.join(projectRoot, 'pages', 'deploy.yml');
|
||||
const repository = process.env.GITHUB_PAGES_REPOSITORY ?? 'https://github.com/DD4SN/DD4SN.github.io.git';
|
||||
|
||||
if (!process.env.GITHUB_PAGES_TOKEN) {
|
||||
throw new Error('GITHUB_PAGES_TOKEN is required to publish Pages output.');
|
||||
}
|
||||
|
||||
function runGit(args, options = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('git', args, {
|
||||
cwd: options.cwd ?? projectRoot,
|
||||
env: options.env ?? process.env,
|
||||
stdio: options.quiet ? 'ignore' : 'inherit',
|
||||
shell: false
|
||||
});
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code) => resolve(code ?? 1));
|
||||
});
|
||||
}
|
||||
|
||||
async function requireGit(args, options) {
|
||||
const code = await runGit(args, options);
|
||||
if (code !== 0) throw new Error(`git ${args[0]} failed with exit code ${code}.`);
|
||||
}
|
||||
|
||||
const temporaryRoot = await mkdtemp(path.join(os.tmpdir(), 'holodori-pages-'));
|
||||
const checkout = path.join(temporaryRoot, 'repository');
|
||||
const askPass = path.join(temporaryRoot, process.platform === 'win32' ? 'askpass.cmd' : 'askpass.sh');
|
||||
const askPassContents = process.platform === 'win32'
|
||||
? '@echo off\necho %1 | findstr /I "Username" >nul\nif %errorlevel%==0 (echo x-access-token) else (echo %GITHUB_PAGES_TOKEN%)\n'
|
||||
: '#!/bin/sh\ncase "$1" in\n *Username*) printf "%s\\n" "x-access-token" ;;\n *) printf "%s\\n" "$GITHUB_PAGES_TOKEN" ;;\nesac\n';
|
||||
|
||||
try {
|
||||
await writeFile(askPass, askPassContents, { mode: 0o700 });
|
||||
const gitEnvironment = {
|
||||
...process.env,
|
||||
GIT_ASKPASS: askPass,
|
||||
GIT_TERMINAL_PROMPT: '0'
|
||||
};
|
||||
|
||||
await requireGit(['clone', repository, checkout], { env: gitEnvironment });
|
||||
|
||||
const generatedPaths = ['site', 'assets', 'icons', 'index.html', 'README.txt', 'VERSION.txt', '.nojekyll'];
|
||||
await Promise.all(generatedPaths.map((generatedPath) => rm(path.join(checkout, generatedPath), { recursive: true, force: true })));
|
||||
await cp(siteSource, checkout, { recursive: true });
|
||||
await writeFile(path.join(checkout, '.nojekyll'), '');
|
||||
|
||||
const workflowTarget = path.join(checkout, '.github', 'workflows');
|
||||
await mkdir(workflowTarget, { recursive: true });
|
||||
await cp(workflowSource, path.join(workflowTarget, 'deploy.yml'));
|
||||
|
||||
await requireGit(['config', 'user.name', 'HolodoriCalc deployment'], { cwd: checkout, env: gitEnvironment });
|
||||
await requireGit(['config', 'user.email', 'actions@users.noreply.github.com'], { cwd: checkout, env: gitEnvironment });
|
||||
await requireGit(['add', '--all', '--', '.'], { cwd: checkout, env: gitEnvironment });
|
||||
|
||||
const diffCode = await runGit(['diff', '--cached', '--quiet'], { cwd: checkout, env: gitEnvironment, quiet: true });
|
||||
if (diffCode === 0) {
|
||||
process.stdout.write('GitHub Pages output is already up to date.\n');
|
||||
} else if (diffCode === 1) {
|
||||
const packageJson = JSON.parse(await readFile(path.join(projectRoot, 'package.json'), 'utf8'));
|
||||
await requireGit(['commit', '-m', `deploy: HolodoriCalc v${packageJson.version}`], { cwd: checkout, env: gitEnvironment });
|
||||
await requireGit(['push', 'origin', 'HEAD:main'], { cwd: checkout, env: gitEnvironment });
|
||||
} else {
|
||||
throw new Error(`git diff failed with exit code ${diffCode}.`);
|
||||
}
|
||||
} finally {
|
||||
const resolvedTemporaryRoot = path.resolve(temporaryRoot);
|
||||
const resolvedSystemTemp = path.resolve(os.tmpdir());
|
||||
if (resolvedTemporaryRoot.startsWith(`${resolvedSystemTemp}${path.sep}`) && path.basename(resolvedTemporaryRoot).startsWith('holodori-pages-')) {
|
||||
await rm(resolvedTemporaryRoot, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
@@ -36,21 +36,38 @@ if (mismatchedVersions.length > 0) {
|
||||
}
|
||||
|
||||
const approvedNotBefore = Date.parse(policy.approvedNotBefore);
|
||||
if (!Number.isFinite(approvedNotBefore) || now < approvedNotBefore) {
|
||||
throw new Error(`The approved master data cannot be published before ${policy.approvedNotBefore}.`);
|
||||
if (!Number.isFinite(approvedNotBefore)) {
|
||||
throw new Error('approvedNotBefore must be a valid ISO-8601 date.');
|
||||
}
|
||||
|
||||
const unreleasedCharts = charts.charts.filter((chart) => !Number.isFinite(chart.releaseTime) || chart.releaseTime > now);
|
||||
const undatedCharts = charts.charts.filter((chart) => !Number.isFinite(chart.releaseTime));
|
||||
if (undatedCharts.length > 0) {
|
||||
throw new Error(`Pages build contains ${undatedCharts.length} undated charts: ${undatedCharts.map((chart) => `${chart.songId}/${chart.difficulty}`).join(', ')}`);
|
||||
}
|
||||
|
||||
const undatedEvents = events.filter((event) => !Number.isFinite(Date.parse(event.startsAt)));
|
||||
if (undatedEvents.length > 0) {
|
||||
throw new Error(`Pages build contains undated events: ${undatedEvents.map((event) => event.id).join(', ')}`);
|
||||
}
|
||||
|
||||
const notReadyReasons = [];
|
||||
if (now < approvedNotBefore) {
|
||||
notReadyReasons.push(`approved master data cannot be published before ${policy.approvedNotBefore}`);
|
||||
}
|
||||
|
||||
const unreleasedCharts = charts.charts.filter((chart) => chart.releaseTime > now);
|
||||
if (unreleasedCharts.length > 0) {
|
||||
throw new Error(`Pages build contains ${unreleasedCharts.length} unreleased or undated charts: ${unreleasedCharts.map((chart) => `${chart.songId}/${chart.difficulty}`).join(', ')}`);
|
||||
notReadyReasons.push(`${unreleasedCharts.length} charts are not released yet: ${unreleasedCharts.map((chart) => `${chart.songId}/${chart.difficulty}`).join(', ')}`);
|
||||
}
|
||||
|
||||
const unreleasedEvents = events.filter((event) => {
|
||||
const startsAt = Date.parse(event.startsAt);
|
||||
return !Number.isFinite(startsAt) || startsAt > now;
|
||||
});
|
||||
const unreleasedEvents = events.filter((event) => Date.parse(event.startsAt) > now);
|
||||
if (unreleasedEvents.length > 0) {
|
||||
throw new Error(`Pages build contains unreleased or undated events: ${unreleasedEvents.map((event) => event.id).join(', ')}`);
|
||||
notReadyReasons.push(`events are not released yet: ${unreleasedEvents.map((event) => event.id).join(', ')}`);
|
||||
}
|
||||
|
||||
if (notReadyReasons.length > 0) {
|
||||
process.stderr.write(`Pages public-data gate is not ready at ${new Date(now).toISOString()}: ${notReadyReasons.join('; ')}.\n`);
|
||||
process.exit(78);
|
||||
}
|
||||
|
||||
process.stdout.write(`Pages public-data gate passed at ${new Date(now).toISOString()}.\n`);
|
||||
|
||||
Reference in New Issue
Block a user