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 - 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 from branch comparison id: version-type run: | # Get version from source branch (current) SOURCE_VERSION=$(node -p "require('./package.json').version") # Get version from target branch (main) git fetch origin main TARGET_VERSION=$(git show origin/main:package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8')).version") echo "📊 Source branch version: $SOURCE_VERSION" echo "📊 Target branch version: $TARGET_VERSION" # Parse versions into components IFS='.' read -r -a SOURCE_PARTS <<< "$SOURCE_VERSION" IFS='.' read -r -a TARGET_PARTS <<< "$TARGET_VERSION" SOURCE_MAJOR=${SOURCE_PARTS[0]} SOURCE_MINOR=${SOURCE_PARTS[1]} SOURCE_PATCH=${SOURCE_PARTS[2]} TARGET_MAJOR=${TARGET_PARTS[0]} TARGET_MINOR=${TARGET_PARTS[1]} TARGET_PATCH=${TARGET_PARTS[2]} # Determine version bump type based on what changed if [ "$SOURCE_MAJOR" -gt "$TARGET_MAJOR" ]; then echo "type=major" >> $GITHUB_OUTPUT echo "🚀 Major version increase detected ($TARGET_MAJOR → $SOURCE_MAJOR)" elif [ "$SOURCE_MINOR" -gt "$TARGET_MINOR" ]; then echo "type=minor" >> $GITHUB_OUTPUT echo "✨ Minor version increase detected ($TARGET_MINOR → $SOURCE_MINOR)" elif [ "$SOURCE_PATCH" -gt "$TARGET_PATCH" ]; then echo "type=patch" >> $GITHUB_OUTPUT echo "🐛 Patch version increase detected ($TARGET_PATCH → $SOURCE_PATCH)" else echo "type=none" >> $GITHUB_OUTPUT echo "⚠️ No version increase detected. Source version must be higher than target." echo "Target: $TARGET_VERSION, Source: $SOURCE_VERSION" exit 1 fi # Store versions for later use echo "source-version=$SOURCE_VERSION" >> $GITHUB_OUTPUT echo "target-version=$TARGET_VERSION" >> $GITHUB_OUTPUT - name: Generate changelog from commits 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") echo "📝 Generating changelog from commits since 1 week ago (no previous tags found)" else COMMITS=$(git log --oneline --no-merges ${LAST_TAG}..HEAD || echo "No new commits") echo "📝 Generating changelog from commits since tag: $LAST_TAG" fi echo "Commits to process:" echo "$COMMITS" # Categorize commits based on conventional commit patterns and keywords FEATURES="" BUGFIXES="" IMPROVEMENTS="" DOCS="" while IFS= read -r commit; do if [[ -z "$commit" ]]; then continue; fi # Extract commit message (remove hash) MSG=$(echo "$commit" | sed 's/^[a-f0-9]* //') # Categorize based on patterns if echo "$MSG" | grep -qiE "^(feat|feature|add|new):|🚀|✨"; then FEATURES="${FEATURES}- $MSG"$'\n' elif echo "$MSG" | grep -qiE "^(fix|bug):|🐛|❌|🔧.*fix"; then BUGFIXES="${BUGFIXES}- $MSG"$'\n' elif echo "$MSG" | grep -qiE "^(docs|doc):|📚|📝"; then DOCS="${DOCS}- $MSG"$'\n' else IMPROVEMENTS="${IMPROVEMENTS}- $MSG"$'\n' fi done <<< "$COMMITS" # Build changelog CHANGELOG="## Changes" if [[ -n "$FEATURES" ]]; then CHANGELOG="$CHANGELOG ### 🚀 Features $FEATURES" fi if [[ -n "$BUGFIXES" ]]; then CHANGELOG="$CHANGELOG ### 🐛 Bug Fixes $BUGFIXES" fi if [[ -n "$IMPROVEMENTS" ]]; then CHANGELOG="$CHANGELOG ### 🔧 Improvements $IMPROVEMENTS" fi if [[ -n "$DOCS" ]]; then CHANGELOG="$CHANGELOG ### 📚 Documentation $DOCS" fi # If no commits found, create a simple message if [[ -z "$FEATURES$BUGFIXES$IMPROVEMENTS$DOCS" ]]; then CHANGELOG="## Changes ### 🔧 Improvements - Version update and maintenance changes" fi echo "Generated changelog:" echo "$CHANGELOG" # 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 calculation script run: | # Create Node.js script to calculate proper version increment cat > calculate-version.js << 'VERSION_SCRIPT_EOF' const fs = require('fs'); const path = require('path'); const versionType = process.argv[2]; const targetVersion = process.argv[3]; const sourceVersion = process.argv[4]; const packagePath = path.join(process.cwd(), 'package.json'); try { // Parse versions const [targetMajor, targetMinor, targetPatch] = targetVersion.split('.').map(Number); const [sourceMajor, sourceMinor, sourcePatch] = sourceVersion.split('.').map(Number); let newVersion; let increment; switch (versionType) { case 'major': // Calculate major increment and reset minor/patch increment = sourceMajor - targetMajor; newVersion = `${targetMajor + increment}.0.0`; break; case 'minor': // Keep major from target, calculate minor increment, reset patch increment = sourceMinor - targetMinor; newVersion = `${targetMajor}.${targetMinor + increment}.0`; break; case 'patch': // Keep major/minor from target, calculate patch increment increment = sourcePatch - targetPatch; newVersion = `${targetMajor}.${targetMinor}.${targetPatch + increment}`; break; default: throw new Error(`Invalid version type: ${versionType}`); } // Ensure increment is positive if (increment <= 0) { throw new Error(`Invalid increment ${increment} for ${versionType} version`); } // Update package.json const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8')); packageJson.version = newVersion; fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2) + '\n'); console.log(`${targetVersion}:${newVersion}:${increment}`); } catch (error) { console.error('Error calculating version:', error.message); process.exit(1); } VERSION_SCRIPT_EOF - name: Calculate new version and create merge branch id: prepare run: | VERSION_TYPE="${{ steps.version-type.outputs.type }}" TARGET_VERSION="${{ steps.version-type.outputs.target-version }}" SOURCE_VERSION="${{ steps.version-type.outputs.source-version }}" # Calculate new version using target + calculated increment VERSION_OUTPUT=$(node calculate-version.js $VERSION_TYPE $TARGET_VERSION $SOURCE_VERSION) CURRENT_VERSION=$(echo $VERSION_OUTPUT | cut -d: -f1) NEW_VERSION=$(echo $VERSION_OUTPUT | cut -d: -f2) INCREMENT=$(echo $VERSION_OUTPUT | cut -d: -f3) echo "📊 Version calculation:" echo " Target (main): $CURRENT_VERSION" echo " Source (branch): $SOURCE_VERSION" echo " New version: $NEW_VERSION" echo " Increment: +$INCREMENT ($VERSION_TYPE)" 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 calculate-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 - 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 }}"