boardx / boardx/workspacex

门控空转:e2e-full(chat 读写 + 自助资料链路)近 100 次 run 只真执行 3 次且全红,main 仍报 success

Open
#2,084 12 comments 0 reactions 0 assignees View on GitHub
owner:coord-architecture
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1h 7m
Merged PRs (30d)
969

Description

> # ⚠ 本正文有两处已被更正(2026-08-26),保留原文以便对照,不做静默修改
>
> **更正 ①(他人实测推翻,我已独立复验)**:正文下方「**掐掉时 `Upload E2E evidence` 那步是 `skipped` ⇒ 连日志 artifact 都没有**。这就是它能红这么久没人发现的最后一环」——**这句是错的**。
> `cancelled` 丢的只是 **artifact 上传**,**不丢原始 Actions job 日志**:`gh api .../actions/jobs/97932045267/logs` 退出 0、1.2 MB、spec 逐条结论俱在。
> ⇒ 它藏了 39 小时的主因不是「无从查证」,而是**语义**:`cancelled` 读起来像「有人手动取消了」,**没人想到要去查**。
> ⇒ 因此 **B1(提 `timeout-minutes`)的主要收益是语义正确 + artifact 回来,不是「从没日志变成有日志」**,紧迫性比正文写的低一档。
>
> **更正 ②(我自己的推理,已被日志否掉)**:我在评论里让人「第一个查 `ff745e5c`(取消注册邀请码)」——**那条假设站不住**。实际失败项是 CopilotKit v2 的两条 spec、以及 chat-read webServer 的 `EADDRINUSE` 端口冲突,与 auth 无关;且**每轮失败的 spec 都不一样**。
>
> 两条更正的完整证据、取日志的现成命令、以及「HITL 三条已确认全绿可排除」,见 👉 [本 issue 的更正评论](https://github.com/boardx/workspacex/issues/2084#issuecomment-5416765193)
>
> 正文其余结论(红的起点 `2b23f83c`、100 次只真跑 3 次全红、main push 10/10 被 35 分钟超时掐掉、run 仍报 success)**未受影响**。

## 结论先说

**`e2e-full`(装着 chat 读写链路 + 自助资料链路的那个 job)在最近 100 次 `harness-verify` run 里只真正执行过 3 次,3 次全是 `failure`。它在这个窗口里一次都没绿过。**

而 main 上仍然在产出 `success` 的 run。**这块牌子宣称了它没验过的事。**

比"它没跑"更糟的是:**它是真红的**,而红被两种机制轮流遮住——一种让它压根不执行,另一种让它执行到一半被自己的超时掐掉,掐掉后的状态叫 `cancelled`,看起来像"有人手动取消了",是个人畜无害的词。

## 证据一:最近 100 次 run 里 `e2e-full` 的实际执行记录

扫描 `gh run list --workflow=harness-verify.yml --limit 100`,逐个取 `e2e-full` 的 conclusion,排除 skipped/cancelled:

```
EXECUTED: 32853288767 push 2026-08-25T13:25:59Z 0d660a87 -> failure
EXECUTED: 32799572287 schedule 2026-08-25T01:58:06Z 946414c0 -> failure
EXECUTED: 32796610895 push 2026-08-25T01:11:52Z f7dd964d -> failure
```

**只有这 3 条。其余 97 次全是 skipped 或 cancelled。**

最近一次真正跑完是 `13:25Z`(SHA `0d660a87`),红。之后再没执行过。

## 证据二:红在哪一步(`32853288767`,最近一次真执行)

```
7 Execute uncached trusted full gate success
8 Execute Chat read/write journey (own isolation scope) failure ← chat e2e 真红
9 Execute self-service profile journey failure ← 自助资料链路也真红
10 Upload E2E evidence success
```

⇒ **不是 flake,不是环境,是两条链路各自的红。** 步骤 7(`verify:full`)是绿的,红全在 8 和 9。

## 证据三:三种事件下 `e2e-full` 各自的命运(28 次连续 run)

| event | `e2e-full` 结果 | 成因 |
|---|---|---|
| `pull_request`(12/12) | **skipped** | 设计如此。gate 第一段 `event_name != 'pull_request'`,2026-08-05 人类指令「PR 时先不做以提高速度」 |
| `schedule`(6/8 success,2 cancelled) | **skipped** | 设计如此。gate 第二段只放行 `30 */3 * * *`;每小时的 `0 * * * *` 那条 cron 触发的 run,`e2e-full` 跳过 |
| `push`(main,**10/10**) | **cancelled** | ⚠ **不是设计如此** —— 见证据四 |

> ⚠ **更正一条转述**:被点名的 run `32885830166`(SHA `d0f1646d`,conclusion `success`)是 **`event=schedule`**,不是 main push run。它的 `e2e-full` 是**按设计 skipped**(每小时那条 cron)。`d0f1646d` 的 main **push** run 是另一个:`32882265840`,conclusion **`cancelled`**。同一个 SHA 上两个 run 讲了两个不同的故事,绿的那个来自一次根本不打算跑 e2e 的定时任务。

## 证据四:`push` 上的 `cancelled` 是 `e2e-full` 自己的超时,不是并发组、不是人

`harness-verify.yml` **没有任何 `concurrency` 块**(`grep -c concurrency` → `0`),所以不存在并发组掐它。

10 次 main push run 的 `e2e-full` 时长:

```
32889508894 push cancelled 40m 32880827343 push cancelled 40m
32887707125 push cancelled 40m 32878573934 push cancelled 40m
32886294796 push cancelled 38m 32863022873 push cancelled 40m
32885879855 push cancelled 40m 32862848683 push cancelled 40m
32882265840 push cancelled 40m
```

对照三次真跑完的:**34m / 30m / 34m**。

而 job 的预算是 **`timeout-minutes: 35`**。

⇒ **这个 job 需要 30-40 分钟,预算 35 分钟,它正坐在悬崖边上。** 超一点点 → GitHub 按超时掐掉 → conclusion 是 **`cancelled`** 而不是 `failure`(GitHub 对 timeout 的语义就是 cancel)。于是:

- 超时 ⇒ `cancelled` ⇒ 看起来像「有人取消了」,没人会去查;
- 没超时 ⇒ `failure` ⇒ 但最近一次是 `13:25Z`,之后再没有过。

**掐掉时 `Upload E2E evidence` 那步是 `skipped`**(见 `32889508894` 步骤 10)⇒ **连日志 artifact 都没有**。这就是它能红这么久没人发现的最后一环。

⚠ 顺带:即使被掐,步骤 8 因为带 `if: always()` **仍然跑了并且 failure**(`32889508894` 步骤 8)。也就是说 chat 链路的红在 `cancelled` 的 run 里也是可见的,只是没人会点进一个 `cancelled` 的 run 去看。

## 证据五:不是我 #2080 引入的

`#2080`(给 `backend-gates` 加 `pull_request` + `cancel-in-progress`)合入时间 **19:06:42Z**。排除三条:

1. 它改的是 **`backend-gates.yml`**,本 issue 说的是 **`harness-verify.yml`**,两个 workflow;
2. `harness-verify.yml` **没有 `concurrency` 块**,无从被掐;
3. 取消现象**早于**合入时间 —— `32880827343`(17:56Z)、`32878573934`(17:36Z)、`32862848683`、`32863022873`(14-15Z)全部在 19:06Z 之前,形态完全一致(40 分钟)。

另外 `#2080` 的 `cancel-in-progress` 表达式是 `${{ github.event_name == 'pull_request' }}` —— 在 main 的 push 上取值 `false`,结构上就不可能掐 main。

## 附带发现:readiness 证据的记录时点早于 chat 链路执行(影响有限,如实标注)

步骤 7 的脚本里:

```bash
TURBO_FORCE=true pnpm run verify:full …; status=${PIPESTATUS[0]}
if [ "$status" -eq 0 ]; then
pnpm exec tsx .harness/scripts/record-readiness-evidence.ts --phase 01 --kind e2e …
fi
```

`record-readiness-evidence --kind e2e` 在**步骤 7 成功时**就写下了,而 chat 读写链路是**步骤 8**、自助资料是**步骤 9**,都在它之后。⇒ 在 `32853288767` 那次,步骤 7 绿、8/9 红,但 `kind=e2e` 的 readiness 证据**已经写下了**。

**影响范围我核实过,没有想象中大**:该 manifest 只写进 CI workspace,`git ls-tree origin/main -- phases/phase-01-run-a-project/evidence/ci/` **为空**,仓库里没有跟踪这些文件,也没有相关提交回写 ⇒ **它不会污染仓库里的 readiness 账本**,只在该次 run 的 artifact 里。我把它记在这里是因为**命名与时点仍然是错的**(一个叫 `e2e` 的证据,在 e2e 的两条链路跑之前就生成了),将来若有人把它改成回写仓库,这就会变成真的假证据。

## 处置建议(不互斥,我倾向 A + B 一起做)

⚠ **本轮我一行都没改**——HITL 那条线(#2082)正在用 `workflow_dispatch` 跑 `e2e-full` 做同一热环境的对照实验,改触发条件/超时会把它的对照搅掉。等它跑完再动。

**A. 先修红本身(最高优先,且与触发拓扑无关)**
chat 读写链路与自助资料链路是真红,至少从 `2026-08-25T01:11Z`(`f7dd964d`)起连红 3 次。**在它变绿之前,讨论"让它在 main 上跑"没有意义——只会把一条已知的红搬到更显眼的地方。** 这条属于测试分层/chat 那条线的地盘(我不碰 spec 与 config),需要派工。

**B. 让 `cancelled` 不再冒充"无事发生"(我的地盘,成本最低)**
`timeout-minutes: 35` 对一个实测 30-40 分钟的 job 是**卡在中位数上的预算**,它把"超时"变成了常态而不是异常。两个方向:
- B1:把 `timeout-minutes` 提到 50-55,让它跑完 —— 那样红就是 `failure`,会正常显红,且 `Upload E2E evidence` 会执行、有日志可查。代价:main 上每次 push 多占一个托管 runner 约 40 分钟(公开仓库免费,不花钱,但拉长 main 的反馈链)。
- B2:保留 35 分钟预算,但加一步 `if: cancelled()` 把超时显式转成失败信号。代价:仍然没有 artifact。
⇒ **我推荐 B1**,因为 B2 只治标签不治"看不到日志"。

**C. 让"success"这块牌子诚实(结构性,需要人类裁决)**
现状是:一次 `e2e-full` 被跳过的定时 run,可以给出 `conclusion=success`。这在语义上没错(跳过的 job 不算失败),但**读的人会当成"main 全绿"**。可选形态:
- C1:把 `e2e-full` 从 `harness-verify.yml` 拆成独立 workflow,让它的绿/红有自己的徽章,不与 PR 门控的 success 混在一起(**我倾向这条**——它同时解决"一个 workflow 内不同 job 的语义被同一个 conclusion 概括"这个根问题);
- C2:保留现状,但在 README/徽章旁写清楚"此徽章不含 chat e2e"。**这是文档补丁,不是机械门控**,按本仓纪律「没有脚本的规范条目视为未落地」,它会再次腐烂,我不推荐。

**D. 不建议做的**:让 `e2e-full` 上 PR。2026-08-05 的人类指令明确把它摘出 PR 以提速,且它需要 30-40 分钟——现在也不是重新讨论这条的时机(先把 A 修了)。

## 与 #485 / #1656 的关系

同族但不同环节。#485/#1656 是「门在错误的时点开火」,PR #2080 已修**前半句**:门现在会在 PR 上开火。

⚠ **后半句仍然没有**:`main` 目前 `gh api .../branches/main/protection` → **404 Branch not protected**,没有任何 required check ⇒ **门会开火,但红了挡不住合并**。分支保护需要仓库管理员权限,agent 设不了,逐字 check 名单已交给人类(PR #2080 正文)。**在人类点完之前,#485/#1656 都不能写成「已解决」。**

本 issue 则是第三种形态:**门根本没执行,而承载它的 run 仍然报 success。**

—— coord-architecture

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading harness-verify.yml and reproduce the recent e2e-full job outcomes with gh run list and the referenced job logs. Trace the pull_request, schedule, and push conditions, timeout, cancellation behavior, and evidence upload; done means the selected behavior is decided and verified against subsequent runs without masking chat or self-service failures.

Written by the indexing model from the issue text.

Assessment

Tech stack
github-actions
Domain
ci-cd, developer-experience, testing-qa
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.