Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 67 additions & 22 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
name: Release - Publish @opennsw package
name: Release - Stage @opennsw package on npm

# Publishes any package under packages/ to the npm registry when a release tag
# is pushed. The package is derived from the tag, so one workflow serves every
# Stages any package under packages/ on the npm registry when a release tag is
# pushed. The package is derived from the tag, so one workflow serves every
# package in the monorepo.
#
# This workflow can only STAGE. It runs `npm stage publish`, which uploads the
# tarball to npm's staging queue; the version is not installable until a
# maintainer approves it on npm with 2FA (see "Approving a staged release" in
# CONTRIBUTING.md). The package's trusted publisher on npmjs.com is limited to
# `npm stage publish`, so even a changed workflow cannot publish directly.
#
# Tag convention: <package-dir>-v<semver>
# - <package-dir> is the folder name under packages/
# - <semver> must match the "version" in that package's package.json
Expand All @@ -14,27 +20,30 @@ name: Release - Publish @opennsw package
# git tag jsonforms-renderers-v0.3.1
# git push origin jsonforms-renderers-v0.3.1
# 3. This workflow resolves the package from the tag, verifies the version,
# builds, and publishes.
# builds, and stages the package on npm.
# 4. A maintainer approves the staged package on npm (2FA). Only then is it live.
#
# Authentication: npm Trusted Publishing via GitHub OIDC — no NPM_TOKEN secret.
# One-time setup per package on npmjs.com: open the package's Settings ->
# Trusted Publisher and register GitHub Actions with this repo and workflow file
# `release.yml`. `id-token: write` + a recent npm CLI also attach build
# provenance (the verified badge on npmjs.com).
# Trusted Publisher, register GitHub Actions with this repo and workflow file
# `release.yml`, and under "Allowed actions" keep only `npm stage publish`. Also
# require two-factor authentication and disallow tokens in the package's
# publishing access, so there is no other way to publish. `id-token: write` + a
# recent npm CLI also attach build provenance (the verified badge on npmjs.com).

on:
push:
tags:
- '*-v*.*.*'

# Don't run two publishes of the same tag concurrently.
# Don't run two stagings of the same tag concurrently.
concurrency:
group: release-${{ github.ref_name }}
cancel-in-progress: false

jobs:
publish:
name: Build and publish to npm
name: Build and stage on npm
runs-on: ubuntu-latest
permissions:
contents: read
Expand All @@ -56,12 +65,19 @@ jobs:
cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml'

# Trusted Publishing / OIDC requires npm CLI >= 11.5.1. The npm bundled
# with Node 22.18 is older, so upgrade it before publishing. Pinned to
# the 11.x line (rather than @latest) because npm 12 requires Node
# >=22.22.2, newer than the Node 22.18.0 pinned above.
- name: Upgrade npm (OIDC trusted publishing support)
run: npm install --global npm@11
# Trusted Publishing / OIDC requires npm CLI >= 11.5.1, and `npm stage`
# requires >= 11.15.0. The npm bundled with Node 22.18 is older, so upgrade
# it before staging. Pinned to the 11.x line (rather than @latest) because
# npm 12 requires Node >=22.22.2, newer than the Node 22.18.0 pinned above.
- name: Upgrade npm (trusted and staged publishing support)
run: |
npm install --global npm@11
required=11.15.0
installed="$(npm --version)"
if [ "$(printf '%s\n%s\n' "$required" "$installed" | sort -V | head -n1)" != "$required" ]; then
echo "::error::npm $installed is older than $required, which 'npm stage publish' needs."
exit 1
fi

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -94,20 +110,49 @@ jobs:
echo "::error::Tag version ($TAG_VERSION) does not match $PKG_PATH/package.json ($PKG_VERSION)."
exit 1
fi
echo "path=$PKG_PATH" >> "$GITHUB_OUTPUT"
echo "name=$PKG_NAME" >> "$GITHUB_OUTPUT"
{
echo "path=$PKG_PATH"
echo "name=$PKG_NAME"
echo "version=$PKG_VERSION"
} >> "$GITHUB_OUTPUT"

- name: Type check
run: pnpm --filter "${{ steps.pkg.outputs.name }}" run type-check

- name: Build package
run: pnpm --filter "${{ steps.pkg.outputs.name }}" run build

# Sanity-check the publishable tarball (no real upload).
- name: Publish dry run
# Sanity-check the tarball (no real upload).
- name: Stage dry run
working-directory: ${{ steps.pkg.outputs.path }}
run: npm publish --provenance --access public --dry-run
run: npm stage publish --provenance --access public --dry-run

- name: Publish to npm
# Uploads the tarball to npm's staging queue. Nothing is installable until a
# maintainer approves it on npm, so a green run here does NOT mean the
# version is live. The output (it carries the stage ID) goes to the job
# summary, with the commands the approver needs.
- name: Stage on npm
working-directory: ${{ steps.pkg.outputs.path }}
run: npm publish --provenance --access public
env:
PKG_NAME: ${{ steps.pkg.outputs.name }}
PKG_VERSION: ${{ steps.pkg.outputs.version }}
run: |
set -o pipefail
npm stage publish --provenance --access public 2>&1 | tee "$RUNNER_TEMP/stage.log"
{
echo "### Staged $PKG_NAME@$PKG_VERSION on npm"
echo
echo "**Not live yet.** A maintainer must approve it with 2FA: on npmjs.com (Staged Packages tab) or with"
echo
echo '```sh'
echo "npm stage list $PKG_NAME"
echo "npm stage download <stage-id> # inspect the tarball first"
echo "npm stage approve <stage-id>"
echo '```'
echo
echo "Output of \`npm stage publish\`:"
echo
echo '```'
cat "$RUNNER_TEMP/stage.log"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
8 changes: 5 additions & 3 deletions .github/workflows/version-bump.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ name: Release - Bump package version
# 3. Tag the merge commit on main and push the tag, e.g.:
# git tag jsonforms-renderers-v0.4.1
# git push origin jsonforms-renderers-v0.4.1
# That tag push triggers release.yml, which builds and publishes to npm.
# That tag push triggers release.yml, which builds and stages the package on
# npm.
# 4. A maintainer approves the staged package on npm (2FA). Only then is it live.
#
# Note: this workflow pushes/opens its PR using the default GITHUB_TOKEN, so
# packages-ci.yml will NOT run on the PR it creates (GitHub doesn't let the
Expand Down Expand Up @@ -108,14 +110,14 @@ jobs:
--body "$(cat <<EOF
Bumps \`${{ steps.pkg.outputs.name }}\` (\`${{ steps.pkg.outputs.path }}\`) to \`${{ inputs.version }}\`.

**After merging**, tag the merge commit on \`main\` and push the tag to publish:
**After merging**, tag the merge commit on \`main\` and push the tag to start the release:

\`\`\`
git tag ${{ inputs.package }}-v${{ inputs.version }}
git push origin ${{ inputs.package }}-v${{ inputs.version }}
\`\`\`

Pushing that tag triggers \`release.yml\`, which builds and publishes to npm.
Pushing that tag triggers \`release.yml\`, which builds and stages the package on npm. A maintainer then approves it on npm (2FA) to make it live.

_Opened by the \`version-bump.yml\` workflow._
EOF
Expand Down
29 changes: 29 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Contributing

This is a pnpm monorepo of independent `@opennsw/*` packages. See the [README](README.md) for setup and common commands, and for the [release steps](README.md#releasing).

## Releasing a package

Packages reach npm with [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC, no token secret) and build provenance, through [staged publishing](https://docs.npmjs.com/staged-publishing/): CI can only _stage_ a version, and a maintainer approves it on npm with 2FA before it is installable. When a release tag is pushed, [`release.yml`](.github/workflows/release.yml) builds the package and stages it.

### Approving a staged release

The `release.yml` run ends with a job summary that has the stage ID and these commands. You need publish access to the package and 2FA on your npm account.

```sh
npm stage list @opennsw/<name> # find the stage ID
npm stage download <stage-id> # optional: inspect the tarball
npm stage approve <stage-id> # asks for 2FA; the version goes live
npm stage reject <stage-id> # discard it instead
```

Or use the **Staged Packages** tab of the package on npmjs.com. Look at the tarball before approving: it should contain `dist`, `README.md` and `package.json`, and the code should match the tag. Prefer an approver other than whoever pushed the tag; npm doesn't enforce that. Check afterwards that the approved version carries its provenance badge.

Don't re-run a release while its version is still staged: a re-run would stage it again. Reject the old stage first.

### npm settings for a package

Set these on npmjs.com, in the package's settings, before its first staged release:

- **Trusted Publisher:** GitHub Actions, repository `OpenNSW/ui-packages`, workflow `release.yml`, with **Allowed actions** set to `npm stage publish` only (not `npm publish`), so CI can stage a version but never make it live. `release.yml` can't stage the package until this is done.
- **Publishing access:** require two-factor authentication and disallow tokens, so nothing else can publish.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,16 @@ pnpm --filter @opennsw/jsonforms-renderers run dev

## Releasing

Each package is published independently via its own tag-triggered workflow using npm [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC, no token secret). For `@opennsw/jsonforms-renderers`:
Each package is released independently via its own tag-triggered workflow, using npm [Trusted Publishing](https://docs.npmjs.com/trusted-publishers) (OIDC, no token secret) and [staged publishing](https://docs.npmjs.com/staged-publishing/): CI stages the version, and a maintainer approves it on npm with 2FA before it is installable. For `@opennsw/jsonforms-renderers`:

1. Bump `"version"` in `packages/jsonforms-renderers/package.json` and merge to `main`.
2. Tag the commit and push the tag:
```bash
git tag jsonforms-renderers-v0.3.1
git push origin jsonforms-renderers-v0.3.1
```
3. The release workflow verifies the tag matches `package.json`, builds, and publishes to npm with provenance.
3. The release workflow verifies the tag matches `package.json`, builds, and stages the package on npm with provenance.
4. A maintainer approves the staged package on npm; only then is it live. See [Approving a staged release](CONTRIBUTING.md#approving-a-staged-release).

## License

Expand Down