Publish to NPM #2
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: Publish to NPM | |
| # This workflow handles all publishing to npm for releases and pre-release nightly | |
| # builds. We must use a single file rather than separate workflow files because | |
| # Trusted Publishing currently supports only a single workflow file. | |
| on: | |
| release: | |
| types: [published] | |
| schedule: | |
| - cron: '0 2 * * *' # Nightly at 02:00 UTC | |
| workflow_dispatch: | |
| inputs: | |
| publish_type: | |
| description: 'Type of publish' | |
| required: true | |
| type: choice | |
| options: | |
| - release | |
| - master-nightly | |
| - minor-nightly | |
| jobs: | |
| # Determine which publish type(s) to run based on trigger | |
| setup: | |
| # Restrict who can trigger a publish to the listed maintainers. Both | |
| # `workflow_dispatch` (manual) and `release` (a published GitHub Release) | |
| # are user-initiated, so both are gated on `github.actor`; a publish | |
| # triggered by anyone else skips this job, and `publish` (needs: setup) | |
| # is skipped with it. `schedule` (nightly cron) is not user-initiated and | |
| # is always allowed. | |
| # This is the UX/first-line guard; actual enforcement against branch-based | |
| # bypass is the `npm-publish` environment + npm trust `--env` binding below, | |
| # and creation of the `v*` release tags is restricted by a tag ruleset. | |
| if: >- | |
| github.event_name == 'schedule' || | |
| ((github.event_name == 'release' || github.event_name == 'workflow_dispatch') && | |
| (github.actor == 'michaelbromley' || github.actor == 'dlhck')) | |
| runs-on: ubuntu-latest | |
| outputs: | |
| matrix: ${{ steps.set-matrix.outputs.matrix }} | |
| steps: | |
| - name: Determine publish matrix | |
| id: set-matrix | |
| run: | | |
| if [ "${{ github.event_name }}" == "release" ]; then | |
| echo 'matrix=["release"]' >> $GITHUB_OUTPUT | |
| elif [ "${{ github.event_name }}" == "schedule" ]; then | |
| echo 'matrix=["master-nightly", "minor-nightly"]' >> $GITHUB_OUTPUT | |
| else | |
| echo 'matrix=["${{ inputs.publish_type }}"]' >> $GITHUB_OUTPUT | |
| fi | |
| publish: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| # The OIDC token minted for this job carries an `environment: npm-publish` | |
| # claim, which the npm trusted publishers require (`npm trust --env`). The | |
| # environment's deployment-branch policy (master, minor, v* tags) is | |
| # enforced by GitHub before the job runs, so a publish cannot originate | |
| # from an arbitrary branch — closing the workflow_dispatch branch-bypass. | |
| # The environment has NO required reviewers, so nightlies stay unattended. | |
| environment: npm-publish | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| # Both nightly jobs publish new versions of the same packages. npm rejects | |
| # a publish with E409 while it is still processing a previous publish of | |
| # the same package, so the jobs must not run concurrently. | |
| max-parallel: 1 | |
| matrix: | |
| type: ${{ fromJson(needs.setup.outputs.matrix) }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 | |
| with: | |
| ref: ${{ matrix.type == 'minor-nightly' && 'minor' || matrix.type == 'master-nightly' && 'master' || '' }} | |
| fetch-depth: ${{ matrix.type == 'release' && 1 || 0 }} | |
| # For nightly builds: check if there were commits in the last 24 hours | |
| - name: Check for new commits | |
| id: commit_check | |
| if: matrix.type != 'release' | |
| run: | | |
| COMMITS=$(git rev-list --count --since="24 hours" HEAD) | |
| echo "Commits found: $COMMITS" | |
| if [ "$COMMITS" -eq 0 ]; then | |
| echo "should_publish=false" >> $GITHUB_OUTPUT | |
| else | |
| echo "should_publish=true" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Setup Node.js | |
| if: matrix.type == 'release' || steps.commit_check.outputs.should_publish == 'true' | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 | |
| with: | |
| node-version: '24.x' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Update npm | |
| if: matrix.type == 'release' || steps.commit_check.outputs.should_publish == 'true' | |
| run: npm install -g npm@latest | |
| - name: Setup Bun | |
| if: matrix.type == 'release' || steps.commit_check.outputs.should_publish == 'true' | |
| uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 | |
| with: | |
| bun-version: '1.3.10' | |
| - name: Install dependencies | |
| if: matrix.type == 'release' || steps.commit_check.outputs.should_publish == 'true' | |
| run: bun install --frozen-lockfile | |
| - name: Build | |
| if: matrix.type == 'release' || steps.commit_check.outputs.should_publish == 'true' | |
| run: bun run build | |
| # Nightly builds: configure git and bump version | |
| - name: Configure Git | |
| if: matrix.type != 'release' && steps.commit_check.outputs.should_publish == 'true' | |
| run: | | |
| git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git config --global user.name "github-actions[bot]" | |
| - name: Get current date | |
| if: matrix.type != 'release' && steps.commit_check.outputs.should_publish == 'true' | |
| id: date | |
| run: echo "date=$(date +'%Y%m%d%H%M')" >> $GITHUB_OUTPUT | |
| - name: Version packages (master-nightly) | |
| if: matrix.type == 'master-nightly' && steps.commit_check.outputs.should_publish == 'true' | |
| run: | | |
| CURRENT_VERSION=$(node -p "require('./lerna.json').version") | |
| IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" | |
| PATCH=$((VERSION_PARTS[2] + 1)) | |
| NEW_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${PATCH}-master-${{ steps.date.outputs.date }}" | |
| bunx lerna version $NEW_VERSION --no-git-tag-version --yes --no-push --force-publish | |
| - name: Version packages (minor-nightly) | |
| if: matrix.type == 'minor-nightly' && steps.commit_check.outputs.should_publish == 'true' | |
| run: | | |
| CURRENT_VERSION=$(node -p "require('./lerna.json').version") | |
| IFS='.' read -r -a VERSION_PARTS <<< "$CURRENT_VERSION" | |
| MINOR=$((VERSION_PARTS[1] + 1)) | |
| NEW_VERSION="${VERSION_PARTS[0]}.${MINOR}.0-minor-${{ steps.date.outputs.date }}" | |
| bunx lerna version $NEW_VERSION --no-git-tag-version --yes --no-push --force-publish | |
| - name: Commit version changes | |
| if: matrix.type != 'release' && steps.commit_check.outputs.should_publish == 'true' | |
| run: | | |
| git add . | |
| git commit -m "chore: Bump version for ${{ matrix.type }} pre-release" | |
| # Publish to npm with appropriate dist-tag. | |
| # Note: we do not use `lerna publish` because at the time of writing, it fails to publish | |
| # using the Trusted Publishing workflow for some reason. | |
| # We use a custom script to handle publishConfig.directory for packages like @vendure/admin-ui | |
| # that need to publish from a nested directory. | |
| # | |
| # Release publishes use `npm stage publish` — the version lands in npm's staging area and | |
| # requires manual maintainer approval (with 2FA) before going live on the public registry. | |
| # Approve all staged packages at once with: scripts/npm-stage-approve-all.sh <version> | |
| # Nightly builds still use direct `npm publish` because they target non-default dist-tags | |
| # (`master` / `minor`) that nobody installs unless they opt in explicitly. | |
| # | |
| # No --tag here is intentional: releases go to the default `latest` tag. | |
| - name: Publish to NPM (release) | |
| if: matrix.type == 'release' | |
| run: | | |
| bunx lerna exec --no-private -- 'publish_dir=$(node -p "require(\"./package.json\").publishConfig?.directory || \".\"") && cd "$publish_dir" && npm stage publish --access public' | |
| # --no-bail: a failure in one package must not skip publishing the rest — | |
| # downstream consumers expect ALL packages to exist at a given nightly | |
| # version. The retry loop handles transient registry errors (e.g. E409 | |
| # while npm is still processing an earlier publish of the same package). | |
| - name: Publish to NPM (master-nightly) | |
| if: matrix.type == 'master-nightly' && steps.commit_check.outputs.should_publish == 'true' | |
| run: | | |
| bunx lerna exec --no-private --no-bail -- 'publish_dir=$(node -p "require(\"./package.json\").publishConfig?.directory || \".\"") && cd "$publish_dir" && for i in 1 2 3; do npm publish --access public --tag master && break; if [ "$i" = 3 ]; then exit 1; fi; echo "npm publish failed, retrying in 30s..."; sleep 30; done' | |
| - name: Publish to NPM (minor-nightly) | |
| if: matrix.type == 'minor-nightly' && steps.commit_check.outputs.should_publish == 'true' | |
| run: | | |
| bunx lerna exec --no-private --no-bail -- 'publish_dir=$(node -p "require(\"./package.json\").publishConfig?.directory || \".\"") && cd "$publish_dir" && for i in 1 2 3; do npm publish --access public --tag minor && break; if [ "$i" = 3 ]; then exit 1; fi; echo "npm publish failed, retrying in 30s..."; sleep 30; done' | |
| - name: Skip publish (no new commits) | |
| if: matrix.type != 'release' && steps.commit_check.outputs.should_publish == 'false' | |
| run: echo "No new commits in last 24 hours – skipping ${{ matrix.type }} publish." |