name: Publish (staged)

on:
  release:
    types: [published]        # cutting a Release creates the tag AND fires this

permissions:
  contents: read              # workflow default (least privilege); only stage-publish also needs id-token, granted on that job

concurrency:
  group: publish-${{ github.workflow }}   # serialize publishes; no dist-tag races
  cancel-in-progress: false               # queue, don't kill an in-flight publish

jobs:
  verify:
    runs-on: ubuntu-latest
    outputs:
      sha: ${{ steps.resolve.outputs.sha }}   # ancestry-checked commit, pinned for downstream
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0   # v7.0.0
        with:
          persist-credentials: false
          fetch-depth: 0                   # full history for the ancestry check below
      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version-file: '.nvmrc'      # pin >= 22.14.0
          package-manager-cache: false     # release-triggered: disable auto-cache (zizmor cache-poisoning)
      - name: Assert Release tag matches package.json version
        env:
          RELEASE_TAG: ${{ github.event.release.tag_name }}
        run: |
          PKG="$(node -p "require('./package.json').version")"
          [ "${RELEASE_TAG#v}" = "$PKG" ] || { echo "tag $RELEASE_TAG != package.json v$PKG"; exit 1; }
      - name: Refuse releases not on the default branch
        id: resolve
        env:
          RELEASE_TAG: ${{ github.event.release.tag_name }}
          DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
        run: |
          git merge-base --is-ancestor "$GITHUB_SHA" "origin/$DEFAULT_BRANCH" \
            || { echo "release $RELEASE_TAG not reachable from $DEFAULT_BRANCH — refusing"; exit 1; }
          echo "sha=$GITHUB_SHA" >> "$GITHUB_OUTPUT"   # downstream checks out this exact SHA, not the mutable tag

  stage-publish:
    needs: verify
    runs-on: ubuntu-latest
    timeout-minutes: 15         # cap a hung publish
    permissions:
      contents: read
      id-token: write           # OIDC trusted publishing: only this job mints the token
    steps:
      - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0   # v7.0.0
        with:
          persist-credentials: false
          ref: ${{ needs.verify.outputs.sha }}   # the ancestry-checked SHA, immune to tag re-pointing (TOCTOU)
      - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
        with:
          node-version-file: '.nvmrc'
          registry-url: 'https://registry.npmjs.org'
          package-manager-cache: false
      - run: npm ci
      - run: npm run build              # mandatory build; no --if-present so a missing script fails loudly
      - run: npm install -g npm@11.15.0    # npm CLI: staged publishing needs npm >= 11.15.0
      - name: Resolve dist-tag (a prerelease must never go to `latest`)
        id: disttag
        env:
          PRERELEASE_TAG: next
        run: |
          VERSION="$(node -p "require('./package.json').version")"
          case "$VERSION" in
            *-*) TAG="$PRERELEASE_TAG" ;;
            *)   TAG="latest" ;;
          esac
          echo "tag=$TAG" >> "$GITHUB_OUTPUT"
      - name: Stage publish
        env:
          DIST_TAG: ${{ steps.disttag.outputs.tag }}
        run: npm stage publish --tag "$DIST_TAG"
