Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 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
65 changes: 54 additions & 11 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,17 +264,45 @@ 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')"
# 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' "$(printf '%s' "$pr" | jq -r '.base.ref')" >>"$GITHUB_OUTPUT"
printf 'base_sha=%s\n' "$(printf '%s' "$pr" | jq -r '.base.sha')" >>"$GITHUB_OUTPUT"
printf 'head_ref=%s\n' "$(printf '%s' "$pr" | jq -r '.head.ref')" >>"$GITHUB_OUTPUT"
printf 'head_sha=%s\n' "$(printf '%s' "$pr" | jq -r '.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="[]"
Expand Down Expand Up @@ -313,16 +341,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 +401,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
68 changes: 67 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,71 @@ 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.
- 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 +736,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
41 changes: 41 additions & 0 deletions docs/product-technical-gap-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -2344,6 +2344,47 @@ 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 three 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.

**Net result.** Stale workflow reruns can no longer grant a draft/closed exemption or dispatch stale
base/head metadata — both the pass/fail decision and the dispatch payload are sourced from one live
PR-state fetch per run, never from the frozen triggering-event payload. Full regression coverage in
Comment thread
seonghobae marked this conversation as resolved.
Outdated
`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