Skip to content

fix(ci-status): leave no red yielded sibling after a full run succeeds - #728

Merged
kyle-sexton merged 4 commits into
mainfrom
fix/ci-status-late-yield
Oct 10, 2026
Merged

kyle-sexton merged 4 commits into
mainfrom
fix/ci-status-late-yield

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Related

Closes #726

Fix

With yield-to-full-run, a contract-only run could stay red after the full run succeeded, with nothing left to re-run it. This PR closes the two paths that caused that.

  • Late sibling. A contract-only run that reads the full run's ci-lanes=success while that run's aggregate step is still running fails superseded. If the full run has already done its last sibling scan, nothing re-runs it. Now, when the newest status is a success written by the current attempt of the only full run in flight, and that step is still running, the yielding run re-lists and re-reads every 5 s for up to 30 s. It passes once the step reads back success. A running step still cannot prove it wrote the status (another workflow could have posted it), so nothing passes sooner. A pending marker, failure, error or missing status still fails at once, and a failed status read or jq parse fails closed.
  • Re-run attempt misread as a full run. When the full run re-runs a failed contract-only run, the new attempt may list only its queued gate job at first. A yielding run counted it as a full run and failed superseded by a run that never re-runs anything. A re-run keeps its event, so a re-run attempt is now classified by its first attempt's jobs (GET /repos/{owner}/{repo}/actions/runs/{run_id}/attempts/1/jobs). If that read fails, the run still counts as a full run.
  • The full run's re-run scan now logs the failed runs it found and why it skipped each one it did not re-run.

Why 30 s. The wait is a fixed constant, not an input. It stays below the default rerun-wait-seconds (90 s) on purpose. If the full run is waiting on this run before it re-runs siblings, both would otherwise wait on each other. Because this run gives up first, it fails, and the full run then re-runs it. It also fits well inside the 3-minute timeout-minutes the README recommends for the ci-status job, so the fail-closed error is reported before the job times out. The time taken after the full run writes the status is normally a few API calls.

Callers without yield-to-full-run behave as before. The harness gains cases for both paths, the 30 s limit, a pending marker, and status read and parse failures.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TRhsMPSTMFo99ik7gkdgQt

kyle-sexton and others added 3 commits October 10, 2026 17:00
A yielding contract-only run could stay red with nothing left to re-run it
in two ways:

- It read the full run's success while that run's aggregate step was still
  running, after its last sibling scan, and failed superseded. It now
  re-lists and re-reads every 5s for up to 30s until that step finishes,
  then carries the success once the step reads back success. A running step
  still never proves the status, so nothing passes sooner.
- It counted a contract-only sibling's re-run attempt, whose new jobs were
  not listed yet, as a full run. A re-run keeps its event, so such an
  attempt is now classified by its first attempt's jobs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhsMPSTMFo99ik7gkdgQt
The full run's re-run scan now names the failed runs it found and, for each
one it passes over, why, so a scan that re-runs nothing shows what it saw.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhsMPSTMFo99ik7gkdgQt
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhsMPSTMFo99ik7gkdgQt
@github-actions

Copy link
Copy Markdown

PR body contract — issue linkage

This PR body does not yet satisfy the issue-linkage contract:

  • Missing a "## Summary" section. Describe what this PR changes and why, in a sentence or two.
  • Missing a "## Verification" section. Record concrete evidence the change works (commands, gates, output).

Edit the body and this comment updates itself on the next run.

@kyle-sexton
kyle-sexton marked this pull request as ready for review October 10, 2026 21:21
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TRhsMPSTMFo99ik7gkdgQt
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-10T21:24:40.509134Z 6093ca0 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@claude

claude Bot commented Oct 10, 2026

Copy link
Copy Markdown

Claude Code is working…

I'll analyze this and get back to you.

View job run

1 similar comment
@claude

claude Bot commented Oct 10, 2026

Copy link
Copy Markdown

Claude Code is working…

I'll analyze this and get back to you.

View job run

@claude

claude Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 7s —— View job


Security review in progress

  • Gather context (PR diff, changed files)
  • Review aggregate-results/action.yml and run.sh for security-relevant logic (injection, trust boundary, token handling)
  • Check run.test.sh and README.md changes for consistency with logic
  • Check for instruction-surface deletions (AGENTS.md / rules files) — none present in this diff
  • Independent hunter/verifier pass on the merge-gate logic (running)
  • Post findings

This diff changes the ci-status merge gate's aggregate-results logic, so it's security-relevant by nature: it decides when the required check goes green. My own read of run.sh found the fail-closed invariants (status writer verification, re-run first-attempt classification, the new 30s yield wait) intact, but I've dispatched an adversarial hunt specifically against the two riskiest new pieces before concluding.

@claude

claude Bot commented Oct 10, 2026 •

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 6m 36s —— View job


Claude's review of PR #728

  • Gather context (diff, changed files, REVIEW.md/CLAUDE.md)
  • Review aggregate-results/run.sh logic changes
  • Review aggregate-results/action.yml changes
  • Review run.test.sh new test coverage
  • Review README.md changes for accuracy
  • Adversarial verification of candidate findings
  • Post findings

No blocking or high-confidence findings. Per REVIEW.md, this repo runs a dedicated security lane, so this pass was scoped to correctness, design, and test coverage.

What I verified by tracing the implementation against the fixtures in run.test.sh (sandbox permissions didn't let me execute the suite, so this is static tracing, not an independent test run):

  • The late-sibling wait (run.sh#L820-861). carried_writer_running is only set inside verify_writer_step when the raw status read is success and that run's step is still running, so a plain pending marker from record-pending (not from an in-flight writer) correctly skips the wait and fails superseded at once — confirmed against the "pending marker newer than the success" case.
  • Worked through the expect_status_reads 2 case line by line: list_in_flight_full_runs and verify_writer_step both hit GET .../runs/4000/jobs, so each wait iteration consumes two slots of the .N.json fixture sequence, not one. With .1.json/.2.json both running and the base fixture success, iteration 1 consumes slots 1–2 (both running, so it sleeps) and iteration 2 consumes slots 3–4 (both now falling back to the success base), matching exactly 2 status reads and 1 logged sleep. This lines up correctly — not a bug.
  • Re-run-attempt misclassification fix (run.sh#L776-791): the fallback to attempts/1/jobs only fires when run_attempt > 1 and the latest attempt's shape is inconclusive, and a re-run of a genuinely full run is still caught because attempt 1's job shape reflects the real topology. The "full run" and "unreadable first attempt" cases both correctly fail closed (counted as full).
  • The 30s ceiling math (YIELD_WRITER_WAIT_SECONDS / YIELD_WRITER_POLL_SECONDS) checks out against the "fails superseded at the ceiling" case (6 × 5s sleeps = 30s total).
  • action.yml and README.md prose both accurately describe the new wait and the re-run-attempt classification; no drift from the implementation found.

Nothing here rises to a reportable finding under the high-signal bar (no incorrect logic, no untested gap with a concrete failure scenario, no security/observability/atomicity issue per REVIEW.md's always-check list).

@github-actions

Copy link
Copy Markdown

claude-security-review has reviewed this pull request through 0ee421e; a later push is reviewed from there.

@github-actions

Copy link
Copy Markdown

claude-review has reviewed this pull request through 0ee421e; a later push is reviewed from there.

@kyle-sexton
kyle-sexton merged commit 594987a into main Oct 10, 2026
29 checks passed
@kyle-sexton
kyle-sexton deleted the fix/ci-status-late-yield branch October 10, 2026 21:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci-status yield: a contract-only run that goes red after the full run's re-run scan is never re-run

1 participant