diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index cd3e08b..0000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,84 +0,0 @@ -# Publish the npm package when a release is published. -# -# Packagist needs nothing here: it picks `humanmade/wp-pattern-library` up from -# the same tag through its GitHub hook. This workflow covers the other half of -# the pair, so one tag really does mean the same code in both registries. -# -# Authentication is npm trusted publishing: npmjs.com holds a trusted publisher -# for this package naming this repository, this workflow file and the `npm` -# environment, and npm exchanges the OIDC token minted below for a short-lived -# publish credential. There is no NPM_TOKEN to store or rotate, and the four -# facts npm matches on are all visible here — moving or renaming this file, or -# dropping the environment, breaks publishing until the trusted publisher is -# updated to match. -# -# The `release: published` trigger only fires for a release created by a personal -# access token. GitHub deliberately does not let the automatic GITHUB_TOKEN -# trigger further workflow runs, so tag-and-release.yml has to cut the release -# with its RELEASE_TOKEN secret for this to run on its own. Without that secret, -# dispatch this workflow by hand after the release exists. - -name: Publish to npm - -on: - release: - types: [ published ] - workflow_dispatch: - -permissions: - contents: read - # Required to mint the OIDC token npm exchanges for publish rights. It is also - # what ties the published tarball to this workflow run and commit, which npm - # records as provenance. - id-token: write - -jobs: - npm: - name: npm publish - runs-on: ubuntu-latest - environment: npm - - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: ${{ github.event.release.tag_name || github.ref }} - - - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: 24 - registry-url: https://registry.npmjs.org - cache: npm - - # Trusted publishing needs npm 11.5.1 or newer. Node 24 ships something - # recent enough today, but the bundled version moves with the runner - # image, so it is pinned forward here rather than left to chance. - - name: Use an npm that can publish over OIDC - run: npm install --global npm@^11.5.1 - - - run: npm ci - - # Publishing is irreversible, so the gate that CI applies to every pull - # request is applied once more against the actual tag being released. - - name: The tag, package.json and plugin.php agree - run: | - set -euo pipefail - - tag="${{ github.event.release.tag_name }}" - npm_version="$(node -p "require('./package.json').version")" - plugin_version="$(sed -n 's/^ \* Version: *//p' plugin.php | head -1 | tr -d '[:space:]')" - - if [ -n "${tag}" ] && [ "${tag#v}" != "${npm_version}" ]; then - echo "::error::Tag ${tag} does not match package.json ${npm_version}." - exit 1 - fi - - if [ "${plugin_version}" != "${npm_version}" ]; then - echo "::error::plugin.php ${plugin_version} does not match package.json ${npm_version}." - exit 1 - fi - - # No NODE_AUTH_TOKEN: the credential comes from the OIDC exchange above. - # Provenance is generated automatically for a trusted-publisher release, - # so `--provenance` is not passed either. - - name: Publish - run: npm publish --access public diff --git a/.github/workflows/tag-and-release.yml b/.github/workflows/tag-and-release.yml index 7dfe9ba..cbdf003 100644 --- a/.github/workflows/tag-and-release.yml +++ b/.github/workflows/tag-and-release.yml @@ -1,17 +1,27 @@ -# Cut a release: tag the target branch, then publish a GitHub release from it. +# Cut a release: tag the target branch, publish a GitHub release from it, and +# publish the npm package from the tag that was just created. # # The tagging and release mechanics live in humanmade/hm-github-actions, shared -# with ~23 other Human Made plugin repositories. Only the version check below is +# with ~23 other Human Made plugin repositories. The two jobs around it are # specific to this repository: one tag drives the Composer package, the npm # package and the action, so those have to agree before the tag exists — # afterwards it is too late. # -# Publishing to npm is handled by publish.yml, which runs when the release this -# workflow creates is published. That only fires if the release is created by a -# personal access token: GitHub deliberately does not let the automatic -# GITHUB_TOKEN trigger further workflow runs. Set a RELEASE_TOKEN secret holding -# a PAT with `contents: write` on this repository, or dispatch "Publish to npm" -# by hand after cutting the release. +# npm publishing is a job here rather than a workflow of its own, because the +# obvious wiring does not work: a release created by the automatic GITHUB_TOKEN +# does not trigger a `release: published` workflow, so a separate publisher +# would sit there waiting for an event that never arrives. Running it in the +# same workflow removes the event from the path entirely. +# +# Packagist needs nothing here: it picks `humanmade/wp-pattern-library` up from +# the same tag through its GitHub hook. +# +# Authentication for the publish is npm trusted publishing. npmjs.com holds a +# trusted publisher for the package naming this repository, THIS WORKFLOW FILE +# and the `npm` environment, and npm exchanges the OIDC token minted by the +# `npm` job for a short-lived publish credential. There is no NPM_TOKEN to store +# or rotate — but renaming or moving this file, or dropping the environment, +# breaks publishing until the trusted publisher is updated to match. name: Tag and Release @@ -68,6 +78,63 @@ jobs: version: ${{ inputs.version }} target_branch: ${{ inputs.target_branch }} secrets: - # Falls back to GITHUB_TOKEN when unset — at the cost of publish.yml not - # being triggered by the release this creates. + # Optional, and no longer on the critical path: npm publishing happens in + # the job below rather than by reacting to the release event. Set it only + # if something else should be triggered by the release this creates. token: ${{ secrets.RELEASE_TOKEN }} + + npm: + name: Publish to npm + needs: tag_and_release + runs-on: ubuntu-latest + environment: npm + permissions: + contents: read + # Required to mint the OIDC token npm exchanges for publish rights. It is + # also what ties the published tarball to this workflow run and commit, + # which npm records as provenance. + id-token: write + + steps: + # The tag, not the branch: the branch can move between the check above and + # this job, and what was released is what should be published. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ inputs.version }} + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24 + registry-url: https://registry.npmjs.org + cache: npm + + # Trusted publishing needs npm 11.5.1 or newer. Node 24 ships something + # recent enough today, but the bundled version moves with the runner + # image, so it is pinned forward here rather than left to chance. + - name: Use an npm that can publish over OIDC + run: npm install --global npm@^11.5.1 + + - run: npm ci + + # Publishing is irreversible, so the check that gated the tag is applied + # once more against the tag's own contents. + - name: The tag, package.json and plugin.php agree + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + + bare="${VERSION#v}" + npm_version="$(node -p "require('./package.json').version")" + plugin_version="$(sed -n 's/^ \* Version: *//p' plugin.php | head -1 | tr -d '[:space:]')" + + if [ "${npm_version}" != "${bare}" ] || [ "${plugin_version}" != "${bare}" ]; then + echo "::error::Version mismatch at tag ${VERSION}. package.json ${npm_version}, plugin.php ${plugin_version}." + exit 1 + fi + + # No NODE_AUTH_TOKEN: the credential comes from the OIDC exchange. If this + # step fails after the tag and release exist, re-run it with "Re-run failed + # jobs" on this run rather than cutting a new version. + - name: Publish + run: npm publish --access public diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9c0f49a..145d5a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -274,21 +274,27 @@ versions have to agree before the tag exists. workflow**, giving it the tag (`v0.4.0`). It re-checks that the tag, `package.json` and `plugin.php` all agree, refuses to overwrite an existing tag, creates the tag, and publishes a GitHub release with generated notes. -4. **Publishing happens automatically.** The `Publish to npm` workflow runs when - that release is published. Packagist picks the tag up through its GitHub - hook. - -Publishing to npm needs no secret. npmjs.com holds a trusted publisher for -`@humanmade/wp-pattern-library` naming this repository, `publish.yml` and the -`npm` environment, and the workflow trades a GitHub OIDC token for short-lived -publish rights. Renaming or moving `publish.yml`, or dropping its `environment: -npm`, breaks publishing until the trusted publisher on npm is updated to match. - -Step 4 also needs a `RELEASE_TOKEN` secret: a personal access token with -`contents: write` on this repository, which `Tag and Release` uses to cut the -release. GitHub deliberately does not let the automatic `GITHUB_TOKEN` trigger -further workflow runs, so a release created without that token is a dead end — -`Publish to npm` never starts, and someone has to dispatch it by hand. + Its last job publishes the npm package from the tag it just created, so + there is nothing further to trigger. Packagist picks the tag up through its + GitHub hook. + +That publish is one job in `tag-and-release.yml`, not a workflow of its own, +because the obvious wiring does not work: a release created by the automatic +`GITHUB_TOKEN` does not trigger a `release: published` workflow, so a separate +publisher waits for an event that never arrives. Keeping it in the same workflow +takes the event off the path. + +Publishing needs no secret. npmjs.com holds a trusted publisher for +`@humanmade/wp-pattern-library` naming this repository, **`tag-and-release.yml`** +and the `npm` environment, and the workflow trades a GitHub OIDC token for +short-lived publish rights. Renaming or moving that file, or dropping its +`environment: npm`, breaks publishing until the trusted publisher on npm is +updated to match. + +If the publish fails after the tag and release already exist — a registry +hiccup, say — use **Re-run failed jobs** on that workflow run. It re-runs the +`npm` job alone, against the tag that is already there. Do not bump the version +and cut a second release to work around a publish failure. Consuming workflows pin the action to a released tag. There is deliberately no moving `@v1` tag while the package is pre-1.0 — which is why step 1 has five