name: Publish to npm on: push: tags: - "v*" pull_request: types: [closed] branches: [main] workflow_dispatch: inputs: version: description: "Version to publish (e.g. 0.4.11). Tag v must already exist." required: true type: string jobs: publish: name: Publish runs-on: ubuntu-latest timeout-minutes: 10 environment: npm-publish permissions: contents: write id-token: write # Run on tag push, manual dispatch, OR when a release/* PR is merged if: >- github.event_name == 'push' || github.event_name == 'workflow_dispatch' || (github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/v')) steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 # On manual dispatch, check out the existing tag so we publish the # exact commit that was tagged — not whatever is currently on main. ref: >- ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || github.ref }} - name: Resolve version id: version run: | if [ "${{ github.event_name }}" = "push" ]; then VERSION="${GITHUB_REF_NAME#v}" elif [ "${{ github.event_name }}" = "workflow_dispatch" ]; then VERSION="${{ inputs.version }}" VERSION="${VERSION#v}" else BRANCH="${{ github.event.pull_request.head.ref }}" VERSION="${BRANCH#release/v}" fi echo "version=${VERSION}" >> "$GITHUB_OUTPUT" # Detect pre-release tag (e.g. 0.1.16-alpha.1 → alpha, 0.1.16-beta.2 → beta) if [[ "$VERSION" =~ -([a-zA-Z]+) ]]; then DIST_TAG="${BASH_REMATCH[1]}" echo "prerelease=true" >> "$GITHUB_OUTPUT" else DIST_TAG="latest" echo "prerelease=false" >> "$GITHUB_OUTPUT" fi echo "dist_tag=${DIST_TAG}" >> "$GITHUB_OUTPUT" echo "Resolved version=${VERSION} dist_tag=${DIST_TAG}" - name: Validate release channel env: VERSION: ${{ steps.version.outputs.version }} DIST_TAG: ${{ steps.version.outputs.dist_tag }} EVENT_NAME: ${{ github.event_name }} PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} run: node scripts/validate-release-channel.mjs - name: Create release tag if: github.event_name == 'pull_request' run: | git tag "v${{ steps.version.outputs.version }}" git push origin "v${{ steps.version.outputs.version }}" - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: 24 registry-url: "https://registry.npmjs.org" - run: corepack enable - run: corepack prepare pnpm@10.17.1 --activate - run: bun install --frozen-lockfile - run: bun run build - run: bun run verify:packed-manifests - name: Publish packages env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: | VERSION="${{ steps.version.outputs.version }}" DIST_TAG="${{ steps.version.outputs.dist_tag }}" FAILED=0 publish_pkg() { local filter="$1" local name="$2" # Check if this version is already published if npm view "${name}@${VERSION}" version >/dev/null 2>&1; then echo "⏭️ ${name}@${VERSION} already published — skipping" return 0 fi echo "📦 Publishing ${name}@${VERSION}..." if pnpm --filter "$filter" publish --access public --no-git-checks --tag "$DIST_TAG"; then echo "✅ ${name}@${VERSION} published" else echo "❌ ${name}@${VERSION} failed to publish" FAILED=1 fi } publish_pkg "@hyperframes/core" "@hyperframes/core" publish_pkg "@hyperframes/engine" "@hyperframes/engine" publish_pkg "@hyperframes/player" "@hyperframes/player" publish_pkg "@hyperframes/producer" "@hyperframes/producer" publish_pkg "@hyperframes/shader-transitions" "@hyperframes/shader-transitions" publish_pkg "@hyperframes/studio" "@hyperframes/studio" # CLI is @hyperframes/cli in the monorepo but published as unscoped "hyperframes" on npm. # Rewrite the name in package.json before publishing, then use npm publish directly # since pnpm --filter won't match the rewritten name. if npm view "hyperframes@${VERSION}" version >/dev/null 2>&1; then echo "⏭️ hyperframes@${VERSION} already published — skipping" else node -e " const fs = require('fs'); const p = 'packages/cli/package.json'; const pkg = JSON.parse(fs.readFileSync(p, 'utf8')); pkg.name = 'hyperframes'; fs.writeFileSync(p, JSON.stringify(pkg, null, 2) + '\n'); " echo "📦 Publishing hyperframes@${VERSION}..." if (cd packages/cli && npm publish --access public --tag "$DIST_TAG"); then echo "✅ hyperframes@${VERSION} published" else echo "❌ hyperframes@${VERSION} failed to publish" FAILED=1 fi fi if [ "$FAILED" -ne 0 ]; then echo "::error::One or more packages failed to publish" exit 1 fi - name: Create GitHub Release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | VERSION="${{ steps.version.outputs.version }}" PRERELEASE="${{ steps.version.outputs.prerelease }}" # Skip if release already exists (idempotent re-runs) if gh release view "v${VERSION}" --repo "${{ github.repository }}" >/dev/null 2>&1; then echo "Release v${VERSION} already exists — skipping" else FLAGS=(--repo "${{ github.repository }}" --title "v${VERSION}" --generate-notes) if [ "$PRERELEASE" = "true" ]; then FLAGS+=(--prerelease) fi gh release create "v${VERSION}" "${FLAGS[@]}" fi