diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8c3b639..2b4f7cd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,33 +1,88 @@ name: release on: - workflow_dispatch: + workflow_call: inputs: semver: - description: 'The semver to use' + description: 'Release bump type: patch, minor or major.' required: true - default: 'patch' - pull_request: - types: [closed] + 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: - contents: read +permissions: {} jobs: release: - runs-on: ubuntu-latest + name: Release + runs-on: ${{ inputs.runs-on }} + environment: ${{ inputs.environment }} permissions: - contents: write - issues: write - pull-requests: write + 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: Use Node.js + - 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: lts/* - - uses: nearform-actions/optic-release-automation-action@08642f7889f3bb519fb69ce2c3bf58e4f900c018 # v4.12.4 + 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 user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + NEW_VERSION=$(npm version "$BUMP_TYPE" --no-git-tag-version) + git add -u # stages package.json and the lockfile, whichever the repo tracks + git commit -m "Bumped v${NEW_VERSION#v}" + echo "NEW_VERSION=${NEW_VERSION#v}" >> "$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: - github-token: ${{ secrets.GITHUB_TOKEN }} - semver: ${{ github.event.inputs.semver }} - sync-semver-tags: true + 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..e43cac9 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 + +`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/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/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: