387a1c26e3
清理已跟踪原始日志与个人本地配置 修复前后端有效门禁、测试和构建问题 补齐生产 Jenkins、SpacetimeDB 本地命令与文档约束 收紧图片编辑器状态、附件与媒体引用测试 排除已下线旧创作入口测试并清理 warning
433 lines
12 KiB
JavaScript
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}`);
|
|
}
|
|
}
|