--- /dev/null
+name: Backport to stable
+permissions:
+ contents: read
+ pull-requests: write
+
+on:
+ push:
+ branches:
+ - dev
+
+jobs:
+ backport:
+ name: Backport PRs with 'backport-to-stable' label to stable
+ runs-on: ubuntu-latest
+ if: github.event.commits[0].distinct == true
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0 # Needed for full git history
+
+ - name: Get merged PR info
+ id: prinfo
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const pr = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'closed',
+ base: 'dev',
+ sort: 'updated',
+ direction: 'desc',
+ per_page: 10
+ });
+ const merged = pr.data.find(p => p.merge_commit_sha === context.payload.head_commit.id);
+ if (!merged) return core.setFailed('No merged PR found for this commit.');
+ core.setOutput('pr_number', merged.number);
+ core.setOutput('pr_title', merged.title);
+ core.setOutput('pr_labels', merged.labels.map(l => l.name).join(','));
+ core.setOutput('merge_commit_sha', merged.merge_commit_sha);
+
+ - name: Check for backport-to-stable label
+ id: checklabel
+ run: |
+ echo "PR labels: ${{ steps.prinfo.outputs.pr_labels }}"
+ if [[ "${{ steps.prinfo.outputs.pr_labels }}" != *"backport-to-stable"* ]]; then
+ echo "No backport-to-stable label, skipping."
+ exit 0
+ fi
+
+ - name: Set up Git user
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create or update backport branch
+ id: create_or_update_backport_branch
+ run: |
+ git fetch origin stable --tags
+ latest_tag=$(git tag --merged origin/stable --sort=-v:refname | head -1)
+ if [[ -z "$latest_tag" ]]; then
+ echo "No tags found on stable branch" >&2
+ exit 1
+ fi
+ version="$latest_tag"
+ IFS='.' read -r major minor patch <<< "$version"
+ next_patch=$((patch + 1))
+ next_version="$major.$minor.$next_patch"
+ branch_name="backport/$next_version"
+ git fetch origin $branch_name || true
+ if git show-ref --verify --quiet refs/remotes/origin/$branch_name; then
+ git checkout -B $branch_name origin/$branch_name
+ else
+ git checkout -b $branch_name origin/stable
+ fi
+ echo "branch_name=$branch_name" >> $GITHUB_OUTPUT
+
+ - name: Cherry-pick commit
+ run: |
+ git cherry-pick ${{ steps.prinfo.outputs.merge_commit_sha }} || {
+ echo 'Cherry-pick failed, please resolve conflicts manually.'
+ exit 1
+ }
+
+ - name: Push backport branch
+ run: |
+ git push origin ${{ steps.create_or_update_backport_branch.outputs.branch_name }}:${{ steps.create_or_update_backport_branch.outputs.branch_name }} --force
+
+ - name: Create or update backport PR with cherry-picked commits
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const { pr_number } = process.env;
+ const next_patch_version = process.env.next_patch_version;
+ const branch = process.env.branch_name;
+ const cherry_commit = process.env.cherry_commit;
+ const prs = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'open',
+ head: `${context.repo.owner}:${branch}`,
+ base: 'stable'
+ });
+ const commit_url = `https://github.com/${context.repo.owner}/${context.repo.repo}/commit/${cherry_commit}`;
+ const commit_item = `- [${cherry_commit.substring(0,7)}](${commit_url})`;
+ if (prs.data.length === 0) {
+ // Create new PR with initial commit in body
+ await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: `[Backport to stable] ${next_patch_version}`,
+ head: branch,
+ base: 'stable',
+ body: `Automated backport PR stable release ${next_patch_version} with cherry-picked commits:\n${commit_item}`
+ });
+ } else {
+ // Update PR body to append new commit if not already present
+ const pr = prs.data[0];
+ let body = pr.body || '';
+ if (!body.includes(commit_item)) {
+ // Try to find the start of the list
+ const listMatch = body.match(/(cherry-picked commits:\n)([\s\S]*)/);
+ if (listMatch) {
+ // Append to existing list
+ const before = listMatch[1];
+ const list = listMatch[2].trim();
+ const newList = list + '\n' + commit_item;
+ body = body.replace(/(cherry-picked commits:\n)([\s\S]*)/, before + newList);
+ } else {
+ // Add new list
+ body = body.trim() + `\n\nCherry-picked commits:\n${commit_item}`;
+ }
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: pr.number,
+ body
+ });
+ }
+ }
+ env:
+ pr_number: ${{ steps.prinfo.outputs.pr_number }}
+ next_patch_version: ${{ steps.nextver.outputs.next_patch_version }}
+ branch_name: ${{ steps.create_or_update_backport_branch.outputs.branch_name }}
+ cherry_commit: ${{ steps.prinfo.outputs.merge_commit_sha }}