Skip to content

fix(opencode): stop two echo-only jobs from serializing the review critical path - #1910

Merged
seonghobae merged 2 commits into
mainfrom
fix/opencode-review-serial-echo-job-chain
Sep 5, 2026
Merged

fix(opencode): stop two echo-only jobs from serializing the review critical path#1910
seonghobae merged 2 commits into
mainfrom
fix/opencode-review-serial-echo-job-chain

Conversation

@seonghobae

Copy link
Copy Markdown
Contributor

The problem

opencode-review.yml chained five jobs in series:

required-workflow-bootstrapadmit-current-headcoverage-source-treecoverage-evidenceopencode-review-target

The middle two exist only to hold branch-protection contexts. Each one's entire body is a single echo, and neither declares outputs: — so both needs: edges through them ordered work without carrying any data.

Ordering is not free here. A job is not created until its needs: complete, so under a saturated queue every link waits out the whole queue again. Measured on naruon#1528 (run 33581213805), where each job's created_at equals the previous job's completed_at — which is what proves the mechanism:

job queue wait actual runtime
required-workflow-bootstrap 7h57m 4s
coverage-source-tree 9h40m 4s
coverage-evidence 13h01m 5s
opencode-review 12h13m

That is roughly 22h41m of queue time spent to print two sentences, with the actual review held behind it. Multiplied across every PR in the 76 repositories the ruleset injects this workflow into.

The change

Both echo jobs now depend on admit-current-head directly. Serial depth 5 → 3; queue waits per PR 4 → 2. No job is removed and no context stops reporting.

Safety, verified rather than assumed

  • coverage-evidence had no if: — it relied entirely on transitive skipping through coverage-source-tree. Its admission gate is now stated explicitly, so an unadmitted head still skips it. Dropping the edge without this would have run a required context on unadmitted heads; this was the real hole in the first draft of the idea.
  • opencode-review-target never reads coverage-evidence at runtime. The only reference was the needs: line itself. The actual consumer is opencode-review-dispatch.yml via scripts/ci/opencode_coverage_identity.py, which resolves the context against the check-runs API on its own schedule and is therefore order-independent by construction.
  • No test asserts this ordering. scripts/ci/test_strix_quick_gate.sh:1203 names both jobs, but as set membership (checks fast-approval ignores), not sequence.
  • Branch protection evaluates required contexts independently. required-workflow-bootstrap, coverage-evidence and opencode-review all still report.

Scope limit — deliberate, and load-bearing

Changes are confined to opencode-review.yml. opencode-review-dispatch.yml has jobs of the same two names whose edge is a genuine data dependency and must not be parallelized: its coverage-source-tree (timeout-minutes: 12) uploads the materialized PR merge tree, and its coverage-evidence (timeout-minutes: 300) downloads it. Verified directly: upload-artifact=True / download-artifact=True respectively. Breaking that edge would silently destroy coverage measurement.

Provenance

The echo-only jobs were first surfaced by an independent Codex audit run against the repo with no knowledge of my earlier conclusion — my own analysis in #1905 had wrongly classified them as load-bearing gate jobs by pattern-matching the documented changed-scope design without opening them. A peer session then measured the queue cost and caught the same-name collision with the dispatch workflow. #1905's "3% of slots, not worth shipping" reasoning is superseded by this data: slot count was the wrong dimension — critical-path latency is the one that matters.

Test plan

  • Full suite on this branch: 2883 passed, 1 skipped, 21 subtests
  • Resolved job graph re-parsed after the edit: max serial depth 3, all three required contexts still present, admission if: on all three gated jobs
  • git diff --stat confirms a single file changed (+22/−2)

🤖 Generated with Claude Code

…itical path

opencode-review.yml chained five jobs in series: required-workflow-bootstrap ->
admit-current-head -> coverage-source-tree -> coverage-evidence ->
opencode-review-target. The middle two exist solely to hold branch-protection
contexts; each one's entire body is a single `echo`, and neither declares
`outputs:`, so both `needs:` edges through them ordered work without carrying
any data.

Ordering is not free. A job is not created until its `needs:` complete, so under
a saturated queue every link waits out the whole queue again. Measured on
naruon#1528 (run 33581213805), where each job's created_at equals the previous
job's completed_at:

  required-workflow-bootstrap  waited 7h57m, ran 4s
  coverage-source-tree         waited 9h40m, ran 4s
  coverage-evidence            waited 13h01m, ran 5s
  opencode-review              waited 12h13m

That is ~22h41m of queue time spent to print two sentences, with the actual
review held behind it.

Both edges are removed and the two context holders now depend on
admit-current-head directly, dropping serial depth from 5 to 3 and the number of
queue waits from 4 to 2. Safety, verified rather than assumed:

- coverage-evidence had no `if:` and relied entirely on transitive skipping
  through coverage-source-tree. Its admission gate is now stated explicitly, so
  an unadmitted head still skips it. Dropping the edge without this would have
  run a required context on unadmitted heads.
- opencode-review-target never reads coverage-evidence at runtime; the only
  reference was the `needs:` line itself. The consumer of that context is
  opencode-review-dispatch.yml via scripts/ci/opencode_coverage_identity.py,
  which resolves it against the check-runs API on its own schedule.
- No test asserts this ordering. scripts/ci/test_strix_quick_gate.sh:1203 names
  both jobs but as set membership, not sequence.
- Branch protection evaluates required contexts independently; all of
  required-workflow-bootstrap, coverage-evidence and opencode-review still
  report.

Scope is deliberately limited to opencode-review.yml. opencode-review-dispatch.yml
has jobs of the same two names whose edge is a real data dependency -- its
coverage-source-tree uploads the materialized PR merge tree and its
coverage-evidence downloads it -- and must not be parallelized. Credit to peer
session review for catching that name collision, and to a Codex audit for
finding the echo-only jobs in the first place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 5 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 6bb58c7e-5e90-43b5-9632-cf010341b3d3

📥 Commits

Reviewing files that changed from the base of the PR and between 525d16b and dc51e4e.

📒 Files selected for processing (1)
  • .github/workflows/opencode-review.yml

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

seonghobae added a commit that referenced this pull request Sep 5, 2026
… dimension

This entry counted runner slots and concluded reclaiming 1 of 33 could not
matter in a capacity-bound queue. Wrong. The two echo-only jobs sit in series
on the review critical path, and a job is not created until its needs:
complete, so each link waits out the whole queue again. Measured on
naruon#1528: 9h40m and 13h01m of queue wait for 4s and 5s of runtime, ~22h41m
total holding the real review behind it, on every PR across 76 repositories.

Slot count made that look like 3%; critical-path latency is the dimension that
governs how long a PR actually takes to clear. Fix shipped as #1910 (serial
depth 5 -> 3, queue waits 4 -> 2).

Also records the two non-obvious safety conditions: coverage-evidence had no
if: of its own and relied on transitive skipping, and opencode-review-dispatch
.yml has same-named jobs whose edge is a real artifact dependency -- job names
are unique only within a workflow file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 5, 2026
…tion

#1910 implements the fix, correctly scoped to opencode-review.yml only: five
serial links to three, queue waits per PR from four to two, with the explicit
admission if: carried onto coverage-evidence and coverage-evidence dropped
from opencode-review-target's needs after confirming that job never reads the
context at runtime.

Adds the cross-family (Codex) reproduction of all three points, including the
artifact name this record had not cited (opencode-coverage-source).

Records the implementing session's own honest note: their change was safe
because they scoped it narrowly, not because they had checked for the name
collision. That generalizes better than the specific fix -- a job name is
unique only within one workflow file, and the same name in another file can
carry the opposite safety property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@seonghobae
seonghobae merged commit c4a89b3 into main Sep 5, 2026
3 of 15 checks passed
@seonghobae
seonghobae deleted the fix/opencode-review-serial-echo-job-chain branch September 5, 2026 08:44
seonghobae added a commit that referenced this pull request Sep 5, 2026
…1904)

* docs(items15-17): measure Detect changed scope gate-job runner waste

Took jobs-per-PR as the metric for the 60-job ceiling complaint and measured
a real baseline: one completed .github PR head produced 57 check runs across
2 attempts (~28/attempt), with "Detect changed scope" the most repeated job
name at 5 per attempt.

The obvious reading -- 5 duplicate gates, 5 wasted runners -- is wrong, and
this entry records the corrected version. Whether a gate is waste depends on
its consumer count: security-scan.yml amortizes one gate across 4 gated jobs
(legitimate; self-gating would trade 1 runner for 4 redundant API calls),
while sast-semgrep.yml and strix.yml each gate exactly one consumer, so each
burns two runner allocations where one suffices.

Real opportunity: 2 runner slots per PR, org-wide (both are ruleset-required
workflows dispatched into ~74 repos). Records the load-bearing constraint any
fix must preserve -- the ruleset ignores on: filters, so the job-level gate
cannot become a trigger-level skip.

Not fixed here: these are live org-wide required workflows and the org CI
cannot complete runs at all right now, so the edit belongs in its own PR that
can actually be validated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(items15-17): two echo-only jobs sit serially on the review critical path

Extends the gate-job measurement with a second, larger finding. A peer
session's read-only Codex pass spotted that opencode-review.yml's
coverage-source-tree job does nothing but echo a string; verified here
against origin/main, which shows the problem is bigger than one job.

opencode-review.yml chains five jobs serially -- bootstrap ->
admit-current-head -> coverage-source-tree -> coverage-evidence ->
opencode-review-target -- and two of those links only print a sentence. A job
is created only after its needs: predecessor finishes, so under queue
saturation each link pays a full fresh queue wait.

Quantified with this session's own item-13 audit data for naruon#1528 (run
33581213805): the two echo-only links waited ~9h40m and ~13h1m respectively,
contributing roughly 22h41m of pure queue latency to one PR while holding the
actual review behind them.

Both jobs are load-bearing as reported branch-protection contexts and cannot
simply be deleted, but their needs: edges are ordering, not data dependency --
neither produces an output the next consumes. Records the parallelization
option and explicitly flags it as needing a check that nothing depends on
these contexts completing in order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(items15-17): close the order-dependency question, add the skip guard

A peer session independently re-pulled run 33581213805 and confirmed the
serialization mechanism rather than inferring it: each job's created_at is
exactly its predecessor's completed_at, so a job is not queued until its
needs: predecessor finishes. Execution was 4 and 5 seconds against 9h40m and
13h1m of waiting.

Closes the order-dependency question this entry left open: no test asserts
the needs: chain order, the merge scheduler reads only a context name and its
exact-head conclusion (CANONICAL_CHECK_NAME), and neither job declares
outputs.

Adds a safety condition the first draft missed: coverage-evidence has no if:
of its own and is skipped only transitively via coverage-source-tree's
admission guard, so cutting that edge without moving the guard would let a
required context run on an unadmitted head.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(items15-17): warn that two files define these job names, one unsafe

Two sessions independently reasoned about "the coverage jobs" without
checking that the name resolves to two different jobs in two workflow files.

opencode-review.yml (required, pull_request_target) holds the echo-only
placeholders this entry analyses. opencode-review-dispatch.yml (privileged,
repository_dispatch) defines jobs with the same names that do the real work:
coverage-source-tree materializes the PR merge tree and uploads it as an
artifact, coverage-evidence downloads that artifact and runs with a 300
minute timeout. There the edge is a hard data dependency, and cutting it
would break coverage measurement outright.

Caught by opening scripts/ci/test_strix_quick_gate.sh, whose assertions
describe coverage-source-tree as materializing and uploading a merge tree --
contradicting "it only echoes" and exposing the second file.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(items15-17): record the implementation and cross-family confirmation

#1910 implements the fix, correctly scoped to opencode-review.yml only: five
serial links to three, queue waits per PR from four to two, with the explicit
admission if: carried onto coverage-evidence and coverage-evidence dropped
from opencode-review-target's needs after confirming that job never reads the
context at runtime.

Adds the cross-family (Codex) reproduction of all three points, including the
artifact name this record had not cited (opencode-coverage-source).

Records the implementing session's own honest note: their change was safe
because they scoped it narrowly, not because they had checked for the name
collision. That generalizes better than the specific fix -- a job name is
unique only within one workflow file, and the same name in another file can
carry the opposite safety property.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
seonghobae pushed a commit that referenced this pull request Sep 5, 2026
Resolves the AGENTS.md and CLAUDE.md conflicts against #1914. Both sides
were purely additive, so both are kept; verified by section count rather
than by the absence of markers -- AGENTS.md 5 -> 6 sections with every
main section retained, CLAUDE.md 7 -> 7 with this PR's bullet living
inside an existing section.

This ends the push freeze on this branch deliberately, and voids the
experiment it was running. A dirty head cannot merge however good its
review, so freezing a conflicted PR to protect a review run protects
nothing; the merge conflict outranks it.

New: "absence of data flow is not evidence that an edge is safe to cut."
Three edges here share one surface signature -- upstream job declares no
outputs, nothing downstream references its needs.*, sits above the real
work -- and they are not equally safe. Detect changed scope is the
documented path-filtering safety mechanism; coverage-source-tree and
coverage-evidence were genuinely ordering-only and #1910 was right to
parallelise them; required-workflow-bootstrap has the same signature as
the second and is a pull_request_target trust boundary, rejecting
untrusted fork PRs and verifying the immutable policy source before
anything downstream starts. Cutting it lets three downstream jobs run
concurrently with the fork check. The discriminator is never the edge's
shape: read every step of the upstream job and ask what becomes possible
if it has not run. Recorded with the cheaper alternative that keeps the
guarantee -- noema-review.yml enforces the same boundary as a per-job
if:, and a job whose if: is false is never created.

That makes two controls in this repository that read as waste, signature
2's trusted_ref pin being the first, so the entry names the structure:
on pull_request_target a control's cost is visible in the queue while its
benefit is invisible until it is gone, and optimization pressure points
at the security gates.

Also records a false negative hit while resolving this very conflict:
git merge-tree plus a grep for ^<<<<<<< returned 0 while a real merge
conflicted in both files. The authoritative check is a real merge in a
scratch worktree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
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.

1 participant