|
| 1 | +name: Sync Version Branches |
| 2 | + |
| 3 | +on: |
| 4 | + pull_request: |
| 5 | + types: [closed] |
| 6 | + branches: |
| 7 | + - main |
| 8 | + |
| 9 | +permissions: |
| 10 | + contents: write |
| 11 | + pull-requests: write |
| 12 | + issues: write |
| 13 | + |
| 14 | +jobs: |
| 15 | + sync: |
| 16 | + # Only run if PR was merged (not just closed) |
| 17 | + if: github.event.pull_request.merged == true |
| 18 | + runs-on: ubuntu-latest |
| 19 | + |
| 20 | + steps: |
| 21 | + - name: Checkout repository |
| 22 | + uses: actions/checkout@v4 |
| 23 | + with: |
| 24 | + fetch-depth: 0 |
| 25 | + token: ${{ secrets.GITHUB_TOKEN }} |
| 26 | + |
| 27 | + - name: Configure Git |
| 28 | + run: | |
| 29 | + git config user.name "github-actions[bot]" |
| 30 | + git config user.email "github-actions[bot]@users.noreply.github.com" |
| 31 | +
|
| 32 | + - name: Detect current version from git tags |
| 33 | + id: detect_version |
| 34 | + run: | |
| 35 | + # Get the latest tag |
| 36 | + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") |
| 37 | + echo "Latest tag: $LATEST_TAG" |
| 38 | +
|
| 39 | + # Extract major.minor version (e.g., v0.3.5 -> 0.3) |
| 40 | + CURRENT_VERSION=$(echo "$LATEST_TAG" | grep -oP 'v?\K\d+\.\d+' || echo "0.0") |
| 41 | + echo "Detected current version: $CURRENT_VERSION" |
| 42 | +
|
| 43 | + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT |
| 44 | +
|
| 45 | + - name: Extract PR labels and determine target branches |
| 46 | + id: determine_branches |
| 47 | + env: |
| 48 | + PR_LABELS: ${{ toJson(github.event.pull_request.labels.*.name) }} |
| 49 | + CURRENT_VERSION: ${{ steps.detect_version.outputs.current_version }} |
| 50 | + run: | |
| 51 | + echo "PR Labels: $PR_LABELS" |
| 52 | + echo "Current version: $CURRENT_VERSION" |
| 53 | +
|
| 54 | + # Parse current version to calculate next versions |
| 55 | + CURRENT_MAJOR=$(echo "$CURRENT_VERSION" | cut -d. -f1) |
| 56 | + CURRENT_MINOR=$(echo "$CURRENT_VERSION" | cut -d. -f2) |
| 57 | +
|
| 58 | + NEXT_MINOR="${CURRENT_MAJOR}.$((CURRENT_MINOR + 1))" |
| 59 | + NEXT_MAJOR="$((CURRENT_MAJOR + 1)).0" |
| 60 | +
|
| 61 | + # Initialize target branches array |
| 62 | + TARGET_BRANCHES="" |
| 63 | +
|
| 64 | + # Parse labels from JSON array |
| 65 | + LABELS=$(echo "$PR_LABELS" | jq -r '.[]') |
| 66 | +
|
| 67 | + # Check if any labels exist |
| 68 | + HAS_LABELS=false |
| 69 | +
|
| 70 | + # Process each label |
| 71 | + while IFS= read -r label; do |
| 72 | + if [ -n "$label" ]; then |
| 73 | + HAS_LABELS=true |
| 74 | +
|
| 75 | + case "$label" in |
| 76 | + major) |
| 77 | + TARGET_BRANCHES="${TARGET_BRANCHES} v${NEXT_MAJOR}.x" |
| 78 | + echo "✓ Found 'major' label → v${NEXT_MAJOR}.x" |
| 79 | + ;; |
| 80 | + minor) |
| 81 | + TARGET_BRANCHES="${TARGET_BRANCHES} v${NEXT_MINOR}.x" |
| 82 | + echo "✓ Found 'minor' label → v${NEXT_MINOR}.x" |
| 83 | + ;; |
| 84 | + patch) |
| 85 | + TARGET_BRANCHES="${TARGET_BRANCHES} v${CURRENT_VERSION}.x" |
| 86 | + echo "✓ Found 'patch' label → v${CURRENT_VERSION}.x" |
| 87 | + ;; |
| 88 | + backport-*) |
| 89 | + # Extract branch name from backport-v0.X.x format |
| 90 | + BACKPORT_BRANCH="${label#backport-}" |
| 91 | + TARGET_BRANCHES="${TARGET_BRANCHES} ${BACKPORT_BRANCH}" |
| 92 | + echo "✓ Found '$label' label → ${BACKPORT_BRANCH}" |
| 93 | + ;; |
| 94 | + esac |
| 95 | + fi |
| 96 | + done <<< "$LABELS" |
| 97 | +
|
| 98 | + # If no version labels found, default to minor |
| 99 | + if [ "$HAS_LABELS" = false ] || [ -z "$TARGET_BRANCHES" ]; then |
| 100 | + TARGET_BRANCHES="v${NEXT_MINOR}.x" |
| 101 | + echo "⚠ No version labels found, defaulting to 'minor' → v${NEXT_MINOR}.x" |
| 102 | + fi |
| 103 | +
|
| 104 | + # Remove duplicates and trim |
| 105 | + TARGET_BRANCHES=$(echo "$TARGET_BRANCHES" | tr ' ' '\n' | sort -u | tr '\n' ' ' | xargs) |
| 106 | +
|
| 107 | + echo "target_branches=$TARGET_BRANCHES" >> $GITHUB_OUTPUT |
| 108 | + echo "" |
| 109 | + echo "📋 Final target branches: $TARGET_BRANCHES" |
| 110 | +
|
| 111 | + - name: Sync to version branches |
| 112 | + id: sync |
| 113 | + env: |
| 114 | + TARGET_BRANCHES: ${{ steps.determine_branches.outputs.target_branches }} |
| 115 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 116 | + PR_TITLE: ${{ github.event.pull_request.title }} |
| 117 | + MERGE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} |
| 118 | + run: | |
| 119 | + echo "🔄 Starting sync process..." |
| 120 | + echo "Merge commit: $MERGE_COMMIT" |
| 121 | + echo "" |
| 122 | +
|
| 123 | + SUCCESS_BRANCHES="" |
| 124 | + FAILED_BRANCHES="" |
| 125 | + CONFLICT_BRANCHES="" |
| 126 | +
|
| 127 | + for BRANCH in $TARGET_BRANCHES; do |
| 128 | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 129 | + echo "Processing branch: $BRANCH" |
| 130 | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 131 | +
|
| 132 | + # Check if branch exists remotely |
| 133 | + if git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then |
| 134 | + echo "✓ Branch $BRANCH exists remotely" |
| 135 | + git fetch origin "$BRANCH" |
| 136 | + git checkout "$BRANCH" |
| 137 | + else |
| 138 | + echo "⚠ Branch $BRANCH does not exist, creating from main..." |
| 139 | + git checkout -b "$BRANCH" origin/main |
| 140 | + git push -u origin "$BRANCH" |
| 141 | + echo "✓ Created branch $BRANCH" |
| 142 | + fi |
| 143 | +
|
| 144 | + # Attempt cherry-pick |
| 145 | + echo "" |
| 146 | + echo "Attempting to cherry-pick $MERGE_COMMIT to $BRANCH..." |
| 147 | +
|
| 148 | + if git cherry-pick -m 1 "$MERGE_COMMIT"; then |
| 149 | + echo "✓ Cherry-pick successful" |
| 150 | +
|
| 151 | + # Push to remote |
| 152 | + if git push origin "$BRANCH"; then |
| 153 | + echo "✓ Successfully pushed to $BRANCH" |
| 154 | + SUCCESS_BRANCHES="${SUCCESS_BRANCHES} ${BRANCH}" |
| 155 | + else |
| 156 | + echo "✗ Failed to push to $BRANCH" |
| 157 | + FAILED_BRANCHES="${FAILED_BRANCHES} ${BRANCH}" |
| 158 | + fi |
| 159 | + else |
| 160 | + echo "✗ Cherry-pick failed with conflicts" |
| 161 | +
|
| 162 | + # Abort the cherry-pick |
| 163 | + git cherry-pick --abort |
| 164 | +
|
| 165 | + # Create a conflict resolution PR |
| 166 | + CONFLICT_BRANCH="sync-conflict-pr${PR_NUMBER}-to-${BRANCH}" |
| 167 | +
|
| 168 | + echo "Creating conflict resolution branch: $CONFLICT_BRANCH" |
| 169 | + git checkout -b "$CONFLICT_BRANCH" "$BRANCH" |
| 170 | +
|
| 171 | + # Try cherry-pick again to preserve conflict state |
| 172 | + git cherry-pick -m 1 "$MERGE_COMMIT" || true |
| 173 | +
|
| 174 | + # Add conflict markers and commit |
| 175 | + git add -A |
| 176 | + git commit -m "WIP: Sync PR #${PR_NUMBER} to ${BRANCH} (conflicts) |
| 177 | +
|
| 178 | +This is an automatic sync from PR #${PR_NUMBER}: ${PR_TITLE} |
| 179 | + |
| 180 | +The cherry-pick resulted in conflicts that need manual resolution. |
| 181 | + |
| 182 | +Original commit: ${MERGE_COMMIT} |
| 183 | +Target branch: ${BRANCH} |
| 184 | + |
| 185 | +Please resolve conflicts and merge this PR to complete the sync." || true |
| 186 | + |
| 187 | + # Push conflict branch |
| 188 | + if git push -u origin "$CONFLICT_BRANCH"; then |
| 189 | + echo "✓ Pushed conflict resolution branch" |
| 190 | + CONFLICT_BRANCHES="${CONFLICT_BRANCHES} ${BRANCH}:${CONFLICT_BRANCH}" |
| 191 | + else |
| 192 | + echo "✗ Failed to push conflict resolution branch" |
| 193 | + FAILED_BRANCHES="${FAILED_BRANCHES} ${BRANCH}" |
| 194 | + fi |
| 195 | + fi |
| 196 | + |
| 197 | + # Return to main for next iteration |
| 198 | + git checkout main |
| 199 | + echo "" |
| 200 | + done |
| 201 | + |
| 202 | + # Save results for comment |
| 203 | + echo "success_branches=$SUCCESS_BRANCHES" >> $GITHUB_OUTPUT |
| 204 | + echo "failed_branches=$FAILED_BRANCHES" >> $GITHUB_OUTPUT |
| 205 | + echo "conflict_branches=$CONFLICT_BRANCHES" >> $GITHUB_OUTPUT |
| 206 | + |
| 207 | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 208 | + echo "📊 Sync Summary" |
| 209 | + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" |
| 210 | + echo "✓ Success: $SUCCESS_BRANCHES" |
| 211 | + echo "⚠ Conflicts: $CONFLICT_BRANCHES" |
| 212 | + echo "✗ Failed: $FAILED_BRANCHES" |
| 213 | + |
| 214 | + - name: Create conflict resolution PRs |
| 215 | + if: steps.sync.outputs.conflict_branches != '' |
| 216 | + env: |
| 217 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 218 | + CONFLICT_BRANCHES: ${{ steps.sync.outputs.conflict_branches }} |
| 219 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 220 | + PR_TITLE: ${{ github.event.pull_request.title }} |
| 221 | + run: | |
| 222 | + echo "Creating PRs for conflict resolution..." |
| 223 | +
|
| 224 | + for ITEM in $CONFLICT_BRANCHES; do |
| 225 | + TARGET_BRANCH=$(echo "$ITEM" | cut -d: -f1) |
| 226 | + CONFLICT_BRANCH=$(echo "$ITEM" | cut -d: -f2) |
| 227 | +
|
| 228 | + echo "Creating PR: $CONFLICT_BRANCH → $TARGET_BRANCH" |
| 229 | +
|
| 230 | + gh pr create \ |
| 231 | + --base "$TARGET_BRANCH" \ |
| 232 | + --head "$CONFLICT_BRANCH" \ |
| 233 | + --title "🔀 Sync PR #${PR_NUMBER} to ${TARGET_BRANCH} (conflicts)" \ |
| 234 | + --body "## ⚠️ Conflict Resolution Needed |
| 235 | +
|
| 236 | +This PR is an automatic sync of PR #${PR_NUMBER} to the \`${TARGET_BRANCH}\` branch. |
| 237 | + |
| 238 | +**Original PR**: #${PR_NUMBER} - ${PR_TITLE} |
| 239 | + |
| 240 | +The cherry-pick resulted in merge conflicts that need manual resolution. |
| 241 | + |
| 242 | +### Steps to resolve: |
| 243 | +1. Review the conflicts in this PR |
| 244 | +2. Resolve conflicts locally or via GitHub UI |
| 245 | +3. Merge this PR to complete the sync |
| 246 | + |
| 247 | +### Original commit |
| 248 | +\`${{ github.event.pull_request.merge_commit_sha }}\` |
| 249 | + |
| 250 | +--- |
| 251 | +🤖 This PR was created automatically by the version branch sync workflow." \ |
| 252 | + --label "sync-conflict" || echo "Failed to create PR for $TARGET_BRANCH" |
| 253 | + done |
| 254 | + |
| 255 | + - name: Comment on original PR |
| 256 | + if: always() |
| 257 | + env: |
| 258 | + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} |
| 259 | + PR_NUMBER: ${{ github.event.pull_request.number }} |
| 260 | + TARGET_BRANCHES: ${{ steps.determine_branches.outputs.target_branches }} |
| 261 | + SUCCESS_BRANCHES: ${{ steps.sync.outputs.success_branches }} |
| 262 | + CONFLICT_BRANCHES: ${{ steps.sync.outputs.conflict_branches }} |
| 263 | + FAILED_BRANCHES: ${{ steps.sync.outputs.failed_branches }} |
| 264 | + run: | |
| 265 | + COMMENT="## 🔄 Version Branch Sync Report |
| 266 | +
|
| 267 | +**Target branches**: \`$TARGET_BRANCHES\` |
| 268 | + |
| 269 | +" |
| 270 | +
|
| 271 | + if [ -n "$SUCCESS_BRANCHES" ]; then |
| 272 | + COMMENT="${COMMENT} |
| 273 | +### ✅ Successfully synced |
| 274 | +" |
| 275 | + for BRANCH in $SUCCESS_BRANCHES; do |
| 276 | + COMMENT="${COMMENT}- \`${BRANCH}\` |
| 277 | +" |
| 278 | + done |
| 279 | + fi |
| 280 | +
|
| 281 | + if [ -n "$CONFLICT_BRANCHES" ]; then |
| 282 | + COMMENT="${COMMENT} |
| 283 | +### ⚠️ Conflicts detected |
| 284 | +" |
| 285 | + for ITEM in $CONFLICT_BRANCHES; do |
| 286 | + TARGET_BRANCH=$(echo "$ITEM" | cut -d: -f1) |
| 287 | + COMMENT="${COMMENT}- \`${TARGET_BRANCH}\` - Conflict resolution PR created |
| 288 | +" |
| 289 | + done |
| 290 | + COMMENT="${COMMENT} |
| 291 | +Please review and resolve conflicts in the generated PRs. |
| 292 | +" |
| 293 | + fi |
| 294 | +
|
| 295 | + if [ -n "$FAILED_BRANCHES" ]; then |
| 296 | + COMMENT="${COMMENT} |
| 297 | +### ❌ Failed |
| 298 | +" |
| 299 | + for BRANCH in $FAILED_BRANCHES; do |
| 300 | + COMMENT="${COMMENT}- \`${BRANCH}\` |
| 301 | +" |
| 302 | + done |
| 303 | + fi |
| 304 | +
|
| 305 | + COMMENT="${COMMENT} |
| 306 | +--- |
| 307 | +🤖 Automated by [sync-version-branches workflow](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})" |
| 308 | + |
| 309 | + gh pr comment "$PR_NUMBER" --body "$COMMENT" |
0 commit comments