From e847947abf29009fe88f707ae077620de8ea615e Mon Sep 17 00:00:00 2001 From: Javier Lougedo Date: Wed, 9 Sep 2026 14:27:13 +0200 Subject: [PATCH 1/6] ci(NOJIRA-1234): extend bot PR automerge to smartling and aikido Gate on the PR author login instead of a single actor, so translation PRs from smartling-github-connector[bot] and security fixes from aikido-autofix[bot] are auto-approved and auto-merged alongside dependabot. Titles are not a reliable signal for these bots, so the allowlist keys off github.actor only. Co-Authored-By: Claude Opus 5 --- .github/workflows/dependabot-automerge.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index fca06d5..f5f2af6 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,4 +1,4 @@ -name: Dependabot auto-approve and auto-merge +name: Bot PR auto-approve and auto-merge on: pull_request_target: @@ -9,9 +9,10 @@ permissions: contents: write jobs: - dependabot: + automerge: runs-on: ubuntu-latest - if: github.actor == 'dependabot[bot]' + # Trusted automation only. Keep this list in sync across blocks-owned repos. + if: contains(fromJSON('["dependabot[bot]", "smartling-github-connector[bot]", "aikido-autofix[bot]"]'), github.actor) steps: - name: Checkout Repo @@ -27,13 +28,13 @@ jobs: - name: Authenticate gh run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token - - name: Approve dependabot PRs + - name: Approve bot PR run: gh pr review --approve "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Merge for dependabot PRs + - name: Enable auto-merge for bot PR run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} From 4b8f31b581034cfdcc5d747b2a78e43872418231 Mon Sep 17 00:00:00 2001 From: Javier Lougedo Date: Wed, 9 Sep 2026 15:36:19 +0200 Subject: [PATCH 2/6] ci(NOJIRA-1234): drop smartling from the automerge allowlist This repo has no Smartling-managed content: the smartling-github-connector[bot] has never opened a PR here and there is no Smartling config. Narrow the allowlist to the bots that actually raise PRs in this repo. Co-Authored-By: Claude Opus 5 --- .github/workflows/dependabot-automerge.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index f5f2af6..ff0b111 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -11,8 +11,8 @@ permissions: jobs: automerge: runs-on: ubuntu-latest - # Trusted automation only. Keep this list in sync across blocks-owned repos. - if: contains(fromJSON('["dependabot[bot]", "smartling-github-connector[bot]", "aikido-autofix[bot]"]'), github.actor) + # Trusted automation only. Smartling is omitted: no translated content in this repo. + if: contains(fromJSON('["dependabot[bot]", "aikido-autofix[bot]"]'), github.actor) steps: - name: Checkout Repo From a4c1c8d474710e820b7f46c17c15ec762febcc84 Mon Sep 17 00:00:00 2001 From: Javier Lougedo Date: Wed, 9 Sep 2026 17:35:38 +0200 Subject: [PATCH 3/6] ci(NOJIRA-1234): hold risky bot dependency PRs for human review A green CI run does not prove a transitive dependency bump is safe: the repo's own tests never exercise how the intermediate package uses the changed API. Approve as before, but only arm auto-merge when the diff looks routine. Held back for a human when any of these match: - the bot's title declares a major version upgrade - a JS manifest touches resolutions/overrides (a forced transitive pin) - go.mod gains a +incompatible major bump - more than 6 manifest dependency lines change at once - the lockfile rewrite exceeds 600 lines Validated against 13 real bot PRs: correctly holds xfiles#543 (docker v24->v25 +incompatible), blocks#3039 (major axios), renderer#1481 and mail-composer#400 (forced resolutions), and correctly passes the single direct minor bumps such as embed#760, pages#620 and purgatory#314. Also drops the checkout and 'apt-get install gh' steps: nothing read the working tree (gh is API-only) and gh ships on ubuntu-latest. All repos now hold a byte-identical file apart from the allowlist line. Co-Authored-By: Claude Opus 5 --- .github/workflows/dependabot-automerge.yml | 81 ++++++++++++++++++---- 1 file changed, 68 insertions(+), 13 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index ff0b111..c0ea463 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -11,31 +11,86 @@ permissions: jobs: automerge: runs-on: ubuntu-latest - # Trusted automation only. Smartling is omitted: no translated content in this repo. + # Trusted automation only. Keep this list in sync across blocks-owned repos. if: contains(fromJSON('["dependabot[bot]", "aikido-autofix[bot]"]'), github.actor) steps: - - name: Checkout Repo - uses: actions/checkout@v7 - with: - fetch-depth: 1 + - name: Approve bot PR + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Install GitHub CLI + # A green CI run does not prove a transitive bump is safe: the repo's own + # tests never exercise how the intermediate package uses the changed API. + # Anything matching below is left for a human instead of auto-merging. + - name: Assess dependency risk + id: risk run: | - sudo apt-get update - sudo apt-get install -y gh + FILES=$(gh api "repos/$REPO/pulls/$PR_NUMBER/files" --paginate) + manifests() { + printf '%s' "$FILES" | jq -r ".[] | select(.filename|test(\"$1\")) | .patch // \"\"" + } + REASON="" - - name: Authenticate gh - run: echo "${{ secrets.GITHUB_TOKEN }}" | gh auth login --with-token + # 1. The bot names major bumps in its own title. + if printf '%s' "$PR_TITLE" | grep -qiE 'major version upgrade'; then + REASON="major version upgrade" + fi - - name: Approve bot PR - run: gh pr review --approve "$PR_URL" + # 2. A forced transitive pin in a JS manifest (resolutions/overrides/glob). + if [ -z "$REASON" ] && manifests 'package\\.json$' \ + | grep -qE '^[ +-].*"(resolutions|overrides)"[[:space:]]*:|^\+[[:space:]]*"\*\*/'; then + REASON="forced transitive override" + fi + + # 3. A Go major bump. + if [ -z "$REASON" ] && manifests 'go\\.mod$' | grep -qE '^\+.*\+incompatible'; then + REASON="go major (+incompatible) bump" + fi + + # 4. A bulk sweep across many packages at once. + N=$(manifests '(go\\.mod|package\\.json)$' \ + | grep -cE '^\+[[:space:]]+([a-zA-Z0-9./_-]+ v[0-9]|"[^"]+"[[:space:]]*:)' || true) + if [ -z "$REASON" ] && [ "${N:-0}" -gt 6 ]; then + REASON="bulk sweep ($N dependency lines changed)" + fi + + # 5. A wide lockfile rewrite means many transitive packages moved, + # even when the manifest diff looks small. + CHURN=$(printf '%s' "$FILES" \ + | jq '[.[] | select(.filename|test("(yarn\\.lock|package-lock\\.json|go\\.sum|pnpm-lock\\.yaml)$")) | .additions + .deletions] | add // 0') + if [ -z "$REASON" ] && [ "${CHURN:-0}" -gt 600 ]; then + REASON="wide lockfile rewrite ($CHURN lines)" + fi + + if [ -n "$REASON" ]; then + echo "risky=true" >> "$GITHUB_OUTPUT" + echo "reason=$REASON" >> "$GITHUB_OUTPUT" + echo "::warning::Not auto-merging: $REASON" + else + echo "risky=false" >> "$GITHUB_OUTPUT" + fi env: - PR_URL: ${{ github.event.pull_request.html_url }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Enable auto-merge for bot PR + if: steps.risk.outputs.risky == 'false' run: gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Flag risky bot PR for human review + if: steps.risk.outputs.risky == 'true' + run: | + gh pr edit "$PR_URL" --add-label needs-human + gh pr comment "$PR_URL" --body \ + "Not auto-merged: **$REASON**. A green CI run does not prove a transitive dependency bump is safe, so this one needs a human look." + env: + PR_URL: ${{ github.event.pull_request.html_url }} + REASON: ${{ steps.risk.outputs.reason }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 409f164c891e35fe8ef5e4395a840ad7665bfffc Mon Sep 17 00:00:00 2001 From: Javier Lougedo Date: Wed, 9 Sep 2026 18:02:23 +0200 Subject: [PATCH 4/6] ci(NOJIRA-1234): withhold approval from risky bot PRs Assess risk first, then approve only when the diff looks routine. A risky PR now gets no approval at all, so it cannot satisfy the required-review count and a human has to sign it off - the label alone was advisory, since a bot approval already met the review requirement. Also close the stale-arming gap: a PR can open looking routine (approved, auto-merge armed) and then be force-pushed into something risky. On the risky path the workflow now calls 'gh pr merge --disable-auto' and dismisses its own earlier approval before labelling and commenting. Co-Authored-By: Claude Opus 5 --- .github/workflows/dependabot-automerge.yml | 33 ++++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index c0ea463..11e46e6 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -15,15 +15,10 @@ jobs: if: contains(fromJSON('["dependabot[bot]", "aikido-autofix[bot]"]'), github.actor) steps: - - name: Approve bot PR - run: gh pr review --approve "$PR_URL" - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # A green CI run does not prove a transitive bump is safe: the repo's own # tests never exercise how the intermediate package uses the changed API. - # Anything matching below is left for a human instead of auto-merging. + # Anything matching below is neither approved nor auto-merged, so it cannot + # satisfy the required-review count and a human has to sign it off. - name: Assess dependency risk id: risk run: | @@ -77,6 +72,13 @@ jobs: PR_TITLE: ${{ github.event.pull_request.title }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Approve bot PR + if: steps.risk.outputs.risky == 'false' + run: gh pr review --approve "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Enable auto-merge for bot PR if: steps.risk.outputs.risky == 'false' run: gh pr merge --auto --squash "$PR_URL" @@ -84,13 +86,26 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Flag risky bot PR for human review + - name: Hold risky bot PR for human review if: steps.risk.outputs.risky == 'true' run: | + # An earlier run may have approved and armed this PR while it still + # looked routine, so undo both before flagging it. + gh pr merge --disable-auto "$PR_URL" || true + + gh api "repos/$REPO/pulls/$PR_NUMBER/reviews" \ + --jq '.[] | select(.state == "APPROVED" and .user.login == "github-actions[bot]") | .id' \ + | while read -r id; do + gh api -X PUT "repos/$REPO/pulls/$PR_NUMBER/reviews/$id/dismissals" \ + -f message="Withdrawn: $REASON" >/dev/null || true + done + gh pr edit "$PR_URL" --add-label needs-human gh pr comment "$PR_URL" --body \ - "Not auto-merged: **$REASON**. A green CI run does not prove a transitive dependency bump is safe, so this one needs a human look." + "Held for human review: **$REASON**. A green CI run does not prove a transitive dependency bump is safe, so this PR was deliberately **not** approved and auto-merge is off. It needs a human to review and merge it." env: PR_URL: ${{ github.event.pull_request.html_url }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} REASON: ${{ steps.risk.outputs.reason }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From eac353c1a1d1d7d796d4cc50bc65654706b9c29a Mon Sep 17 00:00:00 2001 From: Javier Lougedo <164027374+Jlougedo-TF@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:22:50 +0200 Subject: [PATCH 5/6] ci(NOJIRA-1234): Hold aikido PRs that touch non-dependency files --- .github/workflows/dependabot-automerge.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 11e46e6..1ae669a 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -59,6 +59,15 @@ jobs: REASON="wide lockfile rewrite ($CHURN lines)" fi + # 6. Aikido only fixes dependencies. Any other touched file is not what + # the bot normally produces, so a human has to look at it. + if [ -z "$REASON" ] && [ "$GITHUB_ACTOR" = "aikido-autofix[bot]" ]; then + OTHER=$(printf '%s' "$FILES" | jq -r '.[].filename | select((split("/")[-1] | test("^(package\\.json|yarn\\.lock|package-lock\\.json|pnpm-lock\\.yaml|go\\.mod|go\\.sum|requirements[^/]*\\.(txt|in))$")) | not)' | head -3 | tr '\n' ' ') + if [ -n "$OTHER" ]; then + REASON="aikido PR touches non-dependency files: $OTHER" + fi + fi + if [ -n "$REASON" ]; then echo "risky=true" >> "$GITHUB_OUTPUT" echo "reason=$REASON" >> "$GITHUB_OUTPUT" From 5758dfd52a03788a7cec2006b2392e95d7424742 Mon Sep 17 00:00:00 2001 From: Javier Lougedo <164027374+Jlougedo-TF@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:15:16 +0200 Subject: [PATCH 6/6] ci(NOJIRA-1234): Revoke bot approval when someone else pushes to the PR --- .github/workflows/dependabot-automerge.yml | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 1ae669a..7324d43 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -11,16 +11,32 @@ permissions: jobs: automerge: runs-on: ubuntu-latest - # Trusted automation only. Keep this list in sync across blocks-owned repos. - if: contains(fromJSON('["dependabot[bot]", "aikido-autofix[bot]"]'), github.actor) + # Trusted automation only. Keyed on the PR author so the job still runs, and + # can revoke, when someone else pushes to a bot PR. Keep in sync across repos. + if: contains(fromJSON('["dependabot[bot]", "aikido-autofix[bot]"]'), github.event.pull_request.user.login) steps: + - name: Check the PR was pushed by its author + id: trust + run: | + # A later push by anyone else (e.g. a collaborator with write access) + # must not inherit an approval or armed auto-merge given to the bot. + if [ "$ACTOR" = "$AUTHOR" ]; then + echo "trusted=true" >> "$GITHUB_OUTPUT" + else + echo "trusted=false" >> "$GITHUB_OUTPUT" + fi + env: + ACTOR: ${{ github.actor }} + AUTHOR: ${{ github.event.pull_request.user.login }} + # A green CI run does not prove a transitive bump is safe: the repo's own # tests never exercise how the intermediate package uses the changed API. # Anything matching below is neither approved nor auto-merged, so it cannot # satisfy the required-review count and a human has to sign it off. - name: Assess dependency risk id: risk + if: steps.trust.outputs.trusted == 'true' run: | FILES=$(gh api "repos/$REPO/pulls/$PR_NUMBER/files" --paginate) manifests() { @@ -95,8 +111,8 @@ jobs: PR_URL: ${{ github.event.pull_request.html_url }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Hold risky bot PR for human review - if: steps.risk.outputs.risky == 'true' + - name: Hold bot PR for human review + if: steps.trust.outputs.trusted == 'false' || steps.risk.outputs.risky == 'true' run: | # An earlier run may have approved and armed this PR while it still # looked routine, so undo both before flagging it. @@ -111,10 +127,10 @@ jobs: gh pr edit "$PR_URL" --add-label needs-human gh pr comment "$PR_URL" --body \ - "Held for human review: **$REASON**. A green CI run does not prove a transitive dependency bump is safe, so this PR was deliberately **not** approved and auto-merge is off. It needs a human to review and merge it." + "Held for human review: **$REASON**. This PR was deliberately **not** approved and auto-merge is off; any earlier approval was withdrawn. It needs a human to review and merge it." env: PR_URL: ${{ github.event.pull_request.html_url }} REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} - REASON: ${{ steps.risk.outputs.reason }} + REASON: ${{ steps.risk.outputs.reason || format('{0} pushed to this PR, not the bot that opened it', github.actor) }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}