Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 138 additions & 0 deletions .github/workflows/backport.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
name: Backport PR

on:
pull_request:
types: [ labeled ]
Comment on lines +4 to +5
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using both pull_request and pull_request_target triggers is potentially dangerous. The pull_request_target event runs in the context of the base repository with write permissions, which can be exploited if the workflow checks out code from a fork. Since this workflow uses actions/checkout@v5 with a ref from the base repository (line 50) and fetches from head_repo (line 60), using only pull_request_target would be more appropriate and secure.

Suggested change
pull_request:
types: [ labeled ]

Copilot uses AI. Check for mistakes.
pull_request_target:
types: [ labeled ]

jobs:
backport:
if: |
startsWith(github.event.label.name, 'backport/3.') &&
github.event.action == 'labeled'
Comment on lines +12 to +13
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition checks github.event.action == 'labeled' redundantly since the workflow already triggers only on the labeled event type (lines 5 and 7). This check is unnecessary and can be removed for clarity.

Suggested change
startsWith(github.event.label.name, 'backport/3.') &&
github.event.action == 'labeled'
startsWith(github.event.label.name, 'backport/3.')

Copilot uses AI. Check for mistakes.
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write

steps:
- name: Extract version from label
id: extract
run: |
LABEL="${{ github.event.label.name }}"
if [[ "$LABEL" =~ ^backport/3\.([0-9]+)$ ]]; then
VERSION="3.${BASH_REMATCH[1]}"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "branch=release/$VERSION" >> "$GITHUB_OUTPUT"
else
echo "Error: Invalid label format '$LABEL'."
exit 1
fi

- name: Get PR info
id: pr_info
uses: actions/github-script@v8
with:
script: |
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: ${{ github.event.pull_request.number }}
});
core.setOutput('title', pr.data.title);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('head_repo', pr.data.head.repo.full_name);

- name: Checkout target release branch
uses: actions/checkout@v5
with:
ref: ${{ steps.extract.outputs.branch }}
fetch-depth: 0

- name: Fetch PR changes and merge
id: apply_changes
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

echo "Fetching changes from origin PR..."
git fetch "https://github.com/${{ steps.pr_info.outputs.head_repo }}" "${{ steps.pr_info.outputs.head_sha }}"
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fetching from an untrusted fork repository (head_repo) could introduce malicious code. If a PR comes from a fork, this fetches commits from that fork's repository. Consider restricting backports to only work when the PR is from the same repository or add validation to ensure head_repo matches the current repository.

Copilot uses AI. Check for mistakes.

HEAD_SHA="${{ steps.pr_info.outputs.head_sha }}"

git log --pretty=format:"Co-authored-by: %an <%ae>" HEAD..$HEAD_SHA > coauthors.tmp

git log --pretty=format:"%(trailers:key=Co-authored-by,valueonly=false,separator=%n)" HEAD..$HEAD_SHA >> coauthors.tmp
Comment on lines +64 to +66
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The git log range syntax HEAD..$HEAD_SHA will show commits reachable from HEAD_SHA that are not reachable from HEAD. Since HEAD is checked out to the target release branch and HEAD_SHA is from the source PR, this range is likely empty or incorrect. The correct syntax should be $HEAD_SHA alone (to show all commits in that SHA) or use a proper merge-base to find the common ancestor.

Suggested change
git log --pretty=format:"Co-authored-by: %an <%ae>" HEAD..$HEAD_SHA > coauthors.tmp
git log --pretty=format:"%(trailers:key=Co-authored-by,valueonly=false,separator=%n)" HEAD..$HEAD_SHA >> coauthors.tmp
git log --pretty=format:"Co-authored-by: %an <%ae>" "$HEAD_SHA" > coauthors.tmp
git log --pretty=format:"%(trailers:key=Co-authored-by,valueonly=false,separator=%n)" "$HEAD_SHA" >> coauthors.tmp

Copilot uses AI. Check for mistakes.

Comment on lines +64 to +67
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the previous git log command - the range HEAD..$HEAD_SHA is incorrect. Additionally, this command appends to the same file, potentially duplicating co-author information. Consider using a single git log command or ensuring proper deduplication.

Suggested change
git log --pretty=format:"Co-authored-by: %an <%ae>" HEAD..$HEAD_SHA > coauthors.tmp
git log --pretty=format:"%(trailers:key=Co-authored-by,valueonly=false,separator=%n)" HEAD..$HEAD_SHA >> coauthors.tmp
git log --pretty=format:"Co-authored-by: %an <%ae>%n%(trailers:key=Co-authored-by,valueonly=false,separator=%n)" "$HEAD_SHA"^! > coauthors.tmp

Copilot uses AI. Check for mistakes.
COAUTHORS=$(cat coauthors.tmp | sort | uniq | grep -v "github-actions\[bot\]" | sed '/^$/d')
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using cat to pipe to other commands is an unnecessary use of cat (UUOC). The command can be simplified to sort coauthors.tmp | uniq | grep -v 'github-actions\[bot\]' | sed '/^$/d' for better efficiency.

Suggested change
COAUTHORS=$(cat coauthors.tmp | sort | uniq | grep -v "github-actions\[bot\]" | sed '/^$/d')
COAUTHORS=$(sort coauthors.tmp | uniq | grep -v "github-actions\[bot\]" | sed '/^$/d')

Copilot uses AI. Check for mistakes.

rm coauthors.tmp

set +e

git merge --squash "$HEAD_SHA" --no-edit
MERGE_EXIT_CODE=$?

set -e

if [ $MERGE_EXIT_CODE -ne 0 ]; then
echo "has_conflicts=true" >> "$GITHUB_OUTPUT"
COMMIT_PREFIX="[Conflict] "
else
echo "has_conflicts=false" >> "$GITHUB_OUTPUT"
COMMIT_PREFIX=""
fi

git add -A

if git diff --cached --quiet; then
echo "No changes to commit. Skipping."
exit 0
fi
Comment on lines +89 to +92
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When this condition is true and the script exits with code 0, subsequent steps (Push backport branch and Create Backport Pull Request) will still execute because the step itself succeeded. This will cause failures in later steps when trying to push a branch that doesn't exist. Consider using continue-on-error with proper conditional checks in subsequent steps, or setting an output flag to skip later steps.

Copilot uses AI. Check for mistakes.

COMMIT_MSG="${COMMIT_PREFIX}[release/${{ steps.extract.outputs.version }}] ${{ steps.pr_info.outputs.title }}"

if [ -n "$COAUTHORS" ]; then
printf "%s\n\n%s" "$COMMIT_MSG" "$COAUTHORS" > commit_msg.txt
else
printf "%s" "$COMMIT_MSG" > commit_msg.txt
fi

git commit -F commit_msg.txt

- name: Push backport branch
id: push
run: |
BACKPORT_BRANCH="pr-backport/${{ steps.extract.outputs.version }}/pr-${{ github.event.pull_request.number }}"
git push origin HEAD:"$BACKPORT_BRANCH" --force
echo "backport_branch=$BACKPORT_BRANCH" >> "$GITHUB_OUTPUT"

- name: Create Backport Pull Request
uses: actions/github-script@v8
with:
script: |
const { owner, repo } = context.repo;
const originalPrNumber = ${{ github.event.pull_request.number }};
const version = "${{ steps.extract.outputs.version }}";
const hasConflicts = "${{ steps.apply_changes.outputs.has_conflicts }}" === "true";

let title = `[release/${version}] ${{ steps.pr_info.outputs.title }}`;
Comment on lines +119 to +120
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The title interpolation uses GitHub Actions expression syntax inside a JavaScript template literal, which won't work correctly. The value ${{ steps.pr_info.outputs.title }} should be extracted as a variable or passed differently. Consider using const title = core.getInput('title') with the value passed as an input parameter, or construct the full title in the shell script before calling this step.

Suggested change
let title = `[release/${version}] ${{ steps.pr_info.outputs.title }}`;
const prTitle = "${{ steps.pr_info.outputs.title }}";
let title = `[release/${version}] ${prTitle}`;

Copilot uses AI. Check for mistakes.
if (hasConflicts) {
title = `[Conflict] ${title}`;
}

let body = `[Backport] #${originalPrNumber} -> \`${{ steps.extract.outputs.branch }}\`\n\n`;
Comment on lines +119 to +125
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the title - GitHub Actions expression syntax cannot be directly used inside JavaScript template literals. The value ${{ steps.extract.outputs.branch }} needs to be passed as a proper variable or input parameter.

Suggested change
let title = `[release/${version}] ${{ steps.pr_info.outputs.title }}`;
if (hasConflicts) {
title = `[Conflict] ${title}`;
}
let body = `[Backport] #${originalPrNumber} -> \`${{ steps.extract.outputs.branch }}\`\n\n`;
const originalTitle = "${{ steps.pr_info.outputs.title }}";
const targetBranch = "${{ steps.extract.outputs.branch }}";
let title = `[release/${version}] ${originalTitle}`;
if (hasConflicts) {
title = `[Conflict] ${title}`;
}
let body = `[Backport] #${originalPrNumber} -> \`${targetBranch}\`\n\n`;

Copilot uses AI. Check for mistakes.

if (hasConflicts) {
body += `> Has Merge Conflicts\n\n`;
}

const { data: pr } = await github.rest.pulls.create({
owner,
repo,
title,
head: "${{ steps.push.outputs.backport_branch }}",
Copy link

Copilot AI Jan 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GitHub Actions expressions in the script block of actions/github-script are evaluated before the JavaScript runs, but referencing step outputs this way inside JavaScript object literals may not work as expected. Consider extracting this value as a const variable at the beginning of the script block for clarity and correctness.

Copilot uses AI. Check for mistakes.
base: "${{ steps.extract.outputs.branch }}",
body
});