Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 34 additions & 25 deletions .github/workflows/opencode-review.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5044,48 +5044,57 @@ jobs:
esac
echo "::endgroup::"

- name: Dispatch merge scheduler after approval
- name: Run merge scheduler after approval
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.opencode_app_token.outputs.token }}
SCHEDULER_MUTATION_TOKEN_SOURCE: ${{ secrets.PR_REVIEW_MERGE_TOKEN != '' && 'PR_REVIEW_MERGE_TOKEN' || secrets.OPENCODE_APPROVE_TOKEN != '' && 'OPENCODE_APPROVE_TOKEN' || steps.opencode_app_token.outputs.available == 'true' && 'opencode-app' || 'missing' }}
GH_REPOSITORY: ${{ github.event.pull_request.base.repo.full_name || github.event.inputs.target_repository || github.repository }}
PR_BASE_REF: ${{ github.event.pull_request.base.ref || github.event.inputs.pr_base_ref || '' }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number || '' }}
run: |
set -euo pipefail
scheduler_ref="$(
if [ -z "${GH_TOKEN:-}" ]; then
echo "::warning::Merge scheduler follow-up skipped after approval because no mutation credential was available. Required-workflow PR events and schedules remain authoritative."
exit 0
fi

default_branch="$(
gh api "repos/${GH_REPOSITORY}" --jq '.default_branch // empty' 2>/dev/null || true
)"
scheduler_ref="${scheduler_ref:-${PR_BASE_REF:-main}}"

scheduler_inputs=(
-f dry_run=false
-f trigger_reviews=false
-f enable_auto_merge=true
-f merge_mode=direct_or_auto
-f update_branches=true
base_branch="${PR_BASE_REF:-${default_branch:-main}}"
project_flow="github-flow"
case "$base_branch" in
develop) project_flow="git-flow" ;;
main|master) project_flow="github-flow" ;;
esac

args=(
--repo "$GH_REPOSITORY"
--base-branch "$base_branch"
--max-prs 1
--project-flow "$project_flow"
--review-workflow "Required OpenCode Review"
--security-workflow "Strix Security Scan"
--review-dispatch-limit 0
--no-trigger-reviews
--enable-auto-merge
--merge-mode direct_or_auto
--update-branches
)
if [ -n "${PR_NUMBER:-}" ]; then
scheduler_inputs+=(-f max_prs=1 -f pr_number="$PR_NUMBER")
else
scheduler_inputs+=(-f max_prs=100)
args+=(--pr-number "$PR_NUMBER")
fi

dispatch_status=1
dispatch_error="$(mktemp)"
scheduler_status=1
for attempt in 1 2 3; do
: >"$dispatch_error"
if gh workflow run pr-review-merge-scheduler.yml \
--repo "$GH_REPOSITORY" \
--ref "$scheduler_ref" \
"${scheduler_inputs[@]}" 2>"$dispatch_error"; then
dispatch_status=0
if python3 scripts/ci/pr_review_merge_scheduler.py "${args[@]}"; then
scheduler_status=0
break
fi
sed 's/^/scheduler dispatch: /' "$dispatch_error" >&2 || true
sleep "$((attempt * 5))"
done

if [ "$dispatch_status" -ne 0 ]; then
printf '::warning::Merge scheduler dispatch failed after approval; leaving OpenCode approval intact. Repository=%s ref=%s. The scheduled and workflow_run scheduler paths remain authoritative.\n' "$GH_REPOSITORY" "$scheduler_ref"
if [ "$scheduler_status" -ne 0 ]; then
printf '::warning::Merge scheduler follow-up failed after approval; leaving OpenCode review intact. Repository=%s base=%s. The scheduled and PR-event scheduler paths remain authoritative.\n' "$GH_REPOSITORY" "$base_branch"
fi
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ review is approved, no current-head failed check is present, and GitHub reports
the PR as behind. After that update, the new head must pass OpenCode, Strix,
required checks, and review-thread gates again before auto-merge or
`--match-head-commit` merge can proceed.
Branch updates run through the workflow `GITHUB_TOKEN`, so GitHub records those
mechanical updates as `github-actions[bot]` rather than an OpenCode app token or
a personal token. That path uses the pull-request branch update API and should
only need `pull-requests: write`; it does not justify widening repository
`contents` permission. Merge or auto-merge is a separate mutation. When a repo
wants GitHub Actions to perform the merge itself, that repo needs an explicit
scheduler-job `contents: write` policy exception and should expect Scorecard or
token-permission policy review to notice it.
Branch updates and merges run through the central scheduler mutation credential:
`PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, the exchanged OpenCode GitHub
App token, or finally the target workflow token. The scheduler reports the
credential class in its decision output. The OpenCode review job does not widen
its own `pull_request_target` job token to repository-write permission; its
immediate post-approval scheduler follow-up uses only an explicit merge token or
the OpenCode app token, otherwise it leaves the separate scheduler required
workflow and schedule authoritative.
That `update_branch` path is deliberately not used for `DIRTY` or
`CONFLICTING` PRs: GitHub cannot synthesize a safe conflict resolution for the
author, so the review must give the author a repair path instead of pretending
Expand All @@ -44,9 +44,9 @@ rebase, `git status --short`, resolved-file staging, normal push, and
Strix, OpenCode, and the scheduler are sourced from the central
`ContextualWisdomLab/.github` workflows rather than copied into each repository.
Required-workflow runs execute in the target repository context, so mechanical
branch updates, stale-thread resolution, and merges use that repository's
`github-actions[bot]` token while the trusted implementation still comes from
the central repository. The scheduler dispatches same-head Strix evidence first,
branch updates, stale-thread resolution, and merges use the configured central
mutation credential while the trusted implementation still comes from the
central repository. The scheduler dispatches same-head Strix evidence first,
then dispatches OpenCode for the same PR head when review evidence is missing or
stale.
This avoids running PR-head review, CodeGraph, coverage, or PoC code as an
Expand Down
10 changes: 5 additions & 5 deletions docs/org-required-workflow-rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,12 @@ The central `.github/workflows/pr-review-merge-scheduler.yml` is now part of the
- Stable required check job name: `scan-pr-queue`
- Trusted source: `ContextualWisdomLab/.github`
- PR-event scope: when GitHub invokes the workflow for a PR, the scheduler passes `--pr-number` and inspects only that PR instead of scanning or mutating the whole repository queue
- Token posture: the workflow passes `GH_TOKEN: ${{ github.token }}` so stale-thread resolution, branch update, auto-merge, and direct merge mutations are attributed to the target repository's `github-actions[bot]`
- Token posture: the workflow passes the first available mutation credential in this order: `PR_REVIEW_MERGE_TOKEN`, `OPENCODE_APPROVE_TOKEN`, exchanged OpenCode GitHub App token, then the target repository workflow token. The scheduler reports the non-secret token source and expected actor class in every mutation decision.
- Flow posture: default branches named `main` or `master` are treated as GitHub Flow; default branches named `develop` are treated as Git Flow unless a repository explicitly sets `PROJECT_FLOW`
- Merge posture: the default merge mode is `direct_or_auto`. When a current-head approved PR is same-repository and GitHub mergeability is `CLEAN`, the scheduler requests an immediate guarded squash merge with `--match-head-commit` before attempting a branch update. When the PR is approved but not yet clean, the scheduler may keep native auto-merge queued while waiting for branch freshness, checks, or GitHub mergeability to clear.
- Merge posture: the default merge mode is `direct_or_auto`. When a current-head approved PR is same-repository and the scheduler has no failed-check, action-required, unresolved-thread, or conflict blocker, it requests an immediate guarded squash merge with `--match-head-commit`. This includes PRs where native GitHub auto-merge is already enabled; native auto-merge is a fallback queue, not the scheduler's first stop when direct merge is possible.
- Fork posture: fork or external-head PRs remain reviewable, but the scheduler does not direct-merge them and does not enable auto-merge for them. A maintainer must make the final merge decision after same-head OpenCode approval, same-head Strix evidence, required checks, and unresolved-thread checks are clean.
- Branch freshness posture: the scheduler also runs after protected base-branch pushes to `main`, `develop`, or `master`, because those pushes can create the GitHub UI state where reviews are satisfied, auto-merge is enabled, checks are stale or failed, and the PR shows `Update branch` without a PR `synchronize` event.
- Auto-merge posture: `auto_merge_enabled` PR events trigger the scheduler so an already stale branch is refreshed immediately after native auto-merge is turned on instead of waiting for the two-hour schedule.
- Auto-merge posture: `auto_merge_enabled` PR events trigger the scheduler so an already stale branch is refreshed immediately after native auto-merge is turned on instead of waiting for the periodic schedule. If the same PR is already mergeable, the scheduler attempts the guarded direct merge immediately.
- Automation boundary: current-head failed checks and `ACTION_REQUIRED` checks are reported before branch updates, so an update attempt does not hide the concrete reason a PR cannot merge. `update-branch` handles approved `BEHIND` PRs and already queued auto-merge PRs only when there is no current-head failed or action-required check to diagnose first. `DIRTY` or `CONFLICTING` PRs still require author or maintainer conflict resolution guidance; current-head approved conflicts may keep or queue native GitHub auto-merge as a wait state while the conflict is repaired, but the scheduler must not treat queued auto-merge as a conflict resolver.
- Retry posture: before retrying OpenCode, the scheduler force-cancels older active OpenCode runs for the same PR number and a previous head SHA. It does not automatically cancel Strix runs because security evidence should not be silently discarded by force-push churn.

Expand Down Expand Up @@ -103,7 +103,7 @@ The active ruleset no longer maintains a repository-name allowlist. Live ruleset

- On 2026-06-30 08:33 KST, organization ruleset `18156473` was changed from an explicit repository-name list to `repository_name.include=["~ALL"]` while keeping `ref_name.include=["~DEFAULT_BRANCH"]` and the same three central required workflow paths from `.github@refs/heads/main`.
- `.github` scheduler default merge mode is now `direct_or_auto`: approved same-repository `CLEAN` PRs request immediate guarded merge, approved non-clean same-repository PRs can queue native auto-merge, and fork or external-head PRs are left for maintainer merge.
- OpenCode approval dispatches the merge scheduler with `pr_number` and `max_prs=1`, so the just-approved PR is inspected immediately instead of waiting for a broad queue scan to reach it.
- OpenCode approval runs the trusted central merge scheduler script directly with `pr_number` and `max_prs=1`, so the just-reviewed PR is inspected immediately even when organization required workflows are not repo-local `workflow_dispatch` targets.
- `.github` PR `#74` changed OpenCode review model order to DeepSeek R1 first and added a catalog fallback pool.
- `.github` PR `#75` removed the Strix finding against the scheduler command wrapper by using `subprocess.run(..., check=True)` and preserving the existing scrubbed failure contract.
- `.github` main Strix run `28218982899` passed after PR `#75` merged.
Expand Down Expand Up @@ -132,7 +132,7 @@ The active ruleset no longer maintains a repository-name allowlist. Live ruleset
- `.github` PR `#145` treats compare API `status: behind` as branch-staleness evidence even when `behind_by` is missing or zero, so an auto-merge-enabled PR with failed checks and a visible GitHub "Update branch" action requests `update_branch` before disabling auto-merge. It merged at 2026-06-29 23:14 KST with merge commit `1ec0f3dcc7250fdf4a5a3ec6c26feaa98cce4f48`.
- Live dry runs on 2026-06-30 00:40 KST found update-branch candidates in `.github` PR `#147` and `naruon` PR `#803`. The follow-up scheduler trigger change runs the central queue scan after base-branch pushes and `auto_merge_enabled` events, so those UI-visible stale-branch states are not left waiting only for the periodic schedule.
- `.github` PR `#151` added protected base-branch `push` triggers and the `auto_merge_enabled` PR event to the central scheduler, then merged at 2026-06-30 00:56 KST with merge commit `00018f7783522447a71acd08a946e3504e18ff74`. The merge created push-triggered scheduler run `28385177585`, proving the new trigger path is registered; the job remained queued because runner assignment was still pending.
- The earlier compare API `behind` handling is superseded by the current immediate-action order: `CLEAN` and current-head approved PRs merge before update-branch, failed or `ACTION_REQUIRED` checks are surfaced before any update attempt, and only approved `BEHIND` PRs without current-head check blockers request `update-branch` as `github-actions[bot]`.
- The earlier compare API `behind` handling is superseded by the current immediate-action order: `CLEAN` and current-head approved PRs merge before update-branch, failed or `ACTION_REQUIRED` checks are surfaced before any update attempt, and only approved `BEHIND` PRs without current-head check blockers request `update-branch` through the configured scheduler mutation credential.
- `.github` PR `#146` taught central OpenCode `coverage-evidence` to discover nested requirements-only Python test projects such as `backend/requirements.txt` plus `backend/tests`, install those requirements, and run tests from that project directory. It merged at 2026-06-29 23:24 KST with merge commit `0393bc1c48b80597d6d35c336aca43aee18e22b9`.
- `.github` PR `#149` tightened the central OpenCode model-failure path and merged at 2026-06-30 00:26 KST with merge commit `919b83faf29237803cfdd0cfd6febbe5ae1a8a3c`. The follow-up commit `6fdffe43b50a2246b3db2790a0ab532618a89c2b` fixed the fallback approval path so pending-check and human-thread evidence are written to real temporary files instead of empty paths. Local verification passed `pytest -q`, `coverage report --fail-under=100`, `interrogate --fail-under=100`, `actionlint -shellcheck=`, targeted OpenCode quick-gate assertions, `bash -n`, and `git diff --check`; the full quick-gate script exceeded the local 300s timeout in this environment.
- Organization ruleset `18156473` previously targeted all live non-fork repositories, including private `aFIPC`, `linux-cluster-ops`, and `xtrmLLMBatchPython`; this has been superseded by the all-repository `~ALL` condition above.
Expand Down
14 changes: 12 additions & 2 deletions scripts/ci/pr_review_merge_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,20 +1332,28 @@ def decide(action: str, reason: str) -> Decision:
merge_state == "CLEAN" or merge_mode in {"direct", "direct_or_auto"}
)
if current_head_approved and merge_before_update:
if pr.get("autoMergeRequest"):
return decide("wait", auto_merge_wait_reason(merge_state))
if not same_repository_head(repo, pr):
return decide("wait", external_head_merge_reason(repo, pr))
if not enable_auto_merge_flag:
if pr.get("autoMergeRequest"):
return decide("wait", auto_merge_wait_reason(merge_state))
return decide("wait", "current head is approved; auto-merge disabled by scheduler inputs")
if merge_mode == "disabled":
if pr.get("autoMergeRequest"):
return decide("wait", auto_merge_wait_reason(merge_state))
return decide("wait", "current head is approved; merge mode disabled by scheduler inputs")
if merge_mode in {"direct", "direct_or_auto"}:
try:
merge_pr(repo, pr, dry_run=dry_run)
except RuntimeError as exc:
if merge_mode != "direct_or_auto" or not direct_merge_can_fallback_to_auto_merge(exc):
raise
if pr.get("autoMergeRequest"):
return decide(
"auto_merge",
"current head is approved; direct merge was blocked by branch policy, "
"so the existing auto-merge request remains queued with the same head guard evidence",
)
enable_auto_merge(repo, pr, dry_run=dry_run)
return decide(
"auto_merge",
Expand All @@ -1360,6 +1368,8 @@ def decide(action: str, reason: str) -> Decision:
)
if merge_mode != "auto":
return decide("wait", f"current head is approved; unsupported merge mode: {merge_mode}")
if pr.get("autoMergeRequest"):
return decide("wait", auto_merge_wait_reason(merge_state))
enable_auto_merge(repo, pr, dry_run=dry_run)
return decide("auto_merge", "current head is approved; auto-merge enabled")

Expand Down
Loading