diff --git a/.github/workflows/forward-port.yml b/.github/workflows/forward-port.yml new file mode 100644 index 00000000..8e533c07 --- /dev/null +++ b/.github/workflows/forward-port.yml @@ -0,0 +1,169 @@ +name: Forward Port to Next + +on: + push: + branches: + - main + +concurrency: + group: forward-port + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +jobs: + forward-port: + runs-on: ubuntu-latest + env: + DISCORD_WEBHOOK: ${{ secrets.DISCORD_METRICS_WEBHOOK }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for existing PR + id: check-pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EXISTING_PR=$(gh pr list --base next --head main --state open --json number --jq '.[0].number // empty') + if [ -n "$EXISTING_PR" ]; then + echo "existing_pr=$EXISTING_PR" >> $GITHUB_OUTPUT + echo "PR #$EXISTING_PR already exists for main → next" + else + echo "existing_pr=" >> $GITHUB_OUTPUT + echo "No existing PR found" + fi + + - name: Check if main has changes not in next + id: check-diff + if: steps.check-pr.outputs.existing_pr == '' + run: | + git fetch origin next + DIFF_COUNT=$(git rev-list --count origin/next..origin/main) + echo "diff_count=$DIFF_COUNT" >> $GITHUB_OUTPUT + if [ "$DIFF_COUNT" -gt 0 ]; then + echo "Found $DIFF_COUNT commit(s) in main not in next" + else + echo "No new commits to forward port" + fi + + - name: Check for merge conflicts + id: check-conflicts + if: steps.check-pr.outputs.existing_pr == '' && steps.check-diff.outputs.diff_count != '0' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + # Try a test merge to detect conflicts + git checkout origin/next + if git merge --no-commit --no-ff origin/main 2>/dev/null; then + echo "has_conflicts=false" >> $GITHUB_OUTPUT + echo "No merge conflicts detected" + else + echo "has_conflicts=true" >> $GITHUB_OUTPUT + # Get list of conflicting files + CONFLICTING_FILES=$(git diff --name-only --diff-filter=U | head -10) + echo "conflicting_files<> $GITHUB_OUTPUT + echo "$CONFLICTING_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "Merge conflicts detected in: $CONFLICTING_FILES" + fi + git merge --abort 2>/dev/null || true + + - name: Create forward-port PR + id: create-pr + if: steps.check-pr.outputs.existing_pr == '' && steps.check-diff.outputs.diff_count != '0' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Get the commits being forward-ported for the PR body + COMMITS=$(git log origin/next..origin/main --oneline --no-decorate | head -20) + COMMIT_COUNT=$(git rev-list --count origin/next..origin/main) + + # Build conflict warning if needed + CONFLICT_WARNING="" + if [ "${{ steps.check-conflicts.outputs.has_conflicts }}" = "true" ]; then + CONFLICT_WARNING=" + > [!WARNING] + > **Merge conflicts detected.** Manual resolution required. + > + > Conflicting files: + > \`\`\` + > ${{ steps.check-conflicts.outputs.conflicting_files }} + > \`\`\` + + ### How to resolve + + \`\`\`bash + # Option 1: Resolve in a temporary branch (recommended) + git fetch origin + git checkout -b resolve-forward-port origin/next + git merge origin/main + # Fix conflicts in your editor, then: + git add . + git commit + git push origin resolve-forward-port + # Create PR from resolve-forward-port → next, then close this PR + + # Option 2: Resolve directly on next + git checkout next + git pull origin next + git merge origin/main + # Fix conflicts, commit, and push + \`\`\` + " + fi + + # Create PR body + BODY="## Forward Port: main → next + + This PR forward-ports changes from \`main\` to \`next\` to ensure hotfixes and releases are included in the next development branch. + $CONFLICT_WARNING + ### Commits ($COMMIT_COUNT total) + \`\`\` + $COMMITS + \`\`\` + $([ "$COMMIT_COUNT" -gt 20 ] && echo "... and $((COMMIT_COUNT - 20)) more") + + --- + *Auto-generated by forward-port workflow*" + + # Create the PR + PR_URL=$(gh pr create \ + --base next \ + --head main \ + --title "chore: forward port main to next" \ + --label "forward-port" \ + --label "automated" \ + --body "$BODY") + + PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT + + # Add conflict label if needed + if [ "${{ steps.check-conflicts.outputs.has_conflicts }}" = "true" ]; then + gh pr edit "$PR_NUMBER" --add-label "has-conflicts" + fi + + - name: Send Discord notification + if: steps.create-pr.outputs.pr_url != '' && env.DISCORD_WEBHOOK != '' + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ env.DISCORD_WEBHOOK }} + status: ${{ steps.check-conflicts.outputs.has_conflicts == 'true' && 'Warning' || 'Success' }} + title: "🔄 Forward Port PR Created" + description: | + **main → next** + + ${{ steps.check-conflicts.outputs.has_conflicts == 'true' && '⚠️ **Merge conflicts detected** - manual resolution required' || '✅ No conflicts - ready for review' }} + + **Commits:** ${{ steps.check-diff.outputs.diff_count }} + **PR:** ${{ steps.create-pr.outputs.pr_url }} + color: ${{ steps.check-conflicts.outputs.has_conflicts == 'true' && '0xFFA500' || '0x58AFFF' }} + username: Task Master Bot + avatar_url: https://raw.githubusercontent.com/eyaltoledano/claude-task-master/main/images/logo.png