diff --git a/scripts/check-maintenance-page.mjs b/scripts/check-maintenance-page.mjs index f0ba8e54f..0c1ece88e 100644 --- a/scripts/check-maintenance-page.mjs +++ b/scripts/check-maintenance-page.mjs @@ -19,7 +19,14 @@ import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + '..', +); +const bashExecutable = + process.platform === 'win32' + ? 'C:\\Program Files\\Git\\bin\\bash.exe' + : 'bash'; const failures = []; const requestedFiles = []; @@ -48,7 +55,10 @@ function validateDefaultPage(filePath) { } for (const [pattern, label] of [ [/(?:今天|今晚|明天|昨天|昨日)/u, '相对日期'], - [/(?:20\d{2}[-/.年]\d{1,2}(?:[-/.月]\d{1,2}日?)?|\d{1,2}月\d{1,2}日)/u, '具体日期'], + [ + /(?:20\d{2}[-/.年]\d{1,2}(?:[-/.月]\d{1,2}日?)?|\d{1,2}月\d{1,2}日)/u, + '具体日期', + ], [/(?:[01]?\d|2[0-3]):[0-5]\d/u, '具体维护时间'], ]) { if (pattern.test(source)) { @@ -58,15 +68,48 @@ function validateDefaultPage(filePath) { } function runScript(scriptPath, args, env) { - return spawnSync('bash', [scriptPath, ...args], { + const bashArgs = args.map((arg) => + path.isAbsolute(arg) ? toBashPath(arg) : arg, + ); + const bashEnv = Object.fromEntries( + Object.entries(env).map(([key, value]) => [ + key, + path.isAbsolute(value) ? toBashPath(value) : value, + ]), + ); + const envAssignments = Object.entries(bashEnv).map( + ([key, value]) => `${key}=${value}`, + ); + const commandArgs = [ + ...envAssignments, + 'bash', + toBashPath(scriptPath), + ...bashArgs, + ]; + const command = `exec env ${commandArgs.map(shellQuote).join(' ')}`; + return spawnSync(bashExecutable, ['-c', command], { cwd: repoRoot, - env: { ...process.env, ...env }, + env: process.env, encoding: 'utf8', }); } +function shellQuote(value) { + return `'${String(value).replaceAll("'", `'"'"'`)}'`; +} + +function toBashPath(filePath) { + const windowsDrive = /^([A-Za-z]):[\\/](.*)$/u.exec(filePath); + if (windowsDrive) { + return `/${windowsDrive[1].toLowerCase()}/${windowsDrive[2].replaceAll('\\', '/')}`; + } + return filePath; +} + function validateRuntimePageLifecycle() { - const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'genarrative-maintenance-')); + const tempRoot = mkdtempSync( + path.join(os.tmpdir(), 'genarrative-maintenance-'), + ); const markerFile = path.join(tempRoot, 'state', 'enabled'); const runtimePageFile = path.join(tempRoot, 'state', 'page.html'); const sourcePageFile = path.join(tempRoot, 'announcement.html'); @@ -81,6 +124,9 @@ function validateRuntimePageLifecycle() { if (!onScriptSource.includes('replace_file_atomically')) { fail('maintenance-on 必须通过统一 helper 原子替换公告页和 marker。'); } + if (!onScriptSource.includes('install -m 0644')) { + fail('maintenance-on 安装运行态公告页时必须显式设置 0644 权限。'); + } if (/\bmv\s+-[^\s]*T\b/u.test(onScriptSource)) { fail('maintenance-on 不得使用 GNU mv 专属的 -T 参数。'); } @@ -95,7 +141,9 @@ function validateRuntimePageLifecycle() { env, ); if (enable.status !== 0) { - fail(`maintenance-on --page-file 执行失败: ${enable.stderr || enable.stdout}`); + fail( + `maintenance-on --page-file 执行失败: ${enable.stderr || enable.stdout}`, + ); return; } if (!existsSync(markerFile)) { @@ -107,7 +155,10 @@ function validateRuntimePageLifecycle() { if (readFileSync(runtimePageFile, 'utf8') !== announcement) { fail('运行态公告页内容与输入文件不一致。'); } - if ((statSync(runtimePageFile).mode & 0o777) !== 0o644) { + if ( + process.platform !== 'win32' && + (statSync(runtimePageFile).mode & 0o777) !== 0o644 + ) { fail('运行态公告页权限必须为 0644。'); } } @@ -130,7 +181,9 @@ function validateRuntimePageLifecycle() { chmodSync(runtimePageFile, 0o644); const genericEnable = runScript(onScript, ['generic maintenance'], env); if (genericEnable.status !== 0) { - fail(`通用 maintenance-on 执行失败: ${genericEnable.stderr || genericEnable.stdout}`); + fail( + `通用 maintenance-on 执行失败: ${genericEnable.stderr || genericEnable.stdout}`, + ); } if (existsSync(runtimePageFile)) { fail('新维护窗口未提供 --page-file 时必须清理残留公告页。'); @@ -265,10 +318,9 @@ function validateGatewayConfiguration() { } } -for (const filePath of - requestedFiles.length > 0 - ? requestedFiles - : [path.join(repoRoot, 'public/maintenance.html')]) { +for (const filePath of requestedFiles.length > 0 + ? requestedFiles + : [path.join(repoRoot, 'public/maintenance.html')]) { validateDefaultPage(filePath); }