name: Release npm # npm line. Tag namespace `npm-v` triggers this — plain `v` tags # are the CLI binary + Homebrew release (release.yml) and intentionally do NOT # trigger here (GitHub glob `npm-v*` does not match `v*`). Splitting npm onto its # own tag lets a prerelease ship to npm's `next` channel without touching the # stable Homebrew cask (and lets a stable cask ship without forcing npm to leave # its rc). Bump with: # git tag npm-vX.Y.Z # stable -> publishes under `latest` # git tag npm-vX.Y.Z-rc.N # prerelease -> publishes under `next` # git push origin # # A manual workflow_dispatch can publish either an opt-in canary from the current # ref or an approved stable/next version when tag creation is restricted. Canary # publishes to the `canary` dist-tag (npm i reasonix@canary) and never moves # `next`/`latest`. build.mjs decides the dist-tag: a `-canary.` build is `canary`, # any other prerelease is `next`, and a stable version is `latest` (#5822 — the # old "promote latest by hand" rule left it pinned at 0.53.2 and downgraded # `npm update -g` users). The verify step below asserts the tag actually landed. on: push: tags: ['npm-v*'] workflow_dispatch: inputs: channel: description: "Publish channel" required: true default: canary type: choice options: - canary - stable base_version: description: "Version to publish. Canary appends -canary.; stable publishes exactly this version." required: true type: string permissions: contents: read jobs: cache-guard: name: cache hit guard runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - run: ./scripts/cache-guard.sh npm: name: publish npm packages needs: cache-guard runs-on: ubuntu-latest # A tag push (npm-v*) or manual stable dispatch publishes a stable/next # release and goes through the `release` environment (esengine approves); # canary dispatches run free. environment: ${{ github.event_name == 'workflow_dispatch' && inputs.channel == 'canary' && 'canary' || 'release' }} steps: - uses: actions/checkout@v7 - uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true - uses: actions/setup-node@v6 with: node-version: '22' registry-url: 'https://registry.npmjs.org' # Tag push uses the npm-v* tag. A canary dispatch synthesizes a # -canary. version; a manual stable dispatch publishes # the requested semver exactly. build.mjs strips the leading `npm-`/`v`. - name: Resolve version id: ver run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then base="${{ inputs.base_version }}"; base="${base#v}" if [ "${{ inputs.channel }}" = "canary" ]; then echo "arg=v${base}-canary.${{ github.run_number }}" >> "$GITHUB_OUTPUT" else echo "arg=v${base}" >> "$GITHUB_OUTPUT" fi else echo "arg=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" fi - run: node npm/build.mjs "${{ steps.ver.outputs.arg }}" --publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} # Assert the dist-tag landed where build.mjs aimed it. This is the guard # that #5822 lacked: `latest` sat on 0.53.2 for months while stables went # to `next`, and nothing noticed until users got downgraded. The registry # applies tags asynchronously after publish, hence the poll. - name: Verify dist-tags run: | set -euo pipefail version="${{ steps.ver.outputs.arg }}" version="${version#npm-}"; version="${version#v}" case "$version" in *-canary.*) tag=canary ;; *-*) tag=next ;; *) tag=latest ;; esac for attempt in 1 2 3 4 5 6; do got="$(npm view reasonix dist-tags."$tag" 2>/dev/null || true)" if [ "$got" = "$version" ]; then echo "dist-tag $tag -> $got" exit 0 fi echo "dist-tag $tag -> ${got:-}, want $version (attempt $attempt)" sleep 10 done echo "::error::npm dist-tag '$tag' never landed on $version" exit 1