Repository navigation
Commit 2919ddb
feat(pm/fleet-write): run_rerun_failed — re-run one ci.yml pull-request run's failed jobs, once, fenced in the relay (#25186)
Refs #25169
Clause-②: no
The fleet-write relay gains one fenced op, `run_rerun_failed`. It
re-runs only the failed jobs of one `ci.yml` pull-request run, once, and
only while that run's head is still its pull's current head, as
`objectstack-fleet[bot]`, through a relay-only seat wrapper,
`scripts/pm/rerun-failed.mjs`. `.github/workflows/fleet-write.yml` is
not edited: the existing stroke-scoped `permission-actions` input
carries the new op (ruling 3, evidence below). The card stays open,
because its last proof is the first real use on a genuine flake after
this merges. No real use was made here, since the relay runs main's
code.
## The row and its seven fences, with where each is enforced
`run_rerun_failed` (`scripts/pm/fleet-write/ops.mjs`) is one request,
`POST /repos/{repo}/actions/runs/{run_id}/rerun-failed-jobs`, with no
body. The platform's optional debug-logging flag is never sent. The
payload is `run_id`, a positive integer, and nothing else. A workflow,
attempt, job, event, ref or debug-logging key beside it is refused by
name at the validator.
| fence | what it requires | authoritative check | early, advisory check
|
|---|---|---|---|
| workflow | the run's `path` is in `RERUN_FAILED_WORKFLOWS`, a closed
list matched whole. Today it is exactly `.github/workflows/ci.yml`
(measured spelling on run 39713789453: no ref suffix). It is a constant
in `ops.mjs`, never a payload key. | relay executor, on the run it reads
just before the POST | wrapper, before the dispatch |
| event | `event` is `pull_request` (push, merge_group, schedule,
release, workflow_dispatch and pull_request_target are refused) | relay
executor, same read | wrapper |
| once | `run_attempt` is 1 | relay executor, same read | wrapper |
| failed | `status` is `completed` and `conclusion` is `failure` | relay
executor, same read | wrapper |
| head | the run's `head_sha` is the CURRENT head of its pull. The pull
is the run's `pull_requests[0].number`, and its head is read with that
pull's own `GET /repos/{repo}/pulls/{n}`, never from the copy the run
embeds. A run with no `pull_requests` entry (a fork pull request's run,
or the field empty) is refused by name. `ci.yml`'s concurrency group is
keyed by the pull number with `cancel-in-progress: true`, so a stale
head's re-run would cancel the current head's run. | relay executor, on
the pull it reads right after the run, the last request before the POST
| wrapper |
| alone | the stroke carries exactly that one action (`alone: true`) |
validator: at the seat's packer and again in the relay before any token
is minted | — |
| grant | `actions: write` is minted for that stroke alone | validator's
`permission_actions` output, read by the workflow's mint step | — |
The five run fences share one set of predicates, `RERUN_FAILED_FENCES` /
`rerunFailedFenceFailures` in `ops.mjs`; each takes the run and the
pull's own answer. The executor's `rerunFailedPreflight` reads attempt
1's job list first, as the read-back's baseline. It then reads `GET
/repos/{repo}/actions/runs/{run_id}` and, for the pull the run names,
that pull's `GET /repos/{repo}/pulls/{n}`, the last two requests before
the POST, and judges the five fences on those answers. A run that
changed between the request and the relay's run, for example one a
person re-ran meanwhile, or a pull pushed to meanwhile, is refused
there. A fence that fails, an unreadable run or pull, an unreadable
baseline or a baseline with no failed job stops the action with its POST
row `not sent`: zero writes, exit 5, every failing fence named with what
was read. The wrapper judges the same five fences earlier and names the
one that fails. That check is advisory and never replaces the
executor's.
## The token (ruling 3): the existing input carries it, so no workflow
edit
`tokenPermissionsOf` adds a stroke-scoped grant whenever an action's op
spends it. `run_rerun_failed` spends `actions`, so the validator writes
`permission_actions=write` for its stroke exactly as it does for
`workflow_dispatch`, and an empty value for every other stroke. The
workflow's `permission-actions: ${{
steps.validate.outputs.permission_actions }}` input then mints it
unchanged. `validate.mjs --self-test` proves this with these cases, in
the new battery:
- "⭐ …and the GitHub output spells permission_actions=write for its
stroke, exactly as for a workflow_dispatch stroke"
- "⛔ fence grant, refused side: every op spending no actions grant
writes an EMPTY permission_actions — each alone in its stroke, and the
stroke of them all that shares one token"
- "⭐ ruling 3, no workflow edit: fleet-write.yml mints
permission-actions from the validator's permission_actions output — the
one input this row rides — and names neither this op nor its path"
`.github/workflows/fleet-write.yml` is therefore untouched, and the PR
is not Tier H.
## What stays refused
`REFUSED_PATH_FAMILIES` carves out exactly
`actions/runs/{digits}/rerun-failed-jobs`. The catch-all reads
`/actions/(?!workflows/[^/]+/dispatches$|runs/\d+/rerun-failed-jobs$)`
and the run family reads `/actions/runs/(?!\d+/rerun-failed-jobs$)`.
`validate.mjs --self-test` proves each of the following is caught by a
refused family, and that a payload naming any of them as an op is
refused with zero writes:
- the whole-run `runs/{id}/rerun`;
- a job's `jobs/{id}/rerun`;
- cancel, force-cancel and delete (`runs/{id}`);
- logs and attempts;
- caches, artifacts, secrets and variables;
- a re-run path with anything around its run id
(`runs/x/rerun-failed-jobs`, `runs/1/rerun-failed-jobs/x`,
`runs/1/attempts/1/rerun-failed-jobs`).
The table now reaches exactly two `actions/*` paths: the dispatch call
and this re-run.
## The read-back
The 201 is an acceptance, not a landing. `confirmRerunFailed`
(`execute.mjs`) re-reads the run every 3 s for up to 60 s until it shows
`run_attempt` 2, then reads attempt 2's job list. The action lands only
on `rerunFailedReadBack` (`ops.mjs`): every attempt-1 failed job that
attempt 2 lists must be `queued` or `in_progress`, and at least one must
be. The row then names the new attempt and those job ids, for example
`attempt 2 · re-run: Test Core (3/6) job 119642047246 queued · not
listed yet: Test Core`. The read-back fails the action in three cases:
- it never sees attempt 2 inside the window: a FAILED action,
UNCONFIRMED, "do not re-send";
- a failed job is listed `completed` in attempt 2;
- the job list cannot be read.
One measured deviation from the ruling's wording: a failed job that
attempt 2 does not list YET is named, not failed. On run 39713789453,
attempt 2 listed the failed shard at once, `queued`, two seconds before
it started. The `Test Core` aggregate that needs every shard was listed
only 16 minutes later, when its need finished. Requiring every attempt-1
failure to be `queued` or `in_progress` at once would have failed every
real use on `ci.yml`.
## The usage line and where it lives
`.claude/skills/pm-dispatch/references/platform-readings.md` is the one
pm-dispatch reference the ruling's grep finds, and the line sits in its
re-run cluster:
> 经 `rerun-failed.mjs` 重跑 PR `ci.yml` 失败 job 仅一次,先在 PR 或卡写明 flake
理由;二败即真红。
The file's line budget is 120 bytes and its ceiling has zero headroom,
so the line names the tool bare, as that file names every tool. It is
paid for by deletion: it replaces the dated reading "读数 2026-09-13:席位
REST `/jobs/{id}/rerun` 403、MCP `rerun_failed_jobs` 201 ⇒ 重跑可做,锁 1 未禁。".
The same REST 403 / MCP 201 reading stands at the file's earlier line,
"读数:`POST /actions/runs/{id}/rerun-failed-jobs` REST 回 403 而 MCP 回
201。". `check:pm-skill-ratchet` stays green at 460 of 460 lines.
`.claude/agents/os-dev.md` is not touched: a dev never re-runs. The head
rule does not fit as well: the line measures 119 of its 120 bytes, so
the `head` fence alone carries it, in code.
## The wrapper, `scripts/pm/rerun-failed.mjs`
The wrapper follows `label-write.mjs`'s style:
- it reads the run and then its pull by the pull's own GET, and judges
the five fences (advisory, exit 2 naming each failing fence, nothing
sent); a pull it cannot read is a prerequisite refusal (exit 3);
- it names attempt 1's failed jobs;
- it packs ONE `run_rerun_failed` action and sends it through
`sendFleetWrite`;
- after a success run it reads attempt 2's job ids back as a reading,
never a verdict;
- after a failed run it re-reads the run and its pull and says what the
fences read now.
It is RELAY-ONLY. A direct route is a prerequisite refusal (exit 3) with
zero writes, and an accepted dispatch with no run is UNCONFIRMED (exit
6), never a direct write. `dispatch.mjs`'s no-run conformance now
discovers it as a sender and drives its `relayMissProbe`. That
conformance learns one named relay-only sender (`relayOnly = true`),
whose control refuses under `direct` instead of writing, with its reads
recorded. The set of relay-only senders is pinned by name, so no sender
can skip the write control unreviewed. The wrapper contains no write
verb of its own, so `write-pace`'s write-tool roster is unchanged.
## Governed tier
`node scripts/pm/check-governed-merges.mjs --pr 25186` reads GOVERNED,
**Tier S**, at head `49804f28cf`. The only governed path is
`.claude/skills/pm-dispatch/references/platform-readings.md`, and no
Tier H path is in the diff: the root `package.json` is not on the
register. The size is 1,408 changed lines, under the 3,000 human-merge
threshold. As the dispatch directs, the maintainer approves and merges
this PR. The seat readies it, which requests the maintainer's review; no
seat approves, queues or arms it.
## Gates
All local readings below were taken at head `49804f28cf`, the final
commit. Fix round 1 added two commits on `f2ee5ad67d`: `120ca3ca58` (the
`head` fence) and `49804f28cf` (the CI wiring). `git merge-tree
--write-tree origin/main HEAD` against `origin/main` `2069b81c26` is
clean (exit 0, a tree id alone), and none of main's five newer commits
touches a file this PR touches, so no merge was made.
- `node scripts/pm/dispatch-gates.mjs --commands` derived 59 families
from the seven changed paths; the root `package.json` adds eight. All 59
ran. On the fresh worktree, five first answered exit 3, PREREQUISITE NOT
MET: `check:doc-formula-expressions`, `check:dts-closure`,
`check:dual-build-cjs-loads`, `check:lean-entry-closure` and
`check:sourcemap-no-sources-content`. Each passed after the build it
declares, run under the verify lock: its two packages, then the whole
workspace (76 of 76 tasks). `--ran` reconciles: "59 derived famil(ies)
accounted for — 59 run, 0 NOT-MEASURED (a DERIVED zero — all 59 recorded
an exit code and none of them is 3)".
- `pnpm check:pm-fleet-write-validate`: 193 cases across 12 batteries.
The new battery has 33 cases (30 before the `head` fence), and the floor
rose from 11 to 12.
- `pnpm check:pm-fleet-write-execute`: 188 cases across 19 batteries.
The new battery has 21 cases (17 before the `head` fence), and the floor
rose from 18 to 19.
- `pnpm check:pm-fleet-write-dispatch` now runs `dispatch.mjs
--self-test` and then `rerun-failed.mjs --self-test`. The first: 247
cases across 22 batteries. The new seat battery has 6 cases, the
conformance adds the new sender and the relay-only roster, and the floor
rose from 21 to 22. The second: 31 cases across 5 batteries, its
seat-fence battery 11 (8 before the `head` fence). `lint.yml` already
runs this script, so the wrapper's self-test reaches CI with no workflow
edit.
- `pnpm check:pm-dispatch-gates` (fast tier) and every other derived
family: exit 0. That includes `check:pm-skill-ratchet`,
`check:pm-skill-id-lint`, `check:pm-write-pace`, `check:entry-guard`,
`check:self-test-wired`, `check:merge-driver`, `check:turbo-task-graph`
and `check:nul-bytes`.
- CI: the PR's check-runs are the record. Ablation: none, because the
diff is scripts, one npm script line and a doc line.
## Acceptance notes
- `.github/workflows/fleet-write.yml`'s comments (its header, the
validate step and the `permission-actions` input) still call
`workflow_dispatch` the ONE consumer of `actions: write`, and say the op
table has no re-run row. Ruling 3 kept the workflow out of this PR, so
those comments are now stale prose. Carrier: none.
- Fix round 1 wired `scripts/pm/rerun-failed.mjs --self-test` into the
root `check:pm-fleet-write-dispatch` script, which `lint.yml` already
runs, so it reaches CI with no workflow edit.
- `.claude/settings.json` has no allow rule for `node
scripts/pm/rerun-failed.mjs` or its `--use-env-proxy` form, while every
sibling write tool has both. A seat's first use may meet a permission
prompt. Carrier: none.
- `RERUN_FAILED_WORKFLOWS` is a list of paths, as the card spells it, so
it admits `.github/workflows/ci.yml` in any fleet repository the
envelope names. objectui has one. The sender gate still requires write
on that repository.
- Fix round 1 added the seventh fence, `head`, by the seat's ruling on
this PR. A stale head's re-run would cancel the current head's run,
because `ci.yml`'s concurrency group is keyed by the pull number with
`cancel-in-progress: true`. pm-dispatch SKILL.md's MCP exception already
requires checking the head first, so the relay path is no weaker than
the old one. The fence does not require the pull to be open; that was
not ruled. Carrier: none.
## 维护者速读(草稿)
- **改了什么**:中继新增一个只做「重跑失败任务」的操作。它只认 `ci.yml` 的 PR 运行,且只认 head 仍是该 PR 当前
head 的那次运行;只在首次运行失败后重跑一次,并且只重跑失败的 job。席位通过新脚本
`scripts/pm/rerun-failed.mjs` 调用,身份是
`objectstack-fleet[bot]`。pm-dispatch 参考文档补了一行使用规则:先写明为何判为
flake,再重跑,二次失败即真红。根 `package.json` 的 `check:pm-fleet-write-dispatch`
同时跑该脚本的自测,CI 由此覆盖它,工作流不改。
- **为什么改**:CI 遇到注册表或网络抖动时,席位此前没有合规的重跑手段,只能等维护者手点「Re-run failed
jobs」(#25155 的 run 39713789453 即是一例)。
- **风险与代价(含回滚)**:这一笔的令牌多带 `actions: write`,所以加了七道闸。工作流、事件、仅一次、已失败、head
为该 PR 当前 head 这五道由中继在执行时实判(head 用 PR 自己的 GET 读,不信 run
里嵌的副本);独占一笔与令牌只为这一笔铸造这两道由校验器判。其余 `actions/*` 路径(整跑重跑、单 job
重跑、取消、删除、缓存、制品、密钥、变量)仍全部拒绝,有自测钉住。工作流文件未改。回滚只需还原本 PR,无数据迁移。
- **席位意见**:
- **你要做的**:审阅并批准、合并本 PR(Tier S,按派发令由维护者合)。合并后,首次真实 flake
由席位按新规则跑一次,并在卡上回读 attempt 2。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01FM29ZqLTrnPNXxijioZwMp)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 9057fd4 commit 2919ddb
7 files changed
Lines changed: 1353 additions & 55 deletions
File tree
- .claude/skills/pm-dispatch/references
- scripts/pm
- fleet-write
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
289 | 289 | | |
290 | 290 | | |
291 | 291 | | |
292 | | - | |
| 292 | + | |
293 | 293 | | |
294 | 294 | | |
295 | 295 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
96 | 96 | | |
97 | 97 | | |
98 | 98 | | |
99 | | - | |
| 99 | + | |
100 | 100 | | |
101 | 101 | | |
102 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
241 | 241 | | |
242 | 242 | | |
243 | 243 | | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
244 | 254 | | |
245 | 255 | | |
246 | 256 | | |
| |||
301 | 311 | | |
302 | 312 | | |
303 | 313 | | |
304 | | - | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
305 | 321 | | |
306 | 322 | | |
307 | 323 | | |
| |||
412 | 428 | | |
413 | 429 | | |
414 | 430 | | |
| 431 | + | |
| 432 | + | |
415 | 433 | | |
416 | 434 | | |
417 | 435 | | |
| |||
1443 | 1461 | | |
1444 | 1462 | | |
1445 | 1463 | | |
| 1464 | + | |
1446 | 1465 | | |
1447 | | - | |
| 1466 | + | |
| 1467 | + | |
1448 | 1468 | | |
1449 | 1469 | | |
1450 | 1470 | | |
| |||
2201 | 2221 | | |
2202 | 2222 | | |
2203 | 2223 | | |
| 2224 | + | |
| 2225 | + | |
| 2226 | + | |
| 2227 | + | |
| 2228 | + | |
| 2229 | + | |
| 2230 | + | |
| 2231 | + | |
| 2232 | + | |
| 2233 | + | |
| 2234 | + | |
| 2235 | + | |
| 2236 | + | |
| 2237 | + | |
| 2238 | + | |
| 2239 | + | |
| 2240 | + | |
| 2241 | + | |
| 2242 | + | |
| 2243 | + | |
| 2244 | + | |
| 2245 | + | |
| 2246 | + | |
| 2247 | + | |
2204 | 2248 | | |
2205 | 2249 | | |
2206 | 2250 | | |
| |||
2410 | 2454 | | |
2411 | 2455 | | |
2412 | 2456 | | |
2413 | | - | |
| 2457 | + | |
2414 | 2458 | | |
| 2459 | + | |
| 2460 | + | |
| 2461 | + | |
| 2462 | + | |
| 2463 | + | |
2415 | 2464 | | |
2416 | 2465 | | |
2417 | 2466 | | |
| |||
2448 | 2497 | | |
2449 | 2498 | | |
2450 | 2499 | | |
2451 | | - | |
| 2500 | + | |
| 2501 | + | |
2452 | 2502 | | |
2453 | 2503 | | |
2454 | 2504 | | |
| |||
2463 | 2513 | | |
2464 | 2514 | | |
2465 | 2515 | | |
| 2516 | + | |
2466 | 2517 | | |
2467 | 2518 | | |
2468 | 2519 | | |
| |||
2472 | 2523 | | |
2473 | 2524 | | |
2474 | 2525 | | |
2475 | | - | |
| 2526 | + | |
| 2527 | + | |
| 2528 | + | |
| 2529 | + | |
| 2530 | + | |
2476 | 2531 | | |
2477 | 2532 | | |
2478 | 2533 | | |
| |||
0 commit comments