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

# A v* tag publishes @heightproof/shared, sdk-node and web3 at that version, in dependency order.
#
# Authentication is npm trusted publishing (OIDC): no npm token is stored in GitHub, and every
# release carries a provenance attestation linking it to this workflow run. Each package must
# list this repository and workflow as a trusted publisher on npmjs.com — see RELEASING.md.
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
dry_run:
description: "Pack and check without publishing"
type: boolean
default: true

concurrency:
group: npm-publish
cancel-in-progress: false

jobs:
publish:
runs-on: ubuntu-latest
permissions:
contents: read
# Exchanged for a short-lived npm credential, and used to sign provenance.
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
registry-url: https://registry.npmjs.org

# Trusted publishing needs npm 11.5.1 or later; Node 22 bundles npm 10.
- name: Upgrade npm
run: npm install -g npm@11.21.1

- name: Install
run: npm ci --include=optional

- name: Build
run: npm run build

- name: Test
run: npm test

- name: Check versions match the tag
if: github.event_name == 'push'
run: |
tag="${GITHUB_REF_NAME#v}"
for pkg in shared sdk-node web3; do
version="$(node -p "require('./packages/$pkg/package.json').version")"
if [ "$version" != "$tag" ]; then
echo "::error::packages/$pkg is $version but the tag is v$tag"
exit 1
fi
# Internal dependencies are pinned exactly, so a stale pin would publish a package
# that installs the previous release of its siblings.
stale="$(node -p "Object.entries(require('./packages/$pkg/package.json').dependencies ?? {}).filter(([n, v]) => n.startsWith('@heightproof/') && v !== '$tag').map(([n, v]) => n + '@' + v).join(' ')")"
if [ -n "$stale" ]; then
echo "::error::packages/$pkg pins $stale, expected $tag"
exit 1
fi
done

- name: Publish
env:
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run }}
run: |
for pkg in shared sdk-node web3; do
name="$(node -p "require('./packages/$pkg/package.json').name")"
version="$(node -p "require('./packages/$pkg/package.json').version")"

# Re-running a release must not fail on packages it already published.
if npm view "$name@$version" version >/dev/null 2>&1; then
echo "$name@$version is already on npm, skipping"
continue
fi

if [ "$DRY_RUN" = "true" ]; then
npm publish -w "packages/$pkg" --access public --dry-run
else
npm publish -w "packages/$pkg" --access public --provenance
fi
done
64 changes: 64 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Releasing

A `v*` tag releases everything at that version:

- `.github/workflows/release.yml` publishes the container images and the Helm chart to GHCR.
- `.github/workflows/npm-publish.yml` publishes `@heightproof/shared`, `@heightproof/sdk-node`
and `@heightproof/web3` to npm, in that order, with provenance.

## Cutting a release

1. Set the version in all three packages, and in the exact pins they hold on each other:

```bash
VERSION=0.2.0
npm version "$VERSION" --no-git-tag-version -w packages/shared -w packages/sdk-node -w packages/web3
npm pkg set "dependencies.@heightproof/shared=$VERSION" -w packages/sdk-node -w packages/web3
npm pkg set "dependencies.@heightproof/sdk-node=$VERSION" -w packages/web3
npm install
```

2. Add the release to `CHANGELOG.md`, then open a PR and merge it once CI is green.
3. Tag the merge commit on `main` and push the tag:

```bash
git switch main && git pull
git tag "v$VERSION" && git push origin "v$VERSION"
```

The workflow refuses to publish when a package version or an internal pin does not match the
tag. It skips any package already published at that version, so a failed run can be re-run.

To check a release without publishing it, run **npm publish** from the Actions tab with
`dry_run` checked.

## One-time setup: trusted publishing

The workflow authenticates with npm [trusted publishing](https://docs.npmjs.com/trusted-publishers)
(OIDC), so no npm token is stored in GitHub. npm only lets you add a trusted publisher to a package
that already exists, so:

1. Publish the first version by hand, from a clean checkout of `main`, as an owner of the
`heightproof` npm organization:

```bash
npm login
npm run build
npm publish -w packages/shared --access public
npm publish -w packages/sdk-node --access public
npm publish -w packages/web3 --access public
```

2. On npmjs.com, for each of the three packages, go to **Settings → Trusted publishing** and add
GitHub Actions with:

| Field | Value |
| ------------ | ----------------- |
| Organization | `macjayz` |
| Repository | `heightproof` |
| Workflow | `npm-publish.yml` |

3. Optionally, under **Settings → Publishing access** for each package, require 2FA and disallow
tokens. Trusted publishing keeps working, and nobody can publish with a leaked token.

Every release after that is just a tag.
Loading