diff --git a/.github/workflows/merge-bot-pull-request.yml b/.github/workflows/merge-bot-pull-request.yml new file mode 100644 index 0000000..5b08735 --- /dev/null +++ b/.github/workflows/merge-bot-pull-request.yml @@ -0,0 +1,96 @@ +name: Merge bot pull request action + +# Auto-merges in-repo Dependabot pull requests: enable on opened or reopened, disable on a maintainer push. +# The merge method follows the base, since the two rulesets allow different forms. +# - develop takes squash. +# - main takes a merge commit. +# An App token is used rather than GITHUB_TOKEN, for two reasons. +# - It fires downstream workflows on merge, which GITHUB_TOKEN deliberately does not. +# - A Dependabot pull request's GITHUB_TOKEN is read-only regardless of who triggered the event. +# The trigger is pull_request_target rather than pull_request, because these jobs hold the App key. +# That resolves the workflow and action SHAs from the trusted base rather than from the pull request head. +# It is safe here because no job checks out pull request code, each one merging by URL alone. +# This repo carries no codegen workflow and no upstream-version tracker, so those jobs are not vendored. +on: + pull_request_target: + types: [ opened, reopened, synchronize ] + +# Concurrency keys on the pull request number rather than github.ref. +# Under pull_request_target github.ref is the base branch, which would serialize every bot pull request. +# cancel-in-progress is false so a follow-up synchronize cannot cancel an in-flight opened run. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: false + +jobs: + + merge-dependabot: + name: Merge dependabot pull request job + runs-on: ubuntu-latest + # Dependabot pull requests raised in this repo, never from a fork. + # Restricted to opened and reopened so the disable job below stays sticky. + if: >- + (github.event.action == 'opened' || github.event.action == 'reopened') && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.event.pull_request.head.repo.full_name == github.repository + permissions: + contents: write + pull-requests: write + + steps: + + - name: Generate GitHub App token step + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + # Every tier is auto-merged, semver-major included. + # The required checks are the gate, not the size of the bump. + # A major that breaks the build fails its checks and never merges. + - name: Merge pull request step + run: | + set -Eeuo pipefail + case "${{ github.event.pull_request.base.ref }}" in + develop) method=--squash ;; + main) method=--merge ;; + *) + echo "::error::Unsupported base branch: ${{ github.event.pull_request.base.ref }}" + exit 1 + ;; + esac + gh pr merge --auto "$method" "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + + disable-auto-merge-on-maintainer-push: + name: Disable auto-merge on maintainer push job + runs-on: ubuntu-latest + # Fires when a maintainer pushes to the bot's branch, which is a synchronize by a non-bot actor. + # Auto-merge is disabled so the maintainer's commits do not merge along with the bot's. + # Re-enabling it is then a deliberate manual act, and the disable call is idempotent. + if: >- + github.event.action == 'synchronize' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login == 'dependabot[bot]' && + github.actor != github.event.pull_request.user.login + permissions: + pull-requests: write + + steps: + + - name: Generate GitHub App token step + # An App token is required because a Dependabot pull request's GITHUB_TOKEN is read-only. + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + client-id: ${{ secrets.CODEGEN_APP_CLIENT_ID }} + private-key: ${{ secrets.CODEGEN_APP_PRIVATE_KEY }} + + - name: Disable auto-merge step + run: gh pr merge --disable-auto "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/TODO.md b/TODO.md index d4df466..1836168 100644 --- a/TODO.md +++ b/TODO.md @@ -23,7 +23,6 @@ The site is built and gated in CI. It is on GitHub, and it is not yet serving it ## Next, in dependency order - Dispatch `publish-release.yml` once to prove the release path, which exists but has never run. -- Decide on `merge-bot-pull-request.yml`. Dependabot is configured and its pull requests will otherwise sit open, and the App secrets it needs are now in place. - Provision the VPS: an unprivileged `blogdeploy` user, the deploy root, and `unattended-upgrades` with automatic reboot. - Restrict the deploy key with `restrict,command=...`, no pty and no forwarding, so it can do nothing but rsync into `releases/` and swap the symlink. Generate per-environment keys so staging cannot reach production. - Choose the staging FQDN, add its DNS record, and expose it through Pangolin as a public resource with **no auth**, since CI's live-URL check has to reach it. Authentication defaults to on for a public resource and has to be turned off deliberately. @@ -38,7 +37,6 @@ The site is built and gated in CI. It is on GitHub, and it is not yet serving it - The staging FQDN name. - `/robots.txt/` and `/osd.xml/` currently sit in `slugs.map` pointing at `/`. The first would be better pointing at the real `/robots.txt`. - Whether the 78 legacy date-archive redirects should point at `/all/` rather than `/`, so a visitor following a `/2015/` link lands somewhere they can find 2015 posts. It is a one-line change to rule R3. -- Whether to carry `merge-bot-pull-request.yml`. Dependabot is configured and its PRs will sit open without it, but it needs the App secrets, so it cannot be proven until those exist. ## Deliberate deviations from the fleet baseline diff --git a/reports/Blog/audit.md b/reports/Blog/audit.md index 86903f3..32ef14b 100644 --- a/reports/Blog/audit.md +++ b/reports/Blog/audit.md @@ -116,7 +116,6 @@ Carried forward rather than closed: - The live redirect gate has not run against this build, so 917 of the 1,245 contracted URLs are asserted rather than proven. - `publish-release.yml` has never been dispatched, so the release path is untested. - No deploy exists, so the publish surface stays deferred and the registry entry stays `publish: []`. -- `merge-bot-pull-request.yml` is not carried. Dependabot is configured and its pull requests will sit open until either the merge bot is added or they are merged by hand. The App secrets it needs are now in place, so this is no longer blocked, only undone. - `checks/README.md` carries a small prose backlog of `dash` and `semicolon` findings, left for the next edit of that file per the correct-as-you-next-edit rule.