Files
Genarrative/scripts/check-pingora-realpath-canary-toggle.mjs
T
kdletters 387a1c26e3 完成全量检查整改
清理已跟踪原始日志与个人本地配置
修复前后端有效门禁、测试和构建问题
补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束
收紧图片编辑器状态、附件与媒体引用测试
排除已下线旧创作入口测试并清理 warning
2026-07-17 16:56:46 +08:00

433 lines
12 KiB
JavaScript

#!/usr/bin/env node
import { spawnSync } from 'node:child_process';
import {
chmodSync,
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
const ENABLE_SCRIPT = 'scripts/deploy/pingora-realpath-canary-enable.sh';
const DISABLE_SCRIPT = 'scripts/deploy/pingora-realpath-canary-disable.sh';
const failures = [];
const tmpRoot = mkdtempSync(path.join(tmpdir(), 'genarrative-realpath-canary-'));
try {
main();
} finally {
rmSync(tmpRoot, { recursive: true, force: true });
}
if (failures.length > 0) {
console.error('[check:pingora-realpath-canary-toggle] FAILED');
for (const failure of failures) {
console.error(`- ${failure}`);
}
process.exit(1);
}
console.log('[check:pingora-realpath-canary-toggle] OK');
function main() {
assertScriptShape();
assertDryRunDoesNotWrite();
assertApplyWritesRenderedConfigAndRunsLive();
assertEnableRollsBackWhenLiveFails();
assertDisableRemovesConfig();
assertDisableRollsBackWhenReloadFails();
assertRejectsUnsafeInputs();
}
function assertScriptShape() {
for (const script of [ENABLE_SCRIPT, DISABLE_SCRIPT]) {
const content = readFileSync(script, 'utf8');
assertIncludes(content, '--apply', `${script} 必须默认 dry-run 并要求显式 --apply。`);
assertIncludes(content, 'nginx -t', `${script} 帮助文案必须说明会先做 nginx -t。`);
assertIncludes(
content,
'zz-genarrative-pingora-realpath-canary.conf',
`${script} 必须默认使用晚于主站配置加载的 conf.d 文件名。`,
);
assertIncludes(
content,
'不能包含换行或 NUL 字符',
`${script} 必须拒绝控制字符参数。`,
);
assertIncludes(content, '不能是文件系统根目录', `${script} 必须拒绝根路径。`);
}
const enableContent = readFileSync(ENABLE_SCRIPT, 'utf8');
assertIncludes(
enableContent,
'check-pingora-canary-live.mjs',
'enable 脚本必须默认运行 realpath live smoke。',
);
assertIncludes(
enableContent,
'恢复写入前配置',
'enable 脚本失败时必须恢复写入前配置。',
);
const disableContent = readFileSync(DISABLE_SCRIPT, 'utf8');
assertIncludes(
disableContent,
'恢复删除前配置',
'disable 脚本失败时必须恢复删除前配置。',
);
}
function assertDryRunDoesNotWrite() {
const fixture = createFixture('dry-run');
const targetPath = path.join(fixture.nginxDir, 'zz-genarrative-pingora-realpath-canary.conf');
const result = runScript(ENABLE_SCRIPT, [
'--probe-token',
'dry-run-token',
'--host',
'dev.genarrative.world',
'--template-path',
fixture.templatePath,
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--live-script',
fixture.liveScript,
]);
assertStatus(result, 0, 'enable dry-run 应成功。');
if (existsSync(targetPath)) {
failures.push('enable dry-run 不应写入目标 Nginx 配置。');
}
assertIncludes(result.stdout, 'dry-run', 'enable dry-run 应输出 dry-run 提示。');
}
function assertApplyWritesRenderedConfigAndRunsLive() {
const fixture = createFixture('apply-ok');
const targetPath = path.join(fixture.nginxDir, 'zz-genarrative-pingora-realpath-canary.conf');
const result = runScript(ENABLE_SCRIPT, [
'--apply',
'--probe-token',
'apply-real-token',
'--host',
'dev.genarrative.world',
'--base-url',
'http://127.0.0.1:18083',
'--template-path',
fixture.templatePath,
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--live-script',
fixture.liveScript,
'--no-status',
]);
assertStatus(result, 0, 'enable apply 应成功。');
const rendered = readFileSync(targetPath, 'utf8');
assertIncludes(rendered, '"apply-real-token"', 'enable apply 应替换 probe token。');
if (rendered.includes('__GENARRATIVE_PINGORA_PROBE_TOKEN__')) {
failures.push('enable apply 后目标配置不应保留 probe token 占位符。');
}
const calls = readFileSync(fixture.callsPath, 'utf8');
assertIncludes(calls, 'nginx -t', 'enable apply 必须执行 nginx -t。');
assertIncludes(calls, 'systemctl reload nginx.service', 'enable apply 必须 reload nginx。');
assertIncludes(
calls,
'live --realpath --base-url http://127.0.0.1:18083 --host dev.genarrative.world',
'enable apply 必须执行 realpath live smoke。',
);
}
function assertEnableRollsBackWhenLiveFails() {
const fixture = createFixture('enable-live-fails', { liveExitCode: 7 });
const targetPath = path.join(fixture.nginxDir, 'zz-genarrative-pingora-realpath-canary.conf');
writeFileSync(targetPath, 'previous-config\n', 'utf8');
const result = runScript(
ENABLE_SCRIPT,
[
'--apply',
'--probe-token',
'rollback-token',
'--host',
'dev.genarrative.world',
'--template-path',
fixture.templatePath,
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--live-script',
fixture.liveScript,
'--no-status',
],
fixture.env,
);
if (result.status === 0) {
failures.push('realpath live smoke 失败时 enable apply 必须失败。');
}
const restored = readFileSync(targetPath, 'utf8');
if (restored !== 'previous-config\n') {
failures.push('realpath live smoke 失败时 enable apply 必须恢复旧配置。');
}
assertIncludes(
result.stderr,
'恢复写入前配置',
'enable live 失败时必须说明已恢复写入前配置。',
);
}
function assertDisableRemovesConfig() {
const fixture = createFixture('disable-ok');
const targetPath = path.join(fixture.nginxDir, 'zz-genarrative-pingora-realpath-canary.conf');
writeFileSync(targetPath, 'enabled-config\n', 'utf8');
const result = runScript(
DISABLE_SCRIPT,
[
'--apply',
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--no-status',
],
fixture.env,
);
assertStatus(result, 0, 'disable apply 应成功。');
if (existsSync(targetPath)) {
failures.push('disable apply 成功后必须删除 realpath canary 配置。');
}
const calls = readFileSync(fixture.callsPath, 'utf8');
assertIncludes(calls, 'nginx -t', 'disable apply 必须执行 nginx -t。');
assertIncludes(calls, 'systemctl reload nginx.service', 'disable apply 必须 reload nginx。');
}
function assertDisableRollsBackWhenReloadFails() {
const fixture = createFixture('disable-reload-fails', { reloadExitCode: 9 });
const targetPath = path.join(fixture.nginxDir, 'zz-genarrative-pingora-realpath-canary.conf');
writeFileSync(targetPath, 'enabled-config\n', 'utf8');
const result = runScript(DISABLE_SCRIPT, [
'--apply',
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--no-status',
]);
if (result.status === 0) {
failures.push('reload 失败时 disable apply 必须失败。');
}
const restored = readFileSync(targetPath, 'utf8');
if (restored !== 'enabled-config\n') {
failures.push('reload 失败时 disable apply 必须恢复删除前配置。');
}
assertIncludes(
result.stderr,
'恢复删除前配置',
'disable reload 失败时必须说明已恢复删除前配置。',
);
}
function assertRejectsUnsafeInputs() {
const fixture = createFixture('unsafe-inputs');
const targetPath = path.join(fixture.nginxDir, 'zz-genarrative-pingora-realpath-canary.conf');
const cases = [
{
name: 'missing token',
script: ENABLE_SCRIPT,
args: [
'--apply',
'--host',
'dev.genarrative.world',
'--template-path',
fixture.templatePath,
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--live-script',
fixture.liveScript,
],
expected: '--apply 必须提供 --probe-token',
},
{
name: 'url host',
script: ENABLE_SCRIPT,
args: [
'--probe-token',
'unsafe-token',
'--host',
'https://dev.genarrative.world',
'--template-path',
fixture.templatePath,
'--target-path',
targetPath,
'--nginx-binary',
fixture.nginxBinary,
'--systemctl-binary',
fixture.systemctlBinary,
'--live-script',
fixture.liveScript,
],
expected: '不能是 URL',
},
{
name: 'relative target',
script: DISABLE_SCRIPT,
args: ['--target-path', 'relative.conf'],
expected: '--target-path 必须是绝对路径',
},
{
name: 'root target',
script: DISABLE_SCRIPT,
args: ['--target-path', '/'],
expected: '--target-path 不能是文件系统根目录',
},
];
for (const item of cases) {
const result = runScript(item.script, item.args);
if (result.status === 0) {
failures.push(`${item.name}: 不安全参数必须失败。`);
}
assertIncludes(
`${result.stderr}\n${result.stdout}`,
item.expected,
`${item.name}: 应输出明确错误。`,
);
}
}
function createFixture(name, options = {}) {
const root = path.join(tmpRoot, name);
const nginxDir = path.join(root, 'nginx-conf');
mkdirSync(nginxDir, { recursive: true });
const callsPath = path.join(root, 'calls.log');
const templatePath = path.join(root, 'genarrative-pingora-realpath-canary.conf');
const nginxBinary = path.join(root, 'nginx');
const systemctlBinary = path.join(root, 'systemctl');
const liveScript = path.join(root, 'live.mjs');
writeFileSync(
templatePath,
[
'server {',
' listen 127.0.0.1:18083;',
' access_log /var/log/nginx/genarrative-pingora-realpath-canary.access.log genarrative_upstream;',
' location = /__genarrative_pingora_realpath_canary/healthz {',
' proxy_set_header X-Genarrative-Pingora-Probe "__GENARRATIVE_PINGORA_PROBE_TOKEN__";',
' proxy_pass http://127.0.0.1:18081/__genarrative_pingora/healthz;',
' }',
'}',
'',
].join('\n'),
'utf8',
);
writeFileSync(
nginxBinary,
[
'#!/usr/bin/env bash',
`echo "nginx $*" >> ${shellQuote(callsPath)}`,
'if [[ "${GENARRATIVE_FAKE_NGINX_T_EXIT:-0}" != "0" ]]; then exit "${GENARRATIVE_FAKE_NGINX_T_EXIT}"; fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
systemctlBinary,
[
'#!/usr/bin/env bash',
`echo "systemctl $*" >> ${shellQuote(callsPath)}`,
`reload_exit="\${GENARRATIVE_FAKE_RELOAD_EXIT:-${Number(options.reloadExitCode || 0)}}"`,
'if [[ "$1" == "reload" && "${reload_exit}" != "0" ]]; then exit "${reload_exit}"; fi',
'exit 0',
'',
].join('\n'),
'utf8',
);
writeFileSync(
liveScript,
[
'#!/usr/bin/env node',
"import { appendFileSync } from 'node:fs';",
`appendFileSync(${JSON.stringify(callsPath)}, 'live ' + process.argv.slice(2).join(' ') + '\\n');`,
`process.exit(Number(process.env.GENARRATIVE_FAKE_LIVE_EXIT || ${Number(options.liveExitCode || 0)}));`,
'',
].join('\n'),
'utf8',
);
chmodExecutable(nginxBinary);
chmodExecutable(systemctlBinary);
chmodExecutable(liveScript);
return {
root,
nginxDir,
callsPath,
templatePath,
nginxBinary,
systemctlBinary,
liveScript,
env: {
GENARRATIVE_FAKE_RELOAD_EXIT: String(options.reloadExitCode || 0),
GENARRATIVE_FAKE_LIVE_EXIT: String(options.liveExitCode || 0),
},
};
}
function runScript(script, args, env = {}) {
return spawnSync('bash', [script, ...args], {
cwd: process.cwd(),
env: { ...process.env, ...env },
encoding: 'utf8',
});
}
function chmodExecutable(file) {
chmodSync(file, 0o755);
}
function shellQuote(value) {
return `'${String(value).replaceAll("'", "'\\''")}'`;
}
function assertStatus(result, expected, message) {
if (result.status !== expected) {
failures.push(
`${message} 实际退出码 ${result.status}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
);
}
}
function assertIncludes(content, needle, message) {
if (!content.includes(needle)) {
failures.push(`${message} 缺少 ${needle}`);
}
}