Skip to content

Commit c8e0011

Browse files
chore(repo): Add back-merge workflow for branch cascade
1 parent 121e7be commit c8e0011

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed

.github/workflows/back_merge.yml

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
name: Back-merge
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
- minor
8+
workflow_dispatch:
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
resolve-branches:
16+
name: Resolve merge target
17+
runs-on: ubuntu-latest
18+
outputs:
19+
source: ${{ github.ref_name }}
20+
target: ${{ steps.target.outputs.branch }}
21+
steps:
22+
- name: Determine target branch
23+
id: target
24+
env:
25+
BRANCH: ${{ github.ref_name }}
26+
run: |
27+
if [ "$BRANCH" = "master" ]; then
28+
echo "branch=minor" >> "$GITHUB_OUTPUT"
29+
elif [ "$BRANCH" = "minor" ]; then
30+
echo "branch=major" >> "$GITHUB_OUTPUT"
31+
fi
32+
33+
back-merge:
34+
name: 'Merge ${{ needs.resolve-branches.outputs.source }} → ${{ needs.resolve-branches.outputs.target }}'
35+
runs-on: ubuntu-latest
36+
needs: resolve-branches
37+
permissions:
38+
contents: write
39+
pull-requests: write
40+
steps:
41+
- name: Generate CI Bot Token
42+
id: app-token
43+
uses: actions/create-github-app-token@v2
44+
with:
45+
app-id: ${{ secrets.CI_BOT_APP_ID }}
46+
private-key: ${{ secrets.CI_BOT_APP_PRIVATE_KEY }}
47+
48+
- name: Checkout target branch
49+
uses: actions/checkout@v4
50+
with:
51+
token: ${{ steps.app-token.outputs.token }}
52+
ref: ${{ needs.resolve-branches.outputs.target }}
53+
fetch-depth: 0
54+
55+
- name: Get CI Bot user ID
56+
id: get-user-id
57+
env:
58+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
59+
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
60+
run: |
61+
echo "user-id=$(gh api "/users/${APP_SLUG}%5Bbot%5D" --jq .id)" >> "$GITHUB_OUTPUT"
62+
63+
- name: Attempt merge
64+
id: merge
65+
env:
66+
SOURCE: ${{ needs.resolve-branches.outputs.source }}
67+
TARGET: ${{ needs.resolve-branches.outputs.target }}
68+
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
69+
USER_ID: ${{ steps.get-user-id.outputs.user-id }}
70+
run: |
71+
git config user.name "${APP_SLUG}[bot]"
72+
git config user.email "${USER_ID}+${APP_SLUG}[bot]@users.noreply.github.com"
73+
74+
git fetch origin "$SOURCE"
75+
76+
if git merge-base --is-ancestor "origin/$SOURCE" HEAD; then
77+
echo "result=up-to-date" >> "$GITHUB_OUTPUT"
78+
echo "::notice::$TARGET is already up to date with $SOURCE"
79+
elif git merge "origin/$SOURCE" --no-edit; then
80+
echo "result=clean" >> "$GITHUB_OUTPUT"
81+
else
82+
git merge --abort
83+
echo "result=conflict" >> "$GITHUB_OUTPUT"
84+
fi
85+
86+
# App token is used for the push (not GITHUB_TOKEN) so that
87+
# the push triggers downstream workflow runs for the cascade.
88+
- name: Push merge
89+
if: steps.merge.outputs.result == 'clean'
90+
env:
91+
TARGET: ${{ needs.resolve-branches.outputs.target }}
92+
run: git push origin "$TARGET"
93+
94+
# GITHUB_TOKEN is sufficient for PR operations — they don't need
95+
# to trigger downstream workflows, and it avoids burning App token quota.
96+
- name: Check for existing back-merge PR
97+
if: steps.merge.outputs.result == 'conflict'
98+
id: check-pr
99+
env:
100+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
101+
SOURCE: ${{ needs.resolve-branches.outputs.source }}
102+
TARGET: ${{ needs.resolve-branches.outputs.target }}
103+
run: |
104+
EXISTING_PR=$(gh pr list \
105+
--base "$TARGET" \
106+
--head "$SOURCE" \
107+
--state open \
108+
--json number \
109+
--jq '.[0].number // empty')
110+
111+
if [ -n "$EXISTING_PR" ]; then
112+
echo "found=true" >> "$GITHUB_OUTPUT"
113+
echo "pr_number=$EXISTING_PR" >> "$GITHUB_OUTPUT"
114+
echo "::notice::Found existing back-merge PR #$EXISTING_PR"
115+
else
116+
echo "found=false" >> "$GITHUB_OUTPUT"
117+
fi
118+
119+
- name: Create back-merge PR
120+
if: steps.merge.outputs.result == 'conflict' && steps.check-pr.outputs.found != 'true'
121+
env:
122+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
123+
SOURCE: ${{ needs.resolve-branches.outputs.source }}
124+
TARGET: ${{ needs.resolve-branches.outputs.target }}
125+
run: |
126+
gh pr create \
127+
--base "$TARGET" \
128+
--head "$SOURCE" \
129+
--title "chore(repo): Back-merge $SOURCE into $TARGET (conflicts)" \
130+
--body "Automatic back-merge of \`$SOURCE\` into \`$TARGET\` failed due to merge conflicts.
131+
132+
To resolve: check out \`$TARGET\`, merge \`$SOURCE\`, resolve the conflicts, and push to \`$TARGET\`.
133+
134+
\`\`\`sh
135+
git checkout $TARGET
136+
git merge origin/$SOURCE
137+
# resolve conflicts
138+
git push origin $TARGET
139+
\`\`\`
140+
141+
> Created automatically by the back-merge workflow."
142+
143+
- name: Comment on existing back-merge PR
144+
if: steps.merge.outputs.result == 'conflict' && steps.check-pr.outputs.found == 'true'
145+
env:
146+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
147+
PR_NUMBER: ${{ steps.check-pr.outputs.pr_number }}
148+
SOURCE: ${{ needs.resolve-branches.outputs.source }}
149+
TARGET: ${{ needs.resolve-branches.outputs.target }}
150+
run: |
151+
gh pr comment "$PR_NUMBER" \
152+
--body "New conflicts detected during back-merge of \`$SOURCE\` into \`$TARGET\`. Please resolve the conflicts in this PR."

0 commit comments

Comments
 (0)