修复(08-08-production-deployment-execution): 保留生产预检真实错误码
这个提交包含在:
@@ -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;取得真实远端错误码后必须立即停止并复核原因。
|
||||
|
||||
@@ -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");
|
||||
|
||||
在新工单中引用
屏蔽一个用户