Skip to content
This repository was archived by the owner on Oct 30, 2026. It is now read-only.

build(deps): bump the minor-and-patch group across 1 directory with 4 updates #37

build(deps): bump the minor-and-patch group across 1 directory with 4 updates

build(deps): bump the minor-and-patch group across 1 directory with 4 updates #37

name: Dependabot Auto-Merge

Check warning on line 1 in .github/workflows/dependabot-auto-merge.yml

View workflow run for this annotation

GitHub Actions / Dependabot Auto-Merge

Workflow execution policy warning (evaluate mode)

On December 2, 2026, GitHub will restrict `pull_request_target` on public repositories by default. To continue allowing the event trigger, configure an Actions policy. Learn more: https://gh.io/securely-using-pull_request_target#default-policy-for-pull_request_target
# `pull_request_target`, not `pull_request`, because of how Dependabot runs are
# isolated: a workflow triggered by Dependabot on `pull_request` receives
# Dependabot secrets rather than Actions secrets, so the organisation's
# RELEASER_APP_* secrets resolve to empty strings and the App token step fails
# before it does anything. `pull_request_target` runs in the base branch's
# context, where those secrets are available, which is also GitHub's own
# recommendation for this pattern. This is observed, not assumed: the same
# workflow in the organisation's other repositories, on runs Dependabot
# triggered, logs "Secret source: Actions" and mints the App token. For
# example coordinode-lsm-tree run 38696382130 (event `pull_request_target`,
# actor and triggering actor `dependabot[bot]`) generated the App token and
# armed auto-merge. The restriction GitHub documents for Dependabot-triggered
# `pull_request_target` applies when the base ref was created by Dependabot;
# here the base is the default branch.
#
# That context carries write-capable credentials, so this workflow must never
# execute code from the pull request. It does not: there is no checkout, and
# every step either reads PR metadata through the API or drives `gh`. Do not add
# a checkout here; if one ever becomes necessary, it must not use the head ref.
#
# Enabling auto-merge only arms it: the merge happens when every check the
# default-branch ruleset requires has passed, as for any other pull request.
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
# Only what the default token needs: `dependabot/fetch-metadata` reads the
# pull request. Approving, disarming and merging are done with the App token
# below, so nothing here needs write access to the repository contents.
pull-requests: read
# One run per pull request, and a new push cancels the run it superseded rather
# than leaving it racing toward a head that no longer exists.
concurrency:
group: dependabot-auto-merge-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
auto-merge:
runs-on: ubuntu-latest
# Only the author here. Who triggered the run decides what the job does, not
# whether it runs at all: a push by anyone else is the case that has to be
# handled, and a job that skips it cannot handle anything.
if: github.event.pull_request.user.login == 'dependabot[bot]'
steps:
# Pinned to commit SHAs, not tags, because `pull_request_target` puts the
# App's private key within reach of whatever these resolve to: retargeting
# a mutable tag would be enough to take it. Dependabot updates a pinned
# SHA the same way it updates a tag, so this costs nothing to maintain.
- name: Generate App Token
id: app-token
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
with:
app-id: ${{ secrets.RELEASER_APP_ID }}
private-key: ${{ secrets.RELEASER_APP_PRIVATE_KEY }}
# Least privilege: `pull-requests` to approve, to read whether
# auto-merge is armed and to disarm it; `contents` because the
# squash-merge writes to the default branch. No `actions` grant: the
# merge is made by the App installation, which holds the workflows
# permission, and the organisation's other repositories auto-merge
# Dependabot updates to workflow files with exactly these two: in
# coordinode-lsm-tree, #909 (changes .github/workflows/benchmark.yml)
# and #693 were merged by the App through auto-merge armed this way.
permission-contents: write
permission-pull-requests: write
# Auto-merge, once enabled, outlives the head it was enabled for. GitHub
# turns it off by itself only when the pusher lacks write access, so a
# maintainer adding commits to an open Dependabot pull request leaves it
# armed, and it then merges those commits. `--match-head-commit` does not
# cover this: it is a precondition checked when the command runs, not a
# standing binding to that SHA.
#
# So the run a foreign push triggers disarms rather than skips. The
# approval is left to the repository's stale-review dismissal, which is
# the setting that exists for it. The push itself cannot merge in the
# meantime: the ruleset requires up-to-date, passing checks, which rerun
# on the new head and take minutes, while this job disarms in seconds.
#
# `github.actor` is who caused the event, and a rerun keeps it: rerunning
# a Dependabot run leaves `actor` as dependabot with `triggering_actor`
# set to whoever asked, and rerunning a foreign-push run keeps the pusher
# in `actor`. So the actor of a `synchronize` event says whether the head
# moved under someone else, on the first attempt and on every rerun
# alike. A reopen moves no head, so it never disarms; see below.
- name: Disarm auto-merge when someone else moved the head
if: github.event.action == 'synchronize' && github.actor != 'dependabot[bot]'
# The head is checked because disarming has no precondition of its own:
# `--match-head-commit` maps to `expectedHeadOid`, which the mutation
# behind `--disable-auto` does not take. Without the check, a slow run
# from one push can disable the auto-merge a later Dependabot run
# legitimately armed.
#
# Every value the script reads arrives through the environment. An
# expression interpolated into `run:` is expanded before the shell sees
# the line, so its contents become script rather than data.
run: |
state=$(gh pr view "$PR_NUMBER" --json autoMergeRequest,headRefOid \
--jq '"\(.autoMergeRequest != null) \(.headRefOid)"')
armed=${state%% *}
current=${state##* }
if [ "$current" != "$HEAD_SHA" ]; then
echo "head is now $current, not the $HEAD_SHA this run saw;"
echo "a later run owns this pull request, leaving its state alone"
elif [ "$armed" = "true" ]; then
gh pr merge --disable-auto "$PR_NUMBER"
echo "auto-merge disarmed: the head moved under a push by $PUSHER"
else
echo "auto-merge was not armed; nothing to disarm"
fi
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PUSHER: ${{ github.triggering_actor }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# No token passed: the action's `github-token` already defaults to
# `github.token`, which the permissions block above scopes to reading the
# pull request. Keyed on the event actor, not on who pressed rerun: a
# maintainer rerunning a failed Dependabot run must be able to restore the
# approval and auto-merge, and both are bound to the head SHA below, so
# the rerun cannot approve anything Dependabot did not push.
#
# A reopen by a maintainer runs this too: closing the pull request
# disarmed its auto-merge, and nothing else would arm it again until
# Dependabot's next push. This action alone does not make that safe: it
# checks only the first commit of the pull request, so a branch someone
# else pushed to on top of Dependabot's commit still passes it. The next
# step checks the rest.
- name: Dependabot metadata
id: metadata
if: github.actor == 'dependabot[bot]' || github.event.action == 'reopened'
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
# Every commit of the pull request is Dependabot's, and the last of them
# is the head this run approves, so the history checked is the history of
# that head. The list endpoint stops at 250 commits; a longer history then
# ends before the head and is refused.
#
# The author alone proves nothing: it is whatever the commit claims, and a
# signature verifies the signer, not the author. A genuine Dependabot
# commit is committed and signed by GitHub itself (committer `web-flow`),
# so a commit someone signed with their own key, under Dependabot's name,
# shows that person as the committer and is refused.
- name: Verify every commit is Dependabot's
id: commits
if: steps.metadata.outcome == 'success'
run: |
gh api --paginate --slurp "repos/${GH_REPO}/pulls/${PR_NUMBER}/commits" |
jq -e 'add as $c
| ($c | length) > 0
and $c[-1].sha == env.HEAD_SHA
and all($c[]; .author.login == "dependabot[bot]"
and .committer.login == "web-flow"
and .commit.verification.verified == true)'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
# The metadata step is skipped for a foreign push, so its outputs are
# empty there and this condition is false: approval happens only on a run
# Dependabot itself triggered, or on a reopen of a branch holding only
# Dependabot's commits, and in both only after the history check above.
#
# Every update merges once CI is green, majors included, where CI runs
# what changed: Python and Rust updates are what the test, integration,
# embedded and demo jobs run on. Two stay manual. A major GitHub Actions
# update: several actions run only in release.yml, on a tag, so a green
# pull request proves nothing about them. Any Docker update: no job
# builds the demo image (the demo job installs the lock without Docker),
# so a base-image change is untested until someone builds it.
#
# `package-ecosystem` is `github_actions`, with an underscore: the action
# takes it from the second segment of the Dependabot branch name
# (`dependabot/github_actions/...`), not from the `github-actions` key
# of dependabot.yml.
- name: Approve and enable auto-merge
if: >-
steps.metadata.outcome == 'success'
&& steps.commits.outcome == 'success'
&& steps.metadata.outputs.package-ecosystem != 'docker'
&& !(steps.metadata.outputs.package-ecosystem == 'github_actions'
&& steps.metadata.outputs.update-type == 'version-update:semver-major')
# Both operations name the head this run actually inspected, rather than
# just the pull request: the approval is recorded against a commit id,
# and the merge refuses outright if the head has moved.
run: |
gh api "repos/${GH_REPO}/pulls/${PR_NUMBER}/reviews" \
-f event=APPROVE -f commit_id="$HEAD_SHA" --silent
gh pr merge --auto --squash --match-head-commit "$HEAD_SHA" "$PR_NUMBER"
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GH_REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}