fix(acceptance): 阻断本地控制面漂移污染验收
本地 K3s 节点此前在 Kubelet 中登记为宿主机资源,负载可挤压 etcd 并在 server 重启后继续污染同一 Run。现在为三节点设置真实可调度预算、独立 etcd/数据卷和锁定依赖镜像,并持续核对 server 与 API/etcd 健康。\n\n每次验收生成独立 Run ID,报告使用独占或原子写入,负载错误记录具体阶段;数据库鉴权不可用返回带 Retry-After 的 503,避免基础设施故障被误报为 401。\n\n验证:Go 全量测试、go vet、gofmt、OpenAPI、bash -n、ShellCheck、报告测试和 k3d 配置解析均通过。
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from 'node:crypto';
|
||||
import { lstat, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
||||
import { lstat, mkdir, readFile, readdir, rename, unlink, writeFile } from 'node:fs/promises';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
|
||||
const shaPattern = /^[0-9a-f]{40}$/;
|
||||
@@ -26,6 +26,29 @@ async function regularJSON(path) {
|
||||
return JSON.parse(await readFile(absolute, 'utf8'));
|
||||
}
|
||||
|
||||
async function writeNewJSON(path, value) {
|
||||
const absolute = resolve(path);
|
||||
await mkdir(dirname(absolute), { recursive: true, mode: 0o700 });
|
||||
await writeFile(absolute, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
||||
}
|
||||
|
||||
async function replaceJSONAtomically(input, output, value) {
|
||||
const inputPath = resolve(input);
|
||||
const outputPath = resolve(output);
|
||||
if (inputPath !== outputPath) {
|
||||
await writeNewJSON(outputPath, value);
|
||||
return;
|
||||
}
|
||||
const temporary = `${outputPath}.tmp-${process.pid}-${Date.now()}`;
|
||||
try {
|
||||
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
||||
await rename(temporary, outputPath);
|
||||
} catch (error) {
|
||||
await unlink(temporary).catch(() => {});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function assertSecretSafe(value, path = '$') {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item, index) => assertSecretSafe(item, `${path}[${index}]`));
|
||||
@@ -85,6 +108,8 @@ async function loadReports(directory) {
|
||||
profile: report.profile,
|
||||
passed: report.passed,
|
||||
phases: report.phases,
|
||||
failure: report.failure,
|
||||
failureOperation: report.failureOperation,
|
||||
startedAt: report.startedAt,
|
||||
finishedAt: report.finishedAt
|
||||
});
|
||||
@@ -150,8 +175,7 @@ async function buildLocal(values) {
|
||||
secretSafe: true
|
||||
};
|
||||
validate(report);
|
||||
await mkdir(dirname(resolve(values.output)), { recursive: true, mode: 0o700 });
|
||||
await writeFile(resolve(values.output), `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
|
||||
await writeNewJSON(values.output, report);
|
||||
process.stdout.write(`acceptance_report_local=PASS sha256=${createHash('sha256').update(JSON.stringify(report)).digest('hex')}\n`);
|
||||
}
|
||||
|
||||
@@ -172,6 +196,10 @@ async function buildLocalPartial(values) {
|
||||
throw new Error('partial local report inputs have incompatible CAS identity');
|
||||
}
|
||||
const completedLoadsPassed = loads.every((item) => item.passed === true);
|
||||
const failureGate = values['failure-gate'] ?? 'local_execution_incomplete';
|
||||
if (!/^[a-z0-9_]+$/.test(failureGate)) {
|
||||
throw new Error('--failure-gate must be a stable lowercase gate ID');
|
||||
}
|
||||
const report = {
|
||||
schemaVersion: 'acceptance-report/v1',
|
||||
runId: runtime.runId,
|
||||
@@ -202,7 +230,7 @@ async function buildLocalPartial(values) {
|
||||
} : null,
|
||||
gates: [
|
||||
{
|
||||
id: 'local_execution_complete',
|
||||
id: failureGate,
|
||||
passed: false,
|
||||
detail: `stopped during ${values['failure-phase']}`
|
||||
},
|
||||
@@ -221,8 +249,7 @@ async function buildLocalPartial(values) {
|
||||
secretSafe: true
|
||||
};
|
||||
validate(report);
|
||||
await mkdir(dirname(resolve(values.output)), { recursive: true, mode: 0o700 });
|
||||
await writeFile(resolve(values.output), `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
|
||||
await writeNewJSON(values.output, report);
|
||||
process.stdout.write('acceptance_report_local_partial=PASS\n');
|
||||
}
|
||||
|
||||
@@ -259,8 +286,7 @@ async function mergeProduction(values) {
|
||||
};
|
||||
local.createdAt = new Date().toISOString();
|
||||
validate(local);
|
||||
await mkdir(dirname(resolve(values.output)), { recursive: true, mode: 0o700 });
|
||||
await writeFile(resolve(values.output), `${JSON.stringify(local, null, 2)}\n`, { mode: 0o600 });
|
||||
await writeNewJSON(values.output, local);
|
||||
process.stdout.write(`acceptance_report_production=PASS promotion_ready=${local.promotion.ready}\n`);
|
||||
}
|
||||
|
||||
@@ -285,7 +311,7 @@ if (command === 'validate') {
|
||||
}
|
||||
report.promotion.trafficMode = 'live';
|
||||
report.promotion.promotedAt = new Date().toISOString();
|
||||
await writeFile(resolve(output), `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
|
||||
await replaceJSONAtomically(input, output, report);
|
||||
process.stdout.write('acceptance_report_promoted=PASS\n');
|
||||
} else if (command === 'attach-monitor') {
|
||||
const input = values.input ?? '';
|
||||
@@ -309,7 +335,7 @@ if (command === 'validate') {
|
||||
report.promotion.ready = false;
|
||||
report.promotion.trafficMode = 'validation';
|
||||
}
|
||||
await writeFile(resolve(output), `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600 });
|
||||
await replaceJSONAtomically(input, output, report);
|
||||
process.stdout.write(`acceptance_report_monitor=PASS monitor_passed=${monitor.passed === true}\n`);
|
||||
} else {
|
||||
throw new Error('usage: report.mjs {validate|build-local|build-local-partial|merge-production|mark-promoted|attach-monitor} [options]');
|
||||
|
||||
Reference in New Issue
Block a user