Skip to content

ci(release): add main/tag guard for non-dry publish; unify publish step #5

ci(release): add main/tag guard for non-dry publish; unify publish step

ci(release): add main/tag guard for non-dry publish; unify publish step #5

Workflow file for this run

# 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')"