fix(delivery): avoid false deployed timestamp
All checks were successful
Push & PR Check / check (push) Successful in 59s
Push & PR Check / check (pull_request) Successful in 1m0s

Rules-Applied: R07,R13
This commit is contained in:
lanrtop 2026-09-23 12:09:38 +09:00
parent 8bc33aaab9
commit dbc1b068ab
4 changed files with 26 additions and 6 deletions

View File

@ -203,6 +203,8 @@ GET /health/version
v1 不新增 deployment history 表。最近部署时间和过程状态来自 Gitea status 的时间字段,当前运行事实来自实时探针;连续历史、受控部署和审计记录等真实需求出现后,再评估 SQLite record。 v1 不新增 deployment history 表。最近部署时间和过程状态来自 Gitea status 的时间字段,当前运行事实来自实时探针;连续历史、受控部署和审计记录等真实需求出现后,再评估 SQLite record。
`deployed_at` 仅在对应部署 context 成功时取其状态更新时间;失败、等待和跳过的状态不能用其更新时间冒充部署完成时间。
### 系统边界变更 ### 系统边界变更
- **Enterprise System HTTP 契约**:新增 `/health/version`;Docker 镜像携带不可变 revision,回滚镜像自然恢复对应旧 revision。 - **Enterprise System HTTP 契约**:新增 `/health/version`;Docker 镜像携带不可变 revision,回滚镜像自然恢复对应旧 revision。
@ -347,6 +349,15 @@ v1 不新增 deployment history 表。最近部署时间和过程状态来自 Gi
- acceptance: "[MCP] `get_delivery_status(project_id)` 对固定 fixture 调用 → Gitea success + probe healthy + SHA 相同返回 deployed;成功部署但 SHA 不同返回 drifted;deploy skipped 且运行旧 SHA 返回 skipped;deploy failure 同时保留实际运行 SHA;来源不可达返回 unavailable;证据不足返回 unknown,且现有 Git/PR/CI/review 顶层字段保持兼容。" - acceptance: "[MCP] `get_delivery_status(project_id)` 对固定 fixture 调用 → Gitea success + probe healthy + SHA 相同返回 deployed;成功部署但 SHA 不同返回 drifted;deploy skipped 且运行旧 SHA 返回 skipped;deploy failure 同时保留实际运行 SHA;来源不可达返回 unavailable;证据不足返回 unknown,且现有 Git/PR/CI/review 顶层字段保持兼容。"
- progress_note: "2026-09-23:真实 Enterprise run #529 的 deploy-api 为 skipped;聚合层新增 skipped 与旧运行 SHA 回归测试,避免普通文档提交误报 drifted。隔离分支 cargo test --lib 187/187;正式版尚未加载新代码,production 探针当前为 401,待 PR/发布后做 MCP 端到端验收。" - progress_note: "2026-09-23:真实 Enterprise run #529 的 deploy-api 为 skipped;聚合层新增 skipped 与旧运行 SHA 回归测试,避免普通文档提交误报 drifted。隔离分支 cargo test --lib 187/187;正式版尚未加载新代码,production 探针当前为 401,待 PR/发布后做 MCP 端到端验收。"
### ✅ P3-D1. 修正 deployment 时间证据与接入说明
- status: done
- complexity: M
- files: src-tauri/src/commands/delivery_status.rs, src-tauri/resources/onboarding/git-workflow.md.tmpl, docs/ai-context/git-workflow.md
- depends: P3-D. deployment 双证据聚合与 MCP 契约
- acceptance: "agent_verify: [cargo test] aggregate_deployment 的 success context 保留 deployed_at,skipped/failed/pending context 的 deployed_at 为 null 且仍保留运行 SHA;[文档对照] 接入模板与 docs/ai-context/git-workflow.md 均说明 drifted 仅限成功部署后 SHA 不一致、skipped 不误报漂移;[git diff] diff --check 通过。"
- done_note: "2026-09-23:聚合回归测试覆盖 success/skipped/failed/pending 的 deployed_at 与运行 SHA;cargo test --lib 187/187、scoped rustfmt、模板与说明文件对照、git diff --check 均通过。"
- retrospective: "【复盘】部署时间证据与接入说明;任务类型: MCP工具;复杂度: M;实际轮数: 1;是否返工: 是;返工原因: skipped context 的更新时间被误写为 deployed_at,模板未同步 skipped 边界;有效策略: 对同一 fixture 同时断言状态、运行 SHA 和时间字段,并逐字对照分发模板;遗留风险: 正式版未加载本 PR,跨项目 MCP 实机仍待合并后终验;auto_applied_rules: R07,R13;rule_effectiveness: R07=有效,R13=未验证。"
### 📋 P3-E. Enterprise production 端到端验收与观测基线 ### 📋 P3-E. Enterprise production 端到端验收与观测基线
- status: todo - status: todo
- complexity: M - complexity: M

View File

@ -109,6 +109,7 @@ PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否
- `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。 - `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。
- `not_configured`:项目尚未声明 `.lian-jing/capabilities.yaml` 的 production delivery profile;这是正常的可选接入状态,不代表部署失败。 - `not_configured`:项目尚未声明 `.lian-jing/capabilities.yaml` 的 production delivery profile;这是正常的可选接入状态,不代表部署失败。
- `deployed` 仅表示 Gitea 部署 context 成功、公开版本探针健康且运行 SHA 与默认分支目标 SHA 完全一致;成功部署后 SHA 不一致才标 `drifted`。本次提交部署 job 被跳过时标 `skipped` 并保留实际运行 SHA,不误报漂移。技术健康不替代业务验收。 - `deployed` 仅表示 Gitea 部署 context 成功、公开版本探针健康且运行 SHA 与默认分支目标 SHA 完全一致;成功部署后 SHA 不一致才标 `drifted`。本次提交部署 job 被跳过时标 `skipped` 并保留实际运行 SHA,不误报漂移。技术健康不替代业务验收。
- `deployment.data.deployed_at` 仅在部署 context 成功时取其状态更新时间;`skipped`、`failed`、`pending` 不提供部署完成时间,实际运行 SHA 仍单独保留。
- production profile 由项目维护,炼境接入包不会覆盖;只声明公开 HTTP(S) JSON URL、精确 Gitea context 和字段名,不放凭证或命令。 - production profile 由项目维护,炼境接入包不会覆盖;只声明公开 HTTP(S) JSON URL、精确 Gitea context 和字段名,不放凭证或命令。
- 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。 - 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。

View File

@ -108,7 +108,8 @@ PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否
- `unknown`:炼境尚未获得该项证据,**不是成功**;例如 CI 成功不能代替 production 健康。 - `unknown`:炼境尚未获得该项证据,**不是成功**;例如 CI 成功不能代替 production 健康。
- `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。 - `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。
- `not_configured`:项目尚未声明 `.lian-jing/capabilities.yaml` 的 production delivery profile;这是正常的可选接入状态,不代表部署失败。 - `not_configured`:项目尚未声明 `.lian-jing/capabilities.yaml` 的 production delivery profile;这是正常的可选接入状态,不代表部署失败。
- `deployed` 仅表示 Gitea 部署 context 成功、公开版本探针健康且运行 SHA 与默认分支目标 SHA 完全一致;`drifted` 表示运行 SHA 不一致。技术健康不替代业务验收。 - `deployed` 仅表示 Gitea 部署 context 成功、公开版本探针健康且运行 SHA 与默认分支目标 SHA 完全一致;成功部署后 SHA 不一致才标 `drifted`。本次提交部署 job 被跳过时标 `skipped` 并保留实际运行 SHA,不误报漂移。技术健康不替代业务验收。
- `deployment.data.deployed_at` 仅在部署 context 成功时取其状态更新时间;`skipped`、`failed`、`pending` 不提供部署完成时间,实际运行 SHA 仍单独保留。
- production profile 由项目维护,炼境接入包不会覆盖;只声明公开 HTTP(S) JSON URL、精确 Gitea context 和字段名,不放凭证或命令。 - production profile 由项目维护,炼境接入包不会覆盖;只声明公开 HTTP(S) JSON URL、精确 Gitea context 和字段名,不放凭证或命令。
- 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。 - 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。

View File

@ -409,7 +409,11 @@ fn aggregate_deployment(
components components
.values() .values()
.next() .next()
.and_then(|(_, deploy, _)| deploy.data["status"]["updated_at"].as_str()) .and_then(|(_, deploy, _)| {
(deploy.state == "success")
.then(|| deploy.data["status"]["updated_at"].as_str())
.flatten()
})
.map(str::to_owned) .map(str::to_owned)
}) })
.flatten(); .flatten();
@ -851,6 +855,7 @@ delivery:
assert_eq!(deployed.state, "deployed"); assert_eq!(deployed.state, "deployed");
assert_eq!(deployed.data["target_revision"], target); assert_eq!(deployed.data["target_revision"], target);
assert_eq!(deployed.data["running_revision"], target); assert_eq!(deployed.data["running_revision"], target);
assert_eq!(deployed.data["deployed_at"], "2026-09-22T03:38:55Z");
assert_eq!(deployed.data["evidence"].as_array().unwrap().len(), 2); assert_eq!(deployed.data["evidence"].as_array().unwrap().len(), 2);
let drifted = aggregate_fixture("success", "healthy", Some(&old)); let drifted = aggregate_fixture("success", "healthy", Some(&old));
@ -859,10 +864,11 @@ delivery:
let failed = aggregate_fixture("failure", "healthy", Some(&old)); let failed = aggregate_fixture("failure", "healthy", Some(&old));
assert_eq!(failed.state, "failed"); assert_eq!(failed.state, "failed");
assert_eq!(failed.data["running_revision"], old); assert_eq!(failed.data["running_revision"], old);
assert_eq!( assert!(failed.data["deployed_at"].is_null());
aggregate_fixture("pending", "healthy", Some(&old)).state, let pending = aggregate_fixture("pending", "healthy", Some(&old));
"pending" assert_eq!(pending.state, "pending");
); assert_eq!(pending.data["running_revision"], old);
assert!(pending.data["deployed_at"].is_null());
assert_eq!( assert_eq!(
aggregate_fixture("success", "unhealthy", Some(&target)).state, aggregate_fixture("success", "unhealthy", Some(&target)).state,
"unhealthy" "unhealthy"
@ -874,6 +880,7 @@ delivery:
let skipped = aggregate_fixture("skipped", "healthy", Some(&old)); let skipped = aggregate_fixture("skipped", "healthy", Some(&old));
assert_eq!(skipped.state, "skipped"); assert_eq!(skipped.state, "skipped");
assert_eq!(skipped.data["running_revision"], old); assert_eq!(skipped.data["running_revision"], old);
assert!(skipped.data["deployed_at"].is_null());
assert_eq!( assert_eq!(
aggregate_fixture("skipped", "unavailable", None).state, aggregate_fixture("skipped", "unavailable", None).state,
"unavailable" "unavailable"