name: Publish to npm permissions: {} 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 env: EVENT_NAME: ${{ github.event_name }} INPUT_VERSION: ${{ inputs.version }} PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} run: | if [ "$EVENT_NAME" = "push" ]; then VERSION="${GITHUB_REF_NAME#v}" elif [ "$EVENT_NAME" = "workflow_dispatch" ]; then VERSION="${INPUT_VERSION}" VERSION="${VERSION#v}" else BRANCH="${PR_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' env: VERSION: ${{ steps.version.outputs.version }} run: | git tag "v$VERSION" git push origin "v$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 }} VERSION: ${{ steps.version.outputs.version }} DIST_TAG: ${{ steps.version.outputs.dist_tag }} run: | 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/parsers" "@hyperframes/parsers" publish_pkg "@hyperframes/lint" "@hyperframes/lint" publish_pkg "@hyperframes/studio-server" "@hyperframes/studio-server" publish_pkg "@hyperframes/core" "@hyperframes/core" publish_pkg "@hyperframes/sdk" "@hyperframes/sdk" 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" publish_pkg "@hyperframes/aws-lambda" "@hyperframes/aws-lambda" publish_pkg "@hyperframes/gcp-cloud-run" "@hyperframes/gcp-cloud-run" # 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 }} VERSION: ${{ steps.version.outputs.version }} PRERELEASE: ${{ steps.version.outputs.prerelease }} REPO: ${{ github.repository }} run: | NOTES_FILE="releases/v${VERSION}.md" # Skip if release already exists (idempotent re-runs) if gh release view "v${VERSION}" --repo "$REPO" >/dev/null 2>&1; then echo "Release v${VERSION} already exists — skipping" else FLAGS=(--repo "$REPO" --title "v${VERSION}") if [ -f "$NOTES_FILE" ]; then FLAGS+=(--notes-file "$NOTES_FILE") else echo "No reviewed release notes found at $NOTES_FILE — using GitHub generated notes" FLAGS+=(--generate-notes) fi if [ "$PRERELEASE" = "true" ]; then FLAGS+=(--prerelease) fi gh release create "v${VERSION}" "${FLAGS[@]}" fi