fix(event-handler): ignore body on GET/HEAD requests when converting to Web Request #181
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto-merge Dependabot PRs | |
| # PROCESS | |
| # | |
| # 1. Runs on every Dependabot pull request against `main` | |
| # 2. Reads the update's metadata (ecosystem, dependency type, semver bump) | |
| # 3. Rejects anything that isn't in scope: a non-major bump of either a direct *development* npm | |
| # dependency or a GitHub Actions dependency | |
| # 4. Rejects any PR whose commits weren't all authored by Dependabot and signed by GitHub | |
| # 5. Only then approves the PR and hands it to GitHub's native auto-merge, which still has to | |
| # satisfy every required status check and branch protection rule before anything merges | |
| # | |
| # An update that is merely out of scope is left alone for a human to review, and the job still | |
| # reports success. A PR that fails the *provenance* gate reports failure, which - because this | |
| # job's context is required, see REQUIRED CHECK below - blocks it until the branch is put right | |
| # with `@dependabot rebase`. | |
| # | |
| # SECURITY NOTE | |
| # | |
| # Deliberately uses `pull_request` (not `pull_request_target`), and never checks out or executes | |
| # the PR's code. On Dependabot-triggered runs GitHub sets `Secret source: Dependabot`, but the | |
| # `permissions:` block below is still honoured (verified against live Dependabot runs), so | |
| # `on: pull_request` is sufficient and the more convoluted `on: workflow_run` indirection isn't | |
| # needed. | |
| # | |
| # Three independent gates have to agree before an approval is issued: | |
| # | |
| # 1. `github.event.pull_request.user.login == 'dependabot[bot]'` - set by GitHub from the identity | |
| # that actually opened the PR, so it can't be spoofed via branch name, title, or body. Combined | |
| # with `head.repo.full_name == github.repository` a fork PR can never reach the approval step. | |
| # 2. Every commit on the branch must be authored by `dependabot[bot]` *and* carry a valid GitHub | |
| # signature. This is the gate that stops a human (or a compromised account with push access to | |
| # the Dependabot branch) from smuggling a commit into an otherwise-legitimate bump. Note | |
| # Dependabot's own commits have `committer: web-flow` - GitHub's signing key - so the check is on | |
| # the *author*, not the committer. | |
| # 3. The scope gate below: direct development npm dependencies or GitHub Actions dependencies | |
| # only, and never a major bump. GitHub Actions have no dev/prod distinction (Dependabot always | |
| # reports them as `direct:production`), and they're pinned to commit SHAs and re-pinned by | |
| # `secure-workflows.yml`, so the dev-dependency requirement is dropped for that ecosystem while | |
| # the non-major guard and provenance check (gate 2) still apply. | |
| # | |
| # Consequences of gate 2 worth knowing: GitHub's "Update branch" button produces a merge commit | |
| # authored by the human who clicked it, which fails the gate and drops the PR back to manual | |
| # review. Use `@dependabot rebase` instead, which rewrites the branch with Dependabot-authored, | |
| # GitHub-signed commits. | |
| # | |
| # REQUIRED CHECK | |
| # | |
| # This job's `auto-merge` context must be added to the branch protection rule's required status | |
| # checks. The gates below decide whether to *grant* an approval; only a required check can stop a | |
| # PR that was granted one earlier from merging later. For pull requests that aren't Dependabot's | |
| # the job is skipped, and GitHub counts a skipped required check as satisfied, so requiring it | |
| # costs human PRs nothing. | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| permissions: {} | |
| # Runs for the same PR queue rather than cancel, so the most recent head SHA is always the last | |
| # to decide. Cancelling would let an older run's cleanup step race the newer run's approval. | |
| concurrency: | |
| group: dependabot-auto-merge-${{ github.event.pull_request.number }} | |
| cancel-in-progress: false | |
| jobs: | |
| auto-merge: | |
| if: > | |
| github.event.pull_request.user.login == 'dependabot[bot]' && | |
| github.event.pull_request.head.repo.full_name == github.repository && | |
| github.event.pull_request.base.ref == 'main' && | |
| github.event.pull_request.draft == false | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # enable GitHub's native auto-merge on the PR | |
| pull-requests: write # approve the PR | |
| steps: | |
| - name: Fetch Dependabot metadata | |
| id: metadata | |
| uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0 | |
| - name: Verify update is in scope | |
| id: scope | |
| env: | |
| ECOSYSTEM: ${{ steps.metadata.outputs.package-ecosystem }} | |
| DEPENDENCY_TYPE: ${{ steps.metadata.outputs.dependency-type }} | |
| UPDATE_TYPE: ${{ steps.metadata.outputs.update-type }} | |
| DEPENDENCY_NAMES: ${{ steps.metadata.outputs.dependency-names }} | |
| run: | | |
| echo "ecosystem=${ECOSYSTEM} type=${DEPENDENCY_TYPE} update=${UPDATE_TYPE} names=${DEPENDENCY_NAMES}" | |
| case "${ECOSYSTEM}" in | |
| npm_and_yarn) | |
| # Only direct development dependencies: prod dependencies ship in published packages. | |
| if [ "${DEPENDENCY_TYPE}" != "direct:development" ]; then | |
| echo "::notice::Not a direct development dependency (${DEPENDENCY_TYPE}) - leaving for manual review" | |
| echo "in-scope=false" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| ;; | |
| github_actions) | |
| # No dev/prod distinction (always direct:production); the non-major guard and the | |
| # commit-provenance check below are the gates that apply. | |
| ;; | |
| *) | |
| echo "::notice::Ecosystem out of scope (${ECOSYSTEM}) - leaving for manual review" | |
| echo "in-scope=false" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| ;; | |
| esac | |
| if [ "${UPDATE_TYPE}" = "version-update:semver-major" ]; then | |
| echo "::notice::Major version bump - leaving for manual review" | |
| echo "in-scope=false" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| echo "in-scope=true" >> "${GITHUB_OUTPUT}" | |
| # Reads the PR's commits from the API rather than the event payload, so a commit pushed | |
| # *after* this run started is still caught - there's no window in which a late push can | |
| # race an in-flight approval. | |
| - name: Verify every commit is authored by Dependabot and signed by GitHub | |
| if: steps.scope.outputs.in-scope == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| commits="$(gh api "repos/${REPO}/pulls/${PR}/commits" --paginate \ | |
| --jq '.[] | [.sha, (.author.login // "unknown"), (.commit.verification.verified | tostring), .commit.verification.reason] | @tsv')" | |
| if [ -z "${commits}" ]; then | |
| echo "::error::Could not read the PR's commits - refusing to approve" | |
| exit 1 | |
| fi | |
| echo "${commits}" | |
| untrusted="$(echo "${commits}" | awk -F'\t' '$2 != "dependabot[bot]" || $3 != "true"')" | |
| if [ -n "${untrusted}" ]; then | |
| echo "::error::PR carries commits that are not Dependabot-authored and GitHub-signed:" | |
| echo "${untrusted}" | |
| exit 1 | |
| fi | |
| - name: Approve the pull request | |
| id: approve | |
| if: steps.scope.outputs.in-scope == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha }} | |
| run: | | |
| # `commit_id` pins the approval to the SHA whose commits were just verified, so an | |
| # approval can never be attributed to code that landed after the check ran. | |
| gh api "repos/${REPO}/pulls/${PR}/reviews" \ | |
| --method POST \ | |
| --field event=APPROVE \ | |
| --field commit_id="${HEAD_SHA}" \ | |
| --field body="Approved automatically: non-major bump of an in-scope dependency (direct development npm dependency or GitHub Actions), with all commits authored by Dependabot and signed by GitHub." | |
| - name: Enable auto-merge | |
| id: enable | |
| if: steps.scope.outputs.in-scope == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: gh pr merge "${PR}" --repo "${REPO}" --squash --auto | |
| # An approval and an armed auto-merge outlive the run that granted them, and GitHub does | |
| # *not* dismiss reviews when a PR branch is updated from its base. Without this step a PR | |
| # that passed the gates once goes on to merge even after a later push fails them - reproduced | |
| # in rehearsal, where clicking "Update branch" put a human-authored merge commit into `main` | |
| # behind an approval granted before it existed, with the gate red. So whenever this run does | |
| # not itself approve, withdraw whatever | |
| # a previous run granted. Only ever touches this workflow's own approvals and its own | |
| # auto-merge, never a maintainer's. | |
| - name: Withdraw approval and auto-merge if the gates no longer pass | |
| if: always() && steps.enable.outcome != 'success' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| enabled_by="$(gh pr view "${PR}" --repo "${REPO}" --json autoMergeRequest \ | |
| --jq '.autoMergeRequest.enabledBy.login // ""')" | |
| case "${enabled_by}" in | |
| *github-actions*) | |
| echo "::warning::Disabling auto-merge that this workflow enabled earlier" | |
| gh pr merge "${PR}" --repo "${REPO}" --disable-auto | |
| ;; | |
| esac | |
| gh api "repos/${REPO}/pulls/${PR}/reviews" --paginate \ | |
| --jq '.[] | select(.user.login == "github-actions[bot]" and .state == "APPROVED") | .id' \ | |
| | while read -r review_id; do | |
| [ -n "${review_id}" ] || continue | |
| echo "::warning::Dismissing automated approval ${review_id}" | |
| gh api "repos/${REPO}/pulls/${PR}/reviews/${review_id}/dismissals" \ | |
| --method PUT \ | |
| --field message="Withdrawn automatically: this pull request no longer satisfies the automated merge gates." \ | |
| --field event=DISMISS | |
| done |