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
121 changes: 121 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
name: Release

# Publishing is a tag push, not a commit to main: `v0.1.0` and the version in
# package.json must agree before anything leaves the runner.
#
# A manual run does everything except publish and create the release, so the
# workflow can be rehearsed from a branch: Actions -> Release -> Run workflow,
# with `tag` set to the tag you intend to push. Only a real `v*` tag publishes.
on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: Tag to check as if it had been pushed, e.g. v0.1.0
required: true
type: string

permissions:
contents: read

concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
name: publish ${{ inputs.tag || github.ref_name }}
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # npm --provenance (sigstore attestation)
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Node 20
uses: actions/setup-node@v4
with:
node-version: 20
registry-url: https://registry.npmjs.org

- name: Enable pnpm from packageManager
run: corepack enable

- name: Install
run: pnpm install --frozen-lockfile

- name: Tag matches package.json
# Fails before the expensive steps: a tag that does not name the version
# in package.json would publish under the wrong number or refuse to
# publish at all, after check and build have already run.
env:
# Empty on a real tag push, where the tag is the ref that triggered us.
INPUT_TAG: ${{ inputs.tag }}
run: |
set -euo pipefail
tag="${INPUT_TAG:-${GITHUB_REF_NAME}}"
version="$(node -p "require('./package.json').version")"
expected="v${version}"
echo "tag=${tag}"
echo "package.json version=${version}"
if [ "${tag}" != "${expected}" ]; then
echo "::error::tag ${tag} does not match package.json version (expected ${expected})"
exit 1
fi

- name: Check
run: pnpm check

- name: Build
run: pnpm build

- name: npm 11 for trusted publishing
# Node 20 bundles npm 10, and npm's OIDC publishing needs 11.5.1 or
# later. The NPM_TOKEN path works on npm 10 too; this keeps both open.
run: |
npm install -g 'npm@^11.5.1'
npm --version

- name: Verify the published surface
# Everything `files` ships, and the two entry points it must contain.
# `--dry-run` produces the tarball without uploading it; provenance is
# only attached by the real publish below.
run: |
set -euo pipefail
npm publish --dry-run --access public
npm pack --dry-run 2>&1 | tee /tmp/pack.txt
grep -q 'dist/index.js' /tmp/pack.txt
grep -q 'dist/index.d.ts' /tmp/pack.txt
grep -q 'LICENSE' /tmp/pack.txt
grep -q 'README.md' /tmp/pack.txt

- name: Publish to npm
# A manual run stops here: only a tag push may upload.
if: ${{ github.event_name == 'push' }}
# NPM_TOKEN is a repository secret set by a maintainer. With npm trusted
# publishing configured for this repository instead, no token is needed
# and this env var is empty — npm then authenticates with the OIDC token
# that `id-token: write` mints. Never commit a token.
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

release:
name: github release ${{ github.ref_name }}
needs: publish
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
# Separate job so the publish step above never holds write access.
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Create GitHub release
run: gh release create "${GITHUB_REF_NAME}" --verify-tag --generate-notes --title "${GITHUB_REF_NAME}"
env:
GH_TOKEN: ${{ github.token }}
48 changes: 48 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,54 @@ thresholds (80% lines and functions, 75% branches) are enforced by
- By contributing you confirm you may license the change under Apache-2.0, and
this project follows the [Code of Conduct](./CODE_OF_CONDUCT.md).

## Releasing

Publishing is a tag push, not a manual `npm publish`. `.github/workflows/release.yml`
runs on any `v*` tag and does the whole release: install from the lockfile,
`pnpm check`, `pnpm build`, a `--dry-run` look at the tarball's contents, publish
with `--provenance`, then a GitHub release with generated notes.

1. Bump `version` in `package.json` in a normal pull request and let CI go green.
2. Rehearse it if you want a log before tagging: Actions → **Release** →
_Run workflow_, with `tag` set to the tag you intend to push. A manual run
executes every step except the upload and the release, which are gated on
`github.event_name == 'push'` — a branch cannot publish, and both the matching
and mismatching tag cases can be checked this way.
3. Merge it, then tag the merge commit: `git tag v0.1.0 && git push origin v0.1.0`.
4. Watch the Release workflow. The tag **must** equal `v` + the `package.json`
version; a mismatch fails the job before `check` and `build` run, so a typo
costs a minute rather than a bad publish. Re-tag rather than editing a pushed
tag in place.

Authentication is one of two setups, owned by a maintainer and never committed:

- **Token:** add a granular npm token with publish rights as the repository
secret `NPM_TOKEN`. The workflow passes it to npm as `NODE_AUTH_TOKEN`.
- **Trusted publishing:** configure this repository and workflow in the package's
npm settings and leave `NPM_TOKEN` unset. `id-token: write` (already in the
workflow) lets npm mint an OIDC token instead.

Either way `--provenance` attaches a signed link between the tarball and this
commit, which is why the publish job asks for `id-token: write`. Both jobs run
with `contents: read`; only the separate release job gets `contents: write`, so
the step that uploads to npm never holds write access to the repository.

Two things that will otherwise fail at the last step:

- Node 20 bundles npm 10, and npm's OIDC publishing needs npm 11.5.1 or later. The
workflow therefore upgrades npm before publishing; the `NPM_TOKEN` path would
work either way.
- Provenance is rejected by the registry (E422) unless `repository.url` in
`package.json` names the repository running the workflow. It does
(`TeethakingBuilds/keygate-client`), so releases must run there, not from a fork;
a fork needs its own package name and its own publish setup.

A version that has already been published cannot be reused: npm rejects the
upload, and the fix is a new version and a new tag. The package is unscoped
(`keygate-client`); if a name change is ever needed, agree on the scoped name in
an issue first, then change `name` in `package.json` and the install line in
[README.md](./README.md) together.

## What this project will not add

Account funding and friendbot, time bounds, fee bumping, transaction batching, a
Expand Down
Loading