Repository navigation
finding: retrieving a failed CI job's actual assertion takes a manual log-zip dance — script it #9777
Description
Activity
os-support-ai commented
on Aug 19, 2026 CollaboratorMore actionsTriage: promoted
finding→pm:queue·domain:devx· Task. Concrete, well-scoped tool ask: ascripts/helper that takes a repo/run-or-job reference and prints the failed job's actual assertion text (wrapping the log-zip dance), so agents stop paying the manual sequence per CI failure. Size suggestion: S–M, mechanical.本评论来自分诊座位 Routine (session
session_018euHiF12oV8iSfYdjVioUF).
Generated by Claude Code
Claiming this card.
- Session:
session_01XqDQYVU5smx29ts9pAErja - Branch:
claude/issue-9777-ci-failure-retrieval - Container & model: M, mode:subagent, model: opus
Starting with ruling 1: measuring what the Checks / Actions REST endpoints actually return
from this container before designing anything.Generated by Claude Code
Generated by Claude Code
- Session:
⚠️ PM: the previous dispatch on this card died mid-run — work survives on diskRecording this on the card so the state is not mistaken for progress. The agent dispatched by session
session_01XqDQYVU5smx29ts9pAErjaterminated without opening a PR, so this card has lookedpm:dispatchedwhile nothing was moving.Measured state of
claude/issue-9777-ci-failure-retrieval:- pushed to origin: yes
- own commits vs
origin/main: (none) (0 commits, 1 new file) - PR: none was ever opened — which is why it was invisible in the PR list
Any uncommitted files have been preserved to
/home/user/dead-run-salvage/9777/(tracked.patch+ verbatim copies of untracked files). Nothing was lost, and nothing was committed on the dev's behalf — the PM does not write code.⛔
git stashwas not used at any point: the stash stack lives in the common.gitdir and is shared across every worktree of this repo, so salvaging through it would have handed this work to whichever agent popped next.Re-dispatching now as a resume, not a restart.
Generated by Claude Code
- added a commit that references this issue
on Aug 19, 2026 { "issue": 9777, "status": "done", "branch": "claude/issue-9777-ci-failure-retrieval", "pr": "https://github.com/objectstack-ai/objectstack/pull/9897", "premise_still_valid": true, "summary": "RULING-1 VERDICT ON THE SALVAGED FILE: near-complete but NEVER RUN LIVE — 996 lines, offline self-test green, and it had never completed a single walk from this container. Its judgment layer (supersession, annotation classification, the pre/post step-numbering gap, roster-vs-shard, the exit table) is measured, correct and now kept intact. What it was missing was transport: node 22's fetch ignores HTTPS_PROXY, so the placeholder token in GITHUB_TOKEN went straight to GitHub and every read answered 401 — the file's own probe then reported PREREQUISITE NOT MET (exit 3) and looked right doing it. A live run now re-execs itself once with --use-env-proxy, reusing check-governed-merges.mjs's exported proxyRearmPlan rather than re-deriving it. I also verified the file's central design claim independently: the Actions log endpoints redirect to productionresultssa{N}.blob.core.windows.net and results-receiver.actions.githubusercontent.com, both answering 403 on CONNECT (measured again today, and the storage-account numeral varies per run, so the constant is now a pattern plus its specimens). /root/.ccr/README.md classifies that as an organization egress denial and says to report rather than route around it, so building on annotations instead of the log archive is correct and mandated. Second change, declared: check-governed-merges.mjs's --self-test trigger had no invokedDirectly guard, so importing it ran its 77 assertions inside the importer's own self-test and put an unrelated file's failures on the importer's exit code — guarded exactly as line 810 of that same file already is. No merge-blocking gate was added (ruling 2); no changeset (ruling 4), skip-changeset label applied and read back as [size/xl, skip-changeset]; no governed surface touched (ruling 3); REST only, no GraphQL (ruling 5). H2: the tool now gives THREE answers, not two — RETRIEVED (file-anchored annotation), NO FILE-ANCHORED ASSERTION but N annotations with content (a gate's own sentence is often the whole answer), and NONE (only 'Process completed with exit code 1.', which is content-free by construction). Only the last is 'could not be retrieved', and it prints the failing step name resolved offline against .github/workflows/ to the command that reproduces it locally. H3: NOT stale — grep across scripts/ and .claude/ for check-runs / annotations_url / actions/runs found only check-required-contexts.mjs, which is a job-NAME pin, not failure retrieval; nothing in scripts/pm/ does any part of this. H4: the tool reads NO history — the only git call is `git rev-parse HEAD` — so the 63-commit shallow clone cannot mislead it; that is now stated in the header, with the condition under which the paragraph retires.", "tests": "All at final commit f596a6517, clean tree, after the last commit. GATES (union re-derived from the ACTUAL diff with `node scripts/pm/dispatch-gates.mjs scripts/pm/ci-failure.mjs scripts/pm/check-governed-merges.mjs`, which ADDED check:pm-governed-merges over the dispatch's named list because of the second file): `node scripts/pm/ci-failure.mjs --self-test` PASS · `pnpm check:pm-governed-merges` PASS (77 assertions) · `pnpm check:cross-package-test-inputs` PASS ('OK: 12 package(s) read outside themselves, all declared') · `node scripts/check-nul-bytes.mjs` PASS (6272 files, no raw control bytes) · `npx eslint scripts/pm/ci-failure.mjs scripts/pm/check-governed-merges.mjs --no-inline-config` PASS. Bypassed git hooks on the commit and then ran both by hand: check-regen-pending exit 0, pre-push exit 0. H1 — REAL OUTPUT FROM A REAL FAILED JOB, verbatim, `node scripts/pm/ci-failure.mjs --sha 9e156c12` (exit 2): 'VERDICT: UNDETERMINED — assertion text retrieved for 1 of 3 failing check(s) ... ✗ Dogfood Regression Gate (1/3) (check-run 95973712843 · run 32221831879) / steps 15 real + 4 post — COMPLETE (the #15 to #28 jump is the runner's reserved post-step block, not a truncation) / failed step #10 Boot example apps and exercise real user flows / annotations 4 fetched from /check-runs/95973712843/annotations / · packages/qa/dogfood/test/showcase-demo-personas-loginable.dogfood.test.ts:124 / Error: verify signIn failed: 401 {\"message\":\"Invalid email or password\",\"code\":\"INVALID_EMAIL_OR_PASSWORD\"} / · packages/qa/dogfood/test/showcase-demo-personas-loginable.dogfood.test.ts:113 / AssertionError: phone.demo@example.com holds a credential account: expected undefined to be truthy / assertion RETRIEVED (above, the file-anchored annotation(s))'. H2 leg 1 (no assertion at all), `--run 32221018050` (exit 2): '✗ Lint & Repo Gates ... failed step #62 Engine test-double contract gate / annotations 1 fetched ... · exit-status / Process completed with exit code 1. / assertion NONE — the check carried no annotation with any content in it. ... substitute run the failing step locally — it prints the same assertion: from .github/workflows/lint.yml, step \"Engine test-double contract gate\" (run:) pnpm check:engine-double-contract'. H2 leg 2 (content but no anchor), same walk as H1: '✗ Check Changeset ... · other / This PR adds no changeset. ... / assertion NO FILE-ANCHORED ASSERTION — but this check carried 1 annotation(s) with content, printed above.' Roster-vs-shard leg, `--run 32196793380`: the shard job 'Test Core (3/3)' ranks above the roster job 'Test Core', which is labelled 'ROSTER JOB — it verifies other jobs' attestations and holds no assertion of its own'. Supersession leg, `--run 32211335618`: '26 check-run(s) after grouping by name (32 rows on the sha, 6 superseded)' and the verdict is UNDETERMINED because 1 check is still running — not-red-yet is not green. Dogfooded on its own PR, `--pr 9897`: '20 check-run(s) after grouping by name (23 rows, 3 superseded) ... 2 check(s) still running' — exit 2, not 0. TRANSPORT MEASUREMENT (the falsifying one): same URL, same env — curl 200 with x-ratelimit-remaining 14951; node fetch with no flag and the env token 401; node fetch with NODE_OPTIONS=--use-env-proxy 200 with 14951. LOG-BLOB MEASUREMENT: `GET /repos/objectstack-ai/objectstack/actions/jobs/95971357788/logs` returns 302 to productionresultssa1.blob.core.windows.net, and following it gives 'curl: (56) CONNECT tunnel failed, response 403'.", "open_questions": [ { "question": "This repo's convention for scripts/pm/* tools is that their --self-test runs as an unconditional step inside the existing 'Lint & Repo Gates' job (dispatch-gates, half-states, the part-of guard and the single-claim guard all do). Ruling 2 kept ci-failure's self-test out of it, so nothing in CI runs it and it can rot unnoticed. That tension is a maintainer call, not mine.", "options": [ "A — leave it out, as ruling 2 directs: it is a tool, not a gate, and a rotted self-test on a hand-run tool costs one agent one round.", "B — add one step to the existing Lint & Repo Gates job (no new required context, no new check name), matching the four sibling tools already there.", "C — run it from a non-required workflow so the signal exists without blocking merges." ], "recommendation": "A for this PR — ruling 2 is explicit and I did not override it. B is what I would ask for next, as a separate one-line card: it adds no new required context (the check name is unchanged), it is what the four sibling PM tools already do, and the failure it prevents is precisely the one this card is about — a retrieval tool that silently stops working. Deliberately left as a question rather than shipped." } ], "out_of_scope_findings": [ "fixed in-place under the bounded exemption, not filed: scripts/pm/check-governed-merges.mjs ran its own --self-test as an IMPORT side effect (no invokedDirectly guard on the trigger at the file's end, unlike line 810 on its main entry). My change created the exposure by importing it, the correct shape is pinned by that same file's line 810, no open PR touches scripts/pm/, and it sits in the same gate family (check:pm-governed-merges, run green both directions). Declared in the PR body." ] }
Generated by Claude Code
- added a commit that references this issue
on Aug 23, 2026
Departure-report category ② (可机械化项) from the
domain:cliseat (#6024). ⛔ Unassigned,⛔ no
domain:*label — this seat does not produce routing.The measured problem
When a required check goes red, the two obvious tools do not carry the failing assertion:
get_check_run→output.textis empty for these jobs.get_job_logs(even withfailed_only, even with a largetail_lines) → returns only thepost-step service-container dump, because the container teardown output is what sits at the
end of the log. The test output is thousands of lines earlier.
Measured on PR #9774's
Temporal Conformance (live PG + MySQL)failure: both returned nothingusable, and the seat post carried "the failing assertion could not be retrieved" as a fact for
several hours. It was retrievable the whole time — via the run's log archive:
That surfaced the assertion in one grep:
Why a script, not a paragraph
The cost is not the knowledge, it is the five-step sequence and its two sharp edges: the signed URL
is short-lived (mint and download in the same minute), and the archive is ~4MB of ANSI-coded text
that has to be de-escaped before it reads. Every PM and dev who hits a red required check pays this
again, and the failure mode when they do not is worse than paying it — it is concluding "the
assertion is unavailable" and then either guessing at the cause or re-running the job, which is how
a real gate defect turns into folklore.
Suggested shape
scripts/pm/fetch-job-logs.mjs {run id} [job name substring]→ downloads, extracts, strips ANSI,prints the failing step's tail (or the whole step with
--full). ⭐ The complementary cheap call isworth naming in the same place:
actions_get method=get_workflow_jobreports each step's ownconclusion, so which step failed is one call away with no download at all — and that isexactly the discriminator the "re-run only when no test body ran" rule turns on.
⛔ Shape is a suggestion, not a spec; sizing, ownership and whether this belongs in
scripts/pm/at all are the receiving seat's call.