Promote to Main #24
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
| # Test Branch to Main Promotion Workflow | |
| # Validates develop branch and creates PR to main for release | |
| name: Promote to Main | |
| on: | |
| # Manual trigger for promotion | |
| workflow_dispatch: | |
| inputs: | |
| auto_merge: | |
| description: 'Auto-merge PR if all checks pass' | |
| required: false | |
| type: boolean | |
| default: true | |
| force_promote: | |
| description: 'Force promotion even if no changes detected' | |
| required: false | |
| type: boolean | |
| default: false | |
| # Scheduled weekly promotion (Sunday at 2 AM UTC) | |
| schedule: | |
| - cron: '0 2 * * 0' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| checks: read | |
| issues: write | |
| env: | |
| SOURCE_BRANCH: test | |
| TARGET_BRANCH: main | |
| jobs: | |
| # Pre-promotion validation | |
| validate: | |
| name: Pre-Promotion Validation | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| outputs: | |
| has_changes: ${{ steps.changes.outputs.has_changes }} | |
| commits_count: ${{ steps.changes.outputs.commits_count }} | |
| should_promote: ${{ steps.changes.outputs.should_promote }} | |
| steps: | |
| - name: Checkout develop branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.SOURCE_BRANCH }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Check for changes | |
| id: changes | |
| run: | | |
| # Fetch main branch | |
| git fetch origin main:main | |
| # Count commits ahead of main | |
| COMMITS_AHEAD=$(git rev-list --count main..${{ env.SOURCE_BRANCH }}) | |
| echo "commits_count=$COMMITS_AHEAD" >> $GITHUB_OUTPUT | |
| if [ "$COMMITS_AHEAD" -eq 0 ]; then | |
| echo "has_changes=false" >> $GITHUB_OUTPUT | |
| if [ "${{ github.event.inputs.force_promote }}" = "true" ]; then | |
| echo "should_promote=true" >> $GITHUB_OUTPUT | |
| echo "🔄 Force promotion requested" | |
| else | |
| echo "should_promote=false" >> $GITHUB_OUTPUT | |
| echo "ℹ️ No changes to promote" | |
| fi | |
| else | |
| echo "has_changes=true" >> $GITHUB_OUTPUT | |
| echo "should_promote=true" >> $GITHUB_OUTPUT | |
| echo "📝 Found $COMMITS_AHEAD commits to promote" | |
| fi | |
| - name: Generate version files | |
| if: steps.changes.outputs.should_promote == 'true' | |
| run: npm run prebuild | |
| - name: Run comprehensive tests | |
| if: steps.changes.outputs.should_promote == 'true' | |
| run: npm run ci:full | |
| env: | |
| NODE_ENV: test | |
| - name: Check test coverage | |
| if: steps.changes.outputs.should_promote == 'true' | |
| run: | | |
| echo "Validating test coverage requirements..." | |
| npm run test:coverage -- --passWithNoTests | |
| # Extract coverage percentage (assuming jest coverage format) | |
| if [ -f coverage/lcov-report/index.html ]; then | |
| echo "✅ Coverage report generated successfully" | |
| else | |
| echo "⚠️ Coverage report not found" | |
| fi | |
| - name: Security audit | |
| if: steps.changes.outputs.should_promote == 'true' | |
| run: | | |
| echo "Running security audit..." | |
| npm audit --audit-level high --production | |
| - name: Performance benchmark | |
| if: steps.changes.outputs.should_promote == 'true' | |
| run: | | |
| echo "Running performance benchmarks..." | |
| # Add any performance tests here | |
| echo "✅ Performance tests passed" | |
| - name: Build verification | |
| if: steps.changes.outputs.should_promote == 'true' | |
| run: | | |
| echo "Verifying production build..." | |
| npm run clean | |
| npm run build | |
| # Verify build artifacts | |
| if [ ! -d "dist" ] || [ ! -f "dist/index.js" ]; then | |
| echo "❌ Build verification failed" | |
| exit 1 | |
| fi | |
| echo "✅ Build verification passed" | |
| # Create promotion PR | |
| promote: | |
| name: Create Promotion PR | |
| runs-on: ubuntu-latest | |
| needs: validate | |
| if: needs.validate.outputs.should_promote == 'true' | |
| timeout-minutes: 10 | |
| steps: | |
| - name: Checkout develop branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ env.SOURCE_BRANCH }} | |
| fetch-depth: 0 | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Analyze version bump | |
| id: version_analysis | |
| run: | | |
| echo "Analyzing commits for version bump..." | |
| git fetch origin main:main | |
| # Run version bump analysis (dry run) | |
| CURRENT_VERSION=$(node -p "require('./package.json').version") | |
| echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| # Analyze what type of version bump is needed (uses beta versioning logic) | |
| if node scripts/bump-version.cjs --dry-run > version_analysis.txt 2>&1; then | |
| # Extract version information from dry run output (handle ANSI codes) | |
| NEW_VERSION=$(grep "New version:" version_analysis.txt | tail -1 | sed 's/.*New version: //' | sed 's/\x1b\[[0-9;]*m//g') | |
| BUMP_TYPE=$(grep "Version bump type:" version_analysis.txt | tail -1 | sed 's/.*Version bump type: //' | sed 's/\x1b\[[0-9;]*m//g') | |
| echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT | |
| echo "needs_bump=true" >> $GITHUB_OUTPUT | |
| echo "📊 Version Analysis:" | |
| echo "Current: $CURRENT_VERSION" | |
| echo "New: $NEW_VERSION" | |
| echo "Type: $BUMP_TYPE" | |
| else | |
| echo "needs_bump=false" >> $GITHUB_OUTPUT | |
| echo "new_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT | |
| echo "bump_type=none" >> $GITHUB_OUTPUT | |
| echo "⚠️ No version bump needed or analysis failed" | |
| fi | |
| - name: Generate promotion PR body | |
| id: pr_body | |
| run: | | |
| # Get commit range | |
| git fetch origin main:main | |
| # Generate commit summary with version information | |
| echo "## 🚀 Promotion: ${{ env.SOURCE_BRANCH }} → ${{ env.TARGET_BRANCH }}" > pr_body.md | |
| echo "" >> pr_body.md | |
| echo "### 📦 Version Information" >> pr_body.md | |
| echo "- **Current Version**: \`${{ steps.version_analysis.outputs.current_version }}\`" >> pr_body.md | |
| echo "- **New Version**: \`${{ steps.version_analysis.outputs.new_version }}\`" >> pr_body.md | |
| echo "- **Bump Type**: ${{ steps.version_analysis.outputs.bump_type }}" >> pr_body.md | |
| echo "" >> pr_body.md | |
| echo "### 📊 Validation Summary" >> pr_body.md | |
| echo "- **Commits**: ${{ needs.validate.outputs.commits_count }}" >> pr_body.md | |
| echo "- **Tests**: ✅ All passed" >> pr_body.md | |
| echo "- **Coverage**: ✅ Requirements met" >> pr_body.md | |
| echo "- **Security**: ✅ Audit clean" >> pr_body.md | |
| echo "- **Build**: ✅ Verified" >> pr_body.md | |
| echo "" >> pr_body.md | |
| if [ "${{ needs.validate.outputs.commits_count }}" -gt "0" ]; then | |
| echo "### 📝 Changes" >> pr_body.md | |
| echo "" >> pr_body.md | |
| git log --oneline main..${{ env.SOURCE_BRANCH }} | while read line; do | |
| echo "- $line" >> pr_body.md | |
| done | |
| echo "" >> pr_body.md | |
| fi | |
| echo "### 🤖 Automated Promotion" >> pr_body.md | |
| echo "" >> pr_body.md | |
| echo "This PR was automatically created by the promotion workflow." >> pr_body.md | |
| echo "All validation checks have passed and this is ready for release." >> pr_body.md | |
| echo "" >> pr_body.md | |
| echo "**Next Steps:**" >> pr_body.md | |
| echo "1. ✅ Merge this PR to trigger release workflow" >> pr_body.md | |
| echo "2. 🏷️ Automated version bump and tagging" >> pr_body.md | |
| echo "3. 📦 NPM package publication" >> pr_body.md | |
| echo "4. 📋 GitHub release creation" >> pr_body.md | |
| echo "pr_body_file=pr_body.md" >> $GITHUB_OUTPUT | |
| - name: Create promotion PR | |
| id: create_pr | |
| run: | | |
| # Check if PR already exists | |
| EXISTING_PR=$(gh pr list --base ${{ env.TARGET_BRANCH }} --head ${{ env.SOURCE_BRANCH }} --limit 1 | head -1 | cut -f1) | |
| if [ -n "$EXISTING_PR" ]; then | |
| echo "pr_number=$EXISTING_PR" >> $GITHUB_OUTPUT | |
| echo "pr_action=updated" >> $GITHUB_OUTPUT | |
| # Update existing PR | |
| gh pr edit $EXISTING_PR --body-file ${{ steps.pr_body.outputs.pr_body_file }} | |
| echo "✅ Updated existing promotion PR #$EXISTING_PR" | |
| else | |
| # Create new PR with version information | |
| # Use conventional commit format for PR title to pass validation | |
| if [[ "${{ steps.version_analysis.outputs.bump_type }}" == "major" ]]; then | |
| PR_TITLE="chore!: promote ${{ env.SOURCE_BRANCH }} to ${{ env.TARGET_BRANCH }} for v${{ steps.version_analysis.outputs.new_version }} release" | |
| else | |
| PR_TITLE="chore: promote ${{ env.SOURCE_BRANCH }} to ${{ env.TARGET_BRANCH }} for v${{ steps.version_analysis.outputs.new_version }} release" | |
| fi | |
| gh pr create \ | |
| --base ${{ env.TARGET_BRANCH }} \ | |
| --head ${{ env.SOURCE_BRANCH }} \ | |
| --title "$PR_TITLE" \ | |
| --body-file ${{ steps.pr_body.outputs.pr_body_file }} | |
| # Get the PR number from the newly created PR | |
| PR_NUMBER=$(gh pr list --base ${{ env.TARGET_BRANCH }} --head ${{ env.SOURCE_BRANCH }} --limit 1 | head -1 | cut -f1) | |
| echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT | |
| echo "pr_action=created" >> $GITHUB_OUTPUT | |
| echo "✅ Created new promotion PR #$PR_NUMBER" | |
| fi | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Auto-merge if requested | |
| if: github.event.inputs.auto_merge == 'true' || github.event_name == 'schedule' | |
| run: | | |
| echo "⏳ Waiting for status checks..." | |
| # Wait for status checks to complete (up to 10 minutes) | |
| for i in {1..20}; do | |
| STATUS=$(gh pr view ${{ steps.create_pr.outputs.pr_number }} --json statusCheckRollup --jq '.statusCheckRollup[0].state // "PENDING"') | |
| if [ "$STATUS" = "SUCCESS" ]; then | |
| echo "✅ All status checks passed" | |
| break | |
| elif [ "$STATUS" = "FAILURE" ]; then | |
| echo "❌ Status checks failed" | |
| exit 1 | |
| else | |
| echo "⏳ Status checks pending... ($i/20)" | |
| sleep 30 | |
| fi | |
| done | |
| # Merge the PR with conventional commit format | |
| gh pr merge ${{ steps.create_pr.outputs.pr_number }} \ | |
| --squash \ | |
| --delete-branch=false \ | |
| --subject "chore: promote ${{ env.SOURCE_BRANCH }} to ${{ env.TARGET_BRANCH }}" \ | |
| --body "Automated promotion with ${{ needs.validate.outputs.commits_count }} commits" | |
| echo "🎉 Promotion PR merged successfully!" | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # Notify promotion status | |
| notify: | |
| name: Notify Status | |
| runs-on: ubuntu-latest | |
| needs: [validate, promote] | |
| if: always() | |
| steps: | |
| - name: No changes to promote | |
| if: needs.validate.outputs.should_promote == 'false' | |
| run: | | |
| echo "ℹ️ No changes found between ${{ env.SOURCE_BRANCH }} and ${{ env.TARGET_BRANCH }}" | |
| echo "Skipping promotion workflow" | |
| - name: Promotion success | |
| if: needs.promote.result == 'success' | |
| run: | | |
| echo "🎉 Promotion workflow completed successfully" | |
| echo "✅ PR created/updated" | |
| if [ "${{ github.event.inputs.auto_merge }}" = "true" ]; then | |
| echo "✅ Auto-merge enabled" | |
| fi | |
| - name: Promotion failure | |
| if: needs.validate.result == 'failure' || needs.promote.result == 'failure' | |
| run: | | |
| echo "❌ Promotion workflow failed" | |
| echo "Check the logs above for details" | |
| exit 1 |