diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b1fead78..556e94b4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,26 +23,66 @@ name: CI # whole change, which restored the old bytes rather than recording the new # ones. # -# The auto-commit steps below push with the default GITHUB_TOKEN, and GitHub -# does not start a workflow run for those pushes (there is no ci.yml run for -# b1be5d5, the "[ci] apply-formatting" commit). So this job sees each push -# exactly once and does not re-enter itself. +# The auto-commit steps below push with a PAT (secrets.PAT), NOT the +# default GITHUB_TOKEN. main's "require a pull request before merging" +# ruleset refuses GITHUB_TOKEN pushes outright: +# +# remote: error: GH006: Protected branch update failed for refs/heads/main. +# remote: - Changes must be made through a pull request. +# +# That is a protection-rule rejection, not a permissions problem, so no +# `permissions:` value can fix it - the token already had contents: write +# when it was refused. The fix is that the PAT's account sits on that +# ruleset's bypass list. GITHUB_TOKEN cannot be given a ruleset bypass at +# all: bypass actors may be repository roles, teams, installed GitHub Apps +# or Dependabot, and the built-in Actions integration is none of those. +# +# CONSEQUENCE, and the reason for the job-level `if:` below: unlike +# GITHUB_TOKEN, a PAT's pushes DO start new workflow runs. Without the guard +# this workflow re-enters itself on its own "[ci] ..." commits, forever. +# Skipping those re-runs loses nothing, because the strict test run at the +# end of this job already ran against the final tree it produced. on: push: branches: - main +# Least privilege: the pushes in this job authenticate with PAT, so +# GITHUB_TOKEN itself never needs write. permissions: - contents: write + contents: read jobs: build: + # See CONSEQUENCE in the header. Do not remove without first moving the + # pushes back to an identity whose commits do not trigger workflows. + if: ${{ !startsWith(github.event.head_commit.message, '[ci] ') }} runs-on: ubuntu-latest steps: + # Fail fast and legibly. Without this, a missing secret surfaces as an + # opaque authentication error at the first auto-commit step, five steps + # later, which reads like a protection problem rather than a config one. + - name: Check PAT is configured + env: + PAT: ${{ secrets.PAT }} + run: | + set -euo pipefail + if [ -z "$PAT" ]; then + echo "secrets.PAT is not set." >&2 + echo "This workflow cannot push to main without it: the branch ruleset" >&2 + echo "rejects GITHUB_TOKEN pushes (GH006). See the header comment." >&2 + exit 1 + fi + - uses: actions/checkout@v7 with: fetch-depth: 0 + # Hands the PAT to git. persist-credentials defaults to true, and + # that is precisely what makes the three git-auto-commit-action + # steps below push as the bypassing account rather than as + # github-actions[bot]. They need no configuration of their own. + token: ${{ secrets.PAT }} - name: Set up Python uses: actions/setup-python@v7