From cb32b05efd3477128e5e5d3b53bfba03c56bfd4f Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 10:30:00 +0545 Subject: [PATCH 1/9] Gate Dependabot auto-merge on CI passing (RealRate-Private#2244) The AI risk gate (#2229) decides *eligibility* from the changelog, but the actual merge used `gh pr merge --auto`, which only waits for *required* status checks. Across the org almost no repo has required checks configured, so today a patch/minor or AI-approved major bump auto-merges even when CI is red -- the tests run but never block the merge. Add review-tooling/gated_merge.sh and route every auto-merge (patch/minor AND AI-approved majors) through it. It waits for the PR's own checks -- every check except this workflow's own run, matched by run id in the check link -- to reach a terminal state, then merges only if all passed; any failed/cancelled check, or a timeout, routes the PR to `needs-manual-review` instead. Why read check conclusions here instead of GitHub required checks: required checks are matched by exact name per repo, and this workflow is injected org-wide across repos whose test jobs are named differently or absent. Requiring a check a repo does not emit would deadlock its PRs. Reading the PR's actual checks works uniformly regardless of naming -- so this gives org-wide "don't merge over red CI" with zero per-repo config. - Fail-closed: the script always exits 0 (this job is itself a required workflow), routing to human on failure/timeout rather than merging. - Kill switch: CI_GATE_ENABLED=false falls back to native `--auto`. - No-CI repos: after a grace period with no checks, merge on the existing signal (patch/minor policy or AI verdict) -- unchanged behavior. - Per-PR concurrency cancels superseded runs so two runs never race to merge. shellcheck + actionlint clean. Decision paths (green/red/pending/no-checks) dry-run-verified against live `gh pr checks` data. --- .github/review-tooling/gated_merge.sh | 115 ++++++++++++++++++++ .github/workflows/dependabot-automerge.yaml | 70 +++++++----- 2 files changed, 160 insertions(+), 25 deletions(-) create mode 100755 .github/review-tooling/gated_merge.sh diff --git a/.github/review-tooling/gated_merge.sh b/.github/review-tooling/gated_merge.sh new file mode 100755 index 0000000..7de1459 --- /dev/null +++ b/.github/review-tooling/gated_merge.sh @@ -0,0 +1,115 @@ +#!/usr/bin/env bash +# Gated auto-merge for Dependabot PRs (RealRate-Private#2229 / #2244). +# +# Waits for the PR's own CI checks -- every check EXCEPT this workflow's own run -- +# to finish, then merges only if all of them passed. If any check failed, or the +# checks do not finish within the timeout, the PR is routed to manual review +# instead of merged. So a bump can never auto-merge over red CI, even in repos +# that have NO *required* status checks configured. +# +# Why read checks here instead of using GitHub "required status checks": +# required checks are matched by exact name, per repo. This workflow is injected +# org-wide across repos whose test jobs are named differently (or absent), so a +# single org rule cannot name them all -- requiring a check a repo does not emit +# would deadlock every PR there. Reading the PR's own check conclusions works +# uniformly regardless of what each repo calls its checks. See #2244. +# +# The job that sources this script is a required workflow in the org +# `dependabot-automerge` ruleset, so this script MUST exit 0 on every expected +# path (merged, held-for-human, no-CI): a non-zero exit would itself block the +# merge for a human too. It only merges; it never fails the gate. +# +# Env: +# PR_URL (required) the PR to gate and merge +# GITHUB_TOKEN (required) authenticates the gh calls +# GITHUB_RUN_ID (auto on runners) this run's id; excludes our own check +# CI_GATE_ENABLED default "true"; "false" = kill switch, native --auto merge +# MERGE_METHOD default "rebase" +# GATE_APPEAR_SECONDS default 180; grace for checks to register before we treat +# the repo as having no gating CI +# GATE_TIMEOUT_SECONDS default 1800; overall budget to wait for checks to finish +# GATE_POLL_SECONDS default 30; poll interval +set -uo pipefail + +: "${PR_URL:?PR_URL is required}" + +CI_GATE_ENABLED="${CI_GATE_ENABLED:-true}" +MERGE_METHOD="${MERGE_METHOD:-rebase}" +APPEAR="${GATE_APPEAR_SECONDS:-180}" +TIMEOUT="${GATE_TIMEOUT_SECONDS:-1800}" +POLL="${GATE_POLL_SECONDS:-30}" + +# Kill switch: fall back to GitHub-native auto-merge (waits only for *required* +# checks). Lets the CI gate be turned off org-wide via one env flip, no revert. +if [ "$CI_GATE_ENABLED" != "true" ]; then + echo "CI_GATE_ENABLED=$CI_GATE_ENABLED -> native auto-merge (no CI gate)." + gh pr merge --auto --"$MERGE_METHOD" "$PR_URL" + exit $? +fi + +pr_state() { gh pr view "$PR_URL" --json state --jq .state 2>/dev/null || echo ""; } + +route_to_human() { + gh pr comment "$PR_URL" --body "🤖 **Auto-merge held:** $1 Routed to manual review." || true + gh label create needs-manual-review --color FBCA04 \ + --description "Dependabot bump — needs manual review before merge" 2>/dev/null || true + gh pr edit "$PR_URL" --add-label needs-manual-review || true +} + +do_merge() { + gh pr merge --"$MERGE_METHOD" "$PR_URL" \ + || route_to_human "all checks passed but the merge was rejected (branch protection, conflict, or a required human review)." +} + +# The PR's checks minus THIS workflow's own run, matched by run id inside the +# check link (rename-proof). Captures stdout even when `gh pr checks` exits +# non-zero (it does when checks are pending/failing); only an empty result -- no +# checks reported yet -- collapses to []. +other_checks() { + local out + out="$(gh pr checks "$PR_URL" --json name,bucket,link 2>/dev/null)" + [ -z "$out" ] && { echo '[]'; return; } + echo "$out" | jq --arg rid "${GITHUB_RUN_ID:-0}" \ + '[.[] | select((.link // "") | contains("/runs/" + $rid + "/") | not)]' +} + +start="$SECONDS" + +# 1) Wait for CI to register. A repo with no gating CI merges on the existing +# signal (patch/minor policy, or the AI verdict for majors) -- unchanged. +while :; do + checks="$(other_checks)" + [ "$(echo "$checks" | jq 'length')" -gt 0 ] && break + if [ $((SECONDS - start)) -ge "$APPEAR" ]; then + echo "No CI checks registered within ${APPEAR}s; merging on the existing signal." + do_merge + exit 0 + fi + sleep "$POLL" +done + +# 2) Wait for every non-self check to reach a terminal state. +while :; do + checks="$(other_checks)" + [ "$(echo "$checks" | jq '[.[] | select(.bucket=="pending")] | length')" -eq 0 ] && break + if [ "$(pr_state)" != "OPEN" ]; then echo "PR no longer open; nothing to do."; exit 0; fi + if [ $((SECONDS - start)) -ge "$TIMEOUT" ]; then + route_to_human "CI did not finish within $((TIMEOUT / 60)) minutes." + exit 0 + fi + sleep "$POLL" +done + +# 3) Any failed / cancelled check -> hold for a human. (`skipping` and `pass` +# are both acceptable.) Otherwise merge. +bad="$(echo "$checks" | jq '[.[] | select(.bucket == "fail" or .bucket == "cancel")]')" +if [ "$(echo "$bad" | jq 'length')" -gt 0 ]; then + route_to_human "CI is not passing ($(echo "$bad" | jq -r '[.[].name] | join(", ")'))." + exit 0 +fi + +if [ "$(pr_state)" != "OPEN" ]; then echo "PR already merged/closed; nothing to do."; exit 0; fi + +echo "All checks passed; merging." +do_merge +exit 0 diff --git a/.github/workflows/dependabot-automerge.yaml b/.github/workflows/dependabot-automerge.yaml index 1df9f8c..12db202 100644 --- a/.github/workflows/dependabot-automerge.yaml +++ b/.github/workflows/dependabot-automerge.yaml @@ -7,17 +7,31 @@ name: Dependabot Auto-Merge # referenced workflow to be directly PR-triggerable (pull_request_target / # pull_request / merge_queue); it must NOT be a reusable (workflow_call) workflow. # -# For patch/minor bumps it approves the PR and enables auto-merge (rebase). For -# major bumps it comments and labels `needs-manual-review` for a human -- EXCEPT -# for repos on AIGATE_ALLOWLIST, where major `github-actions` bumps get an AI -# risk verdict (GitHub Models) and are auto-merged if judged low-risk/non-breaking. -# GitHub's required status checks still gate the actual merge, so a bump that -# breaks CI never lands regardless of the AI verdict. See RealRate-Private#2229. +# For patch/minor bumps it approves the PR; for major bumps it comments and +# labels `needs-manual-review` for a human -- EXCEPT for repos on +# AIGATE_ALLOWLIST, where major `github-actions` bumps get an AI risk verdict +# (GitHub Models) and are eligible for auto-merge if judged low-risk/non-breaking. +# +# Every auto-merge (patch/minor AND AI-approved majors) goes through +# review-tooling/gated_merge.sh, which waits for the PR's own CI checks to finish +# and merges only if they all pass -- otherwise it routes to manual review. This +# replaces `gh pr merge --auto` (which only waits for *required* status checks): +# because this workflow is injected across repos whose checks are named +# differently or absent, we cannot express "tests must pass" as a per-repo +# required check, so the script reads the PR's actual check conclusions instead. +# A failed test therefore blocks the merge even where no required checks exist. +# See RealRate-Private#2229 (AI gate) and #2244 (CI gate). on: pull_request_target: types: [opened, synchronize] +# One in-flight run per PR. A new event (e.g. a rebase/synchronize) cancels the +# previous run so two runs never wait on CI and race to merge the same PR. +concurrency: + group: dependabot-automerge-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: write pull-requests: write @@ -31,9 +45,13 @@ jobs: # AI-gated auto-merge of MAJOR github-actions bumps (RealRate-Private#2229). # Repos listed here opt in; all others keep comment + needs-manual-review. # AIGATE_DRY_RUN=true posts the AI verdict as a comment but does NOT merge; - # =false enforces (verdict=auto -> approve + auto-merge, still gated by CI). + # =false enforces (verdict=auto -> approve + gated auto-merge). AIGATE_ALLOWLIST: '["realrate/RealRate-Private"]' AIGATE_DRY_RUN: "false" + # CI gate (RealRate-Private#2244): when "true", auto-merges wait for the + # PR's checks to pass before merging. Set "false" as a kill switch to fall + # back to GitHub-native `--auto` (waits only for *required* checks). + CI_GATE_ENABLED: "true" # Guard on the PR author rather than github.actor (the triggering user, # which can be a maintainer on a rebase/synchronize of the Dependabot branch). if: github.event.pull_request.user.login == 'dependabot[bot]' @@ -45,6 +63,15 @@ jobs: with: github-token: "${{ secrets.GITHUB_TOKEN }}" + # Our own org tooling (gated_merge.sh + the AI review script). Pinned ref, + # NOT the PR head -- pull_request_target runs with write perms, so we never + # check out untrusted bump code. Used by every auto-merge path below. + - name: Check out merge tooling + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 + with: + repository: realrate/.github + path: _tooling + - name: Approve PR if: | steps.metadata.outputs.update-type == 'version-update:semver-patch' || @@ -54,14 +81,16 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Enable auto-merge + - name: Auto-merge patch/minor bump (gated on CI) if: | steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor' - run: gh pr merge --auto --rebase "$PR_URL" + run: bash _tooling/.github/review-tooling/gated_merge.sh env: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CI_GATE_ENABLED: ${{ env.CI_GATE_ENABLED }} + MERGE_METHOD: rebase # The generic major-bump comment/label below handle every major bump EXCEPT # the AI-gated case (allowlisted repo + github-actions ecosystem), which is @@ -106,17 +135,6 @@ jobs: # external secret); on any failure the script emits verdict=human, so we never # auto-merge on an errored verdict. # --------------------------------------------------------------------------- - - name: Check out AI review tooling - if: | - steps.metadata.outputs.update-type == 'version-update:semver-major' && - steps.metadata.outputs.package-ecosystem == 'github_actions' && - github.event.action == 'opened' && - contains(fromJSON(env.AIGATE_ALLOWLIST), github.repository) - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 - with: - repository: realrate/.github - path: _aigate - - name: AI risk verdict for major github-actions bump id: aiverdict continue-on-error: true # a failed verdict must fall through to manual review, not fail the required check @@ -135,10 +153,10 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} DEP_NAMES: ${{ steps.metadata.outputs.dependency-names }} run: | - python3 -m pip install --quiet --disable-pip-version-check -r _aigate/.github/review-tooling/requirements.txt + python3 -m pip install --quiet --disable-pip-version-check -r _tooling/.github/review-tooling/requirements.txt gh pr view "$PR_URL" --json title --jq .title > pr_title.txt gh pr view "$PR_URL" --json body --jq '.body // ""' > pr_body.txt - python3 _aigate/.github/review-tooling/review_dependabot_major.py \ + python3 _tooling/.github/review-tooling/review_dependabot_major.py \ --title-file pr_title.txt --body-file pr_body.txt \ --dep-names "$DEP_NAMES" --out verdict.json { @@ -146,7 +164,7 @@ jobs: echo "reason=$(jq -r .reason verdict.json)" } >> "$GITHUB_OUTPUT" - - name: Auto-merge major github-actions bump (verdict=auto) + - name: Auto-merge major github-actions bump (verdict=auto, gated on CI) if: | steps.aiverdict.outcome == 'success' && steps.aiverdict.outputs.verdict == 'auto' && @@ -155,10 +173,12 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REASON: ${{ steps.aiverdict.outputs.reason }} + CI_GATE_ENABLED: ${{ env.CI_GATE_ENABLED }} + MERGE_METHOD: rebase run: | - gh pr comment "$PR_URL" --body "🤖 **AI risk gate: auto-merge.** ${REASON} Required CI checks must still pass before the merge completes." + gh pr comment "$PR_URL" --body "🤖 **AI risk gate: auto-merge.** ${REASON} CI checks must still pass before the merge completes." gh pr review --approve "$PR_URL" - gh pr merge --auto --rebase "$PR_URL" + bash _tooling/.github/review-tooling/gated_merge.sh - name: Route major github-actions bump to human (not auto-merged) # Runs for every eligible PR that is NOT being auto-merged: verdict=human, From 5da501134750472e4a08afa517ceacb76afb8418 Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 10:45:55 +0545 Subject: [PATCH 2/9] Run the AI risk gate for github-actions majors in every repo Drop AIGATE_ALLOWLIST so the AI verdict runs for MAJOR github-actions bumps in every repo the shared workflow is injected into, not just the RealRate-Private pilot. patch/minor (auto, CI-gated) and major pip/docker (comment + human) are unchanged. The generic major-bump comment/label now fire only for non-github-actions majors, since github-actions majors are handled by the AI block in every repo. Still fail-closed (AI error -> human) and enforce/observe is still controlled by AIGATE_DRY_RUN. actionlint clean. --- .github/workflows/dependabot-automerge.yaml | 28 ++++++++++----------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yaml b/.github/workflows/dependabot-automerge.yaml index 12db202..eb79d17 100644 --- a/.github/workflows/dependabot-automerge.yaml +++ b/.github/workflows/dependabot-automerge.yaml @@ -8,9 +8,10 @@ name: Dependabot Auto-Merge # pull_request / merge_queue); it must NOT be a reusable (workflow_call) workflow. # # For patch/minor bumps it approves the PR; for major bumps it comments and -# labels `needs-manual-review` for a human -- EXCEPT for repos on -# AIGATE_ALLOWLIST, where major `github-actions` bumps get an AI risk verdict -# (GitHub Models) and are eligible for auto-merge if judged low-risk/non-breaking. +# labels `needs-manual-review` for a human -- EXCEPT major `github-actions` +# bumps, which in EVERY repo get an AI risk verdict (GitHub Models) and are +# eligible for auto-merge if judged low-risk/non-breaking. Major pip/docker bumps +# stay human. # # Every auto-merge (patch/minor AND AI-approved majors) goes through # review-tooling/gated_merge.sh, which waits for the PR's own CI checks to finish @@ -42,11 +43,10 @@ jobs: auto-merge: runs-on: ubuntu-latest env: - # AI-gated auto-merge of MAJOR github-actions bumps (RealRate-Private#2229). - # Repos listed here opt in; all others keep comment + needs-manual-review. + # AI-gated auto-merge of MAJOR github-actions bumps, in every repo the + # shared workflow is injected into (RealRate-Private#2229). # AIGATE_DRY_RUN=true posts the AI verdict as a comment but does NOT merge; # =false enforces (verdict=auto -> approve + gated auto-merge). - AIGATE_ALLOWLIST: '["realrate/RealRate-Private"]' AIGATE_DRY_RUN: "false" # CI gate (RealRate-Private#2244): when "true", auto-merges wait for the # PR's checks to pass before merging. Set "false" as a kill switch to fall @@ -92,9 +92,9 @@ jobs: CI_GATE_ENABLED: ${{ env.CI_GATE_ENABLED }} MERGE_METHOD: rebase - # The generic major-bump comment/label below handle every major bump EXCEPT - # the AI-gated case (allowlisted repo + github-actions ecosystem), which is - # handled by the AI block further down so we don't double-comment/label. + # The generic major-bump comment/label below handle every NON-github-actions + # major bump. github-actions majors are handled by the AI block further down + # (in every repo), so we don't double-comment/label them here. - name: Comment on major bump PR # Only comment when the PR is first opened, not on every synchronize, # so re-pushes/rebases of the Dependabot branch don't post duplicate @@ -103,7 +103,7 @@ jobs: if: | steps.metadata.outputs.update-type == 'version-update:semver-major' && github.event.action == 'opened' && - !(steps.metadata.outputs.package-ecosystem == 'github_actions' && contains(fromJSON(env.AIGATE_ALLOWLIST), github.repository)) + steps.metadata.outputs.package-ecosystem != 'github_actions' run: | gh pr comment "$PR_URL" --body "⚠️ **Major version bump detected** — this PR requires manual review before merging. Please check the changelog for breaking changes." env: @@ -113,7 +113,7 @@ jobs: - name: Add label to major bump PR if: | steps.metadata.outputs.update-type == 'version-update:semver-major' && - !(steps.metadata.outputs.package-ecosystem == 'github_actions' && contains(fromJSON(env.AIGATE_ALLOWLIST), github.repository)) + steps.metadata.outputs.package-ecosystem != 'github_actions' # Create the label if the repo doesn't have it, and never fail the job on # labelling: this workflow is a required check, so a labelling error must # NOT block a human from merging a major bump after manual review. @@ -126,7 +126,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # --------------------------------------------------------------------------- - # AI risk gate for MAJOR github-actions bumps (allowlisted repos only). + # AI risk gate for MAJOR github-actions bumps (every repo). # Runs once, on `opened` (the dependency/version is fixed for the PR's life, # so re-evaluating on every synchronize would only add noise). The verdict # step is read-only and NEVER merges; a separate deterministic step acts on @@ -141,8 +141,7 @@ jobs: if: | steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.package-ecosystem == 'github_actions' && - github.event.action == 'opened' && - contains(fromJSON(env.AIGATE_ALLOWLIST), github.repository) + github.event.action == 'opened' env: # Inference runs on GitHub Models via the built-in token (`models: read`); # no external API key. GITHUB_TOKEN also authenticates the `gh` calls below. @@ -187,7 +186,6 @@ jobs: steps.metadata.outputs.update-type == 'version-update:semver-major' && steps.metadata.outputs.package-ecosystem == 'github_actions' && github.event.action == 'opened' && - contains(fromJSON(env.AIGATE_ALLOWLIST), github.repository) && !(steps.aiverdict.outcome == 'success' && steps.aiverdict.outputs.verdict == 'auto' && env.AIGATE_DRY_RUN != 'true') env: PR_URL: ${{ github.event.pull_request.html_url }} From f6ba8986a27552b8c2af70187f57de1bee9c10f4 Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 10:53:18 +0545 Subject: [PATCH 3/9] Run the AI risk gate for EVERY major bump (pip/docker too) Extend the AI verdict to all major Dependabot bumps in every ecosystem, not just github-actions: drop the `package-ecosystem == 'github_actions'` condition from the AI verdict / auto-merge / route-to-human steps, and remove the now-redundant generic major-bump comment+label steps (every major is handled by the AI block). Generalize the classifier prompt from GitHub-Actions-specific ("pinned Action", "inputs/outputs", "Node version") to ecosystem-aware: it now judges a library (pip), a container base image (docker), or a CI action, and is told the ecosystem via a new --ecosystem arg fed from fetch-metadata. Same strictness: any breaking public-API / input / default / platform-support / security change, or any uncertainty, -> human. patch/minor unchanged (auto, CI-gated). Still fail-closed (AI error -> human); still CI-gated by gated_merge.sh; enforce/observe via AIGATE_DRY_RUN. actionlint clean; script compiles; fail-closed fallback verified offline. --- .../review_dependabot_major.cpython-314.pyc | Bin 0 -> 8858 bytes .../review-tooling/review_dependabot_major.py | 38 +++++++----- .github/workflows/dependabot-automerge.yaml | 57 ++++-------------- 3 files changed, 33 insertions(+), 62 deletions(-) create mode 100644 .github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc diff --git a/.github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc b/.github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc new file mode 100644 index 0000000000000000000000000000000000000000..93c9f9552dc44577f8f424782a733599fbb2abd6 GIT binary patch literal 8858 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/dev/null || true - gh pr edit "$PR_URL" --add-label "needs-manual-review" || true - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # --------------------------------------------------------------------------- - # AI risk gate for MAJOR github-actions bumps (every repo). + # AI risk gate for EVERY MAJOR bump (any ecosystem, every repo). # Runs once, on `opened` (the dependency/version is fixed for the PR's life, # so re-evaluating on every synchronize would only add noise). The verdict # step is read-only and NEVER merges; a separate deterministic step acts on @@ -135,12 +101,11 @@ jobs: # external secret); on any failure the script emits verdict=human, so we never # auto-merge on an errored verdict. # --------------------------------------------------------------------------- - - name: AI risk verdict for major github-actions bump + - name: AI risk verdict for major bump id: aiverdict continue-on-error: true # a failed verdict must fall through to manual review, not fail the required check if: | steps.metadata.outputs.update-type == 'version-update:semver-major' && - steps.metadata.outputs.package-ecosystem == 'github_actions' && github.event.action == 'opened' env: # Inference runs on GitHub Models via the built-in token (`models: read`); @@ -151,19 +116,20 @@ jobs: MODEL_ID: openai/gpt-4.1 PR_URL: ${{ github.event.pull_request.html_url }} DEP_NAMES: ${{ steps.metadata.outputs.dependency-names }} + ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }} run: | python3 -m pip install --quiet --disable-pip-version-check -r _tooling/.github/review-tooling/requirements.txt gh pr view "$PR_URL" --json title --jq .title > pr_title.txt gh pr view "$PR_URL" --json body --jq '.body // ""' > pr_body.txt python3 _tooling/.github/review-tooling/review_dependabot_major.py \ --title-file pr_title.txt --body-file pr_body.txt \ - --dep-names "$DEP_NAMES" --out verdict.json + --dep-names "$DEP_NAMES" --ecosystem "$ECOSYSTEM" --out verdict.json { echo "verdict=$(jq -r .verdict verdict.json)" echo "reason=$(jq -r .reason verdict.json)" } >> "$GITHUB_OUTPUT" - - name: Auto-merge major github-actions bump (verdict=auto, gated on CI) + - name: Auto-merge major bump (verdict=auto, gated on CI) if: | steps.aiverdict.outcome == 'success' && steps.aiverdict.outputs.verdict == 'auto' && @@ -179,12 +145,11 @@ jobs: gh pr review --approve "$PR_URL" bash _tooling/.github/review-tooling/gated_merge.sh - - name: Route major github-actions bump to human (not auto-merged) - # Runs for every eligible PR that is NOT being auto-merged: verdict=human, + - name: Route major bump to human (not auto-merged) + # Runs for every major bump that is NOT being auto-merged: verdict=human, # dry-run, or a failed/empty verdict. Adds the same needs-manual-review label. if: | steps.metadata.outputs.update-type == 'version-update:semver-major' && - steps.metadata.outputs.package-ecosystem == 'github_actions' && github.event.action == 'opened' && !(steps.aiverdict.outcome == 'success' && steps.aiverdict.outputs.verdict == 'auto' && env.AIGATE_DRY_RUN != 'true') env: From 261408153805db9ded51685b57d27663a7078486 Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 10:57:48 +0545 Subject: [PATCH 4/9] Reflect uv (not pip) as the repo's Python ecosystem in wording Cosmetic: the repo's dependabot.yml declares package-ecosystem: uv for Python (plus github-actions, docker), so update the classifier docstring/prompt and the workflow header from 'pip' to 'Python (uv/pip)'. No behavior change -- the AI gate is ecosystem-agnostic (the ecosystem is a hint, not a gate), so it already covers uv bumps regardless of the exact string fetch-metadata reports. --- .../review_dependabot_major.cpython-314.pyc | Bin 8858 -> 8906 bytes .../review-tooling/review_dependabot_major.py | 10 +++++----- .github/workflows/dependabot-automerge.yaml | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc b/.github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc index 93c9f9552dc44577f8f424782a733599fbb2abd6..0fba013968ab6ab18ab2418bde24b8e7936ac251 100644 GIT binary patch delta 138 zcmbQ`ddihon~#@^0SI;#hh$l7ivtV)plOm@=abjAkLP(J#1$;SF`&|0{{&&5zqht diff --git a/.github/review-tooling/review_dependabot_major.py b/.github/review-tooling/review_dependabot_major.py index 6249a4f..667c86b 100644 --- a/.github/review-tooling/review_dependabot_major.py +++ b/.github/review-tooling/review_dependabot_major.py @@ -2,8 +2,8 @@ """Render a machine-readable risk verdict for a MAJOR Dependabot bump. Used by the shared Dependabot auto-merge workflow to decide whether a major -version bump -- any ecosystem: pip, docker, or github-actions -- is safe to -auto-merge. The AI only *classifies* the changelog; it never merges. The +version bump -- any ecosystem: Python (uv / pip), docker, or github-actions -- is +safe to auto-merge. The AI only *classifies* the changelog; it never merges. The workflow's CI gate (gated_merge.sh) still requires the PR's own checks to pass before the merge lands, so a bump that breaks CI never merges regardless of this verdict. @@ -35,9 +35,9 @@ SYSTEM_PROMPT = ( "You classify whether a MAJOR version bump of a software dependency is safe " "to auto-merge for a typical consumer that uses it in a standard, documented " - "way. The dependency may be a library/package (e.g. pip), a container base " - "image (e.g. docker), or a pinned CI action (e.g. github-actions); judge it " - "according to its ecosystem. " + "way. The dependency may be a library/package (e.g. a Python package managed " + "by uv or pip), a container base image (e.g. docker), or a pinned CI action " + "(e.g. github-actions); judge it according to its ecosystem. " "Return verdict='auto' ONLY when the changelog shows NO breaking change that " "could affect such a consumer: e.g. internal refactors, transitive dependency " "bumps, runtime/engine upgrades, new OPTIONAL features or inputs, or bug fixes. " diff --git a/.github/workflows/dependabot-automerge.yaml b/.github/workflows/dependabot-automerge.yaml index 78d5f8b..d0b6f83 100644 --- a/.github/workflows/dependabot-automerge.yaml +++ b/.github/workflows/dependabot-automerge.yaml @@ -8,9 +8,11 @@ name: Dependabot Auto-Merge # pull_request / merge_queue); it must NOT be a reusable (workflow_call) workflow. # # Patch/minor bumps are approved and CI-gate-merged. EVERY major bump (any -# ecosystem -- github-actions, pip, docker -- in every repo) gets an AI risk +# ecosystem -- github-actions, uv/pip, docker -- in every repo) gets an AI risk # verdict (GitHub Models) and is auto-merged if judged low-risk/non-breaking, -# else commented + labeled `needs-manual-review` for a human. +# else commented + labeled `needs-manual-review` for a human. The ecosystem is a +# hint to the classifier, not a gate, so it adapts to whatever fetch-metadata +# reports (this repo uses `uv` for Python, not `pip`). # # Every auto-merge (patch/minor AND AI-approved majors) goes through # review-tooling/gated_merge.sh, which waits for the PR's own CI checks to finish From f6c7cc64d4799cc6c3a66fdca06130339e8b305c Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 11:06:04 +0545 Subject: [PATCH 5/9] Ship the AI gate in observe mode (AIGATE_DRY_RUN=true) Roll out the org-wide, all-ecosystem AI gate in dry-run first: major bumps get their AI verdict posted as a comment but are NOT auto-merged (routed to human), so we can watch the verdicts on real bumps -- especially uv/Python, where the test suite has known blind spots -- before enforcing. Flip to "false" to enforce. patch/minor auto-merge and the CI gate are unaffected by this flag. --- .github/workflows/dependabot-automerge.yaml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yaml b/.github/workflows/dependabot-automerge.yaml index d0b6f83..b993fea 100644 --- a/.github/workflows/dependabot-automerge.yaml +++ b/.github/workflows/dependabot-automerge.yaml @@ -44,11 +44,15 @@ jobs: auto-merge: runs-on: ubuntu-latest env: - # AI-gated auto-merge of MAJOR github-actions bumps, in every repo the - # shared workflow is injected into (RealRate-Private#2229). + # AI-gated auto-merge of MAJOR bumps, in every repo the shared workflow is + # injected into (RealRate-Private#2229). # AIGATE_DRY_RUN=true posts the AI verdict as a comment but does NOT merge; # =false enforces (verdict=auto -> approve + gated auto-merge). - AIGATE_DRY_RUN: "false" + # Shipping in OBSERVE mode (true): watch the AI verdicts on real major bumps + # -- especially uv/Python, where our tests have known blind spots -- then + # flip to "false" to enforce. patch/minor auto-merge + the CI gate are + # unaffected by this flag. + AIGATE_DRY_RUN: "true" # CI gate (RealRate-Private#2244): when "true", auto-merges wait for the # PR's checks to pass before merging. Set "false" as a kill switch to fall # back to GitHub-native `--auto` (waits only for *required* checks). From f91a898c404d3f29e53b623c6c1dbaebf786fd8f Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 11:14:48 +0545 Subject: [PATCH 6/9] Enforce the AI+CI auto-merge gate for all major bumps (AIGATE_DRY_RUN=false) Confirmed target: every MAJOR bump (libraries/uv, github-actions, docker; any repo) -> AI risk check -> tests -> auto-merged if the AI judges it low-risk AND CI passes; AI-flagged risk or a failed check routes to a human. patch/minor auto-merge + the CI gate unaffected. --- .github/workflows/dependabot-automerge.yaml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yaml b/.github/workflows/dependabot-automerge.yaml index b993fea..0601594 100644 --- a/.github/workflows/dependabot-automerge.yaml +++ b/.github/workflows/dependabot-automerge.yaml @@ -45,14 +45,13 @@ jobs: runs-on: ubuntu-latest env: # AI-gated auto-merge of MAJOR bumps, in every repo the shared workflow is - # injected into (RealRate-Private#2229). + # injected into (RealRate-Private#2229). Covers ALL ecosystems: libraries + # (uv), github-actions, and docker. # AIGATE_DRY_RUN=true posts the AI verdict as a comment but does NOT merge; - # =false enforces (verdict=auto -> approve + gated auto-merge). - # Shipping in OBSERVE mode (true): watch the AI verdicts on real major bumps - # -- especially uv/Python, where our tests have known blind spots -- then - # flip to "false" to enforce. patch/minor auto-merge + the CI gate are - # unaffected by this flag. - AIGATE_DRY_RUN: "true" + # =false enforces. ENFORCING: a major bump the AI judges low-risk AND whose + # CI passes is auto-merged; AI-flagged risk or a failed check routes to a + # human. patch/minor auto-merge + the CI gate are unaffected by this flag. + AIGATE_DRY_RUN: "false" # CI gate (RealRate-Private#2244): when "true", auto-merges wait for the # PR's checks to pass before merging. Set "false" as a kill switch to fall # back to GitHub-native `--auto` (waits only for *required* checks). From d71471f668a29f3207e0b6e84d88c237ce48b517 Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 11:49:51 +0545 Subject: [PATCH 7/9] Untrack committed .pyc + add .gitignore Remove .github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc, accidentally committed in f6ba898 when the script was compiled locally during validation. Add a .gitignore (__pycache__/, *.py[cod]) so Python bytecode is never committed again. --- .../review_dependabot_major.cpython-314.pyc | Bin 8906 -> 0 bytes .gitignore | 3 +++ 2 files changed, 3 insertions(+) delete mode 100644 .github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc create mode 100644 .gitignore diff --git a/.github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc b/.github/review-tooling/__pycache__/review_dependabot_major.cpython-314.pyc deleted file mode 100644 index 0fba013968ab6ab18ab2418bde24b8e7936ac251..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8906 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bcfeae --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Python bytecode / caches (e.g. from running review-tooling scripts locally) +__pycache__/ +*.py[cod] From 94d3f8882463cf667b29d12742d974f327f3bad8 Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 12:01:48 +0545 Subject: [PATCH 8/9] Clarify: the openai pin is the GitHub Models client, not the OpenAI API Comment-only. Spell out in requirements.txt that the openai package is used solely as the client library against the GitHub Models endpoint with the built-in GITHUB_TOKEN -- no OpenAI API key/account/billing. Prevents the recurring 'why is openai here' confusion for reviewers. --- .github/review-tooling/requirements.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/review-tooling/requirements.txt b/.github/review-tooling/requirements.txt index d7188d2..91cd520 100644 --- a/.github/review-tooling/requirements.txt +++ b/.github/review-tooling/requirements.txt @@ -1,2 +1,6 @@ -# Pinned deps for review_dependabot_major.py (the AI risk gate on major bumps (GitHub Models client)). +# Pinned deps for review_dependabot_major.py (the AI risk gate on major bumps). +# NOTE: this is only the client LIBRARY. It is pointed at the GitHub Models +# endpoint (https://models.github.ai/inference) and authenticated with the +# workflow's built-in GITHUB_TOKEN. We do NOT use the OpenAI API, an OpenAI API +# key, or OpenAI billing -- inference runs entirely on GitHub Models. openai==2.45.0 From 8a6826d2e62e1510481eec94640a56c7b9a23ccc Mon Sep 17 00:00:00 2001 From: Bibek Chaudhary Date: Mon, 13 Jul 2026 12:06:57 +0545 Subject: [PATCH 9/9] Switch the AI verdict tooling from pip/requirements.txt to uv Align the classifier with the org's uv standard: declare its single dep inline via PEP 723 (dependencies = ["openai==2.45.0"]) and run it with `uv run --script`, provisioned by a pinned astral-sh/setup-uv step. Delete requirements.txt and drop the pip install. --script forces script mode so uv ignores the caller repo's pyproject/project env (verified: runs fine even from a dir with an unresolvable pyproject). Validated end-to-end: uv run --script provisions openai in an isolated env and the fail-closed path still returns verdict=human with no token. actionlint clean. --- .github/review-tooling/requirements.txt | 6 ------ .github/review-tooling/review_dependabot_major.py | 8 ++++++++ .github/workflows/dependabot-automerge.yaml | 11 +++++++++-- 3 files changed, 17 insertions(+), 8 deletions(-) delete mode 100644 .github/review-tooling/requirements.txt diff --git a/.github/review-tooling/requirements.txt b/.github/review-tooling/requirements.txt deleted file mode 100644 index 91cd520..0000000 --- a/.github/review-tooling/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# Pinned deps for review_dependabot_major.py (the AI risk gate on major bumps). -# NOTE: this is only the client LIBRARY. It is pointed at the GitHub Models -# endpoint (https://models.github.ai/inference) and authenticated with the -# workflow's built-in GITHUB_TOKEN. We do NOT use the OpenAI API, an OpenAI API -# key, or OpenAI billing -- inference runs entirely on GitHub Models. -openai==2.45.0 diff --git a/.github/review-tooling/review_dependabot_major.py b/.github/review-tooling/review_dependabot_major.py index 667c86b..318e32a 100644 --- a/.github/review-tooling/review_dependabot_major.py +++ b/.github/review-tooling/review_dependabot_major.py @@ -1,4 +1,12 @@ #!/usr/bin/env python3 +# /// script +# requires-python = ">=3.11" +# dependencies = ["openai==2.45.0"] +# /// +# Dependencies are declared inline (PEP 723) and provisioned by `uv run --script` +# on the runner -- no requirements.txt, no project venv. `openai` is only the +# client library, pointed at the GitHub Models endpoint (see below); not the +# OpenAI API. """Render a machine-readable risk verdict for a MAJOR Dependabot bump. Used by the shared Dependabot auto-merge workflow to decide whether a major diff --git a/.github/workflows/dependabot-automerge.yaml b/.github/workflows/dependabot-automerge.yaml index 0601594..9ddc91c 100644 --- a/.github/workflows/dependabot-automerge.yaml +++ b/.github/workflows/dependabot-automerge.yaml @@ -106,6 +106,12 @@ jobs: # external secret); on any failure the script emits verdict=human, so we never # auto-merge on an errored verdict. # --------------------------------------------------------------------------- + - name: Set up uv (runs the AI verdict script) + if: | + steps.metadata.outputs.update-type == 'version-update:semver-major' && + github.event.action == 'opened' + uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5 + - name: AI risk verdict for major bump id: aiverdict continue-on-error: true # a failed verdict must fall through to manual review, not fail the required check @@ -123,10 +129,11 @@ jobs: DEP_NAMES: ${{ steps.metadata.outputs.dependency-names }} ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }} run: | - python3 -m pip install --quiet --disable-pip-version-check -r _tooling/.github/review-tooling/requirements.txt gh pr view "$PR_URL" --json title --jq .title > pr_title.txt gh pr view "$PR_URL" --json body --jq '.body // ""' > pr_body.txt - python3 _tooling/.github/review-tooling/review_dependabot_major.py \ + # PEP 723 inline deps are provisioned by uv in an isolated env; --script + # forces script mode so the surrounding repo's project is ignored. + uv run --script _tooling/.github/review-tooling/review_dependabot_major.py \ --title-file pr_title.txt --body-file pr_body.txt \ --dep-names "$DEP_NAMES" --ecosystem "$ECOSYSTEM" --out verdict.json {