name: Publish to npm on: push: tags: - 'v*' # No workflow-level permissions — scoped per job below. # Concurrency convention: see CONTRIBUTING.md → "GitHub Actions — Concurrency Convention". # Tag refs are unique per release, so distinct tags run in parallel. Re-pushes of the # same tag serialize. cancel-in-progress: false — never cancel a publish mid-flight. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: false jobs: ci: uses: ./.github/workflows/ci.yml permissions: contents: read actions: read # No pull-requests:write — `ci.yml`'s save-pr-meta job is gated on # `github.event_name == 'pull_request'`, so it never runs during a # tag-triggered publish. Least-privilege for release-critical paths. publish: needs: ci runs-on: ubuntu-latest timeout-minutes: 15 permissions: contents: write id-token: write steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: 20 registry-url: https://registry.npmjs.org # Hermetic install for the published artifact — no cache carry-over # from non-tag contexts. setup-node v5+ caches by default when a # packageManager field is present in package.json, so the explicit # opt-out is required to clear the zizmor cache-poisoning audit. # ~30s slower per release; runs rarely. package-manager-cache: false - name: Build gitnexus-shared run: npm install && npm run build working-directory: gitnexus-shared - run: npm ci working-directory: gitnexus - name: Verify version consistency shell: bash run: | TAG_VERSION="${GITHUB_REF#refs/tags/v}" if ! [[ "$TAG_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$ ]]; then echo "::error::Tag does not follow semver: v$TAG_VERSION" exit 1 fi PKG_VERSION=$(node -p "require('./package.json').version") if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then echo "::error::Tag version (v$TAG_VERSION) does not match package.json version ($PKG_VERSION)" exit 1 fi echo "Version verified: $PKG_VERSION" working-directory: gitnexus - name: Build run: npm run build working-directory: gitnexus - name: Dry-run publish run: npm publish --dry-run working-directory: gitnexus - name: Publish to npm run: npm publish --provenance --access public working-directory: gitnexus env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Extract release notes from CHANGELOG id: changelog shell: bash run: | VERSION="${GITHUB_REF#refs/tags/v}" NOTES=$(awk "/^## \\[$VERSION\\]/{found=1; next} /^## \\[/{if(found) exit} found" gitnexus/CHANGELOG.md) if [ -z "$NOTES" ]; then echo "::warning::No CHANGELOG entry found for v$VERSION, falling back to auto-generated notes" echo "fallback=true" >> "$GITHUB_OUTPUT" else echo "$NOTES" > /tmp/release-notes.md echo "fallback=false" >> "$GITHUB_OUTPUT" fi - name: Create GitHub Release uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v2 with: body_path: ${{ steps.changelog.outputs.fallback == 'false' && '/tmp/release-notes.md' || '' }} generate_release_notes: ${{ steps.changelog.outputs.fallback == 'true' }}