Files
Genarrative/scripts/check-pingora-health-patrol-env-switch.mjs
kdletters 071faa482c 统一 Rust 与 TypeScript 格式化门禁
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口

完成项目 TypeScript/Prettier 与 Rust 全量格式化

修复 Pingora expected executable 门禁的空白敏感误报

同步开发运维文档与 AGC skill pack 格式化忽略规则
2026-09-01 16:28:34 +08:00

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}`);
}
}