Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3bc646f
fix(scheduler): stop repository_dispatch defaulting review/merge/bran…
seonghobae Aug 22, 2026
7724e15
fix(ci): update strix quick-gate literal-match assertions for the toJ…
seonghobae Aug 22, 2026
cb64eb3
docs(changelog): correct per-flag independence claim (CodeRabbit find…
seonghobae Aug 22, 2026
c97b5cf
Merge remote-tracking branch 'origin/main' into fix/repository-dispat…
seonghobae Aug 22, 2026
bfe0ac5
Merge remote-tracking branch 'origin/main' into fix/repository-dispat…
seonghobae Aug 22, 2026
4017e95
Merge remote-tracking branch 'origin/main' into fix/repository-dispat…
seonghobae Aug 22, 2026
23f022c
fix(scheduler): also reject the JSON string "false" for the three dis…
seonghobae Aug 22, 2026
23a014e
Merge branch 'main' into fix/repository-dispatch-boolean-default-coer…
opencode-agent[bot] Aug 23, 2026
21b4c58
Merge remote-tracking branch 'origin/main' into fix/repository-dispat…
seonghobae Aug 23, 2026
e6af2af
Merge remote-tracking branch 'origin/main' into fix/repository-dispat…
claude Aug 30, 2026
3a52db2
Merge branch 'main' into fix/repository-dispatch-boolean-default-coer…
seonghobae Aug 30, 2026
ea8a398
Merge branch 'main' into fix/repository-dispatch-boolean-default-coer…
seonghobae Aug 30, 2026
ac80624
Merge remote-tracking branch 'origin/main' into pr-1238
claude Sep 3, 2026
ecbeb5a
merge: main@f2f91b80 into #1238 (fix/repository-dispatch-boolean-defa…
seonghobae Sep 5, 2026
4a4ddc2
Merge branch 'main' into fix/repository-dispatch-boolean-default-coer…
opencode-agent[bot] Sep 6, 2026
db09352
Merge branch 'main' into fix/repository-dispatch-boolean-default-coer…
opencode-agent[bot] Sep 6, 2026
aacbdb4
Merge branch 'main' into fix/repository-dispatch-boolean-default-coer…
opencode-agent[bot] Sep 17, 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
28 changes: 22 additions & 6 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,25 @@ jobs:
MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || '100' }}
PROJECT_FLOW_INPUT: ${{ github.event.client_payload.project_flow || inputs.project_flow || vars.PROJECT_FLOW || '' }}
PULL_REQUEST_NUMBER: ${{ github.event.pull_request.number || github.event.workflow_run.pull_requests[0].number || github.event.client_payload.pr_number || inputs.pr_number || '' }}
TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_run' || github.event_name == 'push' || github.event_name == 'pull_request_target' || (github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false) || inputs.trigger_reviews == true }}
# toJSON(...) != 'false' below (not a direct comparison against the
# boolean literal): GitHub Actions coerces both `false` and an
# absent/`null` client_payload property to 0 for a mixed-type
# inequality check, so that direct-comparison form is false -- not
# true, as the "default enabled unless explicitly disabled" intent
# requires -- whenever a repository_dispatch payload omits the field
# entirely. Stringifying first compares 'null' against 'false', which
# is exact and unaffected by that coercion. Verified empirically: a
# targeted merge-scheduler dispatch with no trigger_reviews field
# produced TRIGGER_REVIEWS=false on the old direct-comparison
# expression before this fix.
TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_run' || github.event_name == 'push' || github.event_name == 'pull_request_target' || (github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.trigger_reviews) != 'false') || inputs.trigger_reviews == true }}
Comment thread
seonghobae marked this conversation as resolved.
Outdated
REVIEW_DISPATCH_LIMIT_INPUT: ${{ github.event.client_payload.review_dispatch_limit || inputs.review_dispatch_limit || vars.REVIEW_DISPATCH_LIMIT || '1' }}
BRANCH_UPDATE_LIMIT_INPUT: ${{ github.event.client_payload.branch_update_limit || inputs.branch_update_limit || vars.BRANCH_UPDATE_LIMIT || '1' }}
ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_run' || (github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false) || inputs.enable_auto_merge == true }}
# Same toJSON(...) != 'false' coercion fix as TRIGGER_REVIEWS above.
ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_run' || (github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.enable_auto_merge) != 'false') || inputs.enable_auto_merge == true }}
MERGE_MODE: ${{ github.event.client_payload.merge_mode || inputs.merge_mode || vars.PR_MERGE_MODE || 'direct_or_auto' }}
UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_run' || (github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false) || inputs.update_branches == true }}
# Same toJSON(...) != 'false' coercion fix as TRIGGER_REVIEWS above.
UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'push' || github.event_name == 'pull_request_target' || github.event_name == 'workflow_run' || (github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.update_branches) != 'false') || inputs.update_branches == true }}
STALE_OPENCODE_MINUTES: ${{ github.event.client_payload.stale_opencode_minutes || inputs.stale_opencode_minutes || vars.STALE_OPENCODE_MINUTES || '90' }}
steps:
- name: Exchange OpenCode app token for scheduler mutations
Expand Down Expand Up @@ -606,10 +619,13 @@ jobs:
ORG_SWEEP_MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || vars.ORG_SWEEP_MAX_PRS || '1000' }}
ORG_SWEEP_REVIEW_DISPATCH_LIMIT: ${{ github.event.client_payload.review_dispatch_limit || inputs.review_dispatch_limit || vars.ORG_SWEEP_REVIEW_DISPATCH_LIMIT || '1' }}
ORG_SWEEP_BRANCH_UPDATE_LIMIT: ${{ github.event.client_payload.branch_update_limit || inputs.branch_update_limit || vars.ORG_SWEEP_BRANCH_UPDATE_LIMIT || '1' }}
ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}
ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}
# toJSON(...) != 'false' coercion fix -- see the TRIGGER_REVIEWS
# comment earlier in this env block for why the natural-looking
# `!= false` form silently defaults to disabled on repository_dispatch.
ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.trigger_reviews) != 'false' || inputs.trigger_reviews == true }}
ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.enable_auto_merge) != 'false' || inputs.enable_auto_merge == true }}
ORG_SWEEP_MERGE_MODE: ${{ github.event.client_payload.merge_mode || inputs.merge_mode || 'direct_or_auto' }}
ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}
ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.update_branches) != 'false' || inputs.update_branches == true }}
ORG_SWEEP_STALE_QUEUE_HOURS: ${{ vars.ORG_SWEEP_STALE_QUEUE_HOURS || '24' }}
# The review-dispatch and branch-update budgets above are organization-wide
# per sweep tick (sized to bound LLM review-provider cost/rate exposure, not
Expand Down
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ Semantic Versioning where the repository publishes a release.

### Fixed

- Made `pr-review-merge-scheduler.yml`'s `TRIGGER_REVIEWS`, `ENABLE_AUTO_MERGE`,
and `UPDATE_BRANCHES` (and their `ORG_SWEEP_` counterparts) actually default
to enabled on a `repository_dispatch`, as their `!= false` comparisons were
written to intend. GitHub Actions coerces both boolean `false` and an
absent/`null` `client_payload` property to `0` for a mixed-type `!=`
comparison, so a targeted self-service dispatch that simply omitted one of
these fields (rather than explicitly setting it to `false`) silently
produced the same `false` result as an explicit opt-out — meaning every
review-dispatch/auto-merge/branch-update self-service call that did not
spell out all three flags as `true` quietly no-op'd. Replaced the direct
comparison with `toJSON(...) != 'false'`, an exact string comparison
unaffected by that coercion. Verified empirically: an identical dispatch
payload produced `TRIGGER_REVIEWS=false` before this fix and
`TRIGGER_REVIEWS=true` after, with no other change.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- Retried the Strix scan up to `STRIX_TRANSIENT_RETRY_PER_MODEL` times, same model, when the log shows the upstream strix-agent Caido sandbox bootstrap timing race (`loginAsGuest failed after N attempts` / `Failed to connect to 127.0.0.1 port <port>`; tracked upstream as usestrix/strix#1036, #1037, #1056). A slow CI runner can exceed strix-agent's fixed 10-attempt sandbox-login budget before its local intercepting proxy is reachable, even though the penetration test itself never started and no vulnerability evidence was produced or lost; the Docker image is already cached from the failed attempt, so a same-model retry is cheap and typically clears the one-off boot race. Not wired into cross-model fallback, since switching LLM models cannot change local sandbox container boot timing.
- Replaced nonexistent `job.workflow_repository` / `job.workflow_sha` / `job.workflow_ref` / `job.workflow_file_path` context references (actionlint: "property ... is not defined in object type") in `pr-review-fix-scheduler.yml`'s called-workflow source verification and `exact-artifact-sbom-attestation.yml`'s trusted-verifier checkout. Both always failed closed on the missing properties (ContextualWisdomLab/.github#1212) or, for the SBOM attestation checkout, silently resolved an empty repository/ref instead of the pinned trusted source (downstream `gh attestation verify --signer-repo`/`--signer-workflow`, using the separately hardcoded `SIGNER_REPOSITORY` constant rather than any workflow_ref, still failed closed on the resulting empty signer identity). `github.workflow_ref`/`github.workflow_sha` are real, documented properties, but for a `workflow_call` target they reflect the top-level *calling* workflow, not the reusable workflow's own file — a prefix match against the reusable workflow's own path can never succeed. `exact-artifact-sbom-attestation.yml`'s checkout now uses `github.workflow_sha` (correct today: it has no callers yet); `pr-review-fix-scheduler.yml`'s identity check instead validates `github.repository`, since every current caller uses a local, same-repo `uses: ./...` where caller and callee share one commit and `github.workflow_sha` is still the right pin. Tracked follow-up for the SBOM attestation checkout once a real (potentially cross-repo) caller exists: ContextualWisdomLab/.github#1228.
- Used the receiving repository's workflow token for same-repository scheduler
Expand Down
18 changes: 18 additions & 0 deletions docs/doctoring/agent-mention-concurrency-isolation.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@ Trusted `@opencode-agent` comments could remain unacknowledged and fail to start

None of these defects is evidence that the requesting maintainer, model, repository allowlist, or final review result is invalid.

**Follow-up — scheduler-side root cause of item 4 closed.** The wrapper-side
workaround above (always send `trigger_reviews=true`, `enable_auto_merge`,
and `update_branches` explicitly) remains correct and in place, but it
addressed only one caller. `pr-review-merge-scheduler.yml`'s
`TRIGGER_REVIEWS`/`ENABLE_AUTO_MERGE`/`UPDATE_BRANCHES` env-block expressions
(and their `ORG_SWEEP_` counterparts) used `client_payload.<field> != false`,
which GitHub Actions evaluates identically for an explicit `false` and for a
field the payload never mentions at all (both coerce to `0` on a mixed-type
`!=` comparison) — so *any* `repository_dispatch` caller that omitted one of
these three fields silently got the disabled behavior, not only this
wrapper. Fixed at the scheduler by comparing `toJSON(client_payload.<field>)
!= 'false'` instead, an exact string comparison the `null`/`false` coercion
cannot collapse. Verified empirically against a live dispatch: an identical
payload produced `TRIGGER_REVIEWS=false` before the fix and `true` after.
Every existing explicit-payload caller (including this wrapper) is
unaffected; a caller that now omits the field gets the originally-intended
default instead of a silent no-op.

## Test-first repair

The permanent regression contracts were committed before their corresponding production changes.
Expand Down
16 changes: 13 additions & 3 deletions tests/test_required_workflow_queue_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,20 +882,30 @@ def test_org_queue_sweep_manual_cadence_inputs_reach_the_sweep_job() -> None:
"ORG_SWEEP_MAX_PRS: ${{ github.event.client_payload.max_prs || inputs.max_prs || vars.ORG_SWEEP_MAX_PRS || '1000' }}"
) in workflow
assert (
"ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.trigger_reviews != false || inputs.trigger_reviews == true }}"
"ORG_SWEEP_TRIGGER_REVIEWS: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.trigger_reviews) != 'false' || inputs.trigger_reviews == true }}"
in workflow
)
assert (
"ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.enable_auto_merge != false || inputs.enable_auto_merge == true }}"
"ORG_SWEEP_ENABLE_AUTO_MERGE: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.enable_auto_merge) != 'false' || inputs.enable_auto_merge == true }}"
) in workflow
assert (
"ORG_SWEEP_MERGE_MODE: ${{ github.event.client_payload.merge_mode || inputs.merge_mode || 'direct_or_auto' }}"
in workflow
)
assert (
"ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && github.event.client_payload.update_branches != false || inputs.update_branches == true }}"
"ORG_SWEEP_UPDATE_BRANCHES: ${{ github.event_name == 'schedule' || github.event_name == 'repository_dispatch' && toJSON(github.event.client_payload.update_branches) != 'false' || inputs.update_branches == true }}"
in workflow
)
# Neither the top-level nor the org-sweep variant of these three flags
# may use the bare `!= false` form: GitHub Actions coerces an absent
# repository_dispatch payload field and boolean `false` to the same
# value (0) for a mixed-type comparison, so `!= false` is false --
# meaning "not explicitly disabled" -- for both a payload that
# explicitly disabled the flag AND one that never mentioned it at all.
# Verified empirically against a live dispatch before this fix.
assert "client_payload.trigger_reviews != false" not in workflow
assert "client_payload.enable_auto_merge != false" not in workflow
assert "client_payload.update_branches != false" not in workflow
assert 'if [ "$ORG_SWEEP_TRIGGER_REVIEWS" = "true" ]; then' in workflow
assert 'if [ "$ORG_SWEEP_ENABLE_AUTO_MERGE" = "true" ]; then' in workflow
assert '--merge-mode "$ORG_SWEEP_MERGE_MODE"' in workflow
Expand Down
Loading