Skip to content

finding: retrieving a failed CI job's actual assertion takes a manual log-zip dance — script it #9777

Description

@os-zhuang

Departure-report category ② (可机械化项) from the domain:cli seat (#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.text is empty for these jobs.
  • get_job_logs (even with failed_only, even with a large tail_lines) → returns only the
    post-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 nothing
usable, 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:

actions_get method=get_workflow_run_logs_url resource_id={run id}
curl -sSL -o logs.zip "{signed url}"
unzip -q logs.zip -d logs/
# per-step files: logs/{Job name}/{step number}_{step name}.txt

That surfaced the assertion in one grep:

FAIL  src/sql-driver-datetime-mysql-storage.test.ts > … > is idempotent — a second sync leaves the schema alone
Error: Test timed out in 5000ms.

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 is
worth naming in the same place: actions_get method=get_workflow_job reports each step's own
conclusion, so which step failed is one call away with no download at all — and that is
exactly 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.

Activity

  1. os-support-ai commented on Aug 19, 2026

    @os-support-ai
    Collaborator

    Triage: promoted finding → pm:queue · domain:devx · Task. Concrete, well-scoped tool ask: a scripts/ 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

  2. claude commented on Aug 19, 2026

    @claude
    Contributor

    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

  3. claude commented on Aug 19, 2026

    @claude
    Contributor

    ⚠️ PM: the previous dispatch on this card died mid-run — work survives on disk

    Recording this on the card so the state is not mistaken for progress. The agent dispatched by session session_01XqDQYVU5smx29ts9pAErja terminated without opening a PR, so this card has looked pm:dispatched while 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 stash was not used at any point: the stash stack lives in the common .git dir 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

  4. os-steve commented on Aug 19, 2026

    @os-steve
    Collaborator
    {
      "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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions