1212 pull_request_target :
1313 types : [opened, synchronize, reopened]
1414
15+ # Read-only. Every write this workflow performs - approving and merging -
16+ # uses DEPENDABOT_AUTOMERGE_TOKEN, not GITHUB_TOKEN, so GITHUB_TOKEN needs no
17+ # write scope. That matters more than usual here: the job runs under
18+ # pull_request_target, so a compromised step or action would otherwise hold a
19+ # repo-write token.
1520permissions :
16- contents : write
17- pull-requests : write
21+ contents : read
22+ pull-requests : read
1823 checks : read
1924
2025jobs :
2126 automerge :
2227 runs-on : ubuntu-latest
23- if : github.actor == 'dependabot[bot]'
28+ # The PR's author, not github.actor. github.actor is whoever triggered
29+ # the event, so on `reopened` - or when anyone else pushes to the branch -
30+ # it is that person, this condition goes false, and the job silently does
31+ # nothing on a real Dependabot PR. Nothing reports that; it just never
32+ # merges.
33+ #
34+ # This deliberately does NOT decide whether a human has touched the
35+ # branch. That is a separate question with a separate answer - see the
36+ # commit-authorship check in the merge step - because the two were
37+ # previously conflated here, and the accidental protection it gave was
38+ # paid for by silent no-ops.
39+ if : github.event.pull_request.user.login == 'dependabot[bot]'
2440 env :
2541 # Auto-merge is opt-in per repository, and provisioning
2642 # DEPENDABOT_AUTOMERGE_TOKEN is what opts in. Without it every step
@@ -40,11 +56,16 @@ jobs:
4056 steps :
4157 - name : Report that auto-merge is not enabled
4258 if : env.HAS_AUTOMERGE_PAT != 'true'
43- run : |
44- echo "::notice::DEPENDABOT_AUTOMERGE_TOKEN is not set for this \
45- repository, so Dependabot auto-merge is disabled and this PR will \
46- not be merged automatically. See runbooks/dependabot-automerge.md \
47- in vln-devsecops/guidance to provision it."
59+ env :
60+ # Folded scalar rather than shell line continuations: a backslash
61+ # continuation inside the quoted string embeds the indentation of
62+ # each continued line into the notice text.
63+ MSG : >-
64+ DEPENDABOT_AUTOMERGE_TOKEN is not set for this repository, so
65+ Dependabot auto-merge is disabled and this PR will not be merged
66+ automatically. See runbooks/dependabot-automerge.md in
67+ vln-devsecops/guidance to provision it.
68+ run : echo "::notice::$MSG"
4869
4970 - name : Fetch Dependabot metadata
5071 id : meta
79100 # yet. A short fixed sleep is not enough: a queued Windows or
80101 # self-hosted runner can take minutes to post its first check run,
81102 # and "no checks yet" is indistinguishable from "this repo has no
82- # other CI". Concluding success too early fails OPEN and merges
83- # unguarded, so only give up waiting after this window.
103+ # other CI". This window separates the two.
84104 settle_seconds=180
85105 # Overall bound once checks have appeared.
86106 max_seconds=1800
@@ -114,8 +134,21 @@ jobs:
114134 sleep "$interval"
115135 continue
116136 fi
117- echo "No sibling check runs appeared within ${settle_seconds}s; nothing to gate on."
118- exit 0
137+ # Fail closed. A repo with no check at all on a Dependabot PR
138+ # has nothing for this gate to gate on, and merging anyway
139+ # would be the original unguarded behaviour wearing a gate's
140+ # name - the PR would look checked because this job went green.
141+ #
142+ # Auto-merge is only supposed to be enabled once a repo has at
143+ # least one PR-triggered check (see
144+ # runbooks/dependabot-automerge.md), so reaching this point
145+ # means the repo is misconfigured. Say so, loudly, rather than
146+ # merging and looking correct.
147+ echo "No sibling check runs appeared on $SHA within" \
148+ "${settle_seconds}s. Refusing to auto-merge: there is" \
149+ "nothing gating this PR. Add a PR-triggered check, or" \
150+ "remove this workflow."
151+ exit 1
119152 fi
120153
121154 incomplete=$(jq '[.[] | select(.status != "completed")] | length' <<<"$runs_json")
@@ -163,8 +196,48 @@ jobs:
163196 (steps.meta.outputs.update-type == 'version-update:semver-minor' ||
164197 steps.meta.outputs.update-type == 'version-update:semver-patch')
165198 run : |
199+ set -euo pipefail
200+
201+ # The gate above validated $SHA, the head at the time this run
202+ # started. If Dependabot has pushed since (a rebase, a follow-up
203+ # bump), the PR now points at a commit this run never checked -
204+ # and `gh pr merge --auto` applies to the PR, not to a SHA, so it
205+ # would enable auto-merge for that unchecked head. Without a
206+ # required status check there is nothing downstream to catch it.
207+ #
208+ # Bail out and let the run triggered by that newer commit do the
209+ # gating. Exit 0, not 1: being superseded is normal, not a failure.
210+ current=$(gh pr view "$PR_URL" --json headRefOid --jq .headRefOid)
211+ if [ "$current" != "$SHA" ]; then
212+ echo "PR head moved $SHA -> $current since this run started;" \
213+ "leaving it to the run for $current."
214+ exit 0
215+ fi
216+
217+ # Auto-merge covers what Dependabot wrote, and nothing else. If
218+ # anyone has pushed onto the branch, the PR now carries changes no
219+ # one reviewed and that the "it's only a version bump" reasoning
220+ # does not apply to, so hand it back to a human.
221+ #
222+ # Commits with no resolvable author count as foreign: an unmatched
223+ # commit email is exactly what an unexpected push looks like, and
224+ # guessing in favour of merging is the wrong way to be wrong.
225+ foreign=$(gh pr view "$PR_URL" --json commits --jq '
226+ [ .commits[]
227+ | (.authors // [])
228+ | if length == 0 then ["unknown"] else map(.login // "unknown") end
229+ | .[]
230+ ] | unique | map(select(. != "dependabot[bot]")) | join(", ")')
231+ if [ -n "$foreign" ]; then
232+ echo "::notice::PR carries commits authored by: $foreign." \
233+ "Auto-merge only covers Dependabot's own commits, so this" \
234+ "PR is left for human review."
235+ exit 0
236+ fi
237+
166238 gh pr review --approve "$PR_URL"
167239 gh pr merge --auto --squash "$PR_URL"
168240 env :
169241 PR_URL : ${{ github.event.pull_request.html_url }}
242+ SHA : ${{ github.event.pull_request.head.sha }}
170243 GH_TOKEN : ${{ secrets.DEPENDABOT_AUTOMERGE_TOKEN }}
0 commit comments