fix(hooks): prevent circuit breaker false positives on deliberate rol… #176
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
| 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" |