Skip to content
Open
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
135 changes: 29 additions & 106 deletions .github/workflows/pr-review-merge-scheduler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,24 @@ concurrency:
github.event_name == 'repository_dispatch' && github.event.client_payload.pr_number != '' && format('pr-{0}', github.event.client_payload.pr_number) ||
github.event_name == 'repository_dispatch' && format('repo-dispatch-{0}', github.repository) ||
github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.event_name == 'pull_request_review' || github.event_name == 'repository_dispatch' }}
cancel-in-progress: >-
${{
github.event_name == 'pull_request_target' ||
github.event_name == 'repository_dispatch' ||
(
github.event_name == 'pull_request_review' &&
(
github.event.action == 'dismissed' ||
(
github.event.action == 'submitted' &&
(
github.event.review.state == 'approved' ||
github.event.review.state == 'changes_requested'
)
)
)
)
}}

# Scorecard Token-Permissions (alert #9): declare a least-privilege default at
# the workflow level. The scan-pr-queue job that actually needs write access
Expand All @@ -109,6 +126,17 @@ jobs:
(
github.event_name != 'repository_dispatch' ||
github.event.client_payload.org_sweep != true
) &&
(
github.event_name != 'pull_request_review' ||
github.event.action == 'dismissed' ||
(
github.event.action == 'submitted' &&
(
github.event.review.state == 'approved' ||
github.event.review.state == 'changes_requested'
)
)
Comment thread
seonghobae marked this conversation as resolved.
)
runs-on: ubuntu-24.04
# Bound scan-pr-queue to a wall-clock ceiling well short of GitHub's
Expand Down Expand Up @@ -377,112 +405,7 @@ jobs:
- name: Self-test scheduler
run: python3 scripts/ci/pr_review_merge_scheduler.py --self-test

- name: Wait for approved OpenCode publication run to finish
id: review_followup
if: >-
github.event_name == 'pull_request_review'
&& github.event.action == 'submitted'
&& github.event.review.state == 'approved'
&& (
github.event.review.user.login == 'opencode-agent'
|| github.event.review.user.login == 'opencode-agent[bot]'
)
env:
GH_TOKEN: ${{ github.token }}
REVIEW_HEAD_SHA: ${{ github.event.review.commit_id }}
REVIEW_PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
echo "proceed=true" >>"$GITHUB_OUTPUT"

if [[ ! "${REVIEW_HEAD_SHA:-}" =~ ^[0-9a-fA-F]{40}$ ]]; then
printf '::warning::Post-approval direct-merge follow-up skipped because the OpenCode App review did not carry a 40-character commit SHA. value=%s.\n' "${REVIEW_HEAD_SHA:-missing}"
echo "proceed=false" >>"$GITHUB_OUTPUT"
exit 0
fi

pull_error_file="$(mktemp)"
if ! pull_json="$(
gh api "repos/${GITHUB_REPOSITORY}/pulls/${REVIEW_PR_NUMBER}" \
2>"$pull_error_file"
)"; then
pull_reason="$(tail -n 1 "$pull_error_file" 2>/dev/null || true)"
[ -n "$pull_reason" ] || pull_reason="GitHub pull-request lookup failed without an error body"
rm -f "$pull_error_file"
printf '::warning::Post-approval direct-merge follow-up skipped because the live pull request snapshot could not be read. PR=%s review_head=%s reason=%s.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$pull_reason"
echo "proceed=false" >>"$GITHUB_OUTPUT"
exit 0
fi
rm -f "$pull_error_file"
live_state="$(jq -r '.state // "unknown"' <<<"$pull_json")"
live_head="$(jq -r '.head.sha // empty' <<<"$pull_json")"
if [ "$live_state" != "open" ] || [ "$live_head" != "$REVIEW_HEAD_SHA" ]; then
printf '::notice::Post-approval direct-merge follow-up skipped because the pull request snapshot changed. PR=%s review_head=%s live_head=%s state=%s.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "${live_head:-missing}" "$live_state"
echo "proceed=false" >>"$GITHUB_OUTPUT"
exit 0
fi

opencode_state="missing"
opencode_reason="no opencode-review check run was visible for the approved head"
for check_attempt in 1 2 3 4 5 6 7 8; do
check_error_file="$(mktemp)"
if checks_json="$(
gh api --paginate --slurp \
"repos/${GITHUB_REPOSITORY}/commits/${REVIEW_HEAD_SHA}/check-runs?per_page=100" \
2>"$check_error_file"
)"; then
opencode_state="$(
jq -r '
[.[].check_runs[]
| select(.name == "opencode-review")
| select(.app.slug == "github-actions")] as $runs
| if ($runs | length) == 0 then "missing"
elif any($runs[]; .status != "completed") then "running"
elif any($runs[]; .conclusion != "success") then
"failed:" + ([$runs[] | (.conclusion // "missing")] | unique | join(","))
else "success"
end
' <<<"$checks_json"
)"
case "$opencode_state" in
success)
printf 'Approved OpenCode publication run completed successfully for PR %s at %s after check attempt %s.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$check_attempt"
rm -f "$check_error_file"
break
;;
failed:*)
opencode_reason="opencode-review completed without success (${opencode_state#failed:})"
rm -f "$check_error_file"
break
;;
running)
opencode_reason="opencode-review is still running for the approved head"
;;
*)
opencode_reason="no opencode-review check run was visible for the approved head"
;;
esac
else
opencode_state="api-error"
opencode_reason="$(tail -n 1 "$check_error_file" 2>/dev/null || true)"
[ -n "$opencode_reason" ] || opencode_reason="GitHub check-runs lookup failed without an error body"
fi
rm -f "$check_error_file"

if [ "$check_attempt" -lt 8 ]; then
check_delay="$((check_attempt * 2))"
printf 'Approved OpenCode publication run is not complete for PR %s at %s after check attempt %s: %s. Retrying in %ss.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$check_attempt" "$opencode_reason" "$check_delay"
sleep "$check_delay"
fi
done

if [ "$opencode_state" != "success" ]; then
printf '::warning::Post-approval direct-merge follow-up skipped because the approved OpenCode publication run did not complete successfully. PR=%s head=%s state=%s reason=%s. Native events and the explicit org-sweep recovery remain authoritative.\n' "$REVIEW_PR_NUMBER" "$REVIEW_HEAD_SHA" "$opencode_state" "$opencode_reason"
echo "proceed=false" >>"$GITHUB_OUTPUT"
fi

- name: Inspect PR review and merge queue
if: steps.review_followup.outputs.proceed != 'false'
env:
GH_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN || secrets.OPENCODE_APPROVE_TOKEN || steps.scheduler_app_token.outputs.token || github.token }}
TARGET_REPOSITORY: ${{ steps.targeted_dispatch.outputs.repository }}
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,16 @@
- Raised `hourly-review-repair.yml`'s discovery ceiling from 50 to 200 while rotating deterministic 50-PR deep-inspection windows by hourly run number. The scheduler hydrates only the selected window and stops immediately after its single dispatch, preserving access to newer PRs without quadrupling expensive review/check/comment work. See `docs/doctoring/hourly-review-repair-single-file-consolidation.md`'s 2026-09-03 follow-up.

## [Unreleased]
- Stop `pull_request_review: submitted` events with state `commented` at the
merge scheduler's job-admission boundary, before a hosted runner is
requested. `approved`, `changes_requested`, and `dismissed` review
transitions retain their existing exact-PR scheduler path and permissions;
COMMENTED submissions also no longer cancel an already-running actionable
review transition through workflow-level concurrency. Actionable review
events now enter the scheduler core directly instead of holding the runner
for up to 56 seconds while polling OpenCode publication. OpenCode's existing
post-publication scheduler call and GitHub's native auto-merge continuation
remain the merge owners.
- Include merge-scheduler entrypoint, core, and regression-test changes in
the existing runtime-quality workflow's trigger and suite selector. Scheduler
workflow edits retain queue checks and also select the full review-repair
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
# Doctoring record: pr-review-merge-scheduler.yml's "fires at every step" pattern is by-design, not a bug (2026-09-03)

> **2026-09-07 correction.** An actionable `pull_request_review` event now runs
> the scheduler core immediately. The removed pre-core step polled the
> `opencode-review` check up to eight times and could hold a runner for 56
> seconds, even though `opencode-review-dispatch.yml` already performs the
> current-head approval check and invokes the scheduler with merge authority
> after publication. The review-event path keeps its existing admission,
> credentials, and fail-closed core checks; it does not gain merge authority.
> Once auto-merge is armed, GitHub's native required-check handling remains the
> terminal continuation. The old fallback message naming an org-wide sweep was
> removed with the obsolete wait step; no replacement workflow was added.

> **2026-09-05 correction.** The broad claim below that every submitted review
> is an actionable approval-state change was incomplete. GitHub emits
> `pull_request_review: submitted` for `COMMENTED` reviews, which do not create
> an `APPROVED` or `CHANGES_REQUESTED` state. On PR #1885, CodeRabbit submitted
> `COMMENTED` reviews at 03:08:52Z, 04:27:31Z, and 05:30:37Z; the central
> scheduler admitted runner-backed runs 33941045179, 33944606701, and
> 33947394894 within seconds. The scheduler still needs the review trigger for
> `APPROVED`, `CHANGES_REQUESTED`, and `dismissed`, but `COMMENTED` is now
> rejected by the `scan-pr-queue` job-level `if` before runner acquisition.
> The executable truth-table contract is
> `tests/test_merge_scheduler_review_event_admission.py`. This correction does
> not reinterpret a bot comment as formal review evidence. It preserves the
> exact-PR concurrency group while narrowing cancellation so a COMMENTED
> submission cannot cancel an already-running APPROVED, CHANGES_REQUESTED, or
> dismissed transition; scheduler permissions remain unchanged.

- **Date:** 2026-09-03
- **Subject:** the user directly observed the scheduler workflow firing repeatedly ("왜 각 모든 단계마다 Trigger
되고 있죠?") after live evidence surfaced today of severe org-wide Actions thrashing (near-zero completion
Expand Down
11 changes: 6 additions & 5 deletions tests/test_current_head_coalescer_self_cancellation.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,10 @@ def test_current_head_coalescer_shares_pr_scoped_scheduler_admission() -> None:
assert "github.repository == 'ContextualWisdomLab/.github'" in coalescer
assert "github.event.pull_request.head.sha" not in concurrency_block
assert "github.event.pull_request.number" in concurrency_block
assert any(
line.startswith("cancel-in-progress:")
and "github.event_name == 'pull_request_target'" in line
for line in active_lines
)
normalized_concurrency = " ".join(active_lines)
assert "cancel-in-progress: >- ${{" in normalized_concurrency
assert "github.event_name == 'pull_request_target'" in normalized_concurrency
assert "github.event_name == 'repository_dispatch'" in normalized_concurrency
assert "github.event.review.state == 'approved'" in normalized_concurrency
assert "github.event.review.state == 'changes_requested'" in normalized_concurrency
assert "queue: max" not in workflow_text
Loading
Loading