修复(08-08-production-deployment-execution): 保留生产预检真实错误码
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-08 20:14:36 +08:00
父节点 8892ca9e38
当前提交 83af7544c4
共修改 4 个文件,包含 116 行新增和 9 行删除
+48 -4
查看文件
@@ -1553,14 +1553,16 @@ DEPLOY_SERVER=<approved-ssh-target> \
- Local orchestrator: `deploy/trialrun/production-preflight.mjs`.
- Remote read-only helper: `deploy/trialrun/production-preflight-remote.sh`.
- Required process inputs: `EXPECTED_PRODUCTION_ORIGIN`,
`EXPECTED_PRODUCTION_DB_NAME`, and `EXPECTED_PENDING_MIGRATIONS`.
`EXPECTED_PRODUCTION_DB_NAME_SHA256`, `EXPECTED_PENDING_MIGRATIONS`, and the
exact deployment target/directory/Compose identity values.
### 3. Contracts
- Production R2 is the inverse of the local migration task: the gate must prove
the target is the expected production HTTPS origin and database identity, and
reject loopback, local regression, staging and trialrun identities. It must
never infer production from an SSH alias, Compose label or directory name.
reject loopback, local regression, staging, shadow and placeholder identities.
A legacy `trialrun` token is neutral; the gate must never infer production or
non-production from an SSH alias, Compose label or directory name alone.
- The strict production-validation wrapper rejects unpublished-commit overrides;
disaster-recovery exceptions use a separately approved recovery flow.
- Expected target values remain process-only. The local orchestrator sends only
@@ -1583,6 +1585,12 @@ DEPLOY_SERVER=<approved-ssh-target> \
insufficient.
- Each preflight run removes only its two known report files before SSH; a
reused report directory must never leave an old PASS status artifact in place.
- After the stable remote summary and report reference are emitted, a non-zero
remote exit is evaluated before Prisma marker completeness. A parsed remote
`code=...` remains the primary stop reason; a non-zero exit without a safe
code becomes `PRODUCTION_PREFLIGHT_REMOTE_FAILED`. Only a zero remote exit
without the required marker becomes `PRISMA_STATUS_OUTPUT_MISSING`. This
ordering preserves both the original safety diagnosis and failure evidence.
- When the strict wrapper invokes `deploy.sh`, it sets
`DEPLOY_STATUS_OUTPUT_MODE=quiet`; the legacy status check must not echo raw
datasource output. Detailed evidence comes from the controlled pre/post
@@ -1603,12 +1611,28 @@ DEPLOY_SERVER=<approved-ssh-target> \
before `deploy.sh`.
- Status output missing/unreadable, failed/rolled-back/diverged history or an
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
-> propagate that code and stop. Non-zero exit without a parseable code ->
`PRODUCTION_PREFLIGHT_REMOTE_FAILED`; never continue to marker validation.
- Zero remote exit without the Prisma marker ->
`PRISMA_STATUS_OUTPUT_MISSING`; do not treat the transport success as a
complete pre-status result.
- Pending set differs from the approved allowlist -> `PAUSED` and retain the
status artifact for human review.
- `terminated` count greater than zero -> record `rollbackWindow=CLOSED` and
disallow compatibility rollback; this task never creates such a row.
### 5. Tests Required
### 5. Good/Base/Bad Cases
- 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
stops before migration or deployment.
- Base: the remote helper exits zero with a readable Prisma marker and the
wrapper continues into migration reconciliation.
- Bad: check for a missing marker before checking the remote exit and replace
the real remote safety code with `PRISMA_STATUS_OUTPUT_MISSING`.
### 6. Tests Required
- `node --check deploy/trialrun/production-preflight.mjs`.
- `bash -n deploy/trialrun/production-preflight-remote.sh`.
@@ -1617,6 +1641,26 @@ DEPLOY_SERVER=<approved-ssh-target> \
- Static ordering must prove production preflight precedes `deploy.sh`, and the
remote status marker precedes migration-history reconciliation. Fixtures must
reject loopback/staging target identities without invoking SSH.
- Cover all remote-result precedence combinations: non-zero with a safe code
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
summary and report reference are emitted before either failure check.
### 7. Wrong vs Correct
#### Wrong
```text
missing Prisma marker -> PRISMA_STATUS_OUTPUT_MISSING
-> inspect remote non-zero exit afterward
```
#### Correct
```text
write stable evidence -> inspect remote non-zero exit and propagate/fallback
-> only after remote success require the Prisma marker
```
## Scenario: Environment Recovery Classification And Secret Custody
@@ -128,3 +128,11 @@
- DB hash 引导成功只证明远端 `.env` 数据库名已形成比较基线,不证明后续 `.env` / `current_database()` / runtime `DATABASE_URL` 三方一致性;远端 preflight 未输出 `target_identity_and_database_identity` PASS,故该验收项保持未通过。
- 未获得 migration reconciliation、pending allowlist 对账或 `terminated` count;rollback window 仍为 `UNKNOWN`,不得写作 OPEN/CLOSED。
- `PRODUCTION_READ_ONLY_PREFLIGHT=1` 未进入 `deploy.sh`;未执行 rsync、`migrate deploy`、应用切换、contract refresh 或业务 API,生产 schema 写入与业务写入均为 0。失败后未重跑、未跳过门禁。
## Pre-status Diagnostic Wrapper Repair Evidence (2026-08-08)
- 已确认外层 `production-preflight.mjs` 在远端非零退出且 Prisma marker 缺失时,先抛出 `PRISMA_STATUS_OUTPUT_MISSING`,遮蔽了远端 helper 已输出的稳定安全码;该结论来自源码顺序与既有脱敏 evidence,不反推或猜测真实远端原因。
- 最小修复保持稳定摘要与 `reportRef` 在失败判断前输出,并把远端非零退出判断提前:可解析 `code=...` 时传播原码,不可解析时固定失败为 `PRODUCTION_PREFLIGHT_REMOTE_FAILED`;只有远端退出 0 且 marker 缺失时才返回 `PRISMA_STATUS_OUTPUT_MISSING`。
- 聚焦回归覆盖四种组合:非零/有安全码/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。
- 本节修复阶段未连接生产、未运行 target/status、未执行 rsync、migration、应用切换或业务 API;生产 schema 写入与业务写入仍为 0。候选提交进入上游且 provenance 重新通过后,才允许重跑只读 pre-status;取得真实远端错误码后必须立即停止并复核原因。
+14 -5
查看文件
@@ -234,6 +234,14 @@ export function validatePrismaStatusText(status) {
return status;
}
export function assertRemotePreflightResult({ status, stdout, stderr, rawStatus }) {
if (status !== 0) {
const remoteCode = `${stdout}\n${stderr || ""}`.match(/code=([A-Z0-9_]+)/u)?.[1];
fail(remoteCode || "PRODUCTION_PREFLIGHT_REMOTE_FAILED");
}
if (rawStatus == null) fail("PRISMA_STATUS_OUTPUT_MISSING");
}
function main() {
try {
const deployServer = String(process.env.DEPLOY_SERVER ?? "").trim();
@@ -337,11 +345,12 @@ function main() {
printStableRemoteSummary(stdout);
console.log(`[production-preflight] reportRef=${reportRef(reportDir)}`);
if (rawStatus == null) fail("PRISMA_STATUS_OUTPUT_MISSING");
if (result.status !== 0) {
const remoteCode = `${stdout}\n${result.stderr || ""}`.match(/code=([A-Z0-9_]+)/u)?.[1];
fail(remoteCode || "PRODUCTION_PREFLIGHT_REMOTE_FAILED");
}
assertRemotePreflightResult({
status: result.status,
stdout,
stderr: result.stderr,
rawStatus,
});
console.log("[production-preflight][PASS] 生产目标与迁移只读门禁通过");
} catch (error) {
const code = error instanceof ProductionPreflightError
@@ -130,6 +130,12 @@ describe("deployment safety contract", () => {
expect(preflight).toContain("PRISMA_STATUS_OUTPUT_UNREADABLE");
expect(preflight).toContain("redacted datasource line");
expect(preflight).toContain("PRODUCTION_PREFLIGHT_REPORT_NOT_FRESH");
expect(preflight.lastIndexOf("printStableRemoteSummary(stdout)")).toBeLessThan(
preflight.lastIndexOf("assertRemotePreflightResult({"),
);
expect(preflight.indexOf("reportRef=${reportRef(reportDir)}")).toBeLessThan(
preflight.lastIndexOf("assertRemotePreflightResult({"),
);
expect(remote).toContain("PRISMA_STATUS_BEGIN");
expect(remote).toContain("PROD_MIGRATION_STATUS_UNKNOWN_PENDING");
expect(remote).toContain("logged_failure");
@@ -227,6 +233,46 @@ describe("deployment safety contract", () => {
);
});
it.each([
{
name: "prefers a remote safety code when the marker is missing",
status: 1,
stdout: "",
stderr: "[production-preflight][PAUSED] code=PROD_DATABASE_IDENTITY_MISMATCH",
rawStatus: null,
expectedCode: "PROD_DATABASE_IDENTITY_MISMATCH",
},
{
name: "prefers a remote safety code when the marker exists",
status: 1,
stdout: "[production-preflight][PAUSED] code=PROD_MIGRATION_STATUS_FAILED",
stderr: "",
rawStatus: "Database schema is up to date!",
expectedCode: "PROD_MIGRATION_STATUS_FAILED",
},
{
name: "fails closed when a remote failure has no parseable code",
status: 1,
stdout: "remote command failed",
stderr: "ssh exited unexpectedly",
rawStatus: null,
expectedCode: "PRODUCTION_PREFLIGHT_REMOTE_FAILED",
},
{
name: "requires the Prisma marker after a successful remote exit",
status: 0,
stdout: "[production-preflight][PASS] target_identity",
stderr: "",
rawStatus: null,
expectedCode: "PRISMA_STATUS_OUTPUT_MISSING",
},
])("$name", async ({ status, stdout, stderr, rawStatus, expectedCode }) => {
const target = await import(pathToFileURL(PRODUCTION_PREFLIGHT).href);
expect(() => target.assertRemotePreflightResult({ status, stdout, stderr, rawStatus })).toThrowError(
expect.objectContaining({ code: expectedCode }),
);
});
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");