|
| 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