name: Version and Publish on: push: branches: - main pull_request: branches: - main types: [closed] jobs: version-and-publish: if: github.event_name == 'push' || (github.event.pull_request.merged == true && (contains(github.event.pull_request.head.ref, 'version/major') || contains(github.event.pull_request.head.ref, 'version/minor') || contains(github.event.pull_request.head.ref, 'version/patch'))) runs-on: ubuntu-latest outputs: new-version: ${{ steps.version-bump.outputs.new-version }} current-version: ${{ steps.version-bump.outputs.current-version }} version-type: ${{ steps.version-type.outputs.type }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' registry-url: 'https://registry.npmjs.org' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 8 - name: Get pnpm store directory shell: bash run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV - name: Setup pnpm cache uses: actions/cache@v4 with: path: ${{ env.STORE_PATH }} key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | ${{ runner.os }}-pnpm-store- - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run tests run: pnpm test - name: Run build run: pnpm build - name: Determine version bump type id: version-type run: | if [[ "${{ github.event.pull_request.head.ref }}" =~ version/major ]]; then echo "type=major" >> $GITHUB_OUTPUT elif [[ "${{ github.event.pull_request.head.ref }}" =~ version/minor ]]; then echo "type=minor" >> $GITHUB_OUTPUT elif [[ "${{ github.event.pull_request.head.ref }}" =~ version/patch ]]; then echo "type=patch" >> $GITHUB_OUTPUT elif [[ "${{ github.event_name }}" == "push" ]]; then # Default to patch for direct pushes to main echo "type=patch" >> $GITHUB_OUTPUT else echo "type=none" >> $GITHUB_OUTPUT fi - name: Configure git if: steps.version-type.outputs.type != 'none' run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Install Claude Code CLI if: steps.version-type.outputs.type != 'none' run: | curl -fsSL https://claude.ai/cli/install.sh | bash echo "$HOME/.claude/bin" >> $GITHUB_PATH - name: Generate changelog with Claude (with fallback) if: steps.version-type.outputs.type != 'none' id: changelog run: | # Get commits since last tag LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -z "$LAST_TAG" ]; then COMMITS=$(git log --oneline --no-merges --since="1 week ago" || echo "No recent commits") else COMMITS=$(git log --oneline --no-merges ${LAST_TAG}..HEAD || echo "No new commits") fi # Create fallback changelog from commits create_fallback_changelog() { echo "## Changes" echo "" if [ "$COMMITS" != "No recent commits" ] && [ "$COMMITS" != "No new commits" ]; then echo "### 📋 Updates" echo "$COMMITS" | sed 's/^[a-f0-9]* /- /' | head -10 else echo "### 📋 Updates" echo "- Version bump and maintenance updates" fi } # Generate changelog using Claude (fail if it fails) if ! command -v claude-code >/dev/null 2>&1; then echo "❌ ERROR: Claude CLI not found. Please ensure Claude Code is properly installed." exit 1 fi echo "🤖 Generating changelog with Claude..." CHANGELOG=$(timeout 60s claude-code << 'CLAUDE_EOF' Please analyze the following git commits and generate a concise changelog in this exact format: ## Changes ### 🚀 Features - Brief description of new features ### 🐛 Bug Fixes - Brief description of bug fixes ### 🔧 Improvements - Brief description of improvements/refactoring ### 📚 Documentation - Brief description of documentation changes ### ⚡ Performance - Brief description of performance improvements Only include sections that have actual changes. Keep each bullet point concise and user-focused. Git commits to analyze: $COMMITS CLAUDE_EOF ) # Check if changelog generation succeeded if [ $? -ne 0 ] || [ -z "$CHANGELOG" ]; then echo "❌ ERROR: Failed to generate changelog with Claude. Workflow cannot continue." echo "Debug info - Commits to analyze:" echo "$COMMITS" exit 1 fi echo "✅ Successfully generated changelog with Claude" # Save changelog to output echo "changelog<> $GITHUB_OUTPUT echo "$CHANGELOG" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create version bump script if: steps.version-type.outputs.type != 'none' run: | # Create Node.js script to safely bump version cat > bump-version.js << 'VERSION_SCRIPT_EOF' const fs = require('fs'); const path = require('path'); const versionType = process.argv[2]; const packagePath = path.join(process.cwd(), 'package.json'); try { const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); const currentVersion = packageJson.version; const [major, minor, patch] = currentVersion.split('.').map(Number); let newVersion; switch (versionType) { case 'major': newVersion = `${major + 1}.0.0`; break; case 'minor': newVersion = `${major}.${minor + 1}.0`; break; case 'patch': newVersion = `${major}.${minor}.${patch + 1}`; break; default: throw new Error(`Invalid version type: ${versionType}`); } packageJson.version = newVersion; fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n'); console.log(`${currentVersion}:${newVersion}`); } catch (error) { console.error('Error bumping version:', error.message); process.exit(1); } VERSION_SCRIPT_EOF - name: Create single atomic commit with all changes if: steps.version-type.outputs.type != 'none' id: version-bump run: | # Extract version type for commit title VERSION_TYPE="${{ steps.version-type.outputs.type }}" # Create clean commit message title if [ "$VERSION_TYPE" = "major" ]; then COMMIT_TITLE="🚀 Major Release" elif [ "$VERSION_TYPE" = "minor" ]; then COMMIT_TITLE="✨ Minor Release" else COMMIT_TITLE="🐛 Patch Release" fi # Get the safe base commit (last release tag or fallback) LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") if [ -n "$LAST_TAG" ]; then BASE_COMMIT=$(git rev-list -n 1 $LAST_TAG) else # Fallback: get commit from 1 week ago or first commit BASE_COMMIT=$(git rev-list --since="1 week ago" --reverse HEAD | head -1 || git rev-list --max-parents=0 HEAD) fi echo "Base commit for squash: $BASE_COMMIT" # Safely reset to base (soft reset preserves working directory) git reset --soft $BASE_COMMIT # Bump version using our safe Node.js script VERSION_OUTPUT=$(node bump-version.js $VERSION_TYPE) CURRENT_VERSION=$(echo $VERSION_OUTPUT | cut -d: -f1) NEW_VERSION=$(echo $VERSION_OUTPUT | cut -d: -f2) echo "current-version=$CURRENT_VERSION" >> $GITHUB_OUTPUT echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT # Synchronize pnpm lockfile if it exists if [ -f pnpm-lock.yaml ]; then echo "Synchronizing pnpm lockfile..." pnpm install --frozen-lockfile --ignore-scripts || echo "Lockfile sync completed with warnings" fi # Create single atomic commit with everything git add -A COMMIT_MSG="$COMMIT_TITLE ${{ steps.changelog.outputs.changelog }}" git commit -m "$COMMIT_MSG" || { echo "Commit failed, checking git status:" git status git diff --cached exit 1 } # Create git tag git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION ${{ steps.changelog.outputs.changelog }}" echo "Created single atomic commit with version $CURRENT_VERSION → $NEW_VERSION" # Clean up temporary files rm -f bump-version.js - name: Push version changes safely if: steps.version-type.outputs.type != 'none' run: | # Check if we need to force push (if git history was rewritten) CURRENT_REMOTE_HEAD=$(git ls-remote origin main | cut -f1) LOCAL_REMOTE_HEAD=$(git rev-parse origin/main) if [ "$CURRENT_REMOTE_HEAD" != "$LOCAL_REMOTE_HEAD" ]; then echo "⚠️ Remote main has been updated by another process" echo "Current remote HEAD: $CURRENT_REMOTE_HEAD" echo "Local remote HEAD: $LOCAL_REMOTE_HEAD" echo "❌ ABORTING: Cannot safely force push. Another workflow may be running." exit 1 fi # Safe force push with lease (only if remote hasn't changed) echo "🚀 Pushing squashed commit to main..." git push --force-with-lease origin main || { echo "❌ ERROR: Force push failed. This likely means:" echo "1. Another workflow pushed to main after this one started" echo "2. Branch protection rules are preventing the push" echo "3. Insufficient permissions" exit 1 } echo "🏷️ Pushing tags..." git push origin --tags - name: Publish to NPM if: steps.version-type.outputs.type != 'none' run: pnpm publish --access public --no-git-checks env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub Release if: steps.version-type.outputs.type != 'none' run: | gh release create "v${{ steps.version-bump.outputs.new-version }}" \ --title "Release v${{ steps.version-bump.outputs.new-version }}" \ --notes "# Release v${{ steps.version-bump.outputs.new-version }} ${{ steps.changelog.outputs.changelog }} --- **Version Info**: ${{ steps.version-type.outputs.type }} release (v${{ steps.version-bump.outputs.current-version }} → v${{ steps.version-bump.outputs.new-version }}) ### Installation \`\`\`bash npm install @xtr-dev/payload-mailing@${{ steps.version-bump.outputs.new-version }} \`\`\` ### Documentation See the [README](https://github.com/xtr-dev/payload-mailing#readme) for usage instructions and full documentation." env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} notify-success: if: github.event_name == 'push' || (github.event.pull_request.merged == true && (contains(github.event.pull_request.head.ref, 'version/major') || contains(github.event.pull_request.head.ref, 'version/minor') || contains(github.event.pull_request.head.ref, 'version/patch'))) needs: version-and-publish runs-on: ubuntu-latest steps: - name: Success notification if: needs.version-and-publish.outputs.new-version != '' run: | echo "🎉 Successfully published version ${{ needs.version-and-publish.outputs.new-version }} to NPM!" echo "📦 Package: https://www.npmjs.com/package/@xtr-dev/payload-mailing" echo "🏷️ GitHub Release: https://github.com/xtr-dev/payload-mailing/releases/tag/v${{ needs.version-and-publish.outputs.new-version }}" echo "🔄 Version Type: ${{ needs.version-and-publish.outputs.version-type }}" echo "📈 Version Change: v${{ needs.version-and-publish.outputs.current-version }} → v${{ needs.version-and-publish.outputs.new-version }}"