ci(release): add main/tag guard for non-dry publish; unify publish step #5
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Release workflow for @loop-engine/* OSS packages | |
| # | |
| # Normal publish trigger: | |
| # git tag v0.2.0 && git push --tags | |
| # → workflow fires automatically, no manual dispatch needed | |
| # | |
| # Pipeline test (dry run): | |
| # GitHub → Actions → RC tag release → Run workflow | |
| # → leave dry_run CHECKED → run | |
| # → exercises install, build, validate:publish, and pnpm publish --dry-run | |
| # | |
| # Manual dispatch with dry_run UNCHECKED: | |
| # Only allowed from main or a version tag — workflow will hard-fail otherwise. | |
| # Use only as an emergency escape hatch, not as a routine publish path. | |
| # | |
| # Tag pushes: inputs.dry_run is undefined; guard step uses if: ${{ !inputs.dry_run }} so it still | |
| # runs, and startsWith(github.ref, 'refs/tags/') passes immediately — do not change that behavior. | |
| # | |
| # Token rotation: | |
| # NPM_TOKEN secret expires July 15, 2026. | |
| # Rotate at: npmjs.com → Account Settings → Access Tokens → Automation token | |
| # Update at: GitHub → loopengine/loop-engine → Settings → Secrets → NPM_TOKEN | |
| # Set calendar reminder for July 1, 2026. | |
| # | |
| # Auth: setup-node with registry-url configures ~/.npmrc; the env var MUST be NODE_AUTH_TOKEN | |
| # (map the GitHub Actions repository secret named NPM_TOKEN to NODE_AUTH_TOKEN below). | |
| name: RC tag release | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: "Dry run only — does not publish to npm. Uncheck only from main with a version tag pushed." | |
| type: boolean | |
| default: true | |
| required: true | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true | |
| jobs: | |
| publish: | |
| name: Publish to npm | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| id-token: write # npm provenance (links tarball to this workflow run) | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9.0.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| registry-url: "https://registry.npmjs.org" | |
| cache: pnpm | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build all packages | |
| run: pnpm build | |
| - name: Validate no workspace:* in publish targets | |
| run: pnpm validate:publish | |
| - name: Enforce main or tag for real publish | |
| if: ${{ !inputs.dry_run }} | |
| run: | | |
| IS_TAG=${{ startsWith(github.ref, 'refs/tags/') }} | |
| IS_MAIN=${{ github.ref_name == 'main' }} | |
| if [ "$IS_TAG" != "true" ] && [ "$IS_MAIN" != "true" ]; then | |
| echo "❌ Real publish is only allowed from main or a version tag." | |
| echo " Current ref: ${{ github.ref }}" | |
| echo " To test the pipeline safely, re-run with dry_run checked." | |
| exit 1 | |
| fi | |
| echo "✅ Ref check passed: ${{ github.ref }}" | |
| - name: Publish to npm | |
| run: | | |
| FLAGS="--access public --no-git-checks" | |
| if [ "${{ inputs.dry_run }}" = "true" ]; then | |
| FLAGS="$FLAGS --dry-run" | |
| echo "ℹ️ Dry run mode — no packages will be published" | |
| else | |
| FLAGS="$FLAGS --provenance" | |
| fi | |
| pnpm publish -r $FLAGS | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| - name: Verify clean install | |
| if: github.event_name == 'push' | |
| run: | | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| mkdir -p /tmp/install-test && cd /tmp/install-test | |
| npm init -y | |
| npm install "@loop-engine/sdk@${VERSION}" | |
| node -e "const { createLoopSystem } = require('@loop-engine/sdk'); console.log('✅ @loop-engine/sdk installs cleanly')" |