Skip to content

Commit 402f15d

Browse files
committed
feat: add automatic version branch sync workflow based on PR labels
1 parent 4bc0d3d commit 402f15d

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
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

Comments
 (0)