fix(delivery): distinguish skipped production deploy
All checks were successful
Push & PR Check / check (push) Successful in 1m18s
Push & PR Check / check (pull_request) Successful in 58s

This commit is contained in:
lanrtop 2026-09-23 11:08:10 +09:00
parent 2b577f6902
commit 8bc33aaab9
6 changed files with 40 additions and 14 deletions

View File

@ -194,7 +194,8 @@ GET /health/version
- `not_configured`:项目没有 production delivery profile。
- `pending`:部署 context 尚未到达最终状态。
- `deployed`:部署成功、探针健康、运行 SHA 与目标 SHA 一致。
- `drifted`:探针可达,但运行 SHA 与目标 SHA 不一致(含回滚后的旧版本)。
- `drifted`:本次部署 context 成功、探针可达,但运行 SHA 与目标 SHA 不一致(含回滚后的旧版本)。
- `skipped`:本次默认分支提交未触发部署;保留实际运行 SHA,不把正常跳过误报为漂移或部署成功。
- `failed`:Gitea 明确报告部署失败;若探针仍健康,必须同时展示实际运行 SHA,不能把线上误报为不可用。
- `unhealthy`:运行 SHA 可读但技术健康失败。
- `unknown`:缺少足以判断的证据。
@ -343,7 +344,8 @@ v1 不新增 deployment history 表。最近部署时间和过程状态来自 Gi
- complexity: M
- files: src-tauri/src/commands/delivery_status.rs, src-tauri/src/mcp_tools.rs, src-tauri/src/mcp_server.rs
- depends: P3-A. Gitea 1.22 真实 deployment signal 实证与归一化, P3-C. production delivery profile、安全探针与 onboarding 自检
- acceptance: "[MCP] `get_delivery_status(project_id)` 对固定 fixture 调用 → Gitea success + probe healthy + SHA 相同返回 deployed;SHA 不同返回 drifted;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 端到端验收。"
### 📋 P3-E. Enterprise production 端到端验收与观测基线
- status: todo

View File

@ -143,7 +143,7 @@ acceptance 是 agent 自验的唯一判断依据,必须是可执行断言:
项目可自主管理 `.lian-jing/capabilities.yaml` 的 `delivery.targets.production`,声明 Gitea Actions 的**精确 status context** 与公开 HTTP JSON 版本探针。炼境只读该声明;接入包不生成或覆盖项目的 production 配置。探针不接受 URL 凭证、认证头、查询参数、非 HTTP(S) 协议或本机/内网地址;连接时过滤域名解析出的私网地址,不走环境代理、不跟随重定向,并限制请求时间与响应大小。
`get_delivery_status` 分别采集 Gitea 默认分支目标 SHA 的部署过程证据和当前服务报告的运行 SHA。只有部署 context 成功、探针健康、两个完整 SHA 相等时才报告 `deployed`。`failed` 保留实际运行版本,`drifted` 表示版本不一致;缺失/不可达证据明确降级。该状态仅是技术交付观测,不替代业务测试、上线决策,也不赋予炼境部署、回滚、SSH 或数据库写权限。
`get_delivery_status` 分别采集 Gitea 默认分支目标 SHA 的部署过程证据和当前服务报告的运行 SHA。只有部署 context 成功、探针健康、两个完整 SHA 相等时才报告 `deployed`。`failed` 保留实际运行版本;部署成功但版本不一致才报告 `drifted`;本次提交的部署 job 被跳过时报告 `skipped` 并保留运行 SHA,不把文档类提交误报为漂移。缺失/不可达证据明确降级。该状态仅是技术交付观测,不替代业务测试、上线决策,也不赋予炼境部署、回滚、SSH 或数据库写权限。
## 三条设计原则

View File

@ -108,7 +108,7 @@ 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,不误报漂移。技术健康不替代业务验收。
- production profile 由项目维护,炼境接入包不会覆盖;只声明公开 HTTP(S) JSON URL、精确 Gitea context 和字段名,不放凭证或命令。
- 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。

View File

@ -342,12 +342,16 @@ fn aggregate_deployment(
let any_pending = components
.values()
.any(|(_, deploy, _)| deploy.state == "pending");
let any_skipped = components
.values()
.any(|(_, deploy, _)| deploy.state == "skipped");
let any_unhealthy = components
.values()
.any(|(_, _, probe)| probe.state == "unhealthy");
let any_drifted = target_sha.is_some_and(|target| {
components.values().any(|(_, _, probe)| {
probe
components.values().any(|(_, deploy, probe)| {
deploy.state == "success"
&& probe
.revision
.as_deref()
.is_some_and(|running| running != target)
@ -384,6 +388,11 @@ fn aggregate_deployment(
)
} else if any_unavailable {
("unavailable", "Gitea 部署状态或运行探针不可达")
} else if any_skipped {
(
"skipped",
"本次默认分支提交未触发部署;保留当前运行版本,不将正常跳过误报为漂移",
)
} else {
("unknown", "缺少足以判断 production 部署的双证据")
};
@ -862,6 +871,17 @@ delivery:
aggregate_fixture("success", "unavailable", None).state,
"unavailable"
);
let skipped = aggregate_fixture("skipped", "healthy", Some(&old));
assert_eq!(skipped.state, "skipped");
assert_eq!(skipped.data["running_revision"], old);
assert_eq!(
aggregate_fixture("skipped", "unavailable", None).state,
"unavailable"
);
assert_eq!(
aggregate_fixture("unknown", "healthy", Some(&old)).state,
"unknown"
);
assert_eq!(
aggregate_fixture("unknown", "healthy", Some(&target)).state,
"unknown"

View File

@ -2007,7 +2007,7 @@ mod tests {
#[test]
fn production_context_uses_remote_default_branch_and_exact_job_name() {
use std::io::{Read, Write};
use std::io::{BufRead, BufReader, Write};
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
listener.set_nonblocking(true).unwrap();
let address = listener.local_addr().unwrap();
@ -2040,9 +2040,13 @@ mod tests {
assert!(started.elapsed() < std::time::Duration::from_secs(5));
std::thread::sleep(std::time::Duration::from_millis(10));
};
let mut request = [0u8; 4096];
let len = stream.read(&mut request).unwrap();
let request = String::from_utf8_lossy(&request[..len]);
// Windows 上从 nonblocking listener 接受的 socket 也可能保持 nonblocking。
stream.set_nonblocking(false).unwrap();
stream
.set_read_timeout(Some(std::time::Duration::from_secs(3)))
.unwrap();
let mut request = String::new();
BufReader::new(&mut stream).read_line(&mut request).unwrap();
assert!(
request.starts_with(&format!("GET {expected_path}")),
"{request}"

View File

@ -187,7 +187,7 @@ pub fn tools_list_result() -> Value {
},
{
"name": "get_delivery_status",
"description": "只读读取项目交付信号:本地 Git/PR/CI/review,以及项目声明的 production Gitea 部署 context 与公开 HTTP 版本探针。仅当部署成功、探针健康且运行 SHA 与 Gitea 默认分支目标 SHA 完全相同才返回 deployed;失败、漂移、未配置或来源不可达分别保留 failed/drifted/not_configured/unavailable,不替项目作发布决定。",
"description": "只读读取项目交付信号:本地 Git/PR/CI/review,以及项目声明的 production Gitea 部署 context 与公开 HTTP 版本探针。仅当部署成功、探针健康且运行 SHA 与 Gitea 默认分支目标 SHA 完全相同才返回 deployed;失败、跳过部署、部署成功后的漂移、未配置或来源不可达分别保留 failed/skipped/drifted/not_configured/unavailable,不替项目作发布决定。",
"inputSchema": {
"type": "object",
"properties": {