Files
redshift 7c53ebc9e5 docs: add CI and release automation guide
Document the release workflow (validate/build/publish-npm/release), npm
trusted publishing via OIDC, the canonical repository.url requirement,
and the @routstr/sdk release-ordering rule in docs/CI.md, linked from
the README release section and project structure.
2026-10-06 22:55:23 +08:00

130 lines
5.1 KiB
Markdown

# CI and release automation
How continuous integration and releases work for `routstrd`. Everything runs on
GitHub Actions; there are **no repository secrets** — publishing uses OIDC
trusted publishing and the workflow's own `GITHUB_TOKEN`.
## Workflow
`.github/workflows/release.yml`.
Triggers:
| Trigger | What runs |
| --- | --- |
| `pull_request` | `validate` + `build` (no publish, no release) |
| `workflow_dispatch` (manual) | `validate` + `build` only — publish and release jobs are tag-gated |
| `push` of a `v*` tag | the full pipeline: validate → build → publish npm → GitHub Release |
Jobs:
- **`validate`** — `bun install --frozen-lockfile`, then (on a tag) asserts the
tag equals `v` + `package.json` version, then `bun run lint` (`tsc --noEmit`)
and `bun test`.
- **`build`** — a 4-way matrix that compiles standalone executables with
`bun build --compile` and smoke-tests each one (`--version`, `--help`):
| runner | target | platform-arch |
| --- | --- | --- |
| `ubuntu-24.04` | `bun-linux-x64` | `linux-x64` |
| `ubuntu-24.04-arm` | `bun-linux-arm64` | `linux-arm64` |
| `macos-15-intel` | `bun-darwin-x64` | `darwin-x64` |
| `macos-15` | `bun-darwin-arm64` | `darwin-arm64` |
Each binary is packed into `routstrd-v<version>-<platform>-<arch>.tar.gz` and
uploaded as a build artifact.
- **`publish-npm`** — tag-gated, `needs: [validate, build]`. Publishes to npm
with provenance (see below). Skips gracefully if that version already exists.
- **`release`** — tag-gated, `needs: build`. Collects the artifacts, writes
`SHA256SUMS`, smoke-tests `install.sh` against the just-built Linux-x64 archive
over a local HTTP server, then creates the GitHub Release with the four
archives, `SHA256SUMS`, and `install.sh`.
## Cutting a release
```sh
# 1. bump package.json version and commit it
# 2. push the tag
git tag v0.4.15
git push origin main v0.4.15
```
The tag push is the single action that triggers everything. The tag **must**
equal `v` + `package.json` version (the tag must not already exist — the release
job uses `gh release create --verify-tag`).
After it finishes, confirm `routstrd@<version>` is on npm (`latest`) and that
the GitHub Release has all four archives plus `install.sh`.
## npm publishing (OIDC trusted publishing)
`publish-npm` publishes with `npm publish --access public --provenance`. There is
no `NPM_TOKEN`:
- The job grants `id-token: write`; npm exchanges the GitHub OIDC token for a
short-lived publish token.
- A one-time trust relationship on npm binds the package to this repo/workflow:
```sh
npm trust github routstrd \
--file release.yml \
--repo Routstr/routstrd \
--allow-publish
```
- The workflow upgrades npm (`npm install -g npm@latest`) because trusted
publishing requires npm ≥ 11.5.1 while Node 22 ships npm 10.x.
- `npm publish` runs the package's `prepublishOnly` (`bun run build`), so `dist/`
is produced in the job.
### Gotchas
- **`repository.url` must use the canonical casing.** Provenance validates it
case-sensitively against the repo (`https://github.com/Routstr/routstrd`). A
lowercase `routstr` fails with `422 … Failed to validate repository
information`. `bun`/`npm` preserve path case when normalizing, so the fix is to
spell it exactly `Routstr`.
- **npm has a post-publish processing delay.** A successful publish reports
`+ routstrd@x.y.z` and "Your package is being processed and may take a few
minutes to become available." The version can take several minutes to become
fetchable. A green workflow does not mean it is instantly installable.
## Why tag push, not `release: published`
`publish-npm` runs off the **tag push**, not a `release: published` event. The
`release` job creates the GitHub Release using the default `GITHUB_TOKEN`, and
GitHub suppresses workflow runs for events generated by that token (loop
prevention) — so a `release: published` workflow would never fire here.
## Idempotency
- `npm publish` is guarded: if `routstrd@<version>` already exists, the job logs
a warning and skips instead of failing.
- `gh release create --verify-tag` fails if the tag is missing; if the release
already exists the job errors, so do not create the release by hand.
## Dependency ordering with `@routstr/sdk`
`routstrd` depends on the **published** `@routstr/sdk` (not a local file link),
and CI installs with `bun install --frozen-lockfile` from the npm registry. When
a release needs a newer SDK:
1. Release `@routstr/sdk` first (it has its own publish pipeline; see the SDK
repo's `.github/workflows/publish.yml`).
2. In the same commit, bump `@routstr/sdk` in `package.json` **and** refresh
`bun.lock` (`bun install`).
3. Then tag and push `routstrd`.
Failure modes if that order is broken:
- Version not yet on npm → `validate` fails with
`No version matching "^x.y.z" found for specifier "@routstr/sdk"`.
- `package.json` bumped but `bun.lock` stale → `bun install --frozen-lockfile`
fails.
## Manual runs
`gh workflow run release.yml --ref main` runs `validate` + `build` only (the
publish and release jobs are gated on a `v*` tag ref). Useful for exercising the
build matrix without releasing.