Skip to content

fix(hooks): prevent circuit breaker false positives on deliberate rol… #176

fix(hooks): prevent circuit breaker false positives on deliberate rol…

fix(hooks): prevent circuit breaker false positives on deliberate rol… #176

Workflow file for this run

name: Release & Publish
# Fully automated: merge to main = release.
# Tag trigger removed — the `tag` job below creates the git tag,
# which was re-triggering this workflow and racing the npm publish.
on:
push:
branches: [main]
jobs:
# ── Gate: skip if this version is already published ──
check:
name: Check if release needed
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
version: ${{ steps.check.outputs.version }}
tag_name: ${{ steps.check.outputs.tag_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Determine release status
id: check
run: |
VERSION=$(node -p "require('./package.json').version")
TAG_NAME="v${VERSION}"
# Skip prerelease versions (staging versions on main)
if echo "$VERSION" | grep -qE '\-'; then
echo "Skipping: prerelease version $VERSION"
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
exit 0
fi
# Block release if CHANGELOG entry is missing
if ! grep -qE "^## \[${VERSION}\]" CHANGELOG.md; then
echo "ERROR: No CHANGELOG entry found for version ${VERSION}"
echo "Add a '## [${VERSION}]' section to CHANGELOG.md before releasing"
exit 1
fi
# Check if already published on npm
NPM_VERSION=$(npm view "@nforma.ai/nforma@${VERSION}" version 2>/dev/null || echo "")
if [[ -n "$NPM_VERSION" ]]; then
echo "Skipping: version $VERSION already on npm"
echo "should_release=false" >> "$GITHUB_OUTPUT"
else
echo "Releasing version $VERSION"
echo "should_release=true" >> "$GITHUB_OUTPUT"
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
# ── Tests ──
test:
name: Pre-release tests
needs: check
if: needs.check.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: npm ci --ignore-scripts || npm install --ignore-scripts
- name: Build artifacts
run: npm run build:hooks && npm run build:machines
- name: Check assets not stale
run: npm run check:assets
- name: Run CI tests
run: npm run test:ci
- name: Run TUI unit tests
run: npm run test:tui
- name: Run install + TUI smoke tests
run: npm run test:install
- name: Verify package contents
run: |
echo "=== Package dry-run ==="
npm pack --dry-run 2>&1 | grep -E 'total files|package size|📦'
echo ""
echo "=== Checking for test files ==="
count=$(npm pack --dry-run 2>&1 | grep -c '\.test\.' || true)
if [ "$count" -gt 0 ]; then
echo "ERROR: $count test files found in package"
exit 1
fi
echo "OK: 0 test files in package"
# ── Create git tag ──
tag:
name: Create git tag
needs: [check, test]
if: needs.check.outputs.should_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create and push tag
env:
TAG_NAME: ${{ needs.check.outputs.tag_name }}
VERSION: ${{ needs.check.outputs.version }}
run: |
if git tag -l "$TAG_NAME" | grep -q "$TAG_NAME"; then
echo "Tag $TAG_NAME already exists — skipping"
exit 0
fi
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"
# ── GitHub Release ──
release:
name: Create GitHub Release
needs: [check, test, tag]
if: |
always() &&
needs.check.outputs.should_release == 'true' &&
needs.test.result == 'success' &&
(needs.tag.result == 'success' || needs.tag.result == 'skipped')
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG_NAME: ${{ needs.check.outputs.tag_name }}
run: |
# Check if release already exists
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "Release $TAG_NAME already exists — skipping"
exit 0
fi
gh release create "$TAG_NAME" \
--title "$TAG_NAME" \
--generate-notes
# ── Publish to npm ──
publish:
name: Publish to npm
needs: [check, test]
if: needs.check.outputs.should_release == 'true'
runs-on: ubuntu-latest
environment: npm-publish
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
scope: '@nforma.ai'
- name: Update npm
run: npm install -g npm@latest
- name: Install dependencies
run: npm ci || npm install
- name: Build hooks
run: npm run build:hooks
- name: Publish to npm
run: npm publish --access public --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Align next dist-tag with latest
run: |
# Invariant: next must never fall behind latest.
# After publishing a stable version to @latest, point @next to the same version.
npm dist-tag add "@nforma.ai/nforma@${{ needs.check.outputs.version }}" next
echo "Aligned @next → ${{ needs.check.outputs.version }}"
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Verify dist-tags
run: |
echo "=== npm dist-tags ==="
npm dist-tag ls @nforma.ai/nforma
echo ""
# Verify invariant: next >= latest
LATEST=$(npm view @nforma.ai/nforma dist-tags.latest)
NEXT=$(npm view @nforma.ai/nforma dist-tags.next)
echo "latest=$LATEST next=$NEXT"
if [ "$NEXT" != "${{ needs.check.outputs.version }}" ]; then
echo "WARNING: next ($NEXT) != published version (${{ needs.check.outputs.version }})"
fi
- name: Print result
env:
VERSION: ${{ needs.check.outputs.version }}
run: |
echo ""
echo "=== Published @nforma.ai/nforma@${VERSION} ==="
echo " npx @nforma.ai/nforma@latest"