071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
815 lines
23 KiB
JavaScript
815 lines
23 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
import { spawnSync } from 'node:child_process';
|
|
import {
|
|
chmodSync,
|
|
copyFileSync,
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readFileSync,
|
|
realpathSync,
|
|
rmSync,
|
|
statSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import path from 'node:path';
|
|
|
|
const SWITCH_SCRIPT = 'scripts/deploy/pingora-health-patrol-env-switch.mjs';
|
|
const ENV_CHECK_SCRIPT = path.join(
|
|
process.cwd(),
|
|
'scripts/check-production-health-patrol-env.mjs',
|
|
);
|
|
const failures = [];
|
|
const tmpRoot = mkdtempSync(
|
|
path.join(tmpdir(), 'genarrative-pingora-health-patrol-switch-'),
|
|
);
|
|
|
|
try {
|
|
main();
|
|
} finally {
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error('[check:pingora-health-patrol-env-switch] FAILED');
|
|
for (const failure of failures) {
|
|
console.error(`- ${failure}`);
|
|
}
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log('[check:pingora-health-patrol-env-switch] OK');
|
|
|
|
function main() {
|
|
assertScriptShape();
|
|
assertDryRunDoesNotModifyEnv();
|
|
assertDryRunEnvFileModeIsPrivate();
|
|
assertApplyPreservesEnvFileModeAndOwnership();
|
|
assertApplySwitchesToPingoraDirectAndPreservesOtherKeys();
|
|
assertApplySwitchesBackToNginxAndClearsHost();
|
|
assertApplyDoesNotWriteWhenVerificationFails();
|
|
assertApplyRejectsSymlinkEnvFileBeforeWrite();
|
|
assertMissingManagedKeysAreAppended();
|
|
assertRejectsDirectLoopbackWithoutHost();
|
|
assertRejectsInvalidAndConflictingHostOptions();
|
|
assertRejectsRelativePaths();
|
|
assertRejectsFilesystemRootPaths();
|
|
assertRejectsControlCharacterInputsBeforeWrite();
|
|
assertRejectsInvalidBoolEnv();
|
|
assertReleaseLayoutUsesBundledCheckScript();
|
|
}
|
|
|
|
function assertScriptShape() {
|
|
const content = readFileSync(SWITCH_SCRIPT, 'utf8');
|
|
assertIncludes(content, '--apply', '切换脚本必须显式要求 --apply 才写 env。');
|
|
assertIncludes(
|
|
content,
|
|
'当前是 dry-run',
|
|
'切换脚本必须在 dry-run 中明确不会写 env。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'check-production-health-patrol-env.mjs',
|
|
'切换脚本必须复用生产 health patrol env 复核脚本。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'Pingora direct 模式使用本机 public base URL 时必须提供 --public-host',
|
|
'切换脚本必须阻断 direct loopback 缺少正式 Host。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'health patrol env 中存在重复配置',
|
|
'切换脚本必须拒绝重复目标键,避免 env 解析歧义。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'runHealthPatrolEnvCheck(checkFile);',
|
|
'切换脚本必须先用临时目标 env 复核。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'writeEnvFileAtomically(config.envFile, nextText);',
|
|
'切换脚本通过复核后才可原子写入真实 env。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'--env-file 不能是符号链接',
|
|
'apply 写入真实 env 前必须拒绝符号链接目标。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'DRY_RUN_ENV_FILE_MODE = 0o600',
|
|
'临时复核 env 文件权限必须固定为 0600。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'chownSync(tempFile, currentStat.uid, currentStat.gid);',
|
|
'真实 env 原子替换必须保留原文件 owner/group。',
|
|
);
|
|
}
|
|
|
|
function assertDryRunDoesNotModifyEnv() {
|
|
const envFile = writeEnv('dry-run.env', {
|
|
GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const before = readFileSync(envFile, 'utf8');
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
]);
|
|
|
|
assertStatus(result, 0, 'dry-run 应成功并通过临时 env 复核。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'apply=false',
|
|
'dry-run 输出必须显示 apply=false。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
'[check:production-health-patrol-env] OK',
|
|
'dry-run 必须用临时 env 跑复核脚本。',
|
|
);
|
|
const after = readFileSync(envFile, 'utf8');
|
|
if (after !== before) {
|
|
failures.push('dry-run 不应修改 health patrol env 文件。');
|
|
}
|
|
}
|
|
|
|
function assertDryRunEnvFileModeIsPrivate() {
|
|
const envFile = writeEnv('dry-run-mode.env', {
|
|
GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const modeCheckScript = path.join(tmpRoot, 'check-dry-run-env-mode.mjs');
|
|
writeFileSync(
|
|
modeCheckScript,
|
|
[
|
|
'#!/usr/bin/env node',
|
|
"import { statSync } from 'node:fs';",
|
|
"const envFileIndex = process.argv.indexOf('--env-file');",
|
|
'const envFile = process.argv[envFileIndex + 1];',
|
|
'const mode = statSync(envFile).mode & 0o777;',
|
|
'if (mode !== 0o600) {',
|
|
' console.error(`dry-run env mode must be 0600, got ${mode.toString(8)}`);',
|
|
' process.exit(1);',
|
|
'}',
|
|
'console.log(`[fake-health-patrol-env-check] dry-run env mode ${mode.toString(8)}`);',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
chmodSync(modeCheckScript, 0o755);
|
|
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--check-script',
|
|
modeCheckScript,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
]);
|
|
|
|
assertStatus(result, 0, 'dry-run 临时复核 env 文件权限应为 0600。');
|
|
assertIncludes(
|
|
result.stdout,
|
|
'dry-run env mode 600',
|
|
'dry-run 必须把 0600 临时 env 交给复核脚本。',
|
|
);
|
|
}
|
|
|
|
function assertApplyPreservesEnvFileModeAndOwnership() {
|
|
const envFile = writeEnv('apply-mode-owner.env', {
|
|
GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
chmodSync(envFile, 0o640);
|
|
const beforeStat = statSync(envFile);
|
|
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
'--apply',
|
|
]);
|
|
|
|
assertStatus(result, 0, 'apply 原子替换应成功。');
|
|
const afterStat = statSync(envFile);
|
|
const beforeMode = beforeStat.mode & 0o777;
|
|
const afterMode = afterStat.mode & 0o777;
|
|
if (afterMode !== beforeMode) {
|
|
failures.push(
|
|
`apply 原子替换必须保留 env 文件权限,实际 ${afterMode.toString(8)},预期 ${beforeMode.toString(8)}。`,
|
|
);
|
|
}
|
|
if (afterStat.uid !== beforeStat.uid || afterStat.gid !== beforeStat.gid) {
|
|
failures.push('apply 原子替换必须保留 env 文件 owner/group。');
|
|
}
|
|
}
|
|
|
|
function assertApplySwitchesToPingoraDirectAndPreservesOtherKeys() {
|
|
const envFile = writeEnv('direct.env', {
|
|
GENARRATIVE_HEALTH_PATROL_API_BASE_URL: 'http://127.0.0.1:8082',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS: '5000',
|
|
});
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
'--apply',
|
|
]);
|
|
|
|
assertStatus(result, 0, 'apply 切到 pingora-direct 应成功。');
|
|
const content = readFileSync(envFile, 'utf8');
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=pingora-direct',
|
|
'apply 必须写入 pingora-direct gateway mode。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=https://127.0.0.1',
|
|
'apply 必须写入直连 public base URL。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=example.com',
|
|
'apply 必须写入正式 public Host。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS=5000',
|
|
'apply 必须保留其它 health patrol env 项。',
|
|
);
|
|
}
|
|
|
|
function assertApplySwitchesBackToNginxAndClearsHost() {
|
|
const envFile = writeEnv('nginx.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'https://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'pingora-direct',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: 'example.com',
|
|
});
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
'--apply',
|
|
]);
|
|
|
|
assertStatus(result, 0, 'apply 切回 nginx 并清空 Host 应成功。');
|
|
const content = readFileSync(envFile, 'utf8');
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx',
|
|
'回退 apply 必须写回 nginx gateway mode。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=http://127.0.0.1',
|
|
'回退 apply 必须恢复 Nginx public base URL。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=',
|
|
'回退 apply 必须清空 public Host。',
|
|
);
|
|
}
|
|
|
|
function assertApplyDoesNotWriteWhenVerificationFails() {
|
|
const envFile = writeEnv('apply-verify-fails.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const before = readFileSync(envFile, 'utf8');
|
|
const failingCheckScript = path.join(
|
|
tmpRoot,
|
|
'failing-health-patrol-env-check.mjs',
|
|
);
|
|
writeFileSync(
|
|
failingCheckScript,
|
|
[
|
|
'#!/usr/bin/env node',
|
|
'console.error("[fake-health-patrol-env-check] forced failure");',
|
|
'process.exit(1);',
|
|
'',
|
|
].join('\n'),
|
|
'utf8',
|
|
);
|
|
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--check-script',
|
|
failingCheckScript,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
'--apply',
|
|
]);
|
|
|
|
assertStatus(result, 1, '复核失败时 apply 必须失败。');
|
|
assertIncludes(
|
|
result.stderr,
|
|
'[fake-health-patrol-env-check] forced failure',
|
|
'复核失败时必须透出复核脚本错误。',
|
|
);
|
|
const after = readFileSync(envFile, 'utf8');
|
|
if (after !== before) {
|
|
failures.push('复核失败时 apply 不应写入真实 health patrol env 文件。');
|
|
}
|
|
}
|
|
|
|
function assertApplyRejectsSymlinkEnvFileBeforeWrite() {
|
|
const targetEnvFile = writeEnv('symlink-target.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const symlinkEnvFile = path.join(tmpRoot, 'symlink-health-patrol.env');
|
|
symlinkSync(targetEnvFile, symlinkEnvFile);
|
|
const before = readFileSync(targetEnvFile, 'utf8');
|
|
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
symlinkEnvFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
'--apply',
|
|
]);
|
|
|
|
assertStatus(result, 1, 'apply 传入符号链接 env 文件时必须失败。');
|
|
assertIncludes(
|
|
result.stderr,
|
|
'--env-file 不能是符号链接',
|
|
'符号链接 env 文件失败时必须给出明确错误。',
|
|
);
|
|
const after = readFileSync(targetEnvFile, 'utf8');
|
|
if (after !== before) {
|
|
failures.push('符号链接 env 文件被拒绝时不应写入真实目标文件。');
|
|
}
|
|
const linkText = readFileSync(symlinkEnvFile, 'utf8');
|
|
if (linkText !== before) {
|
|
failures.push('符号链接 env 文件被拒绝时不应替换或改写链接内容。');
|
|
}
|
|
}
|
|
|
|
function assertMissingManagedKeysAreAppended() {
|
|
const envFile = path.join(tmpRoot, 'append.env');
|
|
writeFileSync(
|
|
envFile,
|
|
['# existing env', 'GENARRATIVE_HEALTH_PATROL_TIMEOUT_MS=5000', ''].join(
|
|
'\n',
|
|
),
|
|
'utf8',
|
|
);
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
'--apply',
|
|
]);
|
|
|
|
assertStatus(result, 0, '缺少目标键时 apply 应追加并通过复核。');
|
|
const content = readFileSync(envFile, 'utf8');
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE=nginx',
|
|
'缺少 gateway mode 时必须追加。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL=http://127.0.0.1',
|
|
'缺少 public base URL 时必须追加。',
|
|
);
|
|
assertIncludes(
|
|
content,
|
|
'GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST=',
|
|
'缺少 public Host 时必须追加。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsDirectLoopbackWithoutHost() {
|
|
const envFile = writeEnv('direct-missing-host.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const result = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--clear-public-host',
|
|
]);
|
|
|
|
assertStatus(result, 1, 'direct loopback 缺少 Host 必须失败。');
|
|
assertIncludes(
|
|
result.stderr,
|
|
'Pingora direct 模式使用本机 public base URL 时必须提供 --public-host',
|
|
'direct loopback 缺少 Host 必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsInvalidAndConflictingHostOptions() {
|
|
const envFile = writeEnv('invalid-host.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const invalidHost = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--public-host',
|
|
'https://example.com',
|
|
]);
|
|
assertStatus(invalidHost, 1, '非法 Host 必须失败。');
|
|
assertIncludes(
|
|
invalidHost.stderr,
|
|
'--public-host 只能是 host 或 host:port',
|
|
'非法 Host 必须给出明确错误。',
|
|
);
|
|
|
|
const conflictingHost = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
'--clear-public-host',
|
|
]);
|
|
assertStatus(conflictingHost, 1, 'Host 与清空 Host 同时传入必须失败。');
|
|
assertIncludes(
|
|
conflictingHost.stderr,
|
|
'--public-host 和 --clear-public-host 不能同时使用',
|
|
'Host 选项冲突必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsRelativePaths() {
|
|
const relativeEnv = runSwitch([
|
|
'--env-file',
|
|
'relative.env',
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
]);
|
|
assertStatus(relativeEnv, 1, '相对 env 路径必须失败。');
|
|
assertIncludes(
|
|
relativeEnv.stderr,
|
|
'--env-file 必须是绝对路径',
|
|
'相对 env 路径必须给出明确错误。',
|
|
);
|
|
|
|
const envFile = writeEnv('relative-check.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const relativeCheck = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--check-script',
|
|
'scripts/check-production-health-patrol-env.mjs',
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
]);
|
|
assertStatus(relativeCheck, 1, '相对 check script 路径必须失败。');
|
|
assertIncludes(
|
|
relativeCheck.stderr,
|
|
'--check-script 必须是绝对路径',
|
|
'相对 check script 路径必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsFilesystemRootPaths() {
|
|
const rootEnv = runSwitch([
|
|
'--env-file',
|
|
'/',
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
]);
|
|
assertStatus(rootEnv, 1, '文件系统根目录 env 路径必须失败。');
|
|
assertIncludes(
|
|
rootEnv.stderr,
|
|
'--env-file 不能是文件系统根目录',
|
|
'文件系统根目录 env 路径必须给出明确错误。',
|
|
);
|
|
|
|
const envFile = writeEnv('root-check-script.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const rootCheckScript = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--check-script',
|
|
'/',
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
]);
|
|
assertStatus(
|
|
rootCheckScript,
|
|
1,
|
|
'文件系统根目录 check script 路径必须失败。',
|
|
);
|
|
assertIncludes(
|
|
rootCheckScript.stderr,
|
|
'--check-script 不能是文件系统根目录',
|
|
'文件系统根目录 check script 路径必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertRejectsControlCharacterInputsBeforeWrite() {
|
|
const envFile = writeEnv('control-character.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const before = readFileSync(envFile, 'utf8');
|
|
|
|
const envFileResult = runSwitch([
|
|
'--env-file',
|
|
`${envFile}\nspoofed`,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
'--apply',
|
|
]);
|
|
assertStatus(envFileResult, 1, '带控制字符的 env 文件路径必须失败。');
|
|
assertIncludes(
|
|
envFileResult.stderr,
|
|
'--env-file 不能包含换行或 NUL 字符',
|
|
'带控制字符的 env 文件路径必须给出明确错误。',
|
|
);
|
|
|
|
const checkScriptResult = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--check-script',
|
|
`${ENV_CHECK_SCRIPT}\nspoofed`,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
'--clear-public-host',
|
|
'--apply',
|
|
]);
|
|
assertStatus(
|
|
checkScriptResult,
|
|
1,
|
|
'带控制字符的 check script 路径必须失败。',
|
|
);
|
|
assertIncludes(
|
|
checkScriptResult.stderr,
|
|
'--check-script 不能包含换行或 NUL 字符',
|
|
'带控制字符的 check script 路径必须给出明确错误。',
|
|
);
|
|
|
|
const baseUrlResult = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1\nspoofed',
|
|
'--clear-public-host',
|
|
'--apply',
|
|
]);
|
|
assertStatus(baseUrlResult, 1, '带控制字符的 public base URL 必须失败。');
|
|
assertIncludes(
|
|
baseUrlResult.stderr,
|
|
'--public-base-url 不能包含换行或 NUL 字符',
|
|
'带控制字符的 public base URL 必须给出明确错误。',
|
|
);
|
|
|
|
const hostResult = runSwitch([
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com\nspoofed',
|
|
'--apply',
|
|
]);
|
|
assertStatus(hostResult, 1, '带控制字符的 public Host 必须失败。');
|
|
assertIncludes(
|
|
hostResult.stderr,
|
|
'--public-host 不能包含换行或 NUL 字符',
|
|
'带控制字符的 public Host 必须给出明确错误。',
|
|
);
|
|
|
|
const after = readFileSync(envFile, 'utf8');
|
|
if (after !== before) {
|
|
failures.push('带控制字符的切换参数被拒绝时不应写入真实 env 文件。');
|
|
}
|
|
}
|
|
|
|
function assertRejectsInvalidBoolEnv() {
|
|
const envFile = writeEnv('invalid-bool-env.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
const result = runSwitch(
|
|
[
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'nginx',
|
|
'--public-base-url',
|
|
'http://127.0.0.1',
|
|
],
|
|
{
|
|
GENARRATIVE_HEALTH_PATROL_CLEAR_PUBLIC_HOST: 'maybe',
|
|
},
|
|
);
|
|
assertStatus(result, 1, 'health patrol env 切换脚本必须拒绝非法布尔 env。');
|
|
assertIncludes(
|
|
result.stderr,
|
|
'GENARRATIVE_HEALTH_PATROL_CLEAR_PUBLIC_HOST 必须是布尔值',
|
|
'health patrol env 切换脚本非法布尔 env 必须给出明确错误。',
|
|
);
|
|
}
|
|
|
|
function assertReleaseLayoutUsesBundledCheckScript() {
|
|
const releaseRoot = path.join(tmpRoot, 'current-release');
|
|
const releaseScript = path.join(
|
|
releaseRoot,
|
|
'scripts/deploy/pingora-health-patrol-env-switch.mjs',
|
|
);
|
|
const releaseCheckScript = path.join(
|
|
releaseRoot,
|
|
'scripts/check-production-health-patrol-env.mjs',
|
|
);
|
|
const envFile = writeEnv('release-layout.env', {
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: 'http://127.0.0.1',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: 'nginx',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
});
|
|
|
|
mkdirSync(path.dirname(releaseScript), { recursive: true });
|
|
mkdirSync(path.dirname(releaseCheckScript), { recursive: true });
|
|
copyFileSync(SWITCH_SCRIPT, releaseScript);
|
|
copyFileSync(
|
|
'scripts/check-production-health-patrol-env.mjs',
|
|
releaseCheckScript,
|
|
);
|
|
chmodSync(releaseScript, 0o755);
|
|
|
|
const result = spawnSync(
|
|
'node',
|
|
[
|
|
'--',
|
|
releaseScript,
|
|
'--env-file',
|
|
envFile,
|
|
'--gateway-mode',
|
|
'pingora-direct',
|
|
'--public-base-url',
|
|
'https://127.0.0.1',
|
|
'--public-host',
|
|
'example.com',
|
|
],
|
|
{ cwd: tmpRoot, encoding: 'utf8', env: cleanEnv() },
|
|
);
|
|
|
|
assertStatus(
|
|
result,
|
|
0,
|
|
'current release 中的切换脚本 dry-run 应能使用随包 env 复核脚本。',
|
|
);
|
|
assertIncludes(
|
|
result.stdout,
|
|
realpathSync(releaseCheckScript),
|
|
'current release 中的切换脚本必须默认调用随包 env 复核脚本。',
|
|
);
|
|
if (!existsSync(releaseCheckScript)) {
|
|
failures.push('release layout 测试必须保留随包 env 复核脚本。');
|
|
}
|
|
}
|
|
|
|
function writeEnv(name, values) {
|
|
const filePath = path.join(tmpRoot, name);
|
|
const body = Object.entries(values)
|
|
.map(([key, value]) => `${key}=${value}`)
|
|
.join('\n');
|
|
writeFileSync(filePath, `${body}\n`, 'utf8');
|
|
return filePath;
|
|
}
|
|
|
|
function runSwitch(args, extraEnv = {}) {
|
|
return spawnSync(
|
|
'node',
|
|
['--', SWITCH_SCRIPT, '--check-script', ENV_CHECK_SCRIPT, ...args],
|
|
{
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
env: {
|
|
...cleanEnv(),
|
|
...extraEnv,
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
function cleanEnv() {
|
|
return {
|
|
...process.env,
|
|
GENARRATIVE_HEALTH_PATROL_ENV_FILE: '',
|
|
GENARRATIVE_HEALTH_PATROL_GATEWAY_MODE: '',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_BASE_URL: '',
|
|
GENARRATIVE_HEALTH_PATROL_PUBLIC_HOST: '',
|
|
GENARRATIVE_HEALTH_PATROL_CLEAR_PUBLIC_HOST: '',
|
|
GENARRATIVE_HEALTH_PATROL_ENV_CHECK_SCRIPT: '',
|
|
};
|
|
}
|
|
|
|
function assertStatus(result, expected, reason) {
|
|
const actual = result.status ?? 0;
|
|
if (actual !== expected) {
|
|
failures.push(
|
|
`${reason} 预期退出码 ${expected},实际 ${actual}。\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertIncludes(content, needle, reason) {
|
|
if (!content.includes(needle)) {
|
|
failures.push(`${reason} 缺少: ${needle}`);
|
|
}
|
|
}
|