sitez

Release

Merging a new version in package.json releases it. The workflow checks the package, stages it on npm, then tags the merged commit vX.Y.Z and publishes a GitHub Release with the tarball. Nobody pushes a tag by hand. This file is the same in every package, copied from ship's package/ folder, so it names no package and reads everything from package.json.

npm uses trusted publishing, with OIDC and provenance, so there's no token to store. The publisher may only stage. The new version waits on npmjs.com until a maintainer approves it with 2FA, so a merge alone can't put a version in front of users.

name: release

on:
  push:
    branches: [main]
    paths: [package.json]
  # Runs it again on `main`, after a failure: `gh workflow run release`.
  workflow_dispatch:

permissions:
  contents: write
  id-token: write
  pull-requests: read

concurrency:
  group: release
  cancel-in-progress: false

jobs:
  # A change to `package.json` that leaves `version` alone, like a dependency bump, finds its tag
  # already there and stops here. So does a package that isn't on npm yet, whose first version is
  # published by hand, since npm trusts this workflow only once the package exists.
  version:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.tag.outputs.tag }}
    steps:
      - uses: actions/checkout@v7
      - id: tag
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          name="$(node -p 'require("./package.json").name')"
          tag="v$(node -p 'require("./package.json").version')"
          if ! curl -sf "https://registry.npmjs.org/$name" >/dev/null; then
            echo "$name isn't on npm yet: publish its first version by hand"
          elif gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/$tag" >/dev/null 2>&1; then
            echo "$tag is already tagged"
          else
            echo "tag=$tag" >> "$GITHUB_OUTPUT"
          fi

  release:
    needs: version
    if: needs.version.outputs.tag != ''
    runs-on: ubuntu-latest
    env:
      TAG: ${{ needs.version.outputs.tag }}
    steps:
      - uses: actions/checkout@v7
      - uses: pnpm/action-setup@v6
      - uses: actions/setup-node@v7
        with:
          node-version-file: package.json
          cache: pnpm

      - run: pnpm install --frozen-lockfile

      # The same script CI and Cloudflare run. A package's own checks, like a size budget, go in
      # `verify`, and publint runs inside `vp pack`.
      - run: pnpm run verify

      # `pnpm pack` runs `prepack` first, so the tarball never ships a stale `dist/`.
      - run: pnpm pack

      # A version with a pre-release part (`-rc.0`) goes to the `next` dist-tag, so it never
      # becomes `latest`. A version already on npm is skipped, so a failed run can be rerun.
      - name: Stage on npm
        run: |
          name="$(node -p 'require("./package.json").name')"
          v="${TAG#v}"
          if curl -sf "https://registry.npmjs.org/$name/$v" >/dev/null; then
            echo "$name@$v is already on npm"
          else
            tag=latest; case "$v" in *-*) tag=next;; esac
            pnpm stage publish ./*.tgz --no-git-checks --provenance --access public --tag "$tag"
          fi

      # Creating the release tags the commit. The notes are generated from the pull requests'
      # labels (`.github/release.yml`). The description of the pull request that bumped the
      # version, down to the first `---` line, goes above them, like what to change in a breaking
      # release.
      - name: Tag and publish release
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          body="$(gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" --jq '.[0].body // ""')"
          notes="$(printf '%s\n' "$body" | sed '/^---[[:space:]]*$/,$d')"
          pre=""; case "$TAG" in *-*) pre=--prerelease;; esac
          gh release create "$TAG" ./*.tgz --title "$TAG" --target "$GITHUB_SHA" \
            $pre --generate-notes --notes "$notes"