music-assistant-server

10.6 KBYML
backport-to-stable.yml
10.6 KB267 lines • yaml
1name: Backport to stable
2permissions:
3  contents: write
4  pull-requests: write
5
6on:
7  push:
8    branches:
9      - dev
10  pull_request_target:
11    types: [labeled]
12    branches:
13      - dev
14
15jobs:
16  backport:
17    name: Backport PRs with 'backport-to-stable' label to stable
18    runs-on: ubuntu-latest
19    if: |
20      (github.event_name == 'push' && github.event.commits[0].distinct == true) ||
21      (github.event_name == 'pull_request_target')
22    steps:
23      - name: Checkout repository
24        uses: actions/checkout@v7
25        with:
26          fetch-depth: 0  # Needed for full git history
27
28      - name: Determine workflow trigger and context
29        id: trigger
30        uses: actions/github-script@v9
31        with:
32          script: |
33            // Determine if this is a push or label event
34            const isPushEvent = context.eventName === 'push';
35            const isLabelEvent = context.eventName === 'pull_request_target';
36
37            core.setOutput('is_push_event', isPushEvent);
38            core.setOutput('is_label_event', isLabelEvent);
39
40            // Early exit for wrong label in label events
41            if (isLabelEvent) {
42              const labelName = context.payload.label?.name;
43              if (labelName !== 'backport-to-stable') {
44                console.log(`Label '${labelName}' is not backport-to-stable, skipping`);
45                process.exit(0);
46              }
47            }
48
49      - name: Get merged PR info
50        id: prinfo
51        uses: actions/github-script@v9
52        with:
53          script: |
54            const isPushEvent = '${{ steps.trigger.outputs.is_push_event }}' === 'true';
55            const isLabelEvent = '${{ steps.trigger.outputs.is_label_event }}' === 'true';
56
57            let merged;
58
59            if (isPushEvent) {
60              // Existing logic: Find PR from commit
61              const pr = await github.rest.pulls.list({
62                owner: context.repo.owner,
63                repo: context.repo.repo,
64                state: 'closed',
65                base: 'dev',
66                sort: 'updated',
67                direction: 'desc',
68                per_page: 10
69              });
70              merged = pr.data.find(p => p.merge_commit_sha === context.payload.head_commit.id);
71              if (!merged) return core.setFailed('No merged PR found for this commit.');
72            } else if (isLabelEvent) {
73              // New logic: Get PR from label event context
74              const pr = context.payload.pull_request;
75
76              // Verify PR is merged (exit gracefully if not)
77              if (!pr.merged_at) {
78                console.log('PR is not merged yet, skipping backport');
79                process.exit(0);
80              }
81
82              // Fetch full PR details to get merge commit SHA
83              const fullPR = await github.rest.pulls.get({
84                owner: context.repo.owner,
85                repo: context.repo.repo,
86                pull_number: pr.number
87              });
88              merged = fullPR.data;
89            }
90
91            core.setOutput('pr_number', merged.number);
92            core.setOutput('pr_title', merged.title);
93            core.setOutput('pr_labels', merged.labels.map(l => l.name).join(','));
94            core.setOutput('merge_commit_sha', merged.merge_commit_sha);
95
96      - name: Check for backport-to-stable label
97        id: checklabel
98        run: |
99          echo "PR labels: ${{ steps.prinfo.outputs.pr_labels }}"
100          if [[ "${{ steps.prinfo.outputs.pr_labels }}" == *"backport-to-stable"* ]]; then
101            echo "backport-to-stable label found, proceeding with backport."
102            echo "should_backport=true" >> $GITHUB_OUTPUT
103          else
104            echo "No backport-to-stable label found, skipping backport."
105            echo "should_backport=false" >> $GITHUB_OUTPUT
106          fi
107
108      - name: Set up Git user
109        if: steps.checklabel.outputs.should_backport == 'true'
110        run: |
111          git config user.name "github-actions[bot]"
112          git config user.email "github-actions[bot]@users.noreply.github.com"
113
114      - name: Calculate next patch version
115        if: steps.checklabel.outputs.should_backport == 'true'
116        id: nextver
117        run: |
118          git fetch origin stable --tags
119          # Filter out beta/rc tags and get only stable versions (e.g., 2.5.5, v2.5.5)
120          latest_tag=$(git tag --merged origin/stable --sort=-v:refname | grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' | head -1)
121          if [[ -z "$latest_tag" ]]; then
122            echo "No stable tags found on stable branch" >&2
123            exit 1
124          fi
125          echo "Latest stable tag: $latest_tag"
126
127          # Remove 'v' prefix if present
128          version="$latest_tag"
129          if [[ "$version" =~ ^v ]]; then
130            version="${version#v}"
131          fi
132
133          # Parse version components
134          if [[ "$version" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
135            major="${BASH_REMATCH[1]}"
136            minor="${BASH_REMATCH[2]}"
137            patch="${BASH_REMATCH[3]}"
138          else
139            echo "Invalid version format: $version" >&2
140            exit 1
141          fi
142
143          next_patch=$((patch + 1))
144          next_version="$major.$minor.$next_patch"
145
146          echo "Current version: $version"
147          echo "Next version: $next_version"
148          echo "next_patch_version=$next_version" >> $GITHUB_OUTPUT
149
150      - name: Create or update backport branch
151        if: steps.checklabel.outputs.should_backport == 'true'
152        id: create_or_update_backport_branch
153        run: |
154          next_version="${{ steps.nextver.outputs.next_patch_version }}"
155          branch_name="backport/$next_version"
156
157          echo "Creating/updating branch: $branch_name"
158
159          # Check if branch already exists on remote
160          git fetch origin $branch_name || true
161          if git show-ref --verify --quiet refs/remotes/origin/$branch_name; then
162            echo "Branch $branch_name already exists, checking out"
163            git checkout -B $branch_name origin/$branch_name
164          else
165            echo "Branch $branch_name does not exist, creating from stable"
166            git checkout -b $branch_name origin/stable
167          fi
168
169          echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
170
171      - name: Cherry-pick commit
172        if: steps.checklabel.outputs.should_backport == 'true'
173        run: |
174          # Check if commit is already in the branch to avoid redundant cherry-picks
175          if git log --format=%H | grep -q "^${{ steps.prinfo.outputs.merge_commit_sha }}$"; then
176            echo "Commit ${{ steps.prinfo.outputs.merge_commit_sha }} already exists in backport branch, skipping cherry-pick"
177            exit 0
178          fi
179
180          # Try cherry-pick with --empty=drop to handle redundant commits gracefully
181          if git cherry-pick --empty=drop ${{ steps.prinfo.outputs.merge_commit_sha }}; then
182            echo "Cherry-pick successful"
183          else
184            echo 'Cherry-pick failed, please resolve conflicts manually.'
185            exit 1
186          fi
187
188      - name: Push backport branch
189        if: steps.checklabel.outputs.should_backport == 'true'
190        run: |
191          git push origin ${{ steps.create_or_update_backport_branch.outputs.branch_name }}:${{ steps.create_or_update_backport_branch.outputs.branch_name }} --force
192
193      - name: Create or update backport PR with cherry-picked commits
194        if: steps.checklabel.outputs.should_backport == 'true'
195        uses: actions/github-script@v9
196        with:
197          script: |
198            const pr_number = process.env.pr_number;
199            const pr_title = process.env.pr_title;
200            const next_patch_version = process.env.next_patch_version;
201            const branch = process.env.branch_name;
202            const cherry_commit = process.env.cherry_commit;
203
204            console.log(`Processing backport for PR #${pr_number}: ${pr_title}`);
205            console.log(`Next patch version: ${next_patch_version}`);
206            console.log(`Branch: ${branch}`);
207            console.log(`Cherry-pick commit: ${cherry_commit}`);
208
209            const prs = await github.rest.pulls.list({
210              owner: context.repo.owner,
211              repo: context.repo.repo,
212              state: 'open',
213              head: `${context.repo.owner}:${branch}`,
214              base: 'stable'
215            });
216
217            const commit_url = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${cherry_commit}`;
218            const commit_item = `- [${cherry_commit.substring(0,7)}](${commit_url}) - ${pr_title} (#${pr_number})`;
219
220            if (prs.data.length === 0) {
221              // Create new PR with initial commit in body
222              console.log('Creating new backport PR');
223              await github.rest.pulls.create({
224                owner: context.repo.owner,
225                repo: context.repo.repo,
226                title: `[Backport to stable] ${next_patch_version}`,
227                head: branch,
228                base: 'stable',
229                body: `Automated backport PR for stable release ${next_patch_version} with cherry-picked commits:\n\n${commit_item}`
230              });
231            } else {
232              // Update PR body to append new commit if not already present
233              console.log('Updating existing backport PR');
234              const pr = prs.data[0];
235              let body = pr.body || '';
236
237              if (!body.includes(cherry_commit.substring(0,7))) {
238                // Try to find the start of the list (case-insensitive)
239                const listMatch = body.match(/(cherry-picked commits:\n\n)([\s\S]*)/i);
240                if (listMatch) {
241                  // Append to existing list
242                  const before = listMatch[1];
243                  const list = listMatch[2].trim();
244                  const newList = list + '\n' + commit_item;
245                  body = body.replace(/(cherry-picked commits:\n\n)([\s\S]*)/i, before + newList);
246                } else {
247                  // Add new list
248                  body = body.trim() + `\n\nCherry-picked commits:\n\n${commit_item}`;
249                }
250
251                await github.rest.pulls.update({
252                  owner: context.repo.owner,
253                  repo: context.repo.repo,
254                  pull_number: pr.number,
255                  body
256                });
257              } else {
258                console.log('Commit already exists in PR body, skipping update');
259              }
260            }
261        env:
262          pr_number: ${{ steps.prinfo.outputs.pr_number }}
263          pr_title: ${{ steps.prinfo.outputs.pr_title }}
264          next_patch_version: ${{ steps.nextver.outputs.next_patch_version }}
265          branch_name: ${{ steps.create_or_update_backport_branch.outputs.branch_name }}
266          cherry_commit: ${{ steps.prinfo.outputs.merge_commit_sha }}
267