Skip to content

fix(ci): exempt draft PRs from OpenCode verdict polling - #1568

Merged
seonghobae merged 19 commits into
mainfrom
fix/opencode-review-draft-poll-exemption
Sep 1, 2026
Merged

fix(ci): exempt draft PRs from OpenCode verdict polling#1568
seonghobae merged 19 commits into
mainfrom
fix/opencode-review-draft-poll-exemption

Conversation

@seonghobae

@seonghobae seonghobae commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Outcome

Ready successor for closed draft #1560 from the identical unchanged branch/head. The connected Ready mutation remains broken by a connector GraphQL schema mismatch, so this replaces only the PR conversation and regenerates exact-head evidence.

Root cause

Protected main@5686de41660d51a7a7f22b8840dfa6ccfe5ff3f1 still has an impossible draft path: the scheduler intentionally does not dispatch ordinary draft PRs without an explicit agent request, but the required opencode-review verdict-poll step has no draft exemption and can wait until the job runtime ceiling for a verdict that cannot be produced.

Fix

  • pass the pull-request draft flag into the required verdict-poll step;
  • exit before Reviews API polling when the PR is draft, mirroring the existing closed-PR exemption;
  • preserve non-draft exact-head verdict requirements and all reviewer/credential/merge authority;
  • execute the production step body in regressions proving draft/closed short-circuit before gh, while non-draft PRs still poll.

Current-main boundary

Exact head is 901dfdf6c1f93becf24096703a6dcacf4a6876b8, directly based on protected main. The repository-wide 99% coverage regression on protected main is independently owned by #1567; this PR does not claim that unrelated gap as passing evidence. Merge only after fresh exact-head checks/reviews and the merged-tree coverage prerequisite are satisfied. No #1560/#1443 evidence transfers.


Devin Review

Reproduced against current main (5686de4) after PR #1443 was closed as
superseded by #1546's receipt-gate redesign: the redesign's PR_DRAFT plumbing
only narrows which reviews opencode_review_receipt_gate.py's
evaluate_receipts() accepts (rejecting a bot APPROVE on a draft) -- it never
exempts a draft PR from needing a receipt at all. pr_review_merge_scheduler.py
skips dispatching a review for an ordinary draft with no @opencode-agent
mention, so nothing ever posts a verdict, and the "Fail closed without a
current-head OpenCode verdict" step's while/sleep poll had no draft check at
all -- it loops until the job's own ~360-minute runtime ceiling kills it.

Add the same PR_DRAFT sourcing the sibling dispatch step already uses and an
early exit mirroring the existing closed-PR exit. Minimal and scoped to the
one missing exemption; the receipt-gate/scheduler architecture is otherwise
untouched, per the #1443 closure's own guidance to fix this fresh against
current main rather than revive that branch.

Copy link
Copy Markdown
Contributor Author

@opencode-agent review exact unchanged head 901dfdf6c1f93becf24096703a6dcacf4a6876b8 against protected main@5686de41660d51a7a7f22b8840dfa6ccfe5ff3f1. Verify that the draft exemption only prevents impossible verdict polling for draft PRs, while non-draft exact-head receipt/review policy remains fail-closed. Treat #1567 as the independent merged-tree coverage prerequisite and do not transfer #1560/#1443 evidence.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 52 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: 27eb6168-ac94-4733-b4a2-695ab615346a

📥 Commits

Reviewing files that changed from the base of the PR and between c11ff39 and f40f8de.

📒 Files selected for processing (8)
  • .github/workflows/opencode-review.yml
  • CHANGELOG.md
  • docs/doctoring/opencode-draft-verdict-cycle.md
  • scripts/ci/test_strix_quick_gate.sh
  • tests/conftest.py
  • tests/test_opencode_live_draft_state_regression.py
  • tests/test_opencode_required_verdict_regression.py
  • tests/test_required_workflow_queue_contract.py
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/opencode-review-draft-poll-exemption

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.

Copy link
Copy Markdown
Contributor Author

@cwl-noema-review independently review exact head 901dfdf6c1f93becf24096703a6dcacf4a6876b8. Confirm the draft-only short-circuit is safe, does not let a ready PR bypass the current-head verdict gate, and preserves the #1546 exact-head lifecycle.

devin-ai-integration[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor Author

@opencode-agent fix the unresolved exact-head Devin finding on the existing branch, then re-review the new head. Add converted_to_draft to the trusted pull_request_target event types so a ready→draft transition creates a fresh PR-scoped run, cancels/replaces the old active verdict poll through existing concurrency, and reaches the draft exemption. Update every exact trigger contract that pins the event set (tests/test_required_workflow_queue_contract.py, scripts/ci/test_strix_quick_gate.sh, and any other current-main assertion found by repo search), plus executable regression coverage for converted_to_draft in tests/test_opencode_required_verdict_regression.py. Keep draft-only behavior: a later ready_for_review or synchronize with draft=false must still require exact-head verdict evidence. Update the dated G-13/baseline traceability entry if this queue-amplification edge is not already recorded. Preserve #1546 receipt/concurrency semantics and #1567 as merged-tree coverage prerequisite.

Copy link
Copy Markdown
Contributor Author

The current Devin finding is valid and blocks merge-readiness. Update the existing branch so pull_request_target.types includes converted_to_draft; the existing PR-scoped concurrency must then cancel an active non-draft poll and run the draft exemption on that event. Add executable coverage that the converted-to-draft action exits before Reviews API access, and a workflow contract that pins the trigger. Preserve the existing non-draft exact-head verdict requirement and do not add unsupported review-thread events. Also carry the fix into scripts/ci/test_strix_quick_gate.sh / queue-contract coverage if those files pin the exact trigger set. Regenerate exact-head evidence after push; no current-head review is sufficient until this is fixed.

Copy link
Copy Markdown
Contributor Author

Keep non-merge-ready until the open Devin bug is fixed on this branch. Add converted_to_draft to the pull_request_target trigger set so an already-ready PR reconverted to draft starts a fresh PR-scoped run that cancels the old verdict poll and takes the draft exemption. Update both trigger contracts (tests/test_required_workflow_queue_contract.py, scripts/ci/test_strix_quick_gate.sh) and execute the production verdict-poll body in a converted_to_draft regression proving Reviews API is never called. Preserve ordinary non-draft exact-head verdict requirements and the existing concurrency identity.

Devin Review on #1568 found that pull_request_target.types never
listed converted_to_draft, so a PR converted to draft while an
earlier event's "Fail closed without a current-head OpenCode
verdict" poll was still in flight never fired a fresh run to cancel
it via the workflow's PR-scoped cancel-in-progress:true concurrency
group -- the stale non-draft poll kept calling the Reviews API
toward the job's runtime ceiling for a verdict a draft PR can never
receive.

converted_to_draft is now in the trigger list. The existing PR_DRAFT
exemption in that step already exits before Reviews API access; the
gap was purely that the trigger never fired for this event, so no
step-body logic changed.

Also ports the known SIGPIPE flake fix (cat >/dev/null in the fake
gh's dispatches branch) into this branch's copy of
test_scheduler_wake_reuses_trusted_receipt_predicate, inherited via
merge from main and confirmed clean over 75 repeated runs.

Full suite: pytest 2251 passed / 1 skipped / 21 subtests (99%
coverage, pre-existing gap owned by #1567); test_strix_quick_gate.sh
full harness: PASS.

Copy link
Copy Markdown
Contributor Author

Pushed 7b48b2fa addressing the Devin Review finding: converted_to_draft was missing from on.pull_request_target.types.

Root cause: the Fail closed without a current-head OpenCode verdict step already exits before ever calling the Reviews API when PR_DRAFT=true (the fix from this PR's first commit) -- but that exemption never got a chance to run for a PR converted to draft mid-poll, because the workflow trigger never fired for that event at all. A synchronize push starts a non-draft poll; if the PR is then converted to draft while that poll is still looping, nothing cancels it.

Fix: added converted_to_draft to the trigger types. The workflow's existing PR-scoped concurrency group (cancel-in-progress: true) then cancels the stale in-flight non-draft poll the moment the fresh converted_to_draft run starts, and that fresh run reaches the same pre-existing PR_DRAFT exemption, exiting before ever calling the Reviews API. No step-body logic changed.

Regressions added:

  • test_fail_closed_step_exempts_a_pr_converted_to_draft_mid_poll -- proves the step body exits closed for the exact PR_ACTION=converted_to_draft value.
  • test_opencode_review_trigger_reacts_to_mid_poll_draft_conversion -- pins that converted_to_draft is actually present in the workflow's own trigger block, since a step-level test alone can't prove the fix is reachable in production.

Both existing literal trigger-type contract pins (tests/test_required_workflow_queue_contract.py, scripts/ci/test_strix_quick_gate.sh) were updated to the new six-element list.

Also ported the known SIGPIPE flake fix (cat >/dev/null in the fake gh's dispatches branch, inherited via merge from main) into this branch's copy of test_scheduler_wake_reuses_trusted_receipt_predicate -- confirmed clean over 75 repeated runs.

Validation: full pytest 2251 passed / 1 skipped / 21 subtests (99% coverage, pre-existing gap owned by #1567); test_strix_quick_gate.sh full harness: PASS.

Ready for fresh exact-head review.


Generated by Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

Devin Review found a second gap on #1568: converted_to_draft now
fires this workflow (previous commit), but the sibling "Request
current-head OpenCode review execution" step -- which runs before
"Fail closed" -- had no draft exemption of its own. It still fetched
the receipt-gate helper source and queried the Reviews API for a PR
that just went draft, and could reach OIDC token exchange and a
repository_dispatch scheduler wake before "Fail closed"'s exemption
ever ran.

Add the same PR_DRAFT early exit, before any API call, mirroring the
existing "Fail closed" step's precedent. ready_for_review and the
explicit draft-review dispatch path in pr_review_merge_scheduler.py
are untouched.

New regressions: test_request_review_step_exempts_a_pr_converted_to_draft_before_any_api_call
proves the step exits before any gh invocation when PR_DRAFT=true;
test_request_review_step_still_dispatches_for_a_non_draft_pr proves
non-draft PRs are unaffected.

Full suite: pytest 2253 passed / 1 skipped / 21 subtests (99%
coverage, pre-existing gap owned by #1567).

Copy link
Copy Markdown
Contributor Author

Pushed 53aec344 addressing the second Devin Review finding: the sibling Request current-head OpenCode review execution step -- which runs before Fail closed -- had no draft exemption of its own. converted_to_draft now fires this workflow (previous commit), but that step still fetched the receipt-gate helper source and queried the Reviews API for a PR that just went draft, and could reach OIDC token exchange and a repository_dispatch scheduler wake before Fail closed's exemption ever ran.

Fix: added the same PR_DRAFT early exit, before any API call, mirroring the existing Fail closed step's precedent exactly. ready_for_review and the explicit draft-review dispatch path in pr_review_merge_scheduler.py are untouched.

Regressions:

  • test_request_review_step_exempts_a_pr_converted_to_draft_before_any_api_call -- proves the step exits before any gh invocation when PR_DRAFT=true.
  • test_request_review_step_still_dispatches_for_a_non_draft_pr -- proves non-draft PRs still reach the helper fetch (unaffected).

Validation: full pytest 2253 passed / 1 skipped / 21 subtests (99% coverage, pre-existing gap owned by #1567).

Ready for fresh exact-head review.


Generated by Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

Reconcile #1568 non-destructively with protected main@b4f7b082536d2be8dceab0a40a484161b50e5acd. Preserve the fixed converted-to-draft trigger and pre-I/O draft exemptions, retain newer protected-main documentation, and carry the concurrent review-event scheduler wake regression in a dedicated test file.

No force push, destructive rebase, gate weakening, or predecessor evidence transfer.
devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Converge #1568 non-destructively with protected main@36ff97a8c25cc47a3d7c817bfc8fb6ba3108f2b7 after the scheduler-coverage chicken-and-egg repair landed. Preserve the live draft/head revalidation and its regressions while retaining current-main queue-contract authority and dropping the temporary duplicate review-event test relocation.

No force push, destructive rebase, gate weakening, or predecessor evidence transfer.
devin-ai-integration[bot]

This comment was marked as resolved.

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 0 new potential issues.

Devin Review

Copy link
Copy Markdown
Contributor Author

/oc

Fix the two still-valid unresolved substantive findings on this same branch/head lineage, then rerun the focused exact-trigger/verdict regressions before resolving anything:

  1. A stale ready-event run must revalidate the live PR draft/head state too, so it cannot cancel a newer draft run and poll indefinitely after the PR has returned to Draft.
  2. After live-state validation, the receipt gate must consume the validated/effective draft state rather than the stale event snapshot, so a stale draft event observing a live Ready PR does not reject/re-dispatch over an existing exact-head approval.

Preserve explicit mention-triggered draft review behavior, PR-scoped concurrency, fail-closed API errors, and all existing review/credential/merge gates. Do not weaken verdict polling or synthesize status evidence. Add deterministic regressions for both event-order inversions and update the same PR branch non-destructively.

devin-ai-integration[bot]

This comment was marked as resolved.

devin-ai-integration[bot]

This comment was marked as resolved.

Apply the queued one-shot repair workflow's already-designed patch
directly instead of waiting on it: the Actions fleet has 800+ runs
queued right now, and Devin flagged that workflow's contents:write
permission on branch-controlled code as a standing exposure for as
long as it sits unexecuted. Applying the identical transformation
here and deleting the workflow in the same commit closes that
window immediately rather than leaving it queued indefinitely.

- _write_refusing_gh -> _write_live_pr_then_refusing_gh: serve the
  one authoritative live PR lookup the production step now performs
  before continuing to refuse every other gh call.
- Thread LIVE_PR_JSON through _run_fail_closed_step,
  _run_request_review_step, and test_scheduler_wake_reuses_trusted_receipt_predicate's
  bespoke fake gh so each fixture answers that lookup consistently
  with its own draft/head scenario.
- Update message assertions to the current production wording
  ("PR is still a draft on the live exact head", "unexpected gh
  invocation after live-state validation").
- Remove the now-executed source-fix-1568-live-state-regressions.yml.

Verified: PYTHONPATH=. python -m pytest tests/test_opencode_required_verdict_regression.py tests/test_opencode_live_draft_state_regression.py tests/test_required_workflow_queue_contract.py -q -> 108 passed;
full PYTHONPATH=. python -m pytest tests -q -> 2281 passed, 1 skipped.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4
…tate lookup

Devin Review flagged that this record still promised an exit before
any API call, but both the request-review and required-verdict
polling steps now make one unconditional gh api live-PR lookup
before exiting on a confirmed live draft state (added after the
initial fix so a stale event-payload PR_DRAFT/head can't be trusted
alone). Update the repair description to match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

CI-red root cause and fix (exact-head-path-policy)

exact-head-path-policy had been failing on every head throughout this PR's history (10 failed + 13 errors on the last check I inspected). Devin's LLM review track reported "0 new potential issues" on the same head — that gap is exactly why this repo runs a separate deterministic pytest gate alongside the review track.

Root causes, all now fixed:

  1. tests/test_required_workflow_queue_contract.py — stale pinned assertion still expected the pre-converted_to_draft 5-element types: [...] list.
  2. tests/conftest.py's uv-cache-clearing autouse fixture — a monkeypatch.setattr in one test and this fixture's teardown raced; whichever session pushed the earlier clear_trusted_uv_process_caches rewrite (capturing bound .cache_clear methods at setup time instead of re-reading the module attribute at teardown) already fixed this — confirmed still holding.
  3. tests/test_opencode_live_draft_state_regression.py::test_draft_exemption_fails_closed_when_live_head_moved — stale message-substring assertion ("...validating draft" vs. the current "...validating live" wording).
  4. tests/test_opencode_required_verdict_regression.py — the biggest gap: test_request_review_step_still_dispatches_for_a_non_draft_pr, test_fail_closed_step_still_polls_for_a_non_draft_pr, and all 5 test_scheduler_wake_reuses_trusted_receipt_predicate[...] cases still used a fake gh that refuses every call, but production now makes one unconditional live-PR-state gh api lookup before reaching the Reviews-API/dispatch logic these tests exercise.

For (4), I found .github/workflows/source-fix-1568-live-state-regressions.yml already queued on push (run 33495878279) with the exact patch designed and reviewed — but the Actions fleet had 800+ runs queued at the time, and Devin had separately flagged (🟥) that the workflow's contents: write on branch-controlled Python/tests was a standing exposure for as long as it sat unexecuted. Rather than wait indefinitely, I applied that same transformation directly (byte-for-byte, after fixing one bug in its precondition check — it assumed "PR_DRAFT": pr_draft, appears exactly once in the file, but it appears twice) and deleted the workflow in the same commit, closing that window immediately instead of leaving it queued.

Also fixed docs/doctoring/opencode-draft-verdict-cycle.md per Devin's 🔍 finding — it still said each step exits "before any GitHub API or token exchange"; updated to describe the live-state lookup that now runs first.

Verified locally: PYTHONPATH=. python -m pytest tests -q → 2281 passed, 1 skipped (no regressions vs. the pre-existing 1 skip).

Pushed as 9bbf5e96 (test fixes + workflow removal) and 2193cda8 (doc correction) on top of the branch's existing head. No force-push; nothing here touches review/merge authority or the exact-head/live-state validation logic itself, only the test fixtures that exercise it.


Generated by Claude Code

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note

This report is out of date. Scroll down for Devin Review's latest report on this PR.

Devin Review found 4 new potential issues.

Devin Review

Comment thread .github/workflows/opencode-review.yml
Comment thread .github/workflows/opencode-review.yml
Comment thread .github/workflows/opencode-review.yml
Comment thread tests/conftest.py

Copy link
Copy Markdown
Contributor Author

@opencode-agent Fix the two new unresolved exact-head defects on the existing branch immediately, test-first and without force-push. 1) Do not let an old-head delayed pull_request_target run cancel the authoritative newer-head run before validation. Redesign concurrency/admission so head-changing events are isolated by head identity (or equivalent trusted stale-run handling) while same-head draft↔ready transitions still share a cancellation domain so converted_to_draft can retire an active same-head verdict poll. Add an executable regression for old synchronize(head A) starting after newer head B is already active, proving head A cannot cancel B, plus same-head converted_to_draft still replaces the poll. 2) Fetch and validate live PR state together with head and draft in both production admission blocks; a delayed non-closed event against a live closed PR must exit before helper fetch/token exchange/dispatch/review polling. Add regressions executing both production step bodies with event action non-closed and live API state=closed; reject missing/null/non-string/unexpected live state fail-closed. Preserve the already-fixed effective live draft logic and current-main semantics. After GREEN, rerun full coverage/docstrings, push, resolve only proven-obsolete threads, and re-fetch fresh review/check evidence.

…closed state

Owner-directed fix for two Devin Review findings on #1568's live-state
validation (head/draft lookup landed in an earlier commit on this branch):

1. Stale runs could cancel the current check. The concurrency group was
   keyed only by repository and PR number; GitHub cancels whichever run is
   currently active in a group when a new one starts, with no notion of
   "older"/"newer", so a delayed run for an older head could cancel the
   newer, authoritative head's still-valid run before that older run's own
   live-head check ever had a chance to reject it. Scope the group by exact
   head SHA too, so different heads no longer share a cancellation domain
   while same-head events (converted_to_draft/ready_for_review, a
   synchronize retry) still do -- preserving converted_to_draft's ability
   to retire an active same-head verdict poll.

2. A delayed non-closed event ignored a live-closed PR: live_pr only ever
   extracted head and draft. Both admission blocks now also extract and
   validate live state, exiting before any further API call when it is
   "closed" -- mirroring the existing PR_ACTION == "closed" event-level
   short-circuit but driven by live truth. A missing, null, non-string, or
   otherwise unrecognized state value fails closed rather than assuming
   open, matching the existing live_head/live_draft validation style.

New regressions: a structural contract test for the head-scoped
concurrency group; step-body coverage for a stale non-closed event against
a live-closed PR (both admission steps), live-closed state taking
precedence over a stale live-draft flag, and each invalid state shape
(missing/null/non-string/unexpected value) failing closed. Updated every
existing LIVE_PR_JSON test fixture to include a state field now that
production requires one.

Verified: PYTHONPATH=. python -m pytest tests -> 2294 passed, 1 skipped,
21 subtests; coverage on scripts/ci -> 100%; interrogate -> 100%.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

Both new exact-head defects fixed on this branch, test-first, no force-push.

1) Stale-run cancellation. The concurrency group (opencode-review.yml) was keyed only by repository + PR number. GitHub cancels whichever run is currently active in a group when a new one starts — no notion of "older"/"newer" — so a delayed old-head run could cancel the authoritative newer-head run before live_head ever got a chance to reject it. Scoped the group by exact head SHA too, so different heads no longer share a cancellation domain, while same-head events (converted_to_draft/ready_for_review transitions, a synchronize retry) still do — preserving converted_to_draft's ability to retire an active same-head verdict poll. This is a declarative YAML property, not something a step-body subprocess test can exercise directly, so the regression is a structural contract test (test_opencode_review_concurrency_group_is_scoped_by_exact_head) pinning the group expression's head-SHA scoping.

2) Live closed-state validation. live_pr only ever extracted head and draft. Both admission blocks now also extract and validate live state, exiting before any further API call (helper fetch, token exchange, dispatch, Reviews polling) when it is "closed" — mirroring the pre-existing PR_ACTION == "closed" event-level short-circuit but driven by live truth instead of a possibly-stale event payload. A missing, null, non-string, or otherwise unrecognized state value fails closed rather than being assumed open. Regressions cover: a stale non-closed event (synchronize) against a live-closed PR for both admission steps, live-closed state taking precedence over a stale live-draft flag, and each invalid state shape (missing/null/non-string/unexpected value) failing closed for both steps.

Preserved: the already-fixed effective-live-draft receipt logic, and current-main semantics elsewhere.

After GREEN: full suite PYTHONPATH=. python -m pytest tests → 2294 passed, 1 skipped, 21 subtests; coverage run -m pytest tests && coverage report → 100% on scripts/ci; interrogate → 100% docstrings.

Pushed as dbdab459. Resolved the two 🟡 threads this fixes (concurrency isolation, live closed-state). Left the two 📝 informational threads (receipt-gate live-draft reuse, conftest cache-clear ordering) open since they describe already-correct behavior rather than an unresolved concern.

@opencode-agent please re-fetch fresh review/check evidence at this head.


Generated by Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

…ronize

Devin Review round 3 on #1568: scoping the concurrency group by exact head
SHA fixed the wrong-direction cancellation (a delayed old-head run could no
longer cancel a newer, authoritative run) but also disabled the legitimate
one -- a genuine new commit no longer shares a group with its own PR's
now-obsolete previous-head poll, so nothing cancels it. That older run's
live-head check ran once, before entering the unbounded Reviews API wait
loop, which never re-validates the head on later iterations; left alone it
occupies a hosted runner until GitHub's own per-job ceiling.

Add a cancel-superseded-opencode-review-runs job, scoped to synchronize
events, mirroring the already-established live-head-validated cleanup
pattern in strix.yml's own cancel-superseded-pr-runs job: list this PR's
other active Required OpenCode Review runs (matched by workflow name/event
plus a display-title or pull_requests[] PR-number match), exclude the
currently-executing run and any run already on the live head, and cancel
the rest -- re-verifying the live head immediately before both the listing
pass and each individual cancellation, so a delayed/stale invocation of
this same job cannot itself wrongly cancel a still-authoritative run.

New regressions: the embedded run-selection jq filter extracted and
executed against synthetic workflow_runs payloads (mirroring how
runtime_verdict() already exercises the required-verdict filter) --
superseded-run selection, current-head/self-run/other-PR/other-workflow
exclusion, and pull_requests[] metadata matching when display_title never
rendered the head suffix; a structural test pins the job's synchronize-only
trigger and actions: write permission.

Verified: PYTHONPATH=. python -m pytest tests -> 2301 passed, 1 skipped,
21 subtests; coverage on scripts/ci -> 100%; interrogate -> 100%; YAML
parses cleanly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

Fixed the third round's 🔴 finding: head-scoping the concurrency group fixed the wrong-direction cancellation but disabled the legitimate one.

A genuine new commit (synchronize, head A→B) no longer shares a group with head A's now-obsolete run, so nothing cancels it. That older run's live-head check ran once, before entering the unbounded Reviews API wait loop, which never re-validates the head — left alone it occupies a hosted runner until GitHub's own per-job ceiling, exactly as Devin describes.

Fix: added cancel-superseded-opencode-review-runs, a job scoped to synchronize events, mirroring the live-head-validated cleanup pattern already established in strix.yml's own cancel-superseded-pr-runs job (per your suggestion). It lists this PR's other active Required OpenCode Review runs, excludes the currently-executing run and any run already on the live head, and cancels the rest — re-verifying the live head immediately before both the listing pass and each individual cancellation, so a delayed/stale invocation of this same cleanup job can't itself wrongly cancel a still-authoritative run.

Regressions: the embedded run-selection jq filter extracted and executed against synthetic workflow_runs payloads (superseded-run selection, current-head/self-run/other-PR/other-workflow exclusion, pull_requests[] metadata matching when display_title lacks the head suffix), plus a structural test pinning the job's synchronize-only trigger and actions: write permission.

Full suite: 2301 passed, 1 skipped, 21 subtests; coverage and docstrings both 100%. Pushed as 4732983c.

@opencode-agent please re-fetch fresh review/check evidence at this head.


Generated by Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

…n cleanup job

Devin Review on #1568: the cleanup job added in the previous commit had a
misplaced trailing line. The job append edit was anchored on the "Fail
closed" step's own closing if/fi block, but that step's script actually
had one more line after it -- echo "Current-head OpenCode verdict:
${verdict}." -- ending the file without a trailing newline, so wc -l
undercounted it and a manual tail read (limit=2) stopped one line short.
The new job's content landed between the fi and that trailing echo,
pulling it into the cleanup job's own script, where $verdict is never
set. Under set -euo pipefail (-u included), every synchronize event
crashed with "verdict: unbound variable", failing the required workflow
on every new commit -- reproduced directly by extracting and executing
the job's script body against fake gh/jq stubs before this fix, and
confirmed GREEN after.

Restored the echo to its correct original position at the end of the
"Fail closed" step, and gave the cleanup job its own closing message.

Also adopted force-cancel as a fallback for a run that resists normal
cancellation, matching strix.yml's own cancel-superseded-pr-runs job
(Devin's accompanying informational finding).

Verified: PYTHONPATH=. python -m pytest tests -> 2301 passed, 1 skipped,
21 subtests; YAML parses cleanly; the cleanup job's script body executed
directly against fake gh/jq stubs -> exit 0, no unbound-variable error
(reproduced the original crash against the pre-fix script first).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BV96rXhqoR3tYZ9AeAVur4

Copy link
Copy Markdown
Contributor Author

Both caught and fixed — and this one was my own mistake, not a design gap.

🔴 Root cause: my previous edit anchored on the "Fail closed" step's closing if/fi block, but that step's script had one more line after it (echo "Current-head OpenCode verdict: ${verdict}.") ending the file with no trailing newline — wc -l undercounted it, and a manual tail-style read I did to confirm the file's end stopped one line short. The new job's content landed before that trailing line instead of after it, pulling it into the cleanup job's own script where $verdict is never set. Under set -euo pipefail, every synchronize event crashed with verdict: unbound variable, failing the required workflow on every new commit — exactly as reported.

Reproduced directly: extracted the cleanup job's script body and ran it against fake gh/jq stubs — confirmed genuine RED (unbound variable) against the pre-fix script, GREEN after moving the line back to its correct original position and giving the cleanup job its own closing message.

🔍 Force-cancel fallback: adopted, matching strix.yml's own cancel-superseded-pr-runs job exactly (cancel then force-cancel on failure) — cheap and directly closes the "resists cancellation" gap you flagged.

Full suite: 2301 passed, 1 skipped, 21 subtests; YAML parses cleanly. Pushed as f40f8deb.

@opencode-agent please re-fetch fresh review/check evidence at this head.


Generated by Claude Code

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 new potential issue.

Devin Review

Comment thread .github/workflows/opencode-review.yml
@seonghobae
seonghobae merged commit 4fff1e2 into main Sep 1, 2026
26 of 42 checks passed
@seonghobae
seonghobae deleted the fix/opencode-review-draft-poll-exemption branch September 1, 2026 11:44
seonghobae added a commit that referenced this pull request Sep 2, 2026
…h already works

Read strix.yml/opencode-review.yml/noema-review.yml directly rather than
trusting the backlog's dead evidence link (naruon run 33581213829, 404,
past retention). All three already cancel a PR's superseded-head review
run on push: Noema via native concurrency cancel-in-progress; Strix and
OpenCode Review via a dedicated, live-head-reverifying explicit
cancellation job instead, specifically because naive same-group native
cancellation caused a real incident for OpenCode Review (Devin Review on
#1568) and would silently discard Strix's scan log. Backed by existing
test coverage (test_opencode_poll_self_retirement.py and others).

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

Verified root cause (peer session's mechanism finding, confirmed here
against real data on .github#1741: 4/4 "Required OpenCode Review"
SHA-scoped checks stuck queued, 0 completed): the coalescer's own
PR-scoped concurrency group used cancel-in-progress:true, so under
near-zero Actions admission a rapid second push cancels the coalescer's
prior in-flight instance before it ever gets a runner -- it never
executes for that PR, so the stale exact-head-SHA runs it exists to
retire (opencode-review.yml deliberately scopes by head SHA per Devin's
#1568 fix, so cross-run eviction never happens for those) pile up
indefinitely instead.

Read scripts/ci/current_head_run_coalescer.py's own module docstring
before changing this: it re-fetches live PR/association/sibling state
immediately before cancelling anything and never trusts the SHA it was
triggered with, so a queued instance surviving a superseding push and
running later is exactly as correct as a fresh one -- letting it run,
rather than cancelling it, has no safety cost. This is the same
blast-radius reasoning already used for pr-review-fix-scheduler.yml/
pr-auto-rebase.yml's existing cancel-in-progress:false choices.

User-directed: "쓸데 없이 Trigger 되는 workflow가 있는 거 같은데요...
고쳐요" -- this is the concrete answer to that question for this one
mechanism; a sibling session is separately investigating
pr-review-merge-scheduler.yml's broader re-fire-on-every-check-
completion pattern.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
Peer session's independently-verified, adversarially-checked audit
(docs/doctoring/item13-stale-head-cancellation-audit-20260903.md,
.github#1760): noema-review.yml's concurrency group was
noema-review-<repo>-<pr> -- PR number only, no head SHA. GitHub
evaluates the concurrency: block at run-creation time, before any
job/step runs, using only the triggering event's own payload; it has
no notion of "older"/"newer" head, it just cancels whatever is
currently active in the group. GitHub does not guarantee webhook/
dispatch delivery order, so a delayed synchronize event for an OLDER
head, arriving after a NEWER head's review is already running, shares
that same group and cancels the newer, valid, in-flight review
immediately -- before the older run ever reaches its own "reject a
stale trigger" step. That step then correctly self-aborts, but only
after the one valid review has already been destroyed, leaving the
actual current head with none.

This is the identical hazard opencode-review.yml already fixed for
Devin Review's `#1568` finding. Ported the same pattern: added the
event's head SHA (github.event.pull_request.head.sha for
pull_request_target, github.event.client_payload.pr_head_sha for the
repository_dispatch retry path -- both fields already used elsewhere
in this same file's run-name:) as a third group-key segment. Different
heads of the same PR can no longer share a group, so a stale event can
never cancel the current head's run; same-head events (a duplicate
delivery) still share one group, so the existing cancel-in-progress
condition (synchronize/closed only, unchanged) still evicts a genuine
duplicate. cancel-in-progress's own conditional logic was deliberately
left untouched -- that policy (protecting an in-flight
repository_dispatch write from cancellation) is orthogonal to this fix.

New assertions in test_required_workflow_queue_contract.py confirm the
SHA is present in the group formula for both trigger paths; confirmed
they fail against the pre-fix group before committing. Full suite
(2668 tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
…lose #1568 instead (#1781)

Explicit user directive (2026-09-03): head-SHA-scoped concurrency groups
(added for Devin Review's #1568 finding -- a delayed, out-of-order run for
an older head could cancel the authoritative run already active for a newer
head) mean every push to a PR gets its own group, so rapid successive
pushes no longer cancel each other's in-flight runs -- they queue up
independently instead. That directly worsens the self-inflicted
queue-thrashing pattern this org measured directly today (236/300 cancelled
runs attributed to concurrent push volume).

Refined during cross-session review (host 1's finding, independently
verified before adopting): the real fix isn't to re-key the group but to
stop cancelling within it. cancel-in-progress: true is what actually causes
the #1568 wrongful kill, independent of whether SHA is in the group key --
scope by repo+PR-number only, but flip cancel-in-progress to false. With
false, nothing in the group is ever preempted regardless of arrival order,
so the #1568 race is structurally impossible here, not just less likely.
A now-queued older-head run still gets a turn once the active run finishes,
but the poll step's own live-head/live-state revalidation (already run
every iteration, needed for correctness regardless of this setting) makes
it self-exit within one poll_interval_seconds instead of running to
completion or publishing stale evidence. Plain repo+PR-number scoping also
means rapid pushes naturally serialize through one queue instead of
spawning N independent per-head groups, which is what actually bounds queue
depth here.

Updated the workflow's own concurrency comment and three test assertions
(two in test_opencode_required_verdict_regression.py, one in
test_required_workflow_queue_contract.py) that pinned the old head-SHA +
cancel-in-progress:true shape.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
Supersedes 31e46db. That fix added the head SHA to the concurrency
group to stop a delayed old-head event from cancelling a valid
newer-head run (#1568-class hazard) -- correct, but costly: a
SHA-scoped group gives every push its own group, so under this org's
saturated Actions ceiling a stale per-push run is only retired by a
separate cleanup job that shares that same congested admission queue
and can itself sit stuck behind it, letting superseded runs pile up.

The user pointed at the actual fix directly: the hazard is caused by
cancel-in-progress:true killing whichever run is active with no
older/newer notion, not by the group lacking SHA. Setting
cancel-in-progress:false closes it completely regardless of group
scoping -- the active run is never preempted -- while GitHub's
unconditional single-pending-slot rule still evicts an intermediate
queued push for free. The pre-existing "Reject a stale trigger" step
already guarantees whichever instance runs never reports against the
wrong commit either way.

Independently corroborated same-day: strix.yml's own concurrency fix
(64e67ef, #1779) already used this exact shape; a peer session applied
the identical pattern to opencode-review.yml after re-deriving the
same reasoning independently.

Full suite (2704 tests) passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
#1786)

Devin Review caught a real deadlock in #1781's redesign, independently
confirmed by two peer sessions before I acted on it: the workflow-level
concurrency: block (line 23, before permissions:/jobs:) applied to the
ENTIRE run as a unit -- every job in the file, including the
structurally-separate cancel-superseded-opencode-review-runs job.

With cancel-in-progress: false, a new push's ENTIRE run -- cleanup job
included -- could not even start until the group freed up, which only
happens when the older run's own opencode-review-target job finishes.
Since OpenCode/Noema inference deliberately has no wall-clock deadline, a
long-running older-head review could then block the newer head's review
indefinitely -- the opposite of what #1781 was supposed to fix.

Fixed by moving concurrency: from workflow-level into job-level, scoped
only to opencode-review-target (the job that actually runs the long
dispatch+poll). This leaves cancel-superseded-opencode-review-runs and the
lightweight bootstrap/coverage jobs completely unblocked: they start
immediately on every push, and the cleanup job's own direct Actions API
cancellation is what frees up the job-level slot for the new push's poll --
no deadlock, and the #1568 stale-cancels-fresh race stays structurally
closed at the same time. Matches strix.yml's existing job-scoped-only
reference pattern (confirmed to never have had workflow-level concurrency).

host 1 applied the equivalent fix to noema-review.yml on #1661 (extracting
its cleanup into a genuinely separate job) after finding this same class of
bug there first.

Updated two test files' assertions to match the new job-level placement,
plus added explicit regression guards (no top-level `^concurrency:`, a
job-level `^    concurrency:` exists) so this can't silently regress back
to workflow-level scoping.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
…ign (#1785)

Flagged by host 1 (relayed via peer 1): cancel-superseded-opencode-review-runs'
own comment still described "exact-head concurrency" protecting the newer
run -- that mechanism was removed in #1781 (bootstrap group is now
cancel-in-progress: false, scoped by repo+PR-number only).

While fixing it, found this job's actual role is more significant than its
old "defense in depth" framing suggested: it's a precise, live-head-
reverified, API-based sweep that already implements "cancel only outdated
runs of the same PR" (re-checks the live head immediately before selecting
cancellation candidates AND immediately before every individual cancel
call), so it's immune to #1568's order-blind-preemption bug by construction.
Now that the bootstrap group no longer auto-cancels anything natively, this
job is the primary active-cancellation path, not a backup. Rewrote the
comment to say so.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
The update-branch merge of this PR against main silently produced TWO
concurrency: keys in scorecard-analysis.yml: this PR's own SHA-scoped,
cancel-in-progress:true group (added to close a #1568-class stale-cancels-
fresh race), and .github#1768's independently-added, already-merged
ref-scoped, cancel-in-progress:false group (added to bound unbounded
concurrent Scorecard scans across a push burst). Neither git nor GitHub's
merge flagged this as a conflict since the two additions don't textually
overlap -- but YAML resolves a duplicate mapping key to its LAST
occurrence, so #1773's own intended design was being silently discarded
at parse time regardless of author intent.

The two designs are also structurally incompatible as a single
concurrency: block, not just redundant: SHA-scoping gives every distinct
commit its own group, meaning nothing ever queues behind anything else --
restoring the unbounded-concurrent-scans problem #1768 exists to prevent.
Given this organization's standing priority of reducing GitHub Actions
queue congestion (a plan-level 60-job ceiling), kept #1768's ref-scoped,
cancel-false group as authoritative and removed this PR's SHA-scoped
block. Updated the test, doctoring record, and CHANGELOG fragment to
match, documenting the conflict and resolution rather than silently
picking a side.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
seonghobae added a commit that referenced this pull request Sep 3, 2026
…low (#1773)

* test(scorecard): require reusable default-branch owner contract

* feat(scorecard): expose reusable default-branch analysis

* docs(scorecard): record owner and thin-caller rollout contract

* test(scorecard): bind concurrency to the exact source SHA

* fix(scorecard): prevent stale events cancelling newer scans

* docs(scorecard): record exact-revision concurrency repair

* docs(scorecard): add unreleased centralization ledger

* test(scorecard): honor BaseLoader empty-value semantics

* test(scorecard): remove implicit YAML parser dependency

* fix(scorecard): resolve duplicate concurrency key from #1768/#1773 merge

The update-branch merge of this PR against main silently produced TWO
concurrency: keys in scorecard-analysis.yml: this PR's own SHA-scoped,
cancel-in-progress:true group (added to close a #1568-class stale-cancels-
fresh race), and .github#1768's independently-added, already-merged
ref-scoped, cancel-in-progress:false group (added to bound unbounded
concurrent Scorecard scans across a push burst). Neither git nor GitHub's
merge flagged this as a conflict since the two additions don't textually
overlap -- but YAML resolves a duplicate mapping key to its LAST
occurrence, so #1773's own intended design was being silently discarded
at parse time regardless of author intent.

The two designs are also structurally incompatible as a single
concurrency: block, not just redundant: SHA-scoping gives every distinct
commit its own group, meaning nothing ever queues behind anything else --
restoring the unbounded-concurrent-scans problem #1768 exists to prevent.
Given this organization's standing priority of reducing GitHub Actions
queue congestion (a plan-level 60-job ceiling), kept #1768's ref-scoped,
cancel-false group as authoritative and removed this PR's SHA-scoped
block. Updated the test, doctoring record, and CHANGELOG fragment to
match, documenting the conflict and resolution rather than silently
picking a side.

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

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
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.

2 participants