Release #4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Release | |
| # Triggered after CI completes on main. The workflow_run trigger guarantees | |
| # we only publish commits that already passed tests on main, while keeping | |
| # release as a separate workflow file so it can be wired to the npm trusted | |
| # publisher (Settings -> Packages -> Trusted Publishers): point npm at this | |
| # workflow filename (`release.yml`) and the `release` environment below. | |
| on: | |
| workflow_run: | |
| workflows: [CI] | |
| branches: [main] | |
| types: [completed] | |
| # Manual trigger - publish from a specific tag, branch or commit SHA. | |
| # Leave `ref` empty to publish from main HEAD. | |
| workflow_dispatch: | |
| inputs: | |
| ref: | |
| description: 'Tag, branch or commit SHA to publish (e.g. v0.1.0). Empty = main HEAD.' | |
| type: string | |
| required: false | |
| default: '' | |
| permissions: | |
| # Needed to push the version tag back to the repository. | |
| contents: write | |
| # Required for npm provenance + npm trusted-publisher OIDC exchange. | |
| id-token: write | |
| jobs: | |
| release: | |
| name: Publish to npm | |
| runs-on: ubuntu-latest | |
| # Skip when CI failed; workflow_dispatch has no workflow_run context, so | |
| # the second clause lets manual runs through. | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') | |
| environment: release | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| # Resolution order: | |
| # 1. workflow_dispatch input (manual republish at a specific ref) | |
| # 2. workflow_run head SHA (the commit that just passed CI) | |
| # 3. github.ref (defensive default; should not be hit) | |
| ref: ${{ inputs.ref != '' && inputs.ref || github.event.workflow_run.head_sha || github.ref }} | |
| # Full history so `git push origin <tag>` succeeds. | |
| fetch-depth: 0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '22' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Resolve package metadata | |
| id: meta | |
| run: | | |
| set -euo pipefail | |
| NAME=$(node -p "require('./package.json').name") | |
| VERSION=$(node -p "require('./package.json').version") | |
| TAG="v$VERSION" | |
| { | |
| echo "name=$NAME" | |
| echo "version=$VERSION" | |
| echo "tag=$TAG" | |
| } >>"$GITHUB_OUTPUT" | |
| - name: Skip if version already on npm | |
| id: check | |
| run: | | |
| set -euo pipefail | |
| if npm view "${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }}" version >/dev/null 2>&1; then | |
| echo "publish=false" >>"$GITHUB_OUTPUT" | |
| echo "::notice::${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }} is already published; skipping." | |
| else | |
| echo "publish=true" >>"$GITHUB_OUTPUT" | |
| echo "::notice::Will publish ${{ steps.meta.outputs.name }}@${{ steps.meta.outputs.version }}." | |
| fi | |
| # `npm publish` runs `prepublishOnly` (typecheck && test && build), so | |
| # we don't pre-build here. | |
| # | |
| # We publish via `npx -y npm@11.5.1` rather than the system npm: | |
| # Trusted Publishing requires npm >= 11.5.1, and Node 22 LTS still | |
| # bundles npm 10.x. | |
| # | |
| # Auth strategy: | |
| # - Preferred: npm Trusted Publishing (OIDC). No secret needed — but it | |
| # only works for a package that ALREADY exists on npm with this repo | |
| # registered as a trusted publisher. | |
| # - Bootstrap / fallback: NPM_TOKEN repo secret. npm only reads a token | |
| # from .npmrc (a NODE_AUTH_TOKEN env var alone is ignored), so we | |
| # write .npmrc explicitly — and only when the secret is set, because | |
| # an empty _authToken line would break the OIDC path. | |
| - name: Publish to npm | |
| if: steps.check.outputs.publish == 'true' | |
| run: | | |
| set -euo pipefail | |
| if [ -n "${NPM_TOKEN:-}" ]; then | |
| echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" >>"$HOME/.npmrc" | |
| echo "::notice::Publishing with the NPM_TOKEN secret (bootstrap/fallback)." | |
| else | |
| echo "::notice::No NPM_TOKEN secret; relying on npm Trusted Publishing (OIDC)." | |
| fi | |
| echo "Publishing with:" | |
| npx -y npm@11.5.1 --version | |
| npx -y npm@11.5.1 publish --provenance | |
| env: | |
| NPM_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| # `npm publish` (prepublishOnly) already built dist/. Package the npm | |
| # tarball as a release asset for offline installs. | |
| - name: Package release assets | |
| if: steps.check.outputs.publish == 'true' | |
| run: | | |
| set -euo pipefail | |
| mkdir -p release-assets | |
| npm pack --pack-destination release-assets >/dev/null | |
| ls -la release-assets | |
| - name: Create git tag and GitHub release | |
| if: steps.check.outputs.publish == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${{ steps.meta.outputs.tag }}" | |
| if gh release view "$TAG" >/dev/null 2>&1; then | |
| echo "::notice::GitHub release $TAG already exists; skipping tag/release creation." | |
| exit 0 | |
| fi | |
| git config user.name "Siarhei Dudko" | |
| git config user.email "siarhei@dudko.dev" | |
| if git rev-parse --verify "refs/tags/$TAG" >/dev/null 2>&1; then | |
| echo "::notice::Git tag $TAG already exists locally; reusing without re-tagging." | |
| else | |
| git tag -a "$TAG" -m "Release $TAG" | |
| git push origin "$TAG" | |
| fi | |
| gh release create "$TAG" \ | |
| --title "$TAG" \ | |
| --generate-notes \ | |
| --target "$(git rev-parse HEAD)" \ | |
| release-assets/* |