From dbc1b068ab93a8868bd89008c62b46f4e85f7f57 Mon Sep 17 00:00:00 2001 From: lanrtop Date: Wed, 23 Sep 2026 12:09:38 +0900 Subject: [PATCH] fix(delivery): avoid false deployed timestamp Rules-Applied: R07,R13 --- .../modules/project-capability-control-plane.md | 11 +++++++++++ docs/ai-context/git-workflow.md | 1 + .../resources/onboarding/git-workflow.md.tmpl | 3 ++- src-tauri/src/commands/delivery_status.rs | 17 ++++++++++++----- 4 files changed, 26 insertions(+), 6 deletions(-) diff --git a/.blueprint/modules/project-capability-control-plane.md b/.blueprint/modules/project-capability-control-plane.md index d391b5b..03b6226 100644 --- a/.blueprint/modules/project-capability-control-plane.md +++ b/.blueprint/modules/project-capability-control-plane.md @@ -203,6 +203,8 @@ GET /health/version v1 不新增 deployment history 表。最近部署时间和过程状态来自 Gitea status 的时间字段,当前运行事实来自实时探针;连续历史、受控部署和审计记录等真实需求出现后,再评估 SQLite record。 +`deployed_at` 仅在对应部署 context 成功时取其状态更新时间;失败、等待和跳过的状态不能用其更新时间冒充部署完成时间。 + ### 系统边界变更 - **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 顶层字段保持兼容。" - 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 端到端验收与观测基线 - status: todo - complexity: M diff --git a/docs/ai-context/git-workflow.md b/docs/ai-context/git-workflow.md index a7da1e7..c747620 100644 --- a/docs/ai-context/git-workflow.md +++ b/docs/ai-context/git-workflow.md @@ -109,6 +109,7 @@ PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否 - `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。 - `not_configured`:项目尚未声明 `.lian-jing/capabilities.yaml` 的 production delivery profile;这是正常的可选接入状态,不代表部署失败。 - `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 和字段名,不放凭证或命令。 - 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。 diff --git a/src-tauri/resources/onboarding/git-workflow.md.tmpl b/src-tauri/resources/onboarding/git-workflow.md.tmpl index a51b2d2..c747620 100644 --- a/src-tauri/resources/onboarding/git-workflow.md.tmpl +++ b/src-tauri/resources/onboarding/git-workflow.md.tmpl @@ -108,7 +108,8 @@ PR 别攒太大(建议每 3~5 个 commit 或几百行内就开一次),否 - `unknown`:炼境尚未获得该项证据,**不是成功**;例如 CI 成功不能代替 production 健康。 - `unavailable`:对应外部来源暂时不可达;保留本地验证与 Gitea 网页作为降级路径。 - `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 和字段名,不放凭证或命令。 - 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。 diff --git a/src-tauri/src/commands/delivery_status.rs b/src-tauri/src/commands/delivery_status.rs index 9528c77..e919859 100644 --- a/src-tauri/src/commands/delivery_status.rs +++ b/src-tauri/src/commands/delivery_status.rs @@ -409,7 +409,11 @@ fn aggregate_deployment( components .values() .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) }) .flatten(); @@ -851,6 +855,7 @@ delivery: assert_eq!(deployed.state, "deployed"); assert_eq!(deployed.data["target_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); let drifted = aggregate_fixture("success", "healthy", Some(&old)); @@ -859,10 +864,11 @@ delivery: let failed = aggregate_fixture("failure", "healthy", Some(&old)); assert_eq!(failed.state, "failed"); assert_eq!(failed.data["running_revision"], old); - assert_eq!( - aggregate_fixture("pending", "healthy", Some(&old)).state, - "pending" - ); + assert!(failed.data["deployed_at"].is_null()); + let pending = aggregate_fixture("pending", "healthy", Some(&old)); + assert_eq!(pending.state, "pending"); + assert_eq!(pending.data["running_revision"], old); + assert!(pending.data["deployed_at"].is_null()); assert_eq!( aggregate_fixture("success", "unhealthy", Some(&target)).state, "unhealthy" @@ -874,6 +880,7 @@ delivery: let skipped = aggregate_fixture("skipped", "healthy", Some(&old)); assert_eq!(skipped.state, "skipped"); assert_eq!(skipped.data["running_revision"], old); + assert!(skipped.data["deployed_at"].is_null()); assert_eq!( aggregate_fixture("skipped", "unavailable", None).state, "unavailable"