完善(08-09-deployment-contract-single-source-harness): 收敛部署合同并补失败路径
这个提交包含在:
@@ -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"),
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户