Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
98ee393
fix(ci): exempt draft PRs from the opencode-review current-head verdi…
claude Aug 30, 2026
951231d
Merge branch 'main' into claude/opencode-review-draft-gate-fix
seonghobae Aug 30, 2026
644255a
fix(ci): rerun the opencode-review draft gate on draft reconversion
claude Aug 30, 2026
f4bf13a
Merge branch 'main' into claude/opencode-review-draft-gate-fix
seonghobae Aug 30, 2026
43d7e89
Merge remote-tracking branch 'origin/main' into claude/opencode-revie…
claude Aug 31, 2026
0999e2c
fix(ci): also skip the OpenCode dispatch step for draft PRs
claude Aug 31, 2026
756987b
Merge origin/main into claude/opencode-review-draft-gate-fix
claude Sep 1, 2026
a279b45
Merge main (opencode-review's event-driven redesign) and re-derive th…
claude Sep 1, 2026
0de29e0
fix(ci): stop trusting stale event payload for opencode-review draft/…
claude Sep 1, 2026
0d897e2
fix(ci): gate opencode-review dispatch on the live verdict alone
claude Sep 1, 2026
ba405e8
fix: remove trailing whitespace in CHANGELOG.md (git diff --check gate)
claude Sep 1, 2026
ba267e9
fix(ci): source opencode-review dispatch payload from live PR state
claude Sep 1, 2026
7dbd94c
Merge remote-tracking branch 'origin/main' into claude/opencode-revie…
claude Sep 1, 2026
d3a31ff
docs(gap-baseline): record opencode-review draft-gate fix traceability
claude Sep 1, 2026
413a362
docs(gap-baseline): correct overstated live-state sourcing claim (Dev…
claude Sep 1, 2026
c3d18f9
fix(ci): match opencode-review formal reviews against the live head SHA
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
76 changes: 64 additions & 12 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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]
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]
Comment thread
seonghobae marked this conversation as resolved.

concurrency:
group: >-
Expand Down Expand Up @@ -264,22 +264,59 @@ jobs:
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_ACTION: ${{ github.event.action }}
run: |
set -euo pipefail
if [ "$PR_ACTION" = "closed" ]; then
if [ -z "${PR_NUMBER:-}" ] || [ -z "${HEAD_SHA:-}" ]; then
echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict."
exit 1
fi
# Decide closed/draft from the pull request's live state, never from
# the triggering event's own stored payload -- a manual re-run of an
# old workflow run (e.g. an old converted_to_draft event) replays
# that event's github.event.* fields verbatim, which would let a
# since-ready, unreviewed PR at the same head SHA pass this required
# check on a stale "still draft" reading (Devin review on #1443).
if ! pr="$(timeout 25 gh api "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}")"; then
echo "::error::Could not fetch the pull request's live state; cannot verify a current-head OpenCode verdict."
exit 1
fi
pr_state="$(printf '%s' "$pr" | jq -r '.state // ""')"
pr_draft="$(printf '%s' "$pr" | jq -r '.draft // false')"
pr_base_ref="$(printf '%s' "$pr" | jq -r '.base.ref')"
pr_base_sha="$(printf '%s' "$pr" | jq -r '.base.sha')"
pr_head_ref="$(printf '%s' "$pr" | jq -r '.head.ref')"
pr_head_sha="$(printf '%s' "$pr" | jq -r '.head.sha')"
# Exposed for the dispatch step below so its repository_dispatch
# payload also comes from this same live fetch, not from
# github.event.pull_request.* -- a manual re-run of an old job
# would otherwise still build the dispatch payload from that stale
# event payload, and opencode-review-dispatch.yml's live
# validate-pr-metadata check hard-rejects a stale base_ref/base_sha,
# so the dispatch would fail even once this step correctly decides
# one is needed (Devin review on #1443).
printf 'base_ref=%s\n' "$pr_base_ref" >>"$GITHUB_OUTPUT"
printf 'base_sha=%s\n' "$pr_base_sha" >>"$GITHUB_OUTPUT"
printf 'head_ref=%s\n' "$pr_head_ref" >>"$GITHUB_OUTPUT"
printf 'head_sha=%s\n' "$pr_head_sha" >>"$GITHUB_OUTPUT"
if [ "$pr_state" = "closed" ]; then
echo "PR closed; a current-head OpenCode verdict is not required."
echo "verdict=CLOSED" >>"$GITHUB_OUTPUT"
exit 0
fi
if [ -z "${PR_NUMBER:-}" ] || [ -z "${HEAD_SHA:-}" ]; then
echo "::error::Missing PR number or head SHA; cannot verify a current-head OpenCode verdict."
exit 1
if [ "$pr_draft" = "true" ]; then
echo "PR is a draft; a current-head OpenCode verdict is not required until it is marked ready for review."
echo "verdict=DRAFT" >>"$GITHUB_OUTPUT"
exit 0
fi
Comment thread
seonghobae marked this conversation as resolved.
if ! reviews="$(timeout 25 gh api --paginate "repos/${TARGET_REPOSITORY}/pulls/${PR_NUMBER}/reviews")"; then
reviews="[]"
fi
verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$HEAD_SHA" '
# Match against the live head SHA fetched above, not the event's
# own HEAD_SHA -- a stale rerun's event payload could otherwise
# match an approval that was only ever valid for a predecessor
# head, or miss a real approval already posted against the actual
# live head (owner direction, Devin review on #1443).
verdict="$(printf '%s\n' "$reviews" | jq -r -s --arg sha "$pr_head_sha" '
(add // [])
| [
.[]
Expand Down Expand Up @@ -313,16 +350,28 @@ jobs:
fi

- name: Request current-head OpenCode review execution
if: github.event.action != 'closed' && steps.verdict.outputs.verdict == ''
# steps.verdict.outputs.verdict alone is the live, authoritative
# signal now: the verdict step resolves closed/draft from a live
# gh api call, not this event's own stored payload, so it is already
# empty only when a real dispatch is actually needed. The former
# extra github.event.action/github.event.pull_request.draft conjuncts
# were themselves stale-payload reads -- a manual re-run of an old
# closed/draft-era job could still suppress this dispatch on a since-
# reopened/ready PR even after the verdict step's own fix, leaving the
# required check red with no review ever requested (Devin review on
# #1443).
if: steps.verdict.outputs.verdict == ''
Comment thread
seonghobae marked this conversation as resolved.
env:
OIDC_AUDIENCE: opencode-github-action
OPENCODE_API_BASE_URL: https://api.opencode.ai
TARGET_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_BRANCH: ${{ github.event.pull_request.base.ref }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# Sourced from the verdict step's own live gh api fetch, not
# github.event.pull_request.* -- see the comment on that step.
BASE_BRANCH: ${{ steps.verdict.outputs.base_ref }}
BASE_SHA: ${{ steps.verdict.outputs.base_sha }}
HEAD_BRANCH: ${{ steps.verdict.outputs.head_ref }}
HEAD_SHA: ${{ steps.verdict.outputs.head_sha }}
run: |
set -euo pipefail
if [ -z "${ACTIONS_ID_TOKEN_REQUEST_TOKEN:-}" ] || [ -z "${ACTIONS_ID_TOKEN_REQUEST_URL:-}" ]; then
Expand Down Expand Up @@ -361,6 +410,9 @@ jobs:
if [ "$VERDICT" = "CLOSED" ]; then
exit 0
fi
if [ "$VERDICT" = "DRAFT" ]; then
exit 0
fi
if [ -z "$VERDICT" ]; then
echo "::error::No APPROVED or CHANGES_REQUESTED from opencode-agent on the current head. This required check is not a review and must not succeed until the authenticated dispatch posts a current-head verdict."
exit 1
Expand Down
78 changes: 77 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,81 @@ 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`'s required `opencode-review-target` check
reporting a hard `exit 1` failure on every push to a draft PR, forever,
until the PR is marked ready for review. Root cause:
`scripts/ci/pr_review_merge_scheduler.py` deliberately never dispatches an
OpenCode review request for a draft PR (`if pr.get("isDraft"): return
Decision(number, "skip", "draft PR")`), but the required check had no
draft handling and unconditionally demanded a current-head verdict on
every `opened`/`synchronize`/`reopened` event regardless. The
`Resolve current-head formal OpenCode verdict` step now exits early with
`verdict=DRAFT` for a draft PR (mirroring its existing `closed` early
exit), the `Request current-head OpenCode review execution` dispatch
step's own `if:` also skips drafts (so a transient OIDC/dispatch failure
can't turn a draft PR's required check red before the exemption even
runs), and the trivial `Fail closed without a current-head OpenCode
verdict` step treats `VERDICT=DRAFT` the same as `VERDICT=CLOSED`. Also
adds `converted_to_draft` to the workflow's `pull_request_target` trigger
types, so a ready PR converted back to draft with no new commit still
gets a fresh required-workflow run that can apply the exemption (without
it, its previously failed check would show failure indefinitely). This
was re-derived from scratch against the event-driven
`opencode-review-target` design `#1507`/`#1532` landed on `main` (the
325-minute synchronous poll loop this fix originally targeted no longer
exists); see `tests/test_opencode_required_verdict_regression.py` for
shell-level regression coverage executing each step's actual production
body, including the new `_run_verdict_step`/`_run_fail_closed_step`
helpers matching the three-step split.
- Fix a review-flagged gap in the draft-gate fix above (Devin review on
`#1443`): the `Resolve current-head formal OpenCode verdict` step decided
closed/draft purely from the triggering event's own stored payload
(`github.event.action`/`github.event.pull_request.draft`). A manual
re-run of an old workflow run (e.g. a stale `converted_to_draft` run)
replays that event's payload verbatim, so a since-ready, unreviewed PR at
the same head SHA could pass this required check on a stale "still draft"
reading — a real required-review bypass, not merely a false alarm. The
step now fetches the pull request's live state (`gh api
repos/.../pulls/<number>`) and decides closed/draft from that instead,
failing closed if the lookup itself fails; the two payload-derived `env:`
vars (`PR_ACTION`/`PR_DRAFT`) are removed entirely from this step.
- Fix a second review-flagged gap in the same lineage (Devin review on
`#1443`): the `Request current-head OpenCode review execution` dispatch
step's own `if:` still gated on `github.event.action`/
`github.event.pull_request.draft` alongside `steps.verdict.outputs.verdict
== ''`. Once the verdict step above was fixed to resolve closed/draft from
live state, that made the dispatch step's stale-payload conjuncts a
liability rather than a safety net: a manual re-run of an old closed/draft
job could still suppress the dispatch for a since-reopened/ready PR at the
same head SHA, leaving the required check red with no review ever
requested. The dispatch step now gates purely on
`steps.verdict.outputs.verdict == ''`, the one live-computed signal that
already means "a real dispatch is needed."
- Fix a third review-flagged gap in the same lineage (Devin review on
`#1443`): even with the dispatch step correctly enabled from the live
verdict, it still built its `repository_dispatch` payload's
`pr_base_ref`/`pr_base_sha`/`pr_head_ref`/`pr_head_sha` fields from
`github.event.pull_request.*` — the same stale-payload source the two
fixes above removed from the pass/fail decision. On a manual re-run of an
old job whose base branch has since advanced (head SHA unchanged),
`opencode-review-dispatch.yml`'s live `validate-pr-metadata` check
hard-rejects that stale `base_sha`, so the dispatch would fail even though
the verdict step correctly decided one was needed — leaving the required
check red with no review ever requested. The verdict step now exposes
`base_ref`/`base_sha`/`head_ref`/`head_sha` as step outputs from the same
live `gh api repos/.../pulls/<number>` response it already uses for
closed/draft, and the dispatch step builds its payload from
`steps.verdict.outputs.*` instead of the event payload.
- Fix a fourth gap in the same lineage (owner direction, agreeing with and
extending a Devin review comment on `#1443`): the formal-review-matching
jq query still keyed off the triggering event's own `HEAD_SHA`, the one
event-derived value the three fixes above hadn't yet touched. A stale
rerun's event payload could therefore match an approval that was only
ever valid for a predecessor head, or miss a real approval already posted
against the actual live head. The verdict step now matches reviews
against the live `head.sha` from the same `gh api` fetch it already uses
for closed/draft/dispatch metadata, so a stale rerun can neither accept a
predecessor-head approval nor miss a live-head one.
- Fail closed when the first top-level Noema JSON candidate is malformed,
preventing a later approval object from overriding malformed preface data;
multiple-object output remains supported when its first object is valid.
Expand Down Expand Up @@ -671,7 +746,8 @@ Semantic Versioning where the repository publishes a release.
Informational, no change: the gap-baseline's repeated review-round
narrative is this repo's own documented, intentional convention
(ADR-0002: the baseline is "an operational snapshot," not a duplicate of
the ADR's design record), not accidental redundancy.- Raise `contextual_orchestrator_review_sidecar.sh`'s
the ADR's design record), not accidental redundancy.
- Raise `contextual_orchestrator_review_sidecar.sh`'s
`ORCHESTRATOR_CATALOG_FAMILY_CAP` default from 4 to 8: root-caused the
live "no provider route passed the Strix plain-chat preflight" outage
blocking `noema-review`/`opencode-review`/`strix` org-wide to
Expand Down
51 changes: 51 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,57 @@ contract assertion, and `docs/adr/0003-contextual-orchestrator-vendored-free-zdr
"today" reference. Landed in the same PR (`#1463`) as the streaming revert,
not split out, since the revert is unsafe without it.

## 2026-09-01 opencode-review.yml draft-gate fix (`.github#1443`): three-round live-state hardening, tied to `#1531` queue pressure

**Buyer/control-plane effect.** Before this fix, every draft PR org-wide showed the required
`opencode-review` check as a hard, permanent `exit 1` failure: `scripts/ci/pr_review_merge_scheduler.py`
deliberately never dispatches an OpenCode review request for a draft PR
(`if pr.get("isDraft"): return Decision(number, "skip", "draft PR")`), but the required check had no
draft handling and unconditionally demanded a current-head verdict. Net effect: draft PRs allocated
required-workflow OpenCode review dispatch work they could never complete or merge from — the exact
kind of wasted required-workflow queue allocation `#1531` tracks — while presenting a false-alarm
failure with no actionable next step for the PR author.

**Fix, in four review-driven rounds, all on `.github#1443`:**
1. The `Resolve current-head formal OpenCode verdict` step now exits early (`verdict=DRAFT`) for a
draft PR, mirroring its existing `closed` early exit, and the `Request current-head OpenCode review
execution` dispatch step's own `if:` matches — so a draft PR no longer dispatches OpenCode review
work it can never merge from. `converted_to_draft` was added to the workflow's `pull_request_target`
trigger types so a ready-to-draft conversion with no new commit still gets a fresh required-workflow
run that can apply the exemption.
2. Devin review found the draft/closed decision, and later the dispatch step's own `if:`, were still
reading `github.event.action`/`github.event.pull_request.draft` — the *triggering event's own stored
payload*. A manual re-run of an old workflow run (e.g. a stale `converted_to_draft` run) replays that
payload verbatim, so a since-ready, unreviewed PR at the same head SHA could pass the required check
on a stale "still draft" reading, or a since-ready PR's real dispatch could stay wrongly suppressed.
Both steps now decide from the pull request's *live* state (one `gh api repos/.../pulls/<number>`
fetch per run), failing closed if the lookup itself fails.
3. Devin review (third round) found that even with the dispatch step correctly gated on the live
verdict, it still built its `repository_dispatch` payload's `pr_base_ref`/`pr_base_sha`/
`pr_head_ref`/`pr_head_sha` from that same stale event payload — so a re-run after the base branch
advanced could still have its dispatch rejected by `opencode-review-dispatch.yml`'s live
`validate-pr-metadata` check, leaving the required check red with no review ever requested. The
verdict step now exposes `base_ref`/`base_sha`/`head_ref`/`head_sha` as step outputs from that same
live fetch, and the dispatch step builds its payload from those outputs instead.
4. Devin review flagged, and the owner's direction extended, one remaining event-derived value: the
formal-review-matching jq query still keyed off the triggering event's own `HEAD_SHA`, the one value
the first three rounds hadn't yet touched. A stale rerun's event payload could therefore match an
approval that was only ever valid for a predecessor head, or miss a real approval already posted
against the actual live head. The verdict step now matches reviews against the same live `head.sha`
it already fetches for closed/draft/dispatch metadata, so a stale rerun can neither accept a
predecessor-head approval nor miss a live-head one. Regression coverage exercises both directions
with an event `head_sha` deliberately distinct from the live one.

**Net result.** Stale workflow reruns can no longer grant a draft/closed exemption, dispatch stale
base/head metadata, or have their formal-review match accept a predecessor-head approval or miss a
live-head one — the exemption decision, the review match, and the dispatch payload are all sourced
from one live PR-state fetch per run, never from the frozen triggering-event payload. Full regression
coverage in `tests/test_opencode_required_verdict_regression.py` executes each step's actual production
bash body against fake `gh` fixtures. This closes one confirmed, now-eliminated source of
required-workflow queue waste (draft PRs); it does not by itself resolve the separate org-wide
runner-capacity congestion
`#1531` also tracks.

## 5. 실행 루프와 고객의 다음 행동

각 hourly pass는 아래 순서를 유지한다.
Expand Down
2 changes: 1 addition & 1 deletion scripts/ci/test_strix_quick_gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ assert_opencode_review_uses_codegraph_and_contextual_orchestrator() {
local opencode_config="$REPO_ROOT/opencode.jsonc"

assert_file_contains "$bootstrap_file" "pull_request_target:" "opencode required workflow loads its metadata-only bootstrap from the protected base ref"
assert_file_contains "$bootstrap_file" "types: [opened, synchronize, reopened, ready_for_review, closed]" "opencode required workflow reacts to current PR head changes and closed-PR cleanup"
assert_file_contains "$bootstrap_file" "types: [opened, synchronize, reopened, ready_for_review, converted_to_draft, closed]" "opencode required workflow reacts to current PR head changes, draft reconversion, and closed-PR cleanup"
assert_file_contains "$bootstrap_file" "required-workflow-bootstrap:" "opencode required workflow materializes at least one job for pull_request ruleset runs"
assert_file_contains "$bootstrap_file" "Required OpenCode workflow materialized without checking out or" "opencode required workflow bootstrap documents its data-only trust boundary"
assert_file_contains "$bootstrap_file" "coverage-source-tree:" "opencode required workflow preserves the stable coverage-source-tree branch-protection context"
Expand Down
Loading
Loading