diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml new file mode 100644 index 0000000..6becdf1 --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yml @@ -0,0 +1,121 @@ +name: Dependabot auto-merge + +# 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. +# +# torch, transformers and scikit-learn never reach this workflow: +# dependabot.yml ignores them, because they are optional extras with their own +# release cadence, pinned deliberately by whoever installs them. +# +# Everything else has real cover here. The required checks run the suite on four +# Pythons across three operating systems, scaffold a project and drive it end to +# end, and fine-tune a real checkpoint -- so a bump that breaks the framework +# fails before it merges rather than after. + +on: pull_request_target + +permissions: + contents: read + +jobs: + auto-merge: + # Who opened the pull request, not who triggered the event. `github.actor` + # is whoever caused this run, so the moment a person touches a Dependabot + # pull request - reopening it, or nudging it after a base change - the job + # skips and the update sits there looking merged-ready and never merging. + # The author never changes, which is the thing actually being asserted. + if: github.event.pull_request.user.login == 'dependabot[bot]' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + # 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@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3 + + - 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: 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 merge --auto --squash "$PR" + + - name: Explain why this one was left alone + if: steps.verdict.outputs.auto != 'true' + env: + REASON: ${{ steps.verdict.outputs.reason }} + run: echo "not auto-merged ($REASON); this pull request needs a human."