diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index c11726e..df0181b 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -2,24 +2,29 @@ name: Release Please # Opens/updates the Release PR (package.json version bump + CHANGELOG.md) on every # push to main. Merging that PR is the release: release-please tags the commit and -# creates the GitHub Release with generated notes, which fires release.yml's -# existing `release: published` trigger to publish to npm over OIDC. +# creates the GitHub Release with generated notes. # -# WHY THE TOKEN MATTERS — and why release.yml needs no changes: +# NO SECRET IS REQUIRED. This runs on GITHUB_TOKEN, and the two things GITHUB_TOKEN +# normally blocks are handled as follows — see docs/releasing.md for the full flow. # -# `main` requires the `lint-test` status check and enforce_admins is true, so there -# is no bypass. release-please opens the Release PR, and GITHUB_TOKEN-created events -# start no workflow run — so with the default token `lint-test` never reports on that -# PR and it can never be merged. +# 1. Checks on the Release PR. `main` requires `lint-test` and enforce_admins is true, +# so there is no bypass. GitHub exempts `pull_request` events with the `opened`, +# `synchronize`, and `reopened` activity types: a PR created by GITHUB_TOKEN gets +# its runs started in an APPROVAL-REQUIRED state rather than suppressed. A user +# with write access clicks "Approve workflows to run" in the merge box, `lint-test` +# reports, and the PR merges normally. Nothing is skipped. # -# A Personal Access Token fixes that, because PAT-created events DO start workflows. -# It also fixes publishing for free: with a PAT the GitHub Release is created by a -# user identity, so `release: published` fires normally and release.yml stays -# untouched. (Under GITHUB_TOKEN that event is suppressed, and avoiding it would -# otherwise mean chaining release.yml as a reusable workflow — plus registering -# release-please.yml as a second npm trusted publisher. The PAT removes all of that.) +# 2. Publishing. `release` is NOT an exempt event, so a Release created here does not +# fire release.yml's `release: published` trigger — npm would silently not update. +# Publish with: gh workflow run release.yml -f dry-run=false +# (That trigger remains live for Releases created by a person, which is how 0.6.0 +# was published.) # -# Scope the PAT to this repository only: contents: write + pull_requests: write. +# To remove both manual steps, set a RELEASE_PLEASE_TOKEN secret — a token-created +# Release carries a user/App identity, so `release: published` fires and no click is +# needed. Scope it to this repo: contents: write + pull_requests: write. Note a secret +# that exists but is UNAUTHORISED (e.g. a fine-grained PAT still pending org approval) +# short-circuits the `||` fallback below and hard-fails with 403 — delete it instead. on: push: branches: [main] @@ -44,8 +49,9 @@ jobs: - name: Release Please uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 with: - # Falls back to GITHUB_TOKEN until the secret exists, so this degrades to - # "Release PRs open but cannot be merged" instead of failing outright. + # Defaults to GITHUB_TOKEN, which is a fully supported path — not a + # degraded one. It costs one "Approve workflows to run" click on the + # Release PR and a manual publish dispatch; see the header above. token: ${{ secrets.RELEASE_PLEASE_TOKEN || secrets.GITHUB_TOKEN }} config-file: release-please-config.json manifest-file: .release-please-manifest.json diff --git a/README.md b/README.md index 2dfca2b..e27e8c9 100644 --- a/README.md +++ b/README.md @@ -586,7 +586,7 @@ Every merge to `main` updates a single open **Release PR** holding the `package. bump and `CHANGELOG.md` entry. Nothing is published until that PR is merged — ordinary merges only update it. -Process, the required repository settings, and the one secret: +Process, the required repository settings, and the two manual steps per release: [`docs/releasing.md`](docs/releasing.md). ## Status diff --git a/docs/releasing.md b/docs/releasing.md index 6d9edbf..179a4b7 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -109,36 +109,52 @@ why a refactor-heavy release still reads as a short list of features and fixes. `CHANGELOG.md` is deliberately **not** in `package.json` `files[]`, so it does not ship in the npm tarball — the allowlist still emits 7 files. -## The one secret - -`release-please.yml` uses `secrets.RELEASE_PLEASE_TOKEN`, falling back to -`GITHUB_TOKEN` if unset. It should be a **fine-grained PAT scoped to this repository -only**, with: - -- **Contents: Read and write** — pushes the changelog commit, creates the tag and Release -- **Pull requests: Read and write** — opens and updates the Release PR - -Why it is needed at all: `main` requires the `lint-test` status check and -`enforce_admins` is `true`, so there is no bypass. Checks only report from a real -workflow run, and `GITHUB_TOKEN`-created events start none — so a `GITHUB_TOKEN` -Release PR can never report `lint-test` and can never be merged. - -It also removes a whole class of machinery. Because a PAT-created Release carries a -*user* identity, `release: published` fires normally and **`release.yml` needs no -changes**; under `GITHUB_TOKEN` that event is suppressed, which would otherwise -require chaining `release.yml` as a reusable workflow — and, since npm validates a -`workflow_call` publish against the *calling* workflow's filename, registering -`release-please.yml` as a **second trusted publisher on both packages**. - -So: **npm trusted publishing needs no changes.** `release.yml` remains the publisher -and stays registered as-is. A fine-grained PAT expires, so it needs periodic rotation -— the one recurring cost of this design, and the reason a GitHub App token -(short-lived per run, no expiry) would be strictly better if the org is willing to -own one. `release-please`'s own docs recommend a PAT for exactly the check-reporting -reason above. - -Until the secret exists the fallback keeps release-please running, so Release PRs -still open — they just cannot be merged. +## Releasing by hand — no token, no secret + +`release-please.yml` runs on `GITHUB_TOKEN` and needs **no secret at all**. That is +possible because GitHub exempts `pull_request` events with the `opened`, +`synchronize`, or `reopened` activity types: when `GITHUB_TOKEN` creates a pull +request, the resulting runs are started in an **approval-required** state instead of +being suppressed outright. Approving them restores the required check, so branch +protection is satisfied normally rather than bypassed. + +The full flow, and the two places a human is involved: + +1. Merge a `feat:`/`fix:` to `main` → release-please opens or updates the Release PR. +2. On that PR, click **Approve workflows to run** in the merge box. `ci.yml` and + `pr-title.yml` then execute, `lint-test` reports, and the PR becomes mergeable like + any other. (Nothing is skipped — the runs are held, not allowed through.) +3. Merge the Release PR → release-please tags the commit, writes the `CHANGELOG.md` + entry, and creates the GitHub Release with generated notes. +4. Publish, because the Release cannot do it for you: + + ``` + gh workflow run release.yml -f dry-run=false + ``` + +**Why step 4 is manual.** GitHub's exemption list covers `workflow_dispatch`, +`repository_dispatch`, and those three `pull_request` activity types — `release` is +**not** on it. So the Release that release-please creates with `GITHUB_TOKEN` does not +fire `release: published`, and npm would silently never be updated. That trigger is +therefore live only for Releases created by a person, which is how 0.6.0 was published. + +### Making it automatic again + +Give release-please a real token and both manual steps disappear — no approve click, +no dispatch. Set `RELEASE_PLEASE_TOKEN` and `release-please.yml` picks it up; the chain +is already `RELEASE_PLEASE_TOKEN || GITHUB_TOKEN`. A token-created Release carries a +user or App identity, so `release: published` fires and `release.yml` stays untouched. + +Scope it to this repository with **Contents: Read and write** and **Pull requests: +Read and write**. A fine-grained PAT needs org-owner approval (the org default is +"Require administrator approval"); a classic PAT with `public_repo` does not, and a +GitHub App needs neither approval nor rotation. + +One trap: `||` falls through only on *empty*, so a secret whose value is +**unauthorised** — a fine-grained PAT still pending approval — short-circuits the +fallback and makes release-please hard-fail with +`403 Resource not accessible by personal access token`. Delete it rather than leaving +it pending; a missing secret is strictly better than a broken one. ## See also