Skip to content

ci: publish to npm with trusted publishing and automate the release - #931

Merged
robdecker merged 4 commits into
mainfrom
chore/oidc-release-automation
Sep 22, 2026
Merged

robdecker merged 4 commits into
mainfrom
chore/oidc-release-automation

Conversation

@robdecker

@robdecker robdecker commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

This pull request is for: (mark with an "x")

  • examples/*
  • modules/next
  • packages/next-drupal
  • starters/basic-starter
  • starters/graphql-starter
  • starters/pages-starter
  • Other

GitHub Issue: n/a

  • I need help adding tests. (mark with an "x")

Describe your changes

Automates releases and moves publishing off a stored npm token.

Why

The release process in MAINTAINING.md no longer works. Three things broke, and because nothing had been released in a while, none of them surfaced until someone tried:

  1. lerna version commits straight to main, which now requires a pull request and a review, so the documented git push is rejected.
  2. yarn publish prompts for a two-factor code when the account has 2FA enabled. CI has no terminal, so it fails with Can't answer a question unless a user TTY.
  3. The publishing token had a short expiry and lapsed. Nothing published in the meantime, so it went unnoticed until npm rejected a publish.

This is also the direction npm is moving. Per GitHub's npm supply chain roadmap: classic tokens are being deprecated, granular tokens with publish rights are being cut to a 7-day lifetime, and the option to bypass 2FA for local publishing is being removed. Both halves of the old process are going away, so this is not worth fixing in place.

Versioning: release-please

release-please reads the conventional commits already enforced by commitlint and keeps a release pull request open with the version bump and changelog. Merging it tags the release and triggers publishing.

The point is that the release commit arrives as a normal pull request, so branch protection is satisfied the ordinary way. Nothing needs to bypass or relax a protection rule.

Tag format is configured to keep continuity with the existing Lerna tags (next-drupal@2.1.0, not next-drupal-v2.1.0), and the manifest starts at the currently published 2.0.1. Prereleases, which the removed Lerna steps used to handle, are now documented via a Release-As: footer.

Lerna stays for running workspace scripts. Only its publish configuration is removed, since it no longer publishes.

Publishing: npm trusted publishing

release.yml publishes with an OIDC credential minted per workflow run and scoped to this repository and workflow filename. Nothing is stored, and it cannot be replayed elsewhere. Provenance attestations are generated automatically because the repository and package are both public.

release-pr.yml keeps publishing experimental versions from a labelled pull request, on the same OIDC path: yarn publish replaced with npm publish, the token reference removed, Node moved to 24. Its same-repo guard is unchanged, so forks still cannot publish.

Both publish jobs assert npm is at least 11.5.1 rather than installing a newer npm, so nothing unpinned is fetched into a job holding id-token: write. Every current Node 24 release already ships npm 11.19.0.

The one credential this adds, and why

This does not leave the repository credential-free. It trades a long-lived npm token for a long-lived GitHub App private key, stored as RELEASE_APP_PRIVATE_KEY. That is worth reviewing rather than glossing over.

release-please cannot open its pull request with the default GITHUB_TOKEN, because this repository does not allow GitHub Actions to create pull requests. There are two ways to fix that:

  • Turn that repository setting on. One checkbox, no new secret. But it is a single switch that also grants approval, so every workflow in the repository would gain the ability to approve pull requests, which weakens the review requirement on main.
  • Use a GitHub App installation token scoped to this repository. Adds a secret, but the token it mints is short-lived, the permissions are narrow, and no workflow gains approval rights.

This takes the second, on the grounds that a review requirement which automation can satisfy is not much of a requirement. The App private key is the one long-lived secret in the release path, it can be rotated without a publishing outage, and it is owned by the organization rather than an individual.

Note that release-please's own README recommends a personal access token here, not an App token. A PAT would work, but it belongs to a person and carries broader scope. GitHub's own Triggering a workflow docs name both an App installation token and a PAT as the options, so this is a deliberate choice between two supported paths rather than a workaround.

The App is created under the chapter-three organization rather than a personal account, so it does not depend on one person's account remaining active.

Both third-party actions are pinned to full commit SHAs.

This change is inert until it is configured

Merging this enables nothing by itself. Three things must exist first, and they are deliberately not in the diff:

  1. A GitHub App owned by the chapter-three org, installed on this repository, with its ID and private key set as repository secrets.
  2. An npm trusted publisher entry for release.yml with no environment.
  3. An npm trusted publisher entry for release-pr.yml with environment Preview.

All of it is written up under "First-time setup" in MAINTAINING.md rather than left in this description, so it survives the merge.

One detail there is easy to get wrong and worth calling out: the App needs Issues read and write as well as Contents and Pull requests. release-please applies its autorelease labels through the issues API, so without it a release tags and then fails partway.

Not verified by CI

The publish path cannot be exercised before merge, because OIDC only works once npm trusts a workflow on the default branch. First real proof will be the 2.1.0 release. Specifically unverified: whether npm accepts an OIDC credential from a pull_request event when the job declares environment: Preview, and whether release-please picks up e56c636 as the 2.1.0 trigger.

The workflows parse, permissions are scoped per job, both action SHAs match the tags named beside them, and the npm version comparison was checked against both a passing and a failing version. That is the limit of what has been checked here.

References

Publishing:

Why the App token is needed:

Tooling:

The release process documented in MAINTAINING.md stopped working when branch
protection was added: `lerna version` commits to `main`, and `main` now requires
a pull request and a review with admin enforcement on. Publishing was broken
separately, because `yarn publish` prompts for a 2FA code and CI has no
terminal, and because the npm token expired unnoticed after 90 days.

Replace both halves.

Versioning moves to release-please. It reads the conventional commits already
enforced by commitlint and keeps a release pull request open with the version
bump and changelog. That pull request is merged like any other, so branch
protection is satisfied normally and nothing needs to bypass it.

Publishing moves to npm trusted publishing. The runner mints a short-lived OIDC
credential scoped to this workflow, so there is no npm token to store, rotate or
leak, and no 2FA prompt to answer. npm records provenance automatically because
the repository and the package are both public.

`release-pr.yml` keeps publishing experimental versions from a labelled pull
request, but on the same OIDC path, with `yarn publish` replaced by `npm publish`
and the NPM_TOKEN reference removed. `googleapis/release-please-action` is pinned
to a full commit SHA.

Lerna stays for running workspace scripts. Its publish configuration is removed
because it no longer publishes.

This change is inert until the trusted publisher entries are configured on
npmjs.com, and the NPM_TOKEN secret should only be deleted once a release has
succeeded on the new path.
@vercel

vercel Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
next-drupal-next Ready Ready Preview Sep 22, 2026 7:09pm UTC
2 Skipped Deployments
Project Deployment Actions Updated
next-drupal Ignored Ignored Sep 22, 2026 7:09pm UTC
next-drupal-v1-6 Ignored Ignored Sep 22, 2026 7:09pm UTC

Request Review

Review found that the release job could not have opened its pull request. This
repository does not allow GitHub Actions to create pull requests, so the default
GITHUB_TOKEN that release-please falls back to is refused and no release pull
request would ever appear.

The repository setting that would allow it is a single switch covering both
creating and approving pull requests. Turning it on would let any workflow
approve, which would weaken the review requirement on `main`, so this mints an
app installation token scoped to this repository instead. It also means CI runs
on the release pull request without someone first releasing the held runs a
GITHUB_TOKEN-authored pull request produces.

Two smaller fixes from the same review:

Drop `npm install -g npm@latest` from both publish jobs. It fetched an unpinned
executable into a job holding `id-token: write`, next to an action pinned to a
full SHA for that exact reason. Every current Node 24 release ships npm 11.19.0,
past the 11.5.1 trusted publishing needs, so the requirement is now asserted and
fails the job loudly if a future runner image regresses.

Document how to cut a prerelease. Removing the Lerna steps took away the only
documented way to produce one, while later sections still branch on prereleases
and a `canary` branch.

Requires two new repository secrets, RELEASE_APP_ID and RELEASE_APP_PRIVATE_KEY,
before the release workflow can run.
The comment said a GITHUB_TOKEN-authored pull request would not trigger CI at
all. It does trigger it, but the runs are held in an approval-required state
until someone releases them. The reason for the app token is unchanged; only
the description of the side benefit was overstated.
…on the app

Review raised that everything needed to stand this up lived only in a pull
request body, which is how the previous process went stale without anyone
noticing. Add a "First-time setup" section covering the two npm trusted
publisher entries, the app and its permissions, and the two repository secrets.

That section also corrects a real gap: the app needs Issues read and write, not
just Contents and Pull requests. release-please applies its `autorelease` labels
through the issues API, so without it a release tags and then fails partway.

Two smaller fixes from the same review. The "Publishing credentials" section
described the npm account settings as a statement of current fact rather than
as setup, so a maintainer could read it and skip configuring them. And the
Drupal module section still told the reader packages are released with Lerna,
which the section below it contradicts.

Drop the permissions block from the release-please job. Both of its steps
authenticate with the app token, so the block only widened a GITHUB_TOKEN that
is never used, in a job whose purpose is to avoid widening anything.
@robdecker
robdecker merged commit 80a89f1 into main Sep 22, 2026
13 checks passed
@robdecker
robdecker deleted the chore/oidc-release-automation branch September 22, 2026 19:36

This branch was successfully deployed

1 active deployment
Preview – next-drupal-next — 925d312e Deployed Sep 22, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants