diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index 89e399f4..a0e3778b 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -19,6 +19,10 @@ jobs: contains(github.event.pull_request.labels.*.name, format('release-pr{0} next-drupal', ':')) && github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name environment: Preview + permissions: + contents: read + pull-requests: write + id-token: write # Required for trusted publishing steps: - name: Init uses: actions/checkout@v4 @@ -28,8 +32,18 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 20 - registry-url: https://registry.npmjs.org/ + node-version: 24 + # Trusted publishing needs npm 11.5.1 or later, which every current Node + # 24 release ships. This asserts it rather than installing a newer npm, + # so nothing unpinned is fetched into a job that can publish. + - name: Verify npm supports trusted publishing + run: | + version="$(npm --version)" + echo "npm $version" + if [ "$(printf '11.5.1\n%s\n' "$version" | sort -V | head -1)" != "11.5.1" ]; then + echo "::error::npm $version is older than the 11.5.1 that trusted publishing requires" + exit 1 + fi - name: Install dependencies run: yarn install --frozen-lockfile - name: Determine version @@ -37,12 +51,12 @@ jobs: id: determine-version env: PR_NUMBER: ${{ github.event.number }} + # Publishes with an OIDC credential minted for this workflow. npm is + # configured to trust this filename, so renaming this file breaks + # publishing until the trusted publisher entry is updated to match. - name: Publish to npm - run: | - cd packages/next-drupal - yarn publish --no-git-checks --access public --tag experimental - env: - NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --tag experimental + working-directory: packages/next-drupal - name: Comment version on PR env: VERSION: ${{ steps.determine-version.outputs.version }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..1e90c201 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,97 @@ +name: release + +on: + push: + branches: + - main + +# Default to read-only. Each job widens only what it needs. +permissions: + contents: read + +# Never let two releases run at once, and never cancel one partway. +concurrency: + group: release + cancel-in-progress: false + +jobs: + # Keeps a release pull request open with the version bump and changelog, + # built from the conventional commits since the last release. Merging that + # pull request is what triggers an actual release: this job then tags it and + # sets releases_created, which gates the publish job below. + release-please: + runs-on: ubuntu-latest + timeout-minutes: 10 + if: github.repository == 'chapter-three/next-drupal' + # No permissions block: both steps authenticate with the app token, so the + # job's own GITHUB_TOKEN stays at the workflow default of contents: read. + # The app's permissions are set on the app itself, not here. + outputs: + releases_created: ${{ steps.release.outputs.releases_created }} + steps: + # The default GITHUB_TOKEN cannot open a pull request unless the + # repository allows Actions to create and approve them, and that setting + # is a single switch covering both. Granting approval to every workflow + # would undercut the review requirement on `main`, so this uses 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. + - name: Mint app token + id: app-token + uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + with: + app-id: ${{ secrets.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + + - name: Run release-please + id: release + uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + # Publishes to npm with trusted publishing. There is no npm token: the + # id-token permission lets the runner mint a short-lived OIDC credential + # that npm accepts only from this workflow in this repository. Because the + # repository and the package are both public, npm also records provenance + # automatically, so no --provenance flag is needed. + publish: + needs: release-please + if: needs.release-please.outputs.releases_created == 'true' + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + id-token: write + steps: + - name: Init + uses: actions/checkout@v4 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + + # Trusted publishing needs npm 11.5.1 or later, which every current Node + # 24 release ships. This asserts it rather than installing a newer npm, + # so nothing unpinned is fetched into a job that can publish. + - name: Verify npm supports trusted publishing + run: | + version="$(npm --version)" + echo "npm $version" + if [ "$(printf '11.5.1\n%s\n' "$version" | sort -V | head -1)" != "11.5.1" ]; then + echo "::error::npm $version is older than the 11.5.1 that trusted publishing requires" + exit 1 + fi + + - name: Install dependencies + run: yarn install --frozen-lockfile + + # `prepare` (tsup) runs as part of publish, so the tarball is built from + # this checkout rather than from whatever happens to be in dist/. + - name: Publish to npm + run: npm publish + working-directory: packages/next-drupal diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 00000000..e4094fba --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + "packages/next-drupal": "2.0.1" +} diff --git a/MAINTAINING.md b/MAINTAINING.md index 26344f68..7ac094b6 100644 --- a/MAINTAINING.md +++ b/MAINTAINING.md @@ -6,7 +6,7 @@ This document is for maintainers to explain the various procedures for all the p ### `next` (Drupal Module) -While maintaining releases for packages, starters and examples is done with Lerna, releases for Drupal modules are controlled by drupal.org’s infrastructure, so these steps don’t involve Lerna. +The `next-drupal` package is released automatically, and starters and examples are tagged by hand. Releases for Drupal modules are different again: they are controlled by drupal.org’s infrastructure, so none of that applies here. 1. Optionally, create a new branch on drupal.org. @@ -34,85 +34,134 @@ While maintaining releases for packages, starters and examples is done with Lern ### `next-drupal` (NPM package) -Since we are using semantic commits, Lerna is able to read the git commits since the last release to auto-generate a CHANGELOG and to determine if the next release should be a: +Releases are automated. You do not run `npm publish` or `lerna publish`, and you +do not need an npm token on your machine. -- major version bump (e.g. 1.0.0 to 2.0.0). This will happen when Lerna finds a `BREAKING CHANGE` commit message. -- minor version bump (e.g. 1.0.0 to 1.1.0). This will happen when Lerna finds a `feat` commit message. -- patch version bump (e.g. 1.0.0 to 1.0.1). This is the default version bump for bug fixes, etc. -- prerelease version bump (e.g. 1.0.0-alpha.0 to 1.0.0-alpha.1) +#### How it works -We’ll be using Lerna’s `--no-push` flag so that Lerna does not push git tags and commits automatically. This allows us to delete any commits and tags locally if we make a mistake. +[release-please](https://github.com/googleapis/release-please) reads the +conventional commits merged to `main` and keeps a pull request open titled +something like `chore(main): release next-drupal 2.1.0`. That pull request holds +the version bump and the generated `CHANGELOG.md`. It updates itself as more +commits land. -1. **Tag a new release** +The commit type decides the version: - - **to make the next logical semantic version**, run: +- `feat` gives a minor bump (2.0.1 to 2.1.0). +- `fix`, `perf` and similar give a patch bump (2.0.1 to 2.0.2). +- A `BREAKING CHANGE:` footer gives a major bump (2.0.1 to 3.0.0). Add this only + when you mean it. +- Other types (`chore`, `docs`, `ci`, `test`) do not trigger a release on their + own. - ``` - npx lerna version --no-push - ``` +Merging the release pull request tags the release and publishes to npm. - - **to make a new alpha prerelease version:** +#### Making a release - If the current version is not a prerelease version, you’ll need to explicitly tell Lerna that the next version should be an alpha release with: +1. **Check the release pull request.** Confirm the version is what you expect + and the changelog reads well. Edit the changelog in the pull request if a + commit message produced an unhelpful entry. - ``` - npx lerna version --conventional-prerelease --no-push - ``` +2. **Merge it.** Squash and merge, like any other pull request. `main` requires + a review, and the release pull request is authored by a bot, so you can + approve it yourself. - When creating a new prerelease version of `next-drupal`, Lerna will automatically determine if it needs to be a `premajor` (2.0.0-alpha.0), `preminor` (1.1.0-alpha.0), or `prepatch` (1.0.1-alpha.0) version. +3. **Watch the `release` workflow.** Merging tags the release and runs the + publish job. - - **to make a new beta prerelease version:** +4. **Confirm.** Check the "Current Tags" section of + [next-drupal's npm page](https://www.npmjs.com/package/next-drupal?activeTab=versions) + and confirm `latest` points at the new version. - If the current version is not a beta version, you’ll need to explicitly tell Lerna that the next version should be a beta release with: +That is the whole process. There is no local step. - ``` - npx lerna version --conventional-prerelease --preid beta --no-push - ``` +#### Prereleases - - **to make a new regular version from a prerelease version:** +To cut a prerelease, add a `Release-As:` footer naming the exact version to a +commit on `main`: - If the current version is a prerelease version, you’ll need to explicitly tell Lerna that the next version should no longer be a prerelease version with: +``` +Release-As: 2.2.0-alpha.0 +``` - ``` - npx lerna version --conventional-graduate --no-push - ``` +release-please then proposes that version instead of the one it calculated. The +starters and the Docs sections below still refer to prereleases and to a +`canary` branch; those steps are about their own git repositories and release +notes, not about publishing this package. - **Confirm changes** +#### Publishing credentials - When Lerna asks “Are you sure you want to create these versions?”, carefully check if the versions listed are the ones you expect. +Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishers/). +The runner mints a short-lived OIDC credential that npm accepts only from +`.github/workflows/release.yml` in this repository. No npm token is stored, so +there is nothing to rotate and nothing to leak. -2. **Push git changes** with: +Two consequences worth knowing: - ``` - git push - git push --tags - ``` +- **Renaming `release.yml` breaks publishing.** npm matches on the workflow + filename. If you rename or move it, update the trusted publisher entry in the + package settings on npmjs.com first. +- **Publishing from a laptop is not supported.** The package rejects token + authentication, so release through the pull request. -3. **Publish the release** +Because both the repository and the package are public, npm records +[provenance](https://docs.npmjs.com/generating-provenance-statements) +automatically. No flag is needed. - Ensure you have authenticated with npmjs.com using `npm login`. +#### First-time setup - Then, while your local git working area is clean of changes and `HEAD` is pointing to the commit created in step 1, have Lerna build, prepare, package and publish your release. +Only needed when wiring this up from scratch, or when rebuilding it after the +app or the npm settings are lost. Everything here is configured outside the +repository, which is why none of it is visible in the codebase. - For a new prerelease version, specify the `canary` dist-tag with: +On npmjs.com, under the package's Settings, add two trusted publisher entries +for the `chapter-three/next-drupal` repository: - ``` - npx lerna publish --dist-tag canary from-git - ``` +| Workflow filename | Environment | +| ----------------- | ----------- | +| `release.yml` | none | +| `release-pr.yml` | `Preview` | - Otherwise, use: +The environment field must match the workflow exactly, so `release.yml` is left +blank and `release-pr.yml` is set to `Preview`. Also enable "Require two-factor +authentication and disallow tokens" under Publishing access, which is what makes +the workflows the only way to publish. - ``` - npx lerna publish from-git - ``` +On GitHub, create an app owned by the `chapter-three` organization and install +it on this repository alone. It needs three repository permissions: **Contents** +read and write, **Issues** read and write, and **Pull requests** read and write. +Issues is not optional: release-please applies its `autorelease` labels through +the issues API, so without it a release tags and then fails. + +Add the app's ID and private key as the repository secrets `RELEASE_APP_ID` and +`RELEASE_APP_PRIVATE_KEY`. Add app managers so more than one person can +administer it. + +Leave "Allow GitHub Actions to create and approve pull requests" disabled. The +app exists so that this setting can stay off: it is a single switch that also +grants approval, which would let automation satisfy the review requirement on +`main`. + +#### Experimental releases from a pull request - Maintainers will need permission to publish to `next-drupal` on npmjs.com. http://npmjs.com/package/next-drupal +To publish a throwaway version from an open pull request, for example so another +project can test a fix before it is released, add the label +`release-pr: next-drupal` to the pull request. The `release-pr` workflow +publishes under the `experimental` dist-tag and comments the install command on +the pull request. -4. **Confirm the release** +This only works for branches in this repository, not forks. - Look at the “Current Tags” section of [next-drupal’s npmjs page](https://www.npmjs.com/package/next-drupal?activeTab=versions) and confirm that the newest release is listed and that the `latest` tag and the `canary` tag point at the expected versions. +#### If the release does not happen -For more information, see Lerna’s [version docs](https://github.com/lerna/lerna/tree/main/libs/commands/version) and [publish docs](https://github.com/lerna/lerna/tree/main/libs/commands/publish). +- **No release pull request appeared.** Nothing since the last release changed a + releasable file, or every commit was a type that does not trigger a release. + Check that the commit touched `packages/next-drupal`. +- **The publish job failed.** Read the job log rather than guessing. If npm + rejects the credential, the likely cause is that `release.yml` was renamed, or + the trusted publisher entry on npmjs.com was deleted or recreated. +- **The version is wrong.** The version comes from commit types. To force a + specific version, use the `Release-As:` footer described under "Prereleases". ### Examples diff --git a/lerna.json b/lerna.json index 4db5e70d..37fd6507 100644 --- a/lerna.json +++ b/lerna.json @@ -10,11 +10,6 @@ }, "run": { "loglevel": "verbose" - }, - "publish": { - "allowBranch": "main", - "conventionalCommits": true, - "message": "chore(release): publish" } } } diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 00000000..42e4a463 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-component-in-tag": true, + "include-v-in-tag": false, + "tag-separator": "@", + "packages": { + "packages/next-drupal": { + "release-type": "node", + "changelog-path": "CHANGELOG.md" + } + } +}