name: Version and Publish on: push: branches: - 'major/**' - 'minor/**' - 'patch/**' pull_request: branches: - main types: [opened, synchronize, reopened, closed] jobs: prepare-release: if: github.event_name == 'push' && (startsWith(github.ref_name, 'major/') || startsWith(github.ref_name, 'minor/') || startsWith(github.ref_name, 'patch/')) runs-on: ubuntu-latest 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: '20' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - 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: | BRANCH_NAME="${{ github.ref_name }}" if [[ "$BRANCH_NAME" =~ ^major/ ]]; then echo "type=major" >> $GITHUB_OUTPUT elif [[ "$BRANCH_NAME" =~ ^minor/ ]]; then echo "type=minor" >> $GITHUB_OUTPUT elif [[ "$BRANCH_NAME" =~ ^patch/ ]]; then echo "type=patch" >> $GITHUB_OUTPUT else echo "type=none" >> $GITHUB_OUTPUT exit 1 fi - name: Generate changelog with Claude 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 # 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 240s 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: Configure git run: | git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" - name: Create version bump script 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: Bump version and create merge branch id: prepare run: | VERSION_TYPE="${{ steps.version-type.outputs.type }}" # 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 # Create merge branch name MERGE_BRANCH="merge/${{ github.ref_name }}" echo "merge-branch=$MERGE_BRANCH" >> $GITHUB_OUTPUT # Create or update merge branch if git ls-remote --exit-code --heads origin $MERGE_BRANCH; then echo "Merge branch $MERGE_BRANCH exists, checking out and updating" git fetch origin $MERGE_BRANCH git checkout -B $MERGE_BRANCH origin/$MERGE_BRANCH git merge --no-ff ${{ github.ref_name }} -m "Update merge branch with latest changes from ${{ github.ref_name }}" else echo "Creating new merge branch $MERGE_BRANCH" git checkout -b $MERGE_BRANCH fi # 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 # Clean up temporary files rm -f bump-version.js # Commit version bump and changes git add -A # Create commit message title if [ "$VERSION_TYPE" = "major" ]; then COMMIT_TITLE="🚀 Major Release v$NEW_VERSION" elif [ "$VERSION_TYPE" = "minor" ]; then COMMIT_TITLE="✨ Minor Release v$NEW_VERSION" else COMMIT_TITLE="🐛 Patch Release v$NEW_VERSION" fi COMMIT_MSG="$COMMIT_TITLE ${{ steps.changelog.outputs.changelog }}" git commit -m "$COMMIT_MSG" || echo "No changes to commit" # Push merge branch git push origin $MERGE_BRANCH - name: Create or update pull request run: | MERGE_BRANCH="${{ steps.prepare.outputs.merge-branch }}" NEW_VERSION="${{ steps.prepare.outputs.new-version }}" VERSION_TYPE="${{ steps.version-type.outputs.type }}" # Create PR title and body if [ "$VERSION_TYPE" = "major" ]; then PR_TITLE="🚀 Release v$NEW_VERSION (Major)" elif [ "$VERSION_TYPE" = "minor" ]; then PR_TITLE="✨ Release v$NEW_VERSION (Minor)" else PR_TITLE="🐛 Release v$NEW_VERSION (Patch)" fi PR_BODY="## Release v$NEW_VERSION This PR contains the prepared release for version **v$NEW_VERSION** ($VERSION_TYPE release). ${{ steps.changelog.outputs.changelog }} ### Pre-Release Checklist - [x] Version bumped in package.json - [x] Changelog generated automatically - [x] Tests passing - [x] Build successful **⚠️ Merging this PR will automatically publish to NPM and create a GitHub release.** --- 🤖 This PR was created automatically from branch \`${{ github.ref_name }}\`" # Check if PR already exists EXISTING_PR=$(gh pr list --head $MERGE_BRANCH --json number --jq '.[0].number' 2>/dev/null || echo "") if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then echo "Updating existing PR #$EXISTING_PR" gh pr edit $EXISTING_PR --title "$PR_TITLE" --body "$PR_BODY" else echo "Creating new PR" gh pr create --title "$PR_TITLE" --body "$PR_BODY" --head $MERGE_BRANCH --base main fi env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} version-and-publish: if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'merge/') runs-on: ubuntu-latest outputs: new-version: ${{ steps.extract-version.outputs.new-version }} package-name: ${{ steps.extract-version.outputs.package-name }} 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: '20' registry-url: 'https://registry.npmjs.org' - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 10 - 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: Extract version and package info id: extract-version run: | # Extract package info from the merged commit PACKAGE_NAME=$(node -p "require('./package.json').name") NEW_VERSION=$(node -p "require('./package.json').version") echo "package-name=$PACKAGE_NAME" >> $GITHUB_OUTPUT echo "new-version=$NEW_VERSION" >> $GITHUB_OUTPUT echo "📦 Package: $PACKAGE_NAME" echo "🚀 New Version: v$NEW_VERSION" - name: Install dependencies run: pnpm install --frozen-lockfile - name: Run final tests run: pnpm test - name: Run final build run: pnpm build - name: Create git tag run: | NEW_VERSION="${{ steps.extract-version.outputs.new-version }}" # Get the commit message (which contains the changelog) COMMIT_MSG=$(git log -1 --pretty=%B) # Configure git git config --global user.name "github-actions[bot]" git config --global user.email "github-actions[bot]@users.noreply.github.com" # Create git tag git tag -a "v$NEW_VERSION" -m "Version $NEW_VERSION $COMMIT_MSG" # Push tags git push origin --tags - name: Publish to NPM run: pnpm publish --access public --no-git-checks env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - name: Create GitHub Release run: | NEW_VERSION="${{ steps.extract-version.outputs.new-version }}" # Get the commit message (which contains the changelog) COMMIT_MSG=$(git log -1 --pretty=%B) gh release create "v$NEW_VERSION" \ --title "Release v$NEW_VERSION" \ --notes "$COMMIT_MSG --- ### Installation \`\`\`bash npm install ${{ steps.extract-version.outputs.package-name }}@$NEW_VERSION \`\`\` ### Documentation See the [README](https://github.com/${{ github.repository }}#readme) for usage instructions and full documentation." env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} notify-success: if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'merge/') 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/${{ needs.version-and-publish.outputs.package-name }}" echo "🏷️ GitHub Release: https://github.com/${{ github.repository }}/releases/tag/v${{ needs.version-and-publish.outputs.new-version }}"