diff --git a/.github/workflows/reusable-release.yml b/.github/workflows/reusable-release.yml new file mode 100644 index 0000000..835609a --- /dev/null +++ b/.github/workflows/reusable-release.yml @@ -0,0 +1,89 @@ +name: release + +on: + workflow_call: + inputs: + semver: + description: 'Release bump type: patch, minor or major.' + required: true + type: string + node-version: + description: 'The Node.js version used to build and publish the package.' + required: false + default: 'lts/*' + type: string + runs-on: + description: 'The runner used to publish the package.' + required: false + default: 'ubuntu-latest' + type: string + environment: + description: 'The deployment environment that gates the release. Configure it with the required reviewers that are allowed to release.' + required: false + default: 'release' + type: string + +permissions: {} + +jobs: + release: + name: Release + runs-on: ${{ inputs.runs-on }} + environment: ${{ inputs.environment }} + permissions: + id-token: write # required for npm provenance via OIDC + contents: write # required to push the release commit and the tag + env: + BUMP_TYPE: ${{ inputs.semver }} + steps: + - name: Validate the semver input + run: | + case "$BUMP_TYPE" in + patch|minor|major) ;; + *) echo "::error::Invalid semver input '$BUMP_TYPE': expected patch, minor or major." && exit 1 ;; + esac + + - name: Check out repo + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: true # explicit: the release commit and tag are pushed below + + - name: Setup Node ${{ inputs.node-version }} + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + check-latest: true + node-version: ${{ inputs.node-version }} + registry-url: 'https://registry.npmjs.org' + package-manager-cache: false # never use caching in release builds + + - name: Bump package version + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + NEW_VERSION=$(npm version "$(printf '%s' "$BUMP_TYPE")" --no-git-tag-version) + git add -u # stages package.json and the lockfile, whichever the repo tracks + git commit -m "Bumped v$(printf '%s' "${NEW_VERSION#v}")" + SAFE_NEW_VERSION=$(printf '%s' "${NEW_VERSION#v}" | tr -d '\r\n') + printf 'NEW_VERSION=%s\n' "$SAFE_NEW_VERSION" >> "$GITHUB_ENV" + + - name: Install dependencies + run: npm install --ignore-scripts --no-audit --no-fund + + - name: Build the release artifacts + run: npm run --if-present release:build + + - name: Publish to npm + run: npm publish --provenance --access public + + - name: Push release commit + run: git push origin HEAD + + - name: Create GitHub release + uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3 + with: + tag_name: v${{ env.NEW_VERSION }} + target_commitish: ${{ github.ref_name }} + name: v${{ env.NEW_VERSION }} + generate_release_notes: true # "What's Changed" from merged PRs + contributors + Full Changelog link + make_latest: true diff --git a/README.md b/README.md index 9b0afcd..689a1dd 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,100 @@ jobs: | `lint` | false | boolean | `false` | Set to `true` to run the `lint` script in a repository's `package.json`. | | `node-versions` | false | string | `'["24", "26"]'` | Provide A JSON array that specifies the Node.js versions on which the job should run. | +## Release workflow + +`reusable-release.yml` is a reusable workflow that bumps the package version, publishes it +to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements) +and creates the matching GitHub release. + +It authenticates to npm through OIDC, so the consuming package **must** be +configured as a [trusted publisher](https://docs.npmjs.com/trusted-publishers) on +npm. No `NPM_TOKEN` secret is needed. + +If the repository defines a `release:build` script in its `package.json`, it is +executed after the dependencies are installed and before `npm publish`. Note that +this workflow does **not** run the test suite: the CI workflow is expected to +have already validated the commit being released. + +### Usage + +Add a `.github/workflows/reusable-release.yml` file to your repository: + +```yml +name: release + +on: + workflow_dispatch: + inputs: + semver: + description: 'Release bump type' + required: true + type: choice + options: + - patch + - minor + - major + +permissions: {} + +jobs: + release: + permissions: + id-token: write + contents: write + uses: fastify/workflows/.github/workflows/reusable-release.yml@v7 + with: + semver: ${{ inputs.semver }} +``` + +Then run it from the *Actions* tab, choosing the bump type. + +See [Restricting who can release](#restricting-who-can-release) to limit the +maintainers that are allowed to approve a release. + +### Inputs + +| Input Name | Required | Type | Default | Description | +| -------------- | -------- | ------ | -------------- | -------------------------------------------------------------------- | +| `semver` | true | string | | The release bump type: `patch`, `minor` or `major`. | +| `node-version` | false | string | `lts/*` | The Node.js version used to build and publish the package. | +| `runs-on` | false | string | `ubuntu-latest`| The runner used to publish the package. | +| `environment` | false | string | `release` | The deployment environment that gates the release. | + +### Required permissions + +The calling job must grant `id-token: write` (npm provenance via OIDC) and +`contents: write` (to push the release commit and the tag). + +### Restricting who can release + +GitHub Actions has no per-workflow access control: anyone with write access to a +repository can start a `workflow_dispatch` run. To restrict releases to a +specific set of maintainers, the job runs inside a **deployment environment** +(`release` by default, configurable through the `environment` input). + +In the consuming repository, go to *Settings -> Environments*, create the +`release` environment and: + +- add the team that is allowed to release (for example `fastify/release`) as a + **required reviewer**, so every run pauses until one of them approves it; +- enable **Prevent self-review**, so the person who started the run cannot + approve their own release; +- optionally limit the **deployment branches** to `main`. + +Until the environment is approved no step of the job runs, so an unauthorised +dispatch cannot bump the version nor publish anything. + +> [!IMPORTANT] +> An environment that is referenced but never configured is created +> automatically **without any protection rule**. Creating the environment and +> adding the reviewers is a manual, per-repository step. + +The same environment name can also be set as the *Environment* field of the npm +[trusted publisher](https://docs.npmjs.com/trusted-publishers) configuration, so +that npm itself rejects any publish that does not come from it. + + ## Acknowledgments Past sponsors: