Skip to content

aggregate-results: contract-only ci-status fails closed when the jobs API lags step data beyond the 15 s retry #883

Open

Description

@kyle-sexton

Problem

A contract-only ci-status run (a pull_request event such as edited, labeled or unlabeled) fails closed even though a full run on the same head recorded ci-lanes=success:

##[warning]could not identify this run's gate job and running step in repos/melodic-software/claude-code-plugins/actions/jobs/<job id>: job 'ci-status' has no in-progress step named 'Aggregate lane results' after 5 reads. Cannot verify the step that wrote the carried status, so this run fails closed.
##[error]no successful ci-lanes status on <sha>; ...

The passing full run then re-runs the contract-only run, which fails the same way, so the newest ci-status on the head stays red. On 2026-10-24 this happened on every re-run attempt of the same run. This is the same message as closed #743, whose fix (#814) added a retry of at most 15 s. Today the jobs API's step data lagged by minutes, so that retry window does not cover it.

Seen in claude-code-plugins at the v0.58.0 pin (5e18f3d6). aggregate-results has not changed between 5e18f3d6 and origin/main (v0.65.7): git diff --stat 5e18f3d6 origin/main -- .github/actions/pr-require-checks/ prints nothing. A re-pin will not fix it.

Evidence

All times are 2026-10-24, UTC. All runs are in melodic-software/claude-code-plugins.

Step data in the jobs REST API (GET /repos/{owner}/{repo}/actions/jobs/{id}) lagged by minutes after the jobs finished:

Job Completed Read steps
120933784217 (attempt 1) 17:09:53 about 17:30 8 entries, 1 to 6 and 16 to 32 missing, steps 8, 10 and 15 still in_progress, step 33 pending, no Aggregate lane results
120933784217 17:09:53 about 17:33 all 21 entries, step 17 Aggregate lane results completed/failure
120935337859, 120935904644, 120937305912, 120935551814, 120936549574 (re-run attempts) about 17:30 []
120937212521 (the writer, the full run's ci-status, success) 17:24:58 17:34:01 []

Earlier the same day, re-run attempts listed all 21 steps (ci-status jobs 120920484811 and 120920481528 of runs 40110078170 and 40110077030, 16:07).

https://www.githubstatus.com listed no incident for 2026-10-24 when checked. The logs API returned HTTP 502 several times during this investigation.

Root cause

Line numbers below are in .github/actions/pr-require-checks/aggregate-results/run.sh at 5e18f3d6.

  1. The own-step lookup needs the step listed as in progress within 15 s.
    • run.sh:535-545 resolves the gate job by job.check_run_id. It also requires exactly one step of that job with .name == $step and .status == "in_progress".
    • run.sh:498 caps the retries at OWN_GATE_RETRY_DELAYS='1 2 4 8' (15 s, 5 reads).
    • When the jobs API serves a stale or empty steps array, every read misses and run.sh:585 warns.
    • verify_writer_step (run.sh:608-613) then fails closed and drops the carried ci-lanes=success.
    • The job identity is already proven at this point: the job has the right REST id and run id and is in_progress. Only the step half fails.
  2. The writer-step check depends on the same step data. If the own step resolved, gate_step_outcome (run.sh:652-656) would read the writer job's steps. Writer job 120937212521 listed [] nine minutes after it completed. That gives absent, which run.sh:635-638 treats as still running. The run marks the success pending and fails closed at the end of its wait. A fix to part 1 alone would move the failure here.
  3. Why it loops. The full run re-runs the failed contract-only run right after it writes ci-lanes. That is when the step data for both jobs is most likely to lag, so the re-run fails the same way.

UNVERIFIED: why GitHub served stale or empty steps today. How fresh step data is in the jobs API is undocumented (see the Evidence section of #743). The lag shown here is observed, not explained.

Proposed fix

These are options for the maintainers. The order is a suggestion.

  1. Own step: when the job is keyed by job.check_run_id and passes the id, run id and in_progress predicate, take STEP_NAME (the step-name input) as the step name without requiring it to be listed. Keep the in-progress-step requirement only for the runner-name fallback (no check_run_id) and for the case where step-name is unset. This removes failure 1 with no new API calls.
  2. Writer step: separate "the writer job is completed and its steps is empty" (lagging data) from "the step is absent from a populated list".
    • (a) For the empty case, wait longer within the job's timeout, then fail closed. The cost: today's lag (over 9 min) can exceed any wait that fits.
    • (b) Verify the writer from a source that does not depend on step data. For example, the full run could record its verdict where a contract-only run can read it and an unrelated workflow cannot forge it. The cost: a new mechanism, which needs design.
    • (c) Accept the writer job's conclusion == success when steps is empty. The cost: this conflicts with the design note at run.sh:596-598. That note says a full run whose contract check failed still records a true lanes success, so the job conclusion is not the lanes verdict. It would need a guard against a writer run with a failing job.
  3. Re-run timing: when the full run re-runs a failed contract-only sibling, it could delay the re-run until its own gate step reads completed in the jobs API. This narrows the loop but does not close it.

Add run.test.sh cases for:

  • a job whose steps is []
  • a job whose steps list omits the running step
  • a completed writer job whose steps is []

Related

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q9bziHugSS2AXBhM2ryQff

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 24, 2026
  2. kyle-sexton commented on Oct 24, 2026

    @kyle-sexton
    MemberAuthor

    The same stale step data also fails a second check, outside this action. In claude-code-plugins .github/workflows/pr-require-checks.yml, the step "Require both review lanes on this head" counts a review lane only when any($s.steps[]?; .name == "Apply the hold" and .conclusion == "success") (line 4084 on main). When GitHub returns an empty steps list, a passing lane reads as uncounted and ci-status fails with "its 'Apply the hold' step did not run".

    Evidence: review status job 120939956686 (claude-code-plugins#9658) concluded success and still listed steps=0 at about 17:50Z, after job 120937212521 had recovered at 17:45Z. A fix here should cover both places, or the caller should treat an empty step list on a completed job as data not yet available rather than as a missing step.

    🤖 Generated with Claude Code

    https://claude.ai/code/session_01Q9bziHugSS2AXBhM2ryQff

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageNot yet classified. Floor until a type and one priority tier are set.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions