fix(delivery): distinguish skipped production deploy
This commit is contained in:
parent
2b577f6902
commit
8bc33aaab9
@ -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
|
||||
|
||||
@ -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 或数据库写权限。
|
||||
|
||||
## 三条设计原则
|
||||
|
||||
|
||||
@ -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 和字段名,不放凭证或命令。
|
||||
- 结果是交付上下文,不替代项目自己的测试、业务验收或发布决定。
|
||||
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user