Files
routstrd/docs/CI.md
T
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

5.1 KiB

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

# 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:

    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.