Skip to content

chore(release): 0.44.2 (#376) #26

chore(release): 0.44.2 (#376)

chore(release): 0.44.2 (#376) #26

Workflow file for this run

name: Publish
# SINGLE publishing workflow — the npm OIDC trusted-publisher target.
#
# npm allows only ONE trusted publisher per package (exact workflow-filename match),
# so the single `@latest` channel lives here:
# • Stable → @latest : push to main whose package.json version is a non-prerelease string
# (`@next` mirrors `@latest`; see CLAUDE.md invariant — there is no separate prerelease channel)
#
# Auth is GitHub OIDC — there is NO NPM_TOKEN. `id-token: write` on the publish job lets
# the npm CLI mint a short-lived, workflow-scoped token and emit provenance automatically.
# OIDC trusted publishing requires Node >= 22.14.0 and npm >= 11.5.1 (npm i -g npm@latest).
#
# npm trusted-publisher config to match this file:
# Publisher = GitHub Actions | Org = nForma-AI | Repo = nForma
# Workflow filename = publish.yml | Environment = npm-publish | Allowed = npm publish
on:
push:
branches: [main]
jobs:
# ── Decide what (if anything) to publish ──
context:
name: Determine publish context
runs-on: ubuntu-latest
outputs:
mode: ${{ steps.ctx.outputs.mode }} # stable | none
version: ${{ steps.ctx.outputs.version }}
dist_tag: ${{ steps.ctx.outputs.dist_tag }} # latest
tag_name: ${{ steps.ctx.outputs.tag_name }}
should_publish: ${{ steps.ctx.outputs.should_publish }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: '22'
- id: ctx
run: |
set -e
# ── Stable path (main push) → @latest ──
VERSION=$(node -p "require('./package.json').version")
TAG_NAME="v${VERSION}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "dist_tag=latest" >> "$GITHUB_OUTPUT"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
# A prerelease version (e.g. 0.44.2-rc.1) cannot ship under the alias policy
# (@next == @latest — there is no separate prerelease channel). Refuse it.
if echo "$VERSION" | grep -qE '\-'; then
echo "ERROR: prerelease version $VERSION is not publishable under the @next=@latest alias policy."
echo "Bump to a stable semver (no -rc.N suffix) and re-push."
exit 1
fi
# Fixed-string match (grep -F) so dots in the version aren't regex wildcards.
if ! grep -qF "## [${VERSION}]" CHANGELOG.md; then
echo "ERROR: No CHANGELOG entry for ${VERSION}"; exit 1
fi
NPM_VERSION=$(npm view "@nforma.ai/nforma@${VERSION}" version 2>/dev/null || echo "")
if [[ -n "$NPM_VERSION" ]]; then
echo "Skipping: $VERSION already on npm"
echo "mode=none" >> "$GITHUB_OUTPUT"; echo "should_publish=false" >> "$GITHUB_OUTPUT"
else
echo "mode=stable" >> "$GITHUB_OUTPUT"; echo "should_publish=true" >> "$GITHUB_OUTPUT"
fi
# ── Pre-publish tests ──
test:
name: Pre-publish tests
needs: context
if: needs.context.outputs.should_publish == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci --ignore-scripts || npm install --ignore-scripts
- run: npm run build:hooks && npm run build:machines
- run: npm run check:assets
- run: npm run lint:isolation
- run: npm run test:ci
- run: npm run test:tui
- run: npm run test:install # install/virgin smoke gate (parity with the old stable release job)
- name: Verify no test files in package
run: |
count=$(npm pack --dry-run 2>&1 | grep -c '\.test\.' || true)
if [ "$count" -gt 0 ]; then echo "ERROR: $count test files in package"; exit 1; fi
echo "OK: 0 test files in package"
# ── Publish via OIDC — NO NPM_TOKEN ──
publish:
name: Publish to npm (${{ needs.context.outputs.dist_tag }})
needs: [context, test]
if: needs.context.outputs.should_publish == 'true'
runs-on: ubuntu-latest
environment: npm-publish
timeout-minutes: 10
permissions:
contents: read
id-token: write # required for OIDC trusted publishing
steps:
- uses: actions/checkout@v4
# NB: intentionally NO registry-url/scope here. setup-node's registry-url writes an
# .npmrc with `always-auth=true` + a placeholder `_authToken`, which forces npm down
# the (dummy) TOKEN path and it never attempts OIDC → E404. The registry + public
# access come from package.json `publishConfig` instead, leaving OIDC to authenticate.
- uses: actions/setup-node@v4
with:
node-version: '22' # setup-node resolves to the latest 22.x, always >= 22.14.0 (the OIDC trusted-publishing minimum)
- name: Update npm (>= 11.5.1 for trusted publishing)
# ^11.5.1 = latest 11.x AND guaranteed >= 11.5.1 (the trusted-publishing
# minimum), with no unattended jump to a new npm major.
run: npm install -g "npm@^11.5.1"
- run: npm ci || npm install
- run: npm run build:hooks
# No NODE_AUTH_TOKEN and no --provenance: OIDC is auto-detected and provenance
# is attached automatically when publishing via a trusted publisher.
- name: Publish via OIDC trusted publisher
run: npm publish --access public --tag "${{ needs.context.outputs.dist_tag }}"
- name: Align @next with @latest
# Best-effort: OIDC trusted publishing authorizes `npm publish`, not necessarily
# `npm dist-tag add`. If the minted token can't move a dist-tag, do NOT fail the
# release (the publish already succeeded) — warn so it can be aligned manually
# (`npm dist-tag add @nforma.ai/nforma@<version> next`). `@next` and `@latest`
# must show the same version after every release (see CLAUDE.md invariant).
run: |
if npm dist-tag add "@nforma.ai/nforma@${{ needs.context.outputs.version }}" next; then
echo "Aligned @next → ${{ needs.context.outputs.version }}"
else
echo "::warning::Could not align @next via OIDC (dist-tag may not be authorized by trusted publishing). Run: npm dist-tag add @nforma.ai/nforma@${{ needs.context.outputs.version }} next"
fi
- name: Show dist-tags
run: npm dist-tag ls @nforma.ai/nforma || true
# ── Git tag + GitHub Release (after a successful publish) ──
# Recovery note: if publish succeeds but this job fails transiently, a re-run finds
# the version already on npm → mode=none → this job is skipped and won't re-create
# the tag/release. Recover manually (both steps below are idempotent):
# git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin vX.Y.Z
# gh release create vX.Y.Z --title vX.Y.Z --generate-notes
release:
name: Tag + GitHub Release
needs: [context, test, publish]
if: needs.context.outputs.should_publish == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create and push tag
env:
TAG_NAME: ${{ needs.context.outputs.tag_name }}
VERSION: ${{ needs.context.outputs.version }}
run: |
# Exact match — `grep -q` would let v0.41.1 match an existing v0.41.10.
if git rev-parse -q --verify "refs/tags/$TAG_NAME" >/dev/null; then
echo "Tag $TAG_NAME already exists — skipping"
else
git config user.email "github-actions[bot]@users.noreply.github.com"
git config user.name "github-actions[bot]"
git tag -a "$TAG_NAME" -m "Release ${VERSION}"
git push origin "$TAG_NAME"
fi
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.context.outputs.tag_name }}
run: |
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "Release $TAG_NAME already exists — skipping"
else
gh release create "$TAG_NAME" --title "$TAG_NAME" --generate-notes
fi