From a6b3c809037d6cac6870b2eba532920eb7d05caa Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Thu, 6 Aug 2026 14:28:05 +0300 Subject: [PATCH 1/2] Auto-merge a group only when every update in it is a patch The first live run, on lemma#2, showed the workflow never auto-merges a grouped update at all. fetch-metadata leaves update-type empty for a group - a group has no single type - and the condition read only that field. Seven documentation dependencies in one PR went to a human, and would have done so even if all seven were patches. That is the group most worth merging unattended, and grouping is why dependabot.yml groups at all. updated-dependencies-json carries one entry per dependency with its own updateType. The decision now requires every entry to be a patch, and moves into its own step with a reason string so the log says why. Every branch that does not positively establish a patch ends at auto=false: a misread refuses rather than merges. --- .github/workflows/dependabot-auto-merge.yml | 85 +++++++++++++++++---- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 2c627ba..3ce2173 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -1,9 +1,19 @@ name: Dependabot auto-merge -# Actions are the only ecosystem Dependabot watches here, and an action bump is -# the update that piles up unread until the queue is too long to review -# honestly. Auto-merge is queued, not immediate: GitHub still waits for the -# required checks, and `scripts/check_site.py` is what those checks run. +# What this does and does not do: +# +# auto-merged - GitHub Actions bumps, and patch bumps of anything else +# left for you - anything with a minor or major in it +# +# A patch release and an action bump are the updates that pile up unread until +# the queue is too long to review honestly. A minor bump can change behaviour, +# so it keeps a human. Auto-merge is queued, not immediate: GitHub still waits +# for the required checks to pass, and a red build leaves the PR open. +# +# Actions are the only ecosystem Dependabot watches here - the site is +# hand-written HTML with no package manifest - so in practice only the first +# branch below is ever taken. The rest is kept identical to the other +# repositories so the six files stay comparable. on: pull_request_target @@ -18,27 +28,74 @@ jobs: contents: write pull-requests: write steps: - # Reads the update type from the PR that Dependabot opened. Nothing from - # the branch is checked out or executed, which is what makes + # Reads the update metadata from the PR that Dependabot opened. Nothing + # from the branch is checked out or executed, which is what makes # pull_request_target safe to use here. - id: metadata uses: dependabot/fetch-metadata@v2 + - id: verdict + name: Decide whether this one can merge itself + env: + ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }} + UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} + UPDATED: ${{ steps.metadata.outputs.updated-dependencies-json }} + # A grouped pull request has no single update type: fetch-metadata + # leaves `update-type` empty and puts one entry per dependency in + # `updated-dependencies-json`. Reading only `update-type` sent every + # grouped bump to a human, including a group where all seven were + # patches - which is exactly the group worth merging unattended, and the + # reason the groups exist at all. + run: | + PATCH="version-update:semver-patch" + + if [ "$ECOSYSTEM" = "github_actions" ]; then + echo "auto=true" >> "$GITHUB_OUTPUT" + echo "reason=an actions bump" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ -n "$UPDATE_TYPE" ] && [ "$UPDATE_TYPE" != "null" ]; then + if [ "$UPDATE_TYPE" = "$PATCH" ]; then + echo "auto=true" >> "$GITHUB_OUTPUT" + echo "reason=a patch bump" >> "$GITHUB_OUTPUT" + else + echo "auto=false" >> "$GITHUB_OUTPUT" + echo "reason=${UPDATE_TYPE#version-update:semver-} is not a patch" >> "$GITHUB_OUTPUT" + fi + exit 0 + fi + + total=$(jq 'length' <<<"$UPDATED") + if [ "$total" -eq 0 ]; then + # No metadata to read. Refusing is the only safe reading of silence. + echo "auto=false" >> "$GITHUB_OUTPUT" + echo "reason=no update metadata to read" >> "$GITHUB_OUTPUT" + exit 0 + fi + + patches=$(jq --arg p "$PATCH" '[.[] | select(.updateType == $p)] | length' <<<"$UPDATED") + if [ "$total" -eq "$patches" ]; then + echo "auto=true" >> "$GITHUB_OUTPUT" + echo "reason=a group of $total, every one a patch" >> "$GITHUB_OUTPUT" + else + echo "auto=false" >> "$GITHUB_OUTPUT" + echo "reason=a group of $total, $((total - patches)) beyond patch" >> "$GITHUB_OUTPUT" + fi + - name: Approve and queue the merge - if: >- - steps.metadata.outputs.package-ecosystem == 'github_actions' || - steps.metadata.outputs.update-type == 'version-update:semver-patch' + if: steps.verdict.outputs.auto == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR: ${{ github.event.pull_request.html_url }} + REASON: ${{ steps.verdict.outputs.reason }} run: | + echo "auto-merging: $REASON" gh pr review --approve "$PR" gh pr merge --auto --squash "$PR" - name: Explain why this one was left alone - if: >- - steps.metadata.outputs.package-ecosystem != 'github_actions' && - steps.metadata.outputs.update-type != 'version-update:semver-patch' + if: steps.verdict.outputs.auto != 'true' env: - TYPE: ${{ steps.metadata.outputs.update-type }} - run: echo "$TYPE is not auto-merged; this pull request needs a human." + REASON: ${{ steps.verdict.outputs.reason }} + run: echo "not auto-merged ($REASON); this pull request needs a human." From d3046a3ae0190bf7944add51cd140030f1a55f1a Mon Sep 17 00:00:00 2001 From: Denis_Drobyshev Date: Thu, 6 Aug 2026 14:31:12 +0300 Subject: [PATCH 2/2] Stop trying to approve, which is what actually broke the merge The five red Dependabot pull requests across the organisation are all this one line: failed to create review: GitHub Actions is not permitted to approve pull requests. (addPullRequestReview) The organisation does not allow Actions to approve pull requests. Under bash -e that call took the whole step down before gh pr merge --auto was ever reached, so nothing was ever queued. The approval was never needed. Branch protection requires the CI check and no reviews, and auto-merge waits on the check either way. If a review requirement is ever added this needs a token that is not GITHUB_TOKEN. --- .github/workflows/dependabot-auto-merge.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 3ce2173..4fffe23 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -83,15 +83,26 @@ jobs: echo "reason=a group of $total, $((total - patches)) beyond patch" >> "$GITHUB_OUTPUT" fi - - name: Approve and queue the merge + - name: Queue the merge if: steps.verdict.outputs.auto == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR: ${{ github.event.pull_request.html_url }} REASON: ${{ steps.verdict.outputs.reason }} + # No `gh pr review --approve`. This organisation does not permit Actions + # to approve pull requests, so that call fails with + # + # GitHub Actions is not permitted to approve pull requests + # + # and under `bash -e` it took the whole step down before the merge was + # ever queued - which is how auto-merge came to be broken in every + # repository at once. + # + # The approval was never needed: branch protection here requires the CI + # check and no reviews. If a review requirement is ever added, this needs + # a token that is not GITHUB_TOKEN, not a retry. run: | echo "auto-merging: $REASON" - gh pr review --approve "$PR" gh pr merge --auto --squash "$PR" - name: Explain why this one was left alone