Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
901dfdf
fix(ci): exempt draft PRs from the opencode-review required-check poll
claude Sep 1, 2026
7b48b2f
fix(ci): add converted_to_draft to opencode-review's trigger set
claude Sep 1, 2026
53aec34
fix(ci): exempt the request-review step from draft-mid-poll dispatch
claude Sep 1, 2026
acf6df6
merge current main into OpenCode draft-cycle repair
seonghobae Sep 1, 2026
b69831e
fix(ci): validate live draft state before exemption
seonghobae Sep 1, 2026
7aac09b
test(ci): adapt draft fixtures to live-state validation
seonghobae Sep 1, 2026
22863b9
test(ci): cover stale draft event against live ready PR
seonghobae Sep 1, 2026
94a898f
merge coverage-restored main into OpenCode draft-cycle repair
seonghobae Sep 1, 2026
0ce78e2
test(ci): cover both stale draft transition directions
seonghobae Sep 1, 2026
9fa3d9d
fix(ci): validate live PR state before review admission
seonghobae Sep 1, 2026
ec1a6db
fix(ci): preserve live false draft state
seonghobae Sep 1, 2026
06b5c4e
merge protected main into OpenCode draft admission repair
seonghobae Sep 1, 2026
5d64284
test(ci): isolate live-state fixtures and uv cache teardown
seonghobae Sep 1, 2026
5431237
ci: repair exact-head live-state regression fixtures
seonghobae Sep 1, 2026
9bbf5e9
test(ci): align live-state regressions with admission contract
claude Sep 1, 2026
2193cda
docs(doctoring): correct draft-exemption repair record for the live-s…
claude Sep 1, 2026
dbdab45
fix(opencode-review): isolate concurrency by head SHA, validate live …
claude Sep 1, 2026
4732983
fix(opencode-review): retire superseded-head runs on legitimate synch…
claude Sep 1, 2026
f40f8de
fix(opencode-review): fix unbound $verdict crash in the superseded-ru…
claude Sep 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 174 additions & 3 deletions .github/workflows/opencode-review.yml
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,31 @@ on:
# content and never binds repository secrets. Privileged review execution is
# isolated in opencode-review-dispatch.yml on repository_dispatch only.
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review, closed]
# `converted_to_draft` is included so a PR going draft mid-poll fires a
# fresh run of this same workflow: the head-scoped concurrency group below
# (`cancel-in-progress: true`) cancels any in-flight non-draft
# "Fail closed without a current-head OpenCode verdict" poll for that
# exact same head. Every non-closed admission path revalidates the live
# PR/head/state before dispatching, exempting, or polling so out-of-order
# draft/ready/closed events cannot publish stale evidence or wait on an
# impossible verdict.
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

concurrency:
# Scoped by exact head SHA (not just PR number) so a delayed, out-of-order
# run for an older head cannot cancel the authoritative run already active
# for a newer head -- GitHub cancels whichever run is currently active in
# the group when a new one starts, with no notion of "older"/"newer", so
# sharing a group across different heads let a stale event retire the
# current head's still-valid run before its own live-head check could ever
# reject it (Devin Review on `#1568`). Same-head events (draft<->ready
# transitions, a synchronize retry) still share one group, so
# `converted_to_draft` still cancels an active same-head verdict poll.
group: >-
Comment thread
seonghobae marked this conversation as resolved.
opencode-review-bootstrap-${{
github.event.pull_request.base.repo.full_name || github.repository }}-${{
github.event.pull_request.number || github.run_id }}
github.event.pull_request.number || github.run_id }}-${{
github.event.pull_request.head.sha || github.run_id }}
Comment thread
seonghobae marked this conversation as resolved.
cancel-in-progress: true

permissions:
Expand Down Expand Up @@ -270,11 +288,39 @@ jobs:
WORKFLOW_SHA: ${{ github.workflow_sha }}
run: |
set -euo pipefail
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_draft="$(printf '%s' "$live_pr" | jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end')"
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Comment thread
seonghobae marked this conversation as resolved.
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if [ -z "$live_head" ] || [ -z "$live_draft" ] || [ -z "$live_state" ]; then
echo "::error::Could not validate live pull request state before review dispatch."
exit 1
fi
if [ "$live_state" != "open" ] && [ "$live_state" != "closed" ]; then
echo "::error::Could not validate live pull request state before review dispatch."
exit 1
fi
if [ "${live_head,,}" != "${HEAD_SHA,,}" ]; then
echo "::error::Pull request head moved while validating live review state."
exit 1
Comment thread
seonghobae marked this conversation as resolved.
fi
if [ "$live_state" = "closed" ]; then
echo "PR is closed on the live exact head; a current-head OpenCode review is not requested."
exit 0
fi
if [ "$live_draft" = "true" ]; then
echo "PR is still a draft on the live exact head; a current-head OpenCode review is not requested until it is marked ready for review."
exit 0
fi
Comment thread
seonghobae marked this conversation as resolved.
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
if [ "$PR_DRAFT" = "true" ]; then
echo "Event draft snapshot is stale; continuing current-head OpenCode review dispatch for the live ready PR."
fi
effective_pr_draft="$live_draft"
helper="$(mktemp)"
trap 'rm -f "$helper"' EXIT
gh api "repos/ContextualWisdomLab/.github/contents/scripts/ci/opencode_review_receipt_gate.py?ref=${WORKFLOW_SHA}" \
--jq .content | base64 --decode >"$helper"
receipt_state="$(python3 - "$helper" "$TARGET_REPOSITORY" "$PR_NUMBER" "$HEAD_SHA" "$PR_DRAFT" <<'PY'
receipt_state="$(python3 - "$helper" "$TARGET_REPOSITORY" "$PR_NUMBER" "$HEAD_SHA" "$effective_pr_draft" <<'PY'
Comment thread
seonghobae marked this conversation as resolved.
import importlib.machinery
import importlib.util
import sys
Expand Down Expand Up @@ -334,6 +380,7 @@ jobs:
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_ACTION: ${{ github.event.action }}
PR_DRAFT: ${{ github.event.pull_request.draft }}
run: |
set -euo pipefail
if [ "$PR_ACTION" = "closed" ]; then
Expand All @@ -344,6 +391,33 @@ jobs:
echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict."
exit 1
fi
live_pr="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"
live_head="$(printf '%s' "$live_pr" | jq -r '.head.sha // empty')"
live_draft="$(printf '%s' "$live_pr" | jq -r 'if (.draft | type) == "boolean" then (.draft | tostring) else empty end')"
live_state="$(printf '%s' "$live_pr" | jq -r 'if (.state | type) == "string" then .state else empty end')"
if [ -z "$live_head" ] || [ -z "$live_draft" ] || [ -z "$live_state" ]; then
echo "::error::Could not validate live pull request state before verdict admission."
exit 1
fi
if [ "$live_state" != "open" ] && [ "$live_state" != "closed" ]; then
echo "::error::Could not validate live pull request state before verdict admission."
exit 1
fi
if [ "${live_head,,}" != "${HEAD_SHA,,}" ]; then
echo "::error::Pull request head moved while validating live verdict state."
exit 1
fi
if [ "$live_state" = "closed" ]; then
echo "PR is closed on the live exact head; a current-head OpenCode verdict is not required."
exit 0
fi
if [ "$live_draft" = "true" ]; then
echo "PR is still a draft on the live exact head; a current-head OpenCode verdict is not required until it is marked ready for review."
exit 0
fi
if [ "$PR_DRAFT" = "true" ]; then
echo "Event draft snapshot is stale; continuing verdict polling for the live ready PR."
fi
verdict=""
while :; do
reviews="$(gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews")"
Expand Down Expand Up @@ -385,3 +459,100 @@ jobs:
exit 1
fi
echo "Current-head OpenCode verdict: ${verdict}."
Comment thread
seonghobae marked this conversation as resolved.

cancel-superseded-opencode-review-runs:
# Scoping the concurrency group above by exact head SHA (so a delayed
# old-head run can no longer cancel the authoritative newer-head run --
# Devin Review on `#1568`) also means a *legitimate* new commit no
# longer auto-cancels its own PR's now-obsolete previous-head poll: that
# older run's own live-head check only ran once, before it entered its
# unbounded Reviews API wait, and nothing in that wait loop re-validates
# the head. Left alone, it would occupy a runner until GitHub's own
# per-job ceiling. This job retires it directly, mirroring the
# live-head-validated cleanup pattern in strix.yml's own
# `cancel-superseded-pr-runs` job: every cancellation candidate and
# every cancellation itself is re-verified against the live PR head
# immediately beforehand, so a run for this job that is itself somehow
# delayed/stale cannot wrongly cancel a still-authoritative run.
if: github.event_name == 'pull_request_target' && github.event.action == 'synchronize'
runs-on: ubuntu-latest
permissions:
actions: write
contents: read
pull-requests: read
env:
GH_TOKEN: ${{ github.token }}
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
TARGET_PR_NUMBER: ${{ github.event.pull_request.number }}
TARGET_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
CURRENT_RUN_ID: ${{ github.run_id }}
steps:
- name: Cancel queued and running OpenCode review runs for a superseded pull request head
shell: bash
run: |
set -euo pipefail

live_head_matches() {
local live_head
if ! live_head="$(gh api "repos/${TARGET_REPOSITORY}/pulls/${TARGET_PR_NUMBER}" --jq '.head.sha' 2>/tmp/opencode-cleanup-gh-error)"; then
echo "::warning::OpenCode review cleanup could not verify the live pull request head; leaving runs unchanged."
sed 's/^/ /' /tmp/opencode-cleanup-gh-error >&2 || true
return 1
fi
[ "${live_head,,}" = "${TARGET_PR_HEAD_SHA,,}" ]
}

cancel_runs() {
local status="$1"
if ! live_head_matches; then
echo "::notice::OpenCode review cleanup target changed before run selection; leaving runs unchanged."
return 0
fi
local runs_url="repos/${TARGET_REPOSITORY}/actions/runs?status=${status}&per_page=100"
local runs_json
if ! runs_json="$(gh api --paginate "$runs_url" 2>/tmp/opencode-cleanup-gh-error)"; then
echo "::warning::OpenCode review cleanup could not inspect ${TARGET_REPOSITORY}; leaving runs unchanged."
sed 's/^/ /' /tmp/opencode-cleanup-gh-error >&2 || true
return 0
fi
local run_ids
if ! run_ids="$(jq -r --arg pr "$TARGET_PR_NUMBER" --arg head_sha "$TARGET_PR_HEAD_SHA" \
--arg repo "$TARGET_REPOSITORY" --arg current "$CURRENT_RUN_ID" '
.workflow_runs[]
| select((.id | tostring) != $current)
| select(.name == "Required OpenCode Review")
| select(.event == "pull_request_target")
| ((.display_title // "") | startswith("Required OpenCode Review " + $repo + "#" + $pr + "@")) as $title_matches
| ((.pull_requests // []) | any((.number | tostring) == $pr)) as $metadata_matches
| select($title_matches or $metadata_matches)
| ((.display_title // "") | endswith("@" + $head_sha)) as $title_is_current
| ((.pull_requests // []) | any(
((.number | tostring) == $pr)
and ((.head.sha // "") | ascii_downcase) == ($head_sha | ascii_downcase)
)) as $metadata_is_current
| select(($title_is_current or $metadata_is_current) | not)
| .id
' <<<"$runs_json")"; then
echo "::warning::OpenCode review cleanup received invalid run data for ${TARGET_REPOSITORY}; leaving runs unchanged."
return 0
fi
while IFS= read -r run_id; do
[ -n "$run_id" ] || continue
if ! live_head_matches; then
echo "::notice::OpenCode review cleanup target changed before cancellation; leaving runs unchanged."
return 0
fi
if gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/cancel" >/dev/null 2>/tmp/opencode-cleanup-cancel-error ||
gh api --method POST "repos/${TARGET_REPOSITORY}/actions/runs/${run_id}/force-cancel" >/dev/null 2>>/tmp/opencode-cleanup-cancel-error; then
echo "Cancelled superseded Required OpenCode Review run ${run_id} in ${TARGET_REPOSITORY} for PR #${TARGET_PR_NUMBER}."
else
echo "::warning::OpenCode review cleanup could not cancel run ${run_id} in ${TARGET_REPOSITORY}; it may have finished or the credential lacks Actions write access."
sed 's/^/ /' /tmp/opencode-cleanup-cancel-error >&2 || true
fi
done <<<"$run_ids"
}

for active_status in queued in_progress requested waiting pending; do
cancel_runs "$active_status"
done
echo "Superseded OpenCode review run cleanup completed."
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@ this file. The format follows Keep a Changelog, and versioned releases follow
Semantic Versioning where the repository publishes a release.

## [Unreleased]
- **Fix `opencode-review.yml` admission gaps around stale/out-of-order events (`#1568`).**
Building on the draft-poll exemption's live PR/head validation, Devin Review found two
further defects. (1) The concurrency group was keyed only by repository and PR number, 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 (GitHub cancels
whichever run is currently active in a group with no notion of "older"/"newer"). Fixed by also
scoping the group by exact head SHA, so different heads no longer share a cancellation domain
while same-head events (a `converted_to_draft`/`ready_for_review` transition, a `synchronize`
retry) still do. (2) A delayed non-closed event ignored a live-closed PR, since `live_pr` only
ever extracted `head` and `draft`. Both admission blocks now also validate live `state` and exit
before any further API call when it is `"closed"`, failing closed on a missing, null,
non-string, or otherwise unrecognized value rather than assuming open. 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 failing closed. Full
suite: 2294 passed, 1 skipped, 21 subtests; `scripts/ci` coverage and docstrings both 100%.
A third Devin Review round then found that head-scoping the concurrency group above, while
fixing the wrong-direction cancellation, also disabled the legitimate one: a genuine new
commit no longer cancels its own PR's now-obsolete previous-head poll, which would otherwise
occupy a runner until GitHub's own per-job ceiling. Added a `cancel-superseded-opencode-review-runs`
job, scoped to `synchronize` events, mirroring the already-established live-head-validated
cleanup pattern in `strix.yml`'s `cancel-superseded-pr-runs` job: it re-verifies the live head
immediately before both listing candidates and cancelling each one, 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 executed against synthetic run payloads
(superseded-run selection, current-head/self-run/other-PR/other-workflow exclusion, and
`pull_requests[]` metadata matching), plus a structural test for the job's trigger and
permissions. Full suite: 2301 passed, 1 skipped, 21 subtests; coverage and docstrings both 100%.
- **Fix a live crash: `noema-review` failed with an unhandled `HTTPError` instead
of failing closed.** Live incident on `ContextualWisdomLab/naruon#1486`:
`scripts/ci/noema_review_gate.py::call_llm`'s `opener.open(request)` call sat
Expand Down
Loading
Loading