完善(08-09-deployment-contract-single-source-harness): 收敛部署合同并补失败路径

这个提交包含在:
2026-08-09 14:40:28 +08:00
父节点 099f58cf3b
当前提交 621934a35e
共修改 13 个文件,包含 648 行新增和 27 行删除
@@ -25,6 +25,23 @@ describe("deployment source provenance", () => {
provenance = await import(pathToFileURL(scriptPath).href);
});
it("derives the complete candidate build environment as one immutable contract", () => {
const commit = "a".repeat(40);
const buildEnv = provenance.deriveCandidateBuildEnv(commit);
expect(Object.keys(buildEnv)).toEqual(["BUILD_SHA", "VITE_BUILD_SHA"]);
expect(buildEnv).toEqual({
BUILD_SHA: commit,
VITE_BUILD_SHA: commit.slice(0, 12),
});
expect(Object.isFrozen(buildEnv)).toBe(true);
expect(() => provenance.deriveCandidateBuildEnv("a".repeat(39))).toThrowError(
expect.objectContaining({ code: "commit_invalid" }),
);
expect(() => provenance.deriveCandidateBuildEnv("A".repeat(40))).toThrowError(
expect.objectContaining({ code: "commit_invalid" }),
);
});
beforeEach(async () => {
repoDir = await mkdtemp(path.join(os.tmpdir(), "hyb-oa-provenance-test-"));
await git(["init", "--initial-branch=main"]);
@@ -72,6 +89,10 @@ describe("deployment source provenance", () => {
published: true,
overrideUsed: false,
upstream: "origin/main",
buildEnv: {
BUILD_SHA: expect.stringMatching(/^[0-9a-f]{40,64}$/u),
VITE_BUILD_SHA: expect.stringMatching(/^[0-9a-f]{12}$/u),
},
});
} finally {
await rm(remoteDir, { recursive: true, force: true });
@@ -1,4 +1,4 @@
const { execFile } = require("node:child_process");
const { execFile, execFileSync } = require("node:child_process");
const { createHash } = require("node:crypto");
const { chmod, mkdtemp, mkdir, rm, symlink, writeFile, readFile } = require("node:fs/promises");
const os = require("node:os");
@@ -60,6 +60,33 @@ describe("deployment safety contract", () => {
});
}
function productionPreflightEnv({ fakeBin, reportDir, overrides = {} }) {
const candidateCommit = execFileSync("git", ["rev-parse", "HEAD"], {
cwd: ROOT_DIR,
encoding: "utf8",
}).trim();
return {
...process.env,
PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`,
DEPLOY_SERVER: "deploy.production.invalid",
REMOTE_DIR: remoteDir,
REMOTE_ENV_FILE: envFile,
COMPOSE_FILE: "deploy/trialrun/docker-compose.yml",
COMPOSE_PROJECT_NAME: "hyb-oa-trialrun",
POSTGRES_DATA_DIR: postgresDir,
STORAGE_HOST_DIR: storageDir,
EXPECTED_PRODUCTION_ORIGIN: "https://oa.hyinda.com",
EXPECTED_PRODUCTION_DB_NAME_SHA256: createHash("sha256").update("hyb_oa", "utf8").digest("hex"),
EXPECTED_PENDING_MIGRATIONS: "",
EXPECTED_DEPLOY_SERVER: "deploy.production.invalid",
EXPECTED_REMOTE_DIR: remoteDir,
EXPECTED_COMPOSE_PROJECT_NAME: "hyb-oa-trialrun",
CANDIDATE_COMMIT: candidateCommit,
PRODUCTION_PREFLIGHT_REPORT_DIR: reportDir,
...overrides,
};
}
it("accepts a complete existing deployment contract", async () => {
await expect(preflight()).resolves.toMatchObject({ stdout: expect.stringContaining("[remote-preflight][PASS]") });
});
@@ -119,6 +146,7 @@ describe("deployment safety contract", () => {
it("requires production target identity and pending migration allowlist before deploy.sh", async () => {
const wrapper = await readFile(path.join(ROOT_DIR, "deploy/trialrun/deploy-existing.sh"), "utf8");
const deploy = await readFile(path.join(ROOT_DIR, "deploy/trialrun/deploy.sh"), "utf8");
const preflight = await readFile(PRODUCTION_PREFLIGHT, "utf8");
const remote = await readFile(PRODUCTION_PREFLIGHT_REMOTE, "utf8");
const bootstrap = await readFile(PRODUCTION_IDENTITY_BOOTSTRAP, "utf8");
@@ -141,6 +169,19 @@ describe("deployment safety contract", () => {
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_UNREADABLE");
expect(preflight).toContain("CANDIDATE_COMMIT_MISSING");
expect(preflight).not.toContain("rev-parse");
expect(preflight).toContain("deriveCandidateBuildEnv");
expect(preflight).toContain("Object.entries(candidateBuildEnv)");
expect(preflight).not.toContain("CANDIDATE_BUILD_ENV_KEYS");
expect(preflight).not.toContain("candidateCommit.slice(0, 12)");
expect(deploy).toContain("Object.entries(p.buildEnv ?? {})");
expect(deploy).toContain("declare -A SOURCE_BUILD_ENV=()");
expect(deploy).toContain('"${#SOURCE_BUILD_ENV[@]}" -eq 2');
expect(deploy).not.toContain("p.buildEnv.BUILD_SHA");
expect(deploy).not.toContain("p.buildEnv.VITE_BUILD_SHA");
expect(deploy).not.toContain("p.shortCommit");
expect(deploy).not.toContain("${BUILD_SHA:0:12}");
expect(deploy).toContain('${VITE_BUILD_SHA}.log');
expect(deploy).toContain('-e EXPECTED_BUILD_SHA="${VITE_BUILD_SHA}"');
expect(preflight).toContain("redacted datasource line");
expect(preflight).toContain("PRODUCTION_PREFLIGHT_REPORT_NOT_FRESH");
expect(preflight.lastIndexOf("printStableRemoteSummary(stdout)")).toBeLessThan(
@@ -149,6 +190,9 @@ describe("deployment safety contract", () => {
expect(preflight.indexOf("reportRef=${reportRef(reportDir)}")).toBeLessThan(
preflight.lastIndexOf("assertRemotePreflightResult({"),
);
expect(preflight.lastIndexOf("assertRemotePreflightResult({")).toBeLessThan(
preflight.lastIndexOf("validatePrismaStatusText(rawStatus);"),
);
expect(remote).toContain("PRISMA_STATUS_BEGIN");
expect(remote).toContain("PROD_MIGRATION_STATUS_UNKNOWN_PENDING");
expect(remote).toContain("logged_failure");
@@ -287,6 +331,95 @@ describe("deployment safety contract", () => {
);
});
it.each([
{
name: "propagates a remote code when the marker is missing",
mode: "code-no-marker",
expectedCode: "PROD_DATABASE_IDENTITY_MISMATCH",
},
{
name: "propagates a remote code when the marker is present",
mode: "code-with-marker",
expectedCode: "PROD_MIGRATION_STATUS_FAILED",
},
{
name: "propagates a remote code when the marker body is unreadable",
mode: "code-with-unreadable-marker",
expectedCode: "PROD_MIGRATION_STATUS_FAILED",
},
{
name: "uses the fallback for a remote failure without a code",
mode: "no-code-no-marker",
expectedCode: "PRODUCTION_PREFLIGHT_REMOTE_FAILED",
},
{
name: "requires the marker after a successful remote exit",
mode: "success-no-marker",
expectedCode: "PRISMA_STATUS_OUTPUT_MISSING",
},
])("runs the real preflight subprocess and $name", async ({ mode, expectedCode }) => {
const fakeBin = path.join(fixtureRoot, `preflight-bin-${mode}`);
const reportDir = path.join(fixtureRoot, `preflight-report-${mode}`);
await mkdir(fakeBin, { recursive: true });
const fakeSsh = path.join(fakeBin, "ssh");
await writeFile(fakeSsh, [
"#!/usr/bin/env node",
"const mode = process.env.FAKE_PREFLIGHT_SSH_MODE;",
"if (mode === 'code-no-marker') {",
" process.stdout.write('[production-preflight][PAUSED] code=PROD_DATABASE_IDENTITY_MISMATCH\\n');",
" process.exit(41);",
"}",
"if (mode === 'code-with-marker') {",
" process.stdout.write('[production-preflight][PRISMA_STATUS_BEGIN]\\n');",
" process.stdout.write('Database schema is up to date!\\n');",
" process.stdout.write('[production-preflight][PRISMA_STATUS_END] exit=0\\n');",
" process.stdout.write('[production-preflight][PAUSED] code=PROD_MIGRATION_STATUS_FAILED\\n');",
" process.exit(42);",
"}",
"if (mode === 'code-with-unreadable-marker') {",
" process.stdout.write('[production-preflight][PRISMA_STATUS_BEGIN]\\n');",
" process.stdout.write('truncated or unknown prisma output\\n');",
" process.stdout.write('[production-preflight][PRISMA_STATUS_END] exit=1\\n');",
" process.stdout.write('[production-preflight][PAUSED] code=PROD_MIGRATION_STATUS_FAILED\\n');",
" process.exit(44);",
"}",
"if (mode === 'no-code-no-marker') {",
" process.stdout.write('remote command failed\\n');",
" process.stderr.write('ssh exited unexpectedly\\n');",
" process.exit(43);",
"}",
"if (mode === 'success-no-marker') {",
" process.stdout.write('[production-preflight][PASS] target_identity\\n');",
" process.exit(0);",
"}",
"process.stderr.write('unexpected fake ssh mode\\n');",
"process.exit(97);",
"",
].join("\n"), { mode: 0o700 });
await chmod(fakeSsh, 0o700);
let failure;
try {
await execFileAsync("node", [PRODUCTION_PREFLIGHT], {
cwd: ROOT_DIR,
env: productionPreflightEnv({
fakeBin,
reportDir,
overrides: { FAKE_PREFLIGHT_SSH_MODE: mode },
}),
});
} catch (error) {
failure = error;
}
expect(failure).toBeDefined();
expect(failure.code).toBe(1);
expect(failure.stderr).toContain(`[production-preflight][PAUSED] code=${expectedCode}`);
expect(failure.stdout).toContain("[production-preflight] reportRef=");
const stableEvidence = await readFile(path.join(reportDir, "preflight-output.txt"), "utf8");
expect(stableEvidence.trim()).not.toBe("");
expect(failure.stdout).toContain(stableEvidence.trim());
});
it("captures a redacted pre-status evidence copy from a controlled SSH fixture", async () => {
const fakeBin = path.join(fixtureRoot, "bin");
const reportDir = path.join(fixtureRoot, "report");
@@ -312,9 +445,9 @@ describe("deployment safety contract", () => {
);
await chmod(fakeSsh, 0o700);
const candidateCommit = require("node:child_process")
.execFileSync("git", ["rev-parse", "HEAD"], { cwd: ROOT_DIR, encoding: "utf8" })
.trim();
const candidateCommit = execFileSync("git", ["rev-parse", "HEAD"], { cwd: ROOT_DIR, encoding: "utf8" }).trim();
const provenance = await import(pathToFileURL(path.join(ROOT_DIR, "deploy/trialrun/check-source-provenance.mjs")).href);
const candidateBuildEnv = provenance.deriveCandidateBuildEnv(candidateCommit);
const result = await execFileAsync("node", [PRODUCTION_PREFLIGHT], {
cwd: ROOT_DIR,
env: {
@@ -351,8 +484,8 @@ describe("deployment safety contract", () => {
expect(sshCommand).not.toContain("hyb_oa");
expect(sshCommand).toContain("EXPECTED_PRODUCTION_ORIGIN_SHA256");
expect(sshCommand).toContain("EXPECTED_PRODUCTION_DB_NAME_SHA256");
expect(sshCommand).toContain(`BUILD_SHA='${candidateCommit}'`);
expect(sshCommand).toContain(`VITE_BUILD_SHA='${candidateCommit.slice(0, 12)}'`);
expect(sshCommand).toContain(`BUILD_SHA='${candidateBuildEnv.BUILD_SHA}'`);
expect(sshCommand).toContain(`VITE_BUILD_SHA='${candidateBuildEnv.VITE_BUILD_SHA}'`);
});
it("bootstraps only an exact historical identity and a unique refresh probe", async () => {
@@ -476,13 +609,14 @@ describe("deployment safety contract", () => {
"set -euo pipefail",
"joined=\"$*\"",
"mode=\"${FAKE_REMOTE_MODE:-happy}\"",
"if [[ -n \"${FAKE_DOCKER_TRACE:-}\" ]]; then printf '%s\\n' \"$joined\" >> \"$FAKE_DOCKER_TRACE\"; fi",
"if [[ \"$1\" == compose && \"${2:-}\" == version ]]; then exit 0; fi",
"if [[ \"$1\" == ps ]]; then",
" if [[ \"$joined\" == *service=server* ]]; then printf 'server1\\n'; else printf 'db1\\n'; fi",
" exit 0",
"fi",
"if [[ \"$1\" == inspect ]]; then printf 'NODE_ENV=production\\nCOOKIE_SECURE=true\\nAPP_ORIGIN=https://oa.example.com\\n'; exit 0; fi",
"if [[ \"$joined\" == *' config'* ]]; then exit 0; fi",
"if [[ \"$joined\" == *' config'* ]]; then [[ \"$mode\" == compose-invalid ]] && exit 91; exit 0; fi",
"if [[ \"$joined\" == *' exec '* && \"$joined\" == *' sh -lc '* ]]; then",
" case \"$mode\" in",
" pending) printf 'Following migration have not yet been applied:\\n20260807193001_followup_migration\\n'; exit 1 ;;",
@@ -519,12 +653,14 @@ describe("deployment safety contract", () => {
const hash = (value) => createHash("sha256").update(value, "utf8").digest("hex");
const buildSha = "a".repeat(40);
const dockerTrace = path.join(fixtureRoot, "fake-docker-calls.log");
const runRemote = (mode, expectedPending = "", buildOverrides = {}) => execFileAsync("bash", [PRODUCTION_PREFLIGHT_REMOTE], {
cwd: remoteDir,
env: {
...process.env,
PATH: `${fakeBin}${path.delimiter}${process.env.PATH}`,
FAKE_REMOTE_MODE: mode,
FAKE_DOCKER_TRACE: dockerTrace,
REMOTE_DIR: remoteDir,
REMOTE_ENV_FILE: envFile,
COMPOSE_FILE: "deploy/trialrun/docker-compose.yml",
@@ -566,6 +702,19 @@ describe("deployment safety contract", () => {
await expect(runRemote("logs")).rejects.toMatchObject({
stderr: expect.stringContaining("PROD_MIGRATION_FAILED_OR_ROLLED_BACK"),
});
await writeFile(dockerTrace, "");
await expect(runRemote("compose-invalid")).rejects.toMatchObject({
stderr: expect.stringContaining("PROD_TARGET_COMPOSE_CONFIG_INVALID"),
});
const composeFailureCalls = await readFile(dockerTrace, "utf8");
expect(composeFailureCalls).toContain(" config");
expect(composeFailureCalls).not.toContain("prisma migrate status");
expect(composeFailureCalls).not.toContain(" exec ");
await writeFile(dockerTrace, "");
await expect(runRemote("happy", "", { BUILD_SHA: "" })).rejects.toMatchObject({
stderr: expect.stringContaining("BUILD_SHA_MISSING"),
});
expect(await readFile(dockerTrace, "utf8")).toBe("");
await expect(runRemote("happy", "", { BUILD_SHA: "a".repeat(39) })).rejects.toMatchObject({
stderr: expect.stringContaining("PROD_TARGET_BUILD_SHA_INVALID"),
});