修复(08-08-production-deployment-execution): 校验候选 Compose 构建标识
这个提交包含在:
@@ -1554,7 +1554,11 @@ DEPLOY_SERVER=<approved-ssh-target> \
|
|||||||
- Remote read-only helper: `deploy/trialrun/production-preflight-remote.sh`.
|
- Remote read-only helper: `deploy/trialrun/production-preflight-remote.sh`.
|
||||||
- Required process inputs: `EXPECTED_PRODUCTION_ORIGIN`,
|
- Required process inputs: `EXPECTED_PRODUCTION_ORIGIN`,
|
||||||
`EXPECTED_PRODUCTION_DB_NAME_SHA256`, `EXPECTED_PENDING_MIGRATIONS`, and the
|
`EXPECTED_PRODUCTION_DB_NAME_SHA256`, `EXPECTED_PENDING_MIGRATIONS`, and the
|
||||||
exact deployment target/directory/Compose identity values.
|
exact deployment target/directory/Compose identity values. The strict wrapper
|
||||||
|
must also pass its provenance-verified `CANDIDATE_COMMIT`; preflight must not
|
||||||
|
fall back to `git rev-parse HEAD`.
|
||||||
|
- Candidate Compose interpolation: `BUILD_SHA=<full candidate commit>` and
|
||||||
|
`VITE_BUILD_SHA=<first 12 hex characters of BUILD_SHA>`.
|
||||||
|
|
||||||
### 3. Contracts
|
### 3. Contracts
|
||||||
|
|
||||||
@@ -1576,6 +1580,14 @@ DEPLOY_SERVER=<approved-ssh-target> \
|
|||||||
candidate manifest. Pending must exactly equal the explicit allowlist;
|
candidate manifest. Pending must exactly equal the explicit allowlist;
|
||||||
pending IDs reported by status must belong to the candidate manifest;
|
pending IDs reported by status must belong to the candidate manifest;
|
||||||
failed, rolled-back, unfinished, diverged or unknown history fails closed.
|
failed, rolled-back, unfinished, diverged or unknown history fails closed.
|
||||||
|
- `docker compose config` validates the candidate configuration that would be
|
||||||
|
deployed, not the currently running container configuration. The local
|
||||||
|
orchestrator derives both Compose build variables only from the
|
||||||
|
provenance-verified candidate: the full commit becomes `BUILD_SHA` and its
|
||||||
|
first 12 characters become `VITE_BUILD_SHA`. The remote helper requires both,
|
||||||
|
validates their hexadecimal lengths and asserts the prefix relationship.
|
||||||
|
Never use `--no-interpolate`, which would bypass required `${VAR:?...}`
|
||||||
|
constraints instead of checking the candidate configuration.
|
||||||
- The preflight helper is read-only: it does not run `migrate dev`, reset,
|
- The preflight helper is read-only: it does not run `migrate dev`, reset,
|
||||||
`db push`, `migrate resolve`, seed, DDL or business writes. `deploy.sh` is
|
`db push`, `migrate resolve`, seed, DDL or business writes. `deploy.sh` is
|
||||||
reached only after target and pre-status gates pass.
|
reached only after target and pre-status gates pass.
|
||||||
@@ -1609,6 +1621,12 @@ DEPLOY_SERVER=<approved-ssh-target> \
|
|||||||
- Missing or invalid expected target input -> stop before SSH mutation.
|
- Missing or invalid expected target input -> stop before SSH mutation.
|
||||||
- Target hash, runtime mode, cookie security or DB identity mismatch -> stop
|
- Target hash, runtime mode, cookie security or DB identity mismatch -> stop
|
||||||
before `deploy.sh`.
|
before `deploy.sh`.
|
||||||
|
- Missing/invalid provenance candidate -> `CANDIDATE_COMMIT_MISSING` or
|
||||||
|
`CANDIDATE_COMMIT_INVALID`; do not infer a candidate from local HEAD.
|
||||||
|
- Invalid full/short build SHA or a short SHA that is not the first 12
|
||||||
|
characters of the full SHA -> `PROD_TARGET_BUILD_SHA_INVALID`,
|
||||||
|
`PROD_TARGET_VITE_BUILD_SHA_INVALID` or `PROD_TARGET_BUILD_SHA_MISMATCH`;
|
||||||
|
stop before Compose config and Prisma status.
|
||||||
- Status output missing/unreadable, failed/rolled-back/diverged history or an
|
- Status output missing/unreadable, failed/rolled-back/diverged history or an
|
||||||
unknown pending migration -> `PAUSED`; do not reset or resolve in place.
|
unknown pending migration -> `PAUSED`; do not reset or resolve in place.
|
||||||
- Non-zero remote exit with a safe `code=...`, with or without a Prisma marker
|
- Non-zero remote exit with a safe `code=...`, with or without a Prisma marker
|
||||||
@@ -1627,10 +1645,16 @@ DEPLOY_SERVER=<approved-ssh-target> \
|
|||||||
- Good: the remote helper exits non-zero with a stable database-identity code;
|
- Good: the remote helper exits non-zero with a stable database-identity code;
|
||||||
the wrapper writes the stable evidence summary, returns that exact code and
|
the wrapper writes the stable evidence summary, returns that exact code and
|
||||||
stops before migration or deployment.
|
stops before migration or deployment.
|
||||||
|
- Good: the verified candidate commit is injected as the full build SHA and
|
||||||
|
its 12-character prefix, so Compose validates exactly the configuration that
|
||||||
|
a later approved deployment would activate.
|
||||||
- Base: the remote helper exits zero with a readable Prisma marker and the
|
- Base: the remote helper exits zero with a readable Prisma marker and the
|
||||||
wrapper continues into migration reconciliation.
|
wrapper continues into migration reconciliation.
|
||||||
- Bad: check for a missing marker before checking the remote exit and replace
|
- Bad: check for a missing marker before checking the remote exit and replace
|
||||||
the real remote safety code with `PRISMA_STATUS_OUTPUT_MISSING`.
|
the real remote safety code with `PRISMA_STATUS_OUTPUT_MISSING`.
|
||||||
|
- Bad: omit build SHA interpolation, use a seven-character Git abbreviation,
|
||||||
|
re-read HEAD inside preflight, or pass `--no-interpolate` merely to make
|
||||||
|
Compose config return zero.
|
||||||
|
|
||||||
### 6. Tests Required
|
### 6. Tests Required
|
||||||
|
|
||||||
@@ -1645,6 +1669,10 @@ DEPLOY_SERVER=<approved-ssh-target> \
|
|||||||
and missing marker; non-zero with a safe code and present marker; non-zero
|
and missing marker; non-zero with a safe code and present marker; non-zero
|
||||||
without a safe code; zero with a missing marker. Also assert the stable
|
without a safe code; zero with a missing marker. Also assert the stable
|
||||||
summary and report reference are emitted before either failure check.
|
summary and report reference are emitted before either failure check.
|
||||||
|
- Assert the SSH command receives the provenance candidate as `BUILD_SHA` and
|
||||||
|
its exact 12-character prefix as `VITE_BUILD_SHA`; reject malformed and
|
||||||
|
individually valid-but-mismatched pairs. Static checks must prove there is no
|
||||||
|
local-HEAD candidate fallback.
|
||||||
|
|
||||||
### 7. Wrong vs Correct
|
### 7. Wrong vs Correct
|
||||||
|
|
||||||
@@ -1662,6 +1690,15 @@ write stable evidence -> inspect remote non-zero exit and propagate/fallback
|
|||||||
-> only after remote success require the Prisma marker
|
-> only after remote success require the Prisma marker
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```text
|
||||||
|
# Wrong: validate a non-interpolated or unrelated working-tree configuration.
|
||||||
|
git rev-parse HEAD -> docker compose config --no-interpolate
|
||||||
|
|
||||||
|
# Correct: validate the exact published candidate configuration.
|
||||||
|
provenance-verified commit -> BUILD_SHA + first 12 chars as VITE_BUILD_SHA
|
||||||
|
-> interpolated docker compose config
|
||||||
|
```
|
||||||
|
|
||||||
## Scenario: Environment Recovery Classification And Secret Custody
|
## Scenario: Environment Recovery Classification And Secret Custody
|
||||||
|
|
||||||
### 1. Scope / Trigger
|
### 1. Scope / Trigger
|
||||||
|
|||||||
@@ -136,3 +136,18 @@
|
|||||||
- 聚焦回归覆盖四种组合:非零/有安全码/marker 缺失,非零/有安全码/marker 存在,非零/无可解析码/marker 缺失,以及退出 0/marker 缺失;`deployment-safety-contract.test.js` 为 19/19 PASS。
|
- 聚焦回归覆盖四种组合:非零/有安全码/marker 缺失,非零/有安全码/marker 存在,非零/无可解析码/marker 缺失,以及退出 0/marker 缺失;`deployment-safety-contract.test.js` 为 19/19 PASS。
|
||||||
- 本地 Node/shell syntax、env contract、task validate、process contract、docs audit、Trellis governance、sensitive scan 与 `git diff --check` 均通过;docs audit 仅保留仓库既有的未注册文档 warning。
|
- 本地 Node/shell syntax、env contract、task validate、process contract、docs audit、Trellis governance、sensitive scan 与 `git diff --check` 均通过;docs audit 仅保留仓库既有的未注册文档 warning。
|
||||||
- 本节修复阶段未连接生产、未运行 target/status、未执行 rsync、migration、应用切换或业务 API;生产 schema 写入与业务写入仍为 0。候选提交进入上游且 provenance 重新通过后,才允许重跑只读 pre-status;取得真实远端错误码后必须立即停止并复核原因。
|
- 本节修复阶段未连接生产、未运行 target/status、未执行 rsync、migration、应用切换或业务 API;生产 schema 写入与业务写入仍为 0。候选提交进入上游且 provenance 重新通过后,才允许重跑只读 pre-status;取得真实远端错误码后必须立即停止并复核原因。
|
||||||
|
|
||||||
|
## Read-only Candidate Compose Attempt Evidence (2026-08-08)
|
||||||
|
|
||||||
|
- 错误优先级修复提交 `83af754` 已进入 `origin/main`;执行前 provenance 为 `published=true`、`remoteVerified=true`,工作区 clean。
|
||||||
|
- 使用 `PRODUCTION_READ_ONLY_PREFLIGHT=1` 重跑:精确身份组通过,既有部署聚合体检仍为 `PASS=13 FAIL=0`;production preflight 在 Compose 配置校验处以真实稳定码 `PROD_TARGET_COMPOSE_CONFIG_INVALID` 停止,外层 `DEPLOY_EXISTING_FAILED` 只表示受控 wrapper 已停止。
|
||||||
|
- 源码核对确认 Compose 文件以 `${BUILD_SHA:?...}` / `${VITE_BUILD_SHA:?...}` 强制要求构建标识;正式 `deploy.sh` 从已验证 provenance 注入完整 commit 与 12 位短 commit,而只读 preflight 未注入两值,因此失败属于 preflight 候选配置输入缺口,不是生产配置漂移。
|
||||||
|
- 本次未生成 Prisma status marker 或脱敏 status 文件,未完成 `.env` / `current_database()` / runtime `DATABASE_URL` 三方一致性、pending 对账或 `terminated` count;rollback window 仍为 `UNKNOWN`。
|
||||||
|
- 失败后立即停止,未进入 `deploy.sh`、rsync、`migrate deploy`、应用切换、refresh 或业务 API;生产 schema 写入与业务写入均为 0。
|
||||||
|
|
||||||
|
## Candidate Compose Contract Repair Evidence (2026-08-08)
|
||||||
|
|
||||||
|
- `deploy-existing.sh` 仍是候选来源唯一入口:它先运行 `check-source-provenance.mjs`,只在 `published=true` / `remoteVerified=true` 后把 `source_commit` 传为 `CANDIDATE_COMMIT`。`production-preflight.mjs` 已移除 `git rev-parse HEAD` fallback,候选缺失直接 fail-closed。
|
||||||
|
- preflight 仅从该候选派生 `BUILD_SHA=<完整 commit>` 与 `VITE_BUILD_SHA=<前 12 位>`,并传给远端 Compose;不使用 7 位 Git 默认短 hash。远端 helper 在 `docker compose config` 前校验完整/短 SHA 格式以及 `VITE_BUILD_SHA === BUILD_SHA.slice(0, 12)`。
|
||||||
|
- 此处 `docker compose config` 验证的是候选发布后将生效的配置,不代表当前运行容器已经使用该 build SHA;运行态仍须由后续只读 runtime/SHA 检查独立证明。禁止用 `--no-interpolate` 绕过 Compose 的必填变量合同。
|
||||||
|
- 聚焦测试保持 19/19 PASS,并新增候选完整/12 位注入、7 位短 SHA 拒绝、合法但不匹配 SHA 拒绝、禁止 HEAD fallback 的覆盖;Node/shell syntax 与 env contract PASS。
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ pass() {
|
|||||||
|
|
||||||
for key in REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE COMPOSE_PROJECT_NAME \
|
for key in REMOTE_DIR REMOTE_ENV_FILE COMPOSE_FILE COMPOSE_PROJECT_NAME \
|
||||||
POSTGRES_DATA_DIR STORAGE_HOST_DIR EXPECTED_PRODUCTION_ORIGIN_SHA256 \
|
POSTGRES_DATA_DIR STORAGE_HOST_DIR EXPECTED_PRODUCTION_ORIGIN_SHA256 \
|
||||||
EXPECTED_PRODUCTION_DB_NAME_SHA256 CANDIDATE_MIGRATIONS_CSV \
|
EXPECTED_PRODUCTION_DB_NAME_SHA256 BUILD_SHA VITE_BUILD_SHA \
|
||||||
|
CANDIDATE_MIGRATIONS_CSV \
|
||||||
EXPECTED_PENDING_MIGRATIONS_CSV; do
|
EXPECTED_PENDING_MIGRATIONS_CSV; do
|
||||||
[[ -n "${!key:-}" || "$key" == "EXPECTED_PENDING_MIGRATIONS_CSV" ]] \
|
[[ -n "${!key:-}" || "$key" == "EXPECTED_PENDING_MIGRATIONS_CSV" ]] \
|
||||||
|| fail "${key}_MISSING"
|
|| fail "${key}_MISSING"
|
||||||
@@ -25,6 +26,12 @@ done
|
|||||||
|| fail "PROD_TARGET_ORIGIN_HASH_INVALID"
|
|| fail "PROD_TARGET_ORIGIN_HASH_INVALID"
|
||||||
[[ "$EXPECTED_PRODUCTION_DB_NAME_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
[[ "$EXPECTED_PRODUCTION_DB_NAME_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|
||||||
|| fail "PROD_TARGET_DB_HASH_INVALID"
|
|| fail "PROD_TARGET_DB_HASH_INVALID"
|
||||||
|
[[ "$BUILD_SHA" =~ ^[0-9a-f]{40,64}$ ]] \
|
||||||
|
|| fail "PROD_TARGET_BUILD_SHA_INVALID"
|
||||||
|
[[ "$VITE_BUILD_SHA" =~ ^[0-9a-f]{12}$ ]] \
|
||||||
|
|| fail "PROD_TARGET_VITE_BUILD_SHA_INVALID"
|
||||||
|
[[ "${BUILD_SHA:0:12}" == "$VITE_BUILD_SHA" ]] \
|
||||||
|
|| fail "PROD_TARGET_BUILD_SHA_MISMATCH"
|
||||||
|
|
||||||
cd "$REMOTE_DIR"
|
cd "$REMOTE_DIR"
|
||||||
[[ -f "$REMOTE_ENV_FILE" && ! -L "$REMOTE_ENV_FILE" && -r "$REMOTE_ENV_FILE" ]] \
|
[[ -f "$REMOTE_ENV_FILE" && ! -L "$REMOTE_ENV_FILE" && -r "$REMOTE_ENV_FILE" ]] \
|
||||||
|
|||||||
@@ -163,16 +163,6 @@ export function validateExactProductionIdentity({
|
|||||||
assertExactIdentity("COMPOSE_PROJECT_NAME", composeProjectName, expectedComposeProjectName);
|
assertExactIdentity("COMPOSE_PROJECT_NAME", composeProjectName, expectedComposeProjectName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function gitText(args) {
|
|
||||||
const result = spawnSync("git", args, {
|
|
||||||
cwd: ROOT_DIR,
|
|
||||||
encoding: "utf8",
|
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
|
||||||
});
|
|
||||||
if (result.status !== 0) fail("CANDIDATE_COMMIT_UNAVAILABLE");
|
|
||||||
return result.stdout.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportRef(reportDir) {
|
function reportRef(reportDir) {
|
||||||
const relative = path.relative(ROOT_DIR, reportDir).replaceAll(path.sep, "/");
|
const relative = path.relative(ROOT_DIR, reportDir).replaceAll(path.sep, "/");
|
||||||
return relative && !relative.startsWith("..") ? relative : "controlled-production-preflight-report";
|
return relative && !relative.startsWith("..") ? relative : "controlled-production-preflight-report";
|
||||||
@@ -283,10 +273,10 @@ function main() {
|
|||||||
expectedComposeProjectName,
|
expectedComposeProjectName,
|
||||||
});
|
});
|
||||||
|
|
||||||
const candidateCommit = String(
|
if (process.env.CANDIDATE_COMMIT == null) fail("CANDIDATE_COMMIT_MISSING");
|
||||||
process.env.CANDIDATE_COMMIT ?? gitText(["rev-parse", "--verify", "HEAD"]),
|
const candidateCommit = String(process.env.CANDIDATE_COMMIT).trim();
|
||||||
).trim();
|
|
||||||
if (!/^[0-9a-f]{40,64}$/iu.test(candidateCommit)) fail("CANDIDATE_COMMIT_INVALID");
|
if (!/^[0-9a-f]{40,64}$/iu.test(candidateCommit)) fail("CANDIDATE_COMMIT_INVALID");
|
||||||
|
const candidateShortCommit = candidateCommit.slice(0, 12);
|
||||||
const candidateManifest = collectMigrationManifest(ROOT_DIR, candidateCommit);
|
const candidateManifest = collectMigrationManifest(ROOT_DIR, candidateCommit);
|
||||||
const candidateSet = new Set(candidateManifest);
|
const candidateSet = new Set(candidateManifest);
|
||||||
if (expectedPending.some((migration) => !candidateSet.has(migration))) {
|
if (expectedPending.some((migration) => !candidateSet.has(migration))) {
|
||||||
@@ -315,6 +305,8 @@ function main() {
|
|||||||
`STORAGE_HOST_DIR=${shellQuote(storageHostDir)}`,
|
`STORAGE_HOST_DIR=${shellQuote(storageHostDir)}`,
|
||||||
`EXPECTED_PRODUCTION_ORIGIN_SHA256=${shellQuote(sha256Text(expectedOrigin))}`,
|
`EXPECTED_PRODUCTION_ORIGIN_SHA256=${shellQuote(sha256Text(expectedOrigin))}`,
|
||||||
`EXPECTED_PRODUCTION_DB_NAME_SHA256=${shellQuote(expectedDbNameHash)}`,
|
`EXPECTED_PRODUCTION_DB_NAME_SHA256=${shellQuote(expectedDbNameHash)}`,
|
||||||
|
`BUILD_SHA=${shellQuote(candidateCommit)}`,
|
||||||
|
`VITE_BUILD_SHA=${shellQuote(candidateShortCommit)}`,
|
||||||
`CANDIDATE_MIGRATIONS_CSV=${shellQuote(candidateManifest.join(","))}`,
|
`CANDIDATE_MIGRATIONS_CSV=${shellQuote(candidateManifest.join(","))}`,
|
||||||
`EXPECTED_PENDING_MIGRATIONS_CSV=${shellQuote(expectedPending.join(","))}`,
|
`EXPECTED_PENDING_MIGRATIONS_CSV=${shellQuote(expectedPending.join(","))}`,
|
||||||
"bash -s",
|
"bash -s",
|
||||||
|
|||||||
@@ -751,6 +751,9 @@ for (const requiredFragment of [
|
|||||||
'validateProductionDbName',
|
'validateProductionDbName',
|
||||||
'collectMigrationManifest',
|
'collectMigrationManifest',
|
||||||
'EXPECTED_PENDING_MIGRATIONS',
|
'EXPECTED_PENDING_MIGRATIONS',
|
||||||
|
'CANDIDATE_COMMIT_MISSING',
|
||||||
|
'BUILD_SHA=',
|
||||||
|
'VITE_BUILD_SHA=',
|
||||||
'PRISMA_STATUS_OUTPUT_MISSING',
|
'PRISMA_STATUS_OUTPUT_MISSING',
|
||||||
'PRISMA_STATUS_OUTPUT_UNREADABLE',
|
'PRISMA_STATUS_OUTPUT_UNREADABLE',
|
||||||
'sanitizePrismaStatus',
|
'sanitizePrismaStatus',
|
||||||
@@ -767,6 +770,9 @@ for (const requiredFragment of [
|
|||||||
for (const requiredFragment of [
|
for (const requiredFragment of [
|
||||||
'EXPECTED_PRODUCTION_ORIGIN_SHA256',
|
'EXPECTED_PRODUCTION_ORIGIN_SHA256',
|
||||||
'EXPECTED_PRODUCTION_DB_NAME_SHA256',
|
'EXPECTED_PRODUCTION_DB_NAME_SHA256',
|
||||||
|
'PROD_TARGET_BUILD_SHA_INVALID',
|
||||||
|
'PROD_TARGET_VITE_BUILD_SHA_INVALID',
|
||||||
|
'PROD_TARGET_BUILD_SHA_MISMATCH',
|
||||||
'PRISMA_STATUS_BEGIN',
|
'PRISMA_STATUS_BEGIN',
|
||||||
'FROM "_prisma_migrations"',
|
'FROM "_prisma_migrations"',
|
||||||
'logged_failure',
|
'logged_failure',
|
||||||
|
|||||||
@@ -128,6 +128,8 @@ describe("deployment safety contract", () => {
|
|||||||
expect(preflight).toContain("CANDIDATE_MIGRATIONS_CSV");
|
expect(preflight).toContain("CANDIDATE_MIGRATIONS_CSV");
|
||||||
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_MISSING");
|
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_MISSING");
|
||||||
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_UNREADABLE");
|
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_UNREADABLE");
|
||||||
|
expect(preflight).toContain("CANDIDATE_COMMIT_MISSING");
|
||||||
|
expect(preflight).not.toContain("rev-parse");
|
||||||
expect(preflight).toContain("redacted datasource line");
|
expect(preflight).toContain("redacted datasource line");
|
||||||
expect(preflight).toContain("PRODUCTION_PREFLIGHT_REPORT_NOT_FRESH");
|
expect(preflight).toContain("PRODUCTION_PREFLIGHT_REPORT_NOT_FRESH");
|
||||||
expect(preflight.lastIndexOf("printStableRemoteSummary(stdout)")).toBeLessThan(
|
expect(preflight.lastIndexOf("printStableRemoteSummary(stdout)")).toBeLessThan(
|
||||||
@@ -140,6 +142,7 @@ describe("deployment safety contract", () => {
|
|||||||
expect(remote).toContain("PROD_MIGRATION_STATUS_UNKNOWN_PENDING");
|
expect(remote).toContain("PROD_MIGRATION_STATUS_UNKNOWN_PENDING");
|
||||||
expect(remote).toContain("logged_failure");
|
expect(remote).toContain("logged_failure");
|
||||||
expect(remote).toContain("PROD_DATABASE_URL_HOST_NON_PRODUCTION_IDENTITY");
|
expect(remote).toContain("PROD_DATABASE_URL_HOST_NON_PRODUCTION_IDENTITY");
|
||||||
|
expect(remote).toContain("PROD_TARGET_BUILD_SHA_MISMATCH");
|
||||||
expect(remote).toContain('FROM "_prisma_migrations"');
|
expect(remote).toContain('FROM "_prisma_migrations"');
|
||||||
expect(remote).toContain("rollbackWindow=OPEN");
|
expect(remote).toContain("rollbackWindow=OPEN");
|
||||||
expect(remote).not.toMatch(/\b(prisma migrate reset|prisma db push|prisma migrate resolve)\b/);
|
expect(remote).not.toMatch(/\b(prisma migrate reset|prisma db push|prisma migrate resolve)\b/);
|
||||||
@@ -337,6 +340,8 @@ describe("deployment safety contract", () => {
|
|||||||
expect(sshCommand).not.toContain("hyb_oa");
|
expect(sshCommand).not.toContain("hyb_oa");
|
||||||
expect(sshCommand).toContain("EXPECTED_PRODUCTION_ORIGIN_SHA256");
|
expect(sshCommand).toContain("EXPECTED_PRODUCTION_ORIGIN_SHA256");
|
||||||
expect(sshCommand).toContain("EXPECTED_PRODUCTION_DB_NAME_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)}'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("bootstraps only an exact historical identity and a unique refresh probe", async () => {
|
it("bootstraps only an exact historical identity and a unique refresh probe", async () => {
|
||||||
@@ -494,7 +499,8 @@ describe("deployment safety contract", () => {
|
|||||||
await chmod(path.join(fakeBin, "docker"), 0o700);
|
await chmod(path.join(fakeBin, "docker"), 0o700);
|
||||||
|
|
||||||
const hash = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
const hash = (value) => createHash("sha256").update(value, "utf8").digest("hex");
|
||||||
const runRemote = (mode, expectedPending = "") => execFileAsync("bash", [PRODUCTION_PREFLIGHT_REMOTE], {
|
const buildSha = "a".repeat(40);
|
||||||
|
const runRemote = (mode, expectedPending = "", buildOverrides = {}) => execFileAsync("bash", [PRODUCTION_PREFLIGHT_REMOTE], {
|
||||||
cwd: remoteDir,
|
cwd: remoteDir,
|
||||||
env: {
|
env: {
|
||||||
...process.env,
|
...process.env,
|
||||||
@@ -508,8 +514,11 @@ describe("deployment safety contract", () => {
|
|||||||
STORAGE_HOST_DIR: storageDir,
|
STORAGE_HOST_DIR: storageDir,
|
||||||
EXPECTED_PRODUCTION_ORIGIN_SHA256: hash("https://oa.example.com"),
|
EXPECTED_PRODUCTION_ORIGIN_SHA256: hash("https://oa.example.com"),
|
||||||
EXPECTED_PRODUCTION_DB_NAME_SHA256: hash("production_db"),
|
EXPECTED_PRODUCTION_DB_NAME_SHA256: hash("production_db"),
|
||||||
|
BUILD_SHA: buildSha,
|
||||||
|
VITE_BUILD_SHA: buildSha.slice(0, 12),
|
||||||
CANDIDATE_MIGRATIONS_CSV: "20260807193000_contract_registry_termination_workspace,20260807193001_followup_migration",
|
CANDIDATE_MIGRATIONS_CSV: "20260807193000_contract_registry_termination_workspace,20260807193001_followup_migration",
|
||||||
EXPECTED_PENDING_MIGRATIONS_CSV: expectedPending,
|
EXPECTED_PENDING_MIGRATIONS_CSV: expectedPending,
|
||||||
|
...buildOverrides,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const result = await runRemote("happy");
|
const result = await runRemote("happy");
|
||||||
@@ -524,6 +533,15 @@ describe("deployment safety contract", () => {
|
|||||||
await expect(runRemote("logs")).rejects.toMatchObject({
|
await expect(runRemote("logs")).rejects.toMatchObject({
|
||||||
stderr: expect.stringContaining("PROD_MIGRATION_FAILED_OR_ROLLED_BACK"),
|
stderr: expect.stringContaining("PROD_MIGRATION_FAILED_OR_ROLLED_BACK"),
|
||||||
});
|
});
|
||||||
|
await expect(runRemote("happy", "", { BUILD_SHA: "a".repeat(39) })).rejects.toMatchObject({
|
||||||
|
stderr: expect.stringContaining("PROD_TARGET_BUILD_SHA_INVALID"),
|
||||||
|
});
|
||||||
|
await expect(runRemote("happy", "", { VITE_BUILD_SHA: "a".repeat(7) })).rejects.toMatchObject({
|
||||||
|
stderr: expect.stringContaining("PROD_TARGET_VITE_BUILD_SHA_INVALID"),
|
||||||
|
});
|
||||||
|
await expect(runRemote("happy", "", { VITE_BUILD_SHA: "b".repeat(12) })).rejects.toMatchObject({
|
||||||
|
stderr: expect.stringContaining("PROD_TARGET_BUILD_SHA_MISMATCH"),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aggregates preflight checks and keeps the root helper self-contained", async () => {
|
it("aggregates preflight checks and keeps the root helper self-contained", async () => {
|
||||||
|
|||||||
在新工单中引用
屏蔽一个用户