Skip to content

Commit 58bef2c

Browse files
penalosapetebacondarwindevin-ai-integration[bot]
committed
[ci] Auto-merge workerd Dependabot updates when CI is green (#13852)
Co-authored-by: Pete Bacon Darwin <pete@bacondarwin.com> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent c78f8ad commit 58bef2c

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
name: "Dependabot - auto-merge workerd updates"
2+
3+
# workerd ships a release every weekday, so the workerd-and-workers-types
4+
# Dependabot group produces a steady stream of mechanical PRs that bump
5+
# `workerd`, `@cloudflare/workers-types`, and miniflare's pinned version in
6+
# lockstep (see .github/dependabot.yml). When CI is green these PRs require
7+
# no human review, so we enable GitHub auto-merge on them — required status
8+
# checks remain the gate, and a failing build still parks the PR for a human.
9+
#
10+
# Security model: this workflow effectively bypasses the human-review
11+
# requirement on PRs whose head branch matches the Dependabot naming pattern,
12+
# so we have to be paranoid about exactly what we're auto-merging. Before
13+
# enabling auto-merge we verify that the PR contains exactly the two commits
14+
# we expect (one from Dependabot, one from `miniflare-dependabot-versioning-prs.yml`)
15+
# and that nothing outside the expected fileset has been touched. If any
16+
# subsequent push violates those invariants we actively *disable* auto-merge,
17+
# so a maintainer pushing a follow-up commit cancels rather than rides the
18+
# auto-merge.
19+
#
20+
# DO NOT add `actions/checkout` to this workflow — `pull_request_target`
21+
# grants write-scoped tokens, and checking out PR-controlled code with
22+
# those tokens is the standard pwn vector.
23+
24+
on:
25+
pull_request_target:
26+
types: [opened, reopened, synchronize, ready_for_review]
27+
28+
permissions:
29+
contents: write
30+
pull-requests: write
31+
32+
jobs:
33+
enable-auto-merge:
34+
if: github.event.pull_request.user.login == 'dependabot[bot]'
35+
runs-on: ubuntu-slim
36+
steps:
37+
- name: Fetch Dependabot metadata
38+
id: meta
39+
uses: dependabot/fetch-metadata@d7267f607e9d3fb96fc2fbe83e0af444713e90b7 # v2.3.0
40+
with:
41+
github-token: ${{ secrets.GITHUB_TOKEN }}
42+
43+
- name: Verify PR matches expected workerd-bump shape
44+
id: verify
45+
if: steps.meta.outputs.dependency-group == 'workerd-and-workers-types'
46+
env:
47+
PR_NUMBER: ${{ github.event.pull_request.number }}
48+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49+
run: |
50+
set -euo pipefail
51+
52+
# Pull commits and changed files via the GitHub API.
53+
commits_json=$(gh pr view "$PR_NUMBER" --json commits)
54+
files_json=$(gh pr view "$PR_NUMBER" --json files)
55+
56+
fail() {
57+
echo "verified=false" >> "$GITHUB_OUTPUT"
58+
echo "reason=$1" >> "$GITHUB_OUTPUT"
59+
echo "::warning::Refusing to enable auto-merge: $1"
60+
exit 0
61+
}
62+
63+
# --- Commit shape ---------------------------------------------------
64+
# Expected: exactly two commits.
65+
# 1. dependabot[bot] author, with a verified GitHub signature.
66+
# 2. The changeset commit pushed by miniflare-dependabot-versioning-prs.yml,
67+
# authored as `Wrangler automated PR updater <wrangler@cloudflare.com>`.
68+
#
69+
# We can't require a signature on the second commit (it's pushed via
70+
# GH_ACCESS_TOKEN, which doesn't sign), so we lean on path/content
71+
# checks below to constrain what that commit can do.
72+
73+
commit_count=$(echo "$commits_json" | jq '.commits | length')
74+
if [ "$commit_count" -ne 2 ]; then
75+
fail "expected exactly 2 commits, found $commit_count"
76+
fi
77+
78+
first_author=$(echo "$commits_json" | jq -r '.commits[0].authors[0].login')
79+
first_oid=$(echo "$commits_json" | jq -r '.commits[0].oid')
80+
if [ "$first_author" != "dependabot[bot]" ]; then
81+
fail "first commit author is '$first_author', expected 'dependabot[bot]'"
82+
fi
83+
84+
# `gh pr view --json commits` doesn't expose signature info, so look
85+
# it up via the REST commit endpoint.
86+
first_verified=$(gh api "repos/${{ github.repository }}/commits/$first_oid" --jq '.commit.verification.verified')
87+
if [ "$first_verified" != "true" ]; then
88+
fail "first commit (Dependabot) does not have a verified signature"
89+
fi
90+
91+
second_email=$(echo "$commits_json" | jq -r '.commits[1].authors[0].email // ""')
92+
second_message=$(echo "$commits_json" | jq -r '.commits[1].messageHeadline // ""')
93+
if [ "$second_email" != "wrangler@cloudflare.com" ]; then
94+
fail "second commit author email is '$second_email', expected 'wrangler@cloudflare.com'"
95+
fi
96+
if ! echo "$second_message" | grep -qE '^Update dependencies of '; then
97+
fail "second commit message '$second_message' does not match expected changeset commit shape"
98+
fi
99+
100+
# --- Changed files allowlist ---------------------------------------
101+
# The only paths a workerd bump should touch:
102+
# - .changeset/dependabot-update-*.md (added by the changeset job)
103+
# - packages/*/package.json (workerd, workers-types pins)
104+
# - pnpm-lock.yaml
105+
# - pnpm-workspace.yaml (catalog entries)
106+
allowed='^(\.changeset/dependabot-update-.*\.md|packages/[^/]+/package\.json|pnpm-lock\.yaml|pnpm-workspace\.yaml)$'
107+
108+
unexpected=$(echo "$files_json" | jq -r '.files[].path' | grep -vE "$allowed" || true)
109+
if [ -n "$unexpected" ]; then
110+
fail "PR touches unexpected files:$(echo "$unexpected" | sed 's/^/ /' | tr '\n' ',')"
111+
fi
112+
113+
# Make sure the changeset is actually present — the changeset job
114+
# may not have run yet, in which case we bail and wait for the
115+
# `synchronize` event from its push.
116+
if ! echo "$files_json" | jq -e '.files[] | select(.path | startswith(".changeset/dependabot-update-"))' > /dev/null; then
117+
fail "changeset file not yet present; waiting for changeset job to push it"
118+
fi
119+
120+
echo "verified=true" >> "$GITHUB_OUTPUT"
121+
122+
- name: Enable auto-merge
123+
if: steps.meta.outputs.dependency-group == 'workerd-and-workers-types' && steps.verify.outputs.verified == 'true'
124+
run: gh pr merge --auto --squash "$PR_URL"
125+
env:
126+
PR_URL: ${{ github.event.pull_request.html_url }}
127+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128+
129+
- name: Disable auto-merge if verification failed
130+
# If a previous run enabled auto-merge but a later push broke the
131+
# invariants, actively cancel auto-merge so the bad commit can't ride.
132+
# `gh pr merge --disable-auto` is a no-op (and exits 0) if auto-merge
133+
# was never enabled, so this is safe to always run on the failure path.
134+
if: always() && steps.meta.outputs.dependency-group == 'workerd-and-workers-types' && steps.verify.outputs.verified != 'true'
135+
run: gh pr merge --disable-auto "$PR_URL" || true
136+
env:
137+
PR_URL: ${{ github.event.pull_request.html_url }}
138+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

0 commit comments

Comments
 (0)