Skip to content

Commit 2fa859f

Browse files
committed
Add backport PR workflow and auto-label creation
1 parent cb28830 commit 2fa859f

File tree

7 files changed

+364
-66
lines changed

7 files changed

+364
-66
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: 'Backport PR'
2+
description: 'Backports a PR to a target branch'
3+
inputs:
4+
pull-request:
5+
required: true
6+
target-branch:
7+
required: true
8+
repository:
9+
required: true
10+
is-draft:
11+
default: false
12+
type: boolean
13+
outputs:
14+
pr-url:
15+
value: ${{ steps.run.outputs.pr-url }}
16+
runs:
17+
using: "composite"
18+
steps:
19+
- name: Run it
20+
id: run
21+
shell: bash
22+
run: |
23+
${{ github.action_path }}/backport.sh "$PULL_REQUEST" "$TARGET_BRANCH" "$REPO"
24+
env:
25+
PULL_REQUEST: ${{ inputs.pull-request }}
26+
TARGET_BRANCH: ${{ inputs.target-branch }}
27+
REPO: ${{ inputs.repository }}
28+
IS_DRAFT: ${{ inputs.is-draft }}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/bin/sh
2+
3+
set -e
4+
5+
PULL_REQUEST=$1
6+
TARGET_BRANCH=$2
7+
REPO=$3
8+
9+
IS_DRAFT=${IS_DRAFT:-false}
10+
if [ "$IS_DRAFT" = "true" ]; then
11+
DRAFT_FLAG="--draft"
12+
fi
13+
14+
if [ -z "$PULL_REQUEST" ] || [ -z "$TARGET_BRANCH" ] || [ -z "$REPO" ]; then
15+
echo "Usage: $0 <PR number> <target branch> <owner/repo>" 1>&2
16+
exit 1
17+
fi
18+
19+
GITHUB_TRIGGERING_ACTOR=${GITHUB_TRIGGERING_ACTOR:-}
20+
21+
repo_name=$(echo "$REPO" | cut -d/ -f2)
22+
repo_owner=$(echo "$REPO" | cut -d/ -f1)
23+
24+
target_slug=$(echo "$TARGET_BRANCH" | sed "s|/|-|")
25+
branch_name="backport-$PULL_REQUEST-$target_slug-$$"
26+
27+
pr_link=https://github.com/$REPO/pull/$PULL_REQUEST
28+
pr_number=$PULL_REQUEST
29+
30+
# Separate calls because otherwise it was creating trouble.. can probably be fixed
31+
old_title=$(gh pr view "$pr_number" --json title --jq '.title')
32+
old_body=$(gh pr view "$pr_number" --json body --jq '.body')
33+
34+
git checkout -b "$branch_name" "origin/$TARGET_BRANCH"
35+
36+
committed_something=""
37+
38+
failed_commit=""
39+
skipped_commits=""
40+
41+
for commit in $(GH_PAGER= gh pr view "$pr_number" --json commits --jq '.commits[].oid'); do
42+
if [ -n "$failed_commit" ]; then
43+
skipped_commits="$skipped_commits
44+
$commit"
45+
continue
46+
fi
47+
# Those commits might be orphaned so we attempt to fetch them
48+
git fetch origin "$commit:refs/remotes/origin/orphaned-commit"
49+
50+
if git cherry-pick --allow-empty "$commit"; then
51+
committed_something="true"
52+
else
53+
echo "Cherry-pick failed, skipping" 1>&2
54+
git cherry-pick --abort
55+
failed_commit="$commit"
56+
fi
57+
done
58+
59+
if [ -z "$committed_something" ]; then
60+
# Github won't allow us to create a PR without any changes so we're making an empty commit here
61+
git commit --allow-empty -m "Please amend this commit"
62+
fi
63+
64+
git push -u origin "$branch_name"
65+
66+
generated_by=""
67+
if [ -n "$GITHUB_TRIGGERING_ACTOR" ]; then
68+
generated_by=$(cat <<EOF
69+
The workflow was triggered by @$GITHUB_TRIGGERING_ACTOR.
70+
EOF
71+
)
72+
fi
73+
74+
failed=""
75+
if [ -n "$failed_commit" ]; then
76+
failed=$(cat <<EOF
77+
Cherry-picking failed, here are the list of commits that are missing:
78+
79+
\`\`\`
80+
$failed_commit
81+
$skipped_commits
82+
\`\`\`
83+
EOF
84+
)
85+
fi
86+
87+
title=$(echo "[$TARGET_BRANCH] $old_title")
88+
body=$(cat <<EOF
89+
**Backport**
90+
91+
Backport of $pr_link
92+
93+
You can make changes to this PR with the following command:
94+
95+
\`\`\`
96+
git clone https://github.com/$REPO
97+
cd $repo_name
98+
git switch $branch_name
99+
\`\`\`
100+
101+
$failed
102+
103+
$generated_by
104+
105+
---
106+
107+
$old_body
108+
EOF
109+
)
110+
111+
PR_URL=$(gh pr create \
112+
--title "$title" \
113+
--body "$body" \
114+
--repo "$REPO" \
115+
--head "$(echo $REPO | cut -d/ -f1):$branch_name" \
116+
--base "$TARGET_BRANCH" \
117+
$DRAFT_FLAG)
118+
echo "Created PR at $PR_URL"
119+
echo "pr-url=$PR_URL" >> $GITHUB_OUTPUT

.github/workflows/backport-pr.yaml

Lines changed: 0 additions & 66 deletions
This file was deleted.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: Backport label
2+
3+
on:
4+
pull_request:
5+
types: [labeled]
6+
branches:
7+
- main
8+
- 'release/**'
9+
10+
jobs:
11+
backport-label:
12+
permissions:
13+
# Necessary to add cherry-pick-done/* label
14+
pull-requests: write
15+
# Necessary to create the PR
16+
#
17+
# NOTE: We'll want to switch to an app token once we have one.
18+
# Reason: GHA is designed to not trigger other workflows when creating a PR
19+
# within a workflow run using the workflow run token.
20+
#
21+
# Temporary workaround is to close/re-open the PR in the UI. This will trigger
22+
# the pull_request event.
23+
contents: write
24+
25+
# For now, only run if the PR is merged
26+
if: github.event.pull_request.merged == true && startsWith(github.event.label.name, 'cherry-pick/')
27+
uses: rancher/steve/.github/workflows/cherry-pick-from-labels.yaml@main
28+
with:
29+
label-added: ${{ github.event.label.name }}
30+
all-labels-json: ${{ toJSON(github.event.pull_request.labels.*.name) }}
31+
pr-number: ${{ github.event.pull_request.number }}
32+
git-user-email: "github-actions[bot]"
33+
git-user-name: "41898282+github-actions[bot]@users.noreply.github.com"
34+
secrets:
35+
token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: Auto-generate cherry-pick labels
2+
3+
on:
4+
workflow_dispatch:
5+
create:
6+
7+
jobs:
8+
generate-cherry-pick-labels:
9+
permissions:
10+
# Necessary to create/edit labels
11+
issues: write
12+
uses: rancher/steve/.github/workflows/generate-cherry-pick-labels.yaml@main
13+
with:
14+
branch-filter-regex: "^main|release/.*"
15+
secrets:
16+
token: ${{ secrets.GITHUB_TOKEN }}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
name: Cherry-pick a PR using labels
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
label-added:
7+
description: "Create the labels in this repository"
8+
required: true
9+
type: string
10+
all-labels-json:
11+
description: "List of all labels for the PR"
12+
required: true
13+
type: string
14+
pr-number:
15+
description: ""
16+
required: true
17+
type: string
18+
git-user-email:
19+
description: ""
20+
required: true
21+
type: string
22+
git-user-name:
23+
description: ""
24+
required: true
25+
type: string
26+
skip-membership-check:
27+
default: false
28+
type: boolean
29+
secrets:
30+
token:
31+
required: true
32+
33+
jobs:
34+
cherry-pick-pr:
35+
runs-on: ubuntu-latest
36+
steps:
37+
- name: Check org membership
38+
id: membership
39+
env:
40+
GH_TOKEN: ${{ secrets.token }}
41+
SKIP: ${{ inputs.skip-membership-check }}
42+
run: |
43+
if [ "$SKIP" = "true" ]; then
44+
echo "is_member=true" >> $GITHUB_OUTPUT
45+
else
46+
if gh api orgs/${GITHUB_REPOSITORY_OWNER}/members --paginate | jq -e --arg GITHUB_ACTOR "$GITHUB_ACTOR" '.[] | select(.login == $GITHUB_ACTOR)' > /dev/null; then
47+
echo "${GITHUB_ACTOR} is a member"
48+
echo "is_member=true" >> $GITHUB_OUTPUT
49+
else
50+
echo "${GITHUB_ACTOR} is not a member" >> $GITHUB_STEP_SUMMARY
51+
echo "is_member=false" >> $GITHUB_OUTPUT
52+
fi
53+
fi
54+
55+
- if: ${{ steps.membership.outputs.is_member == 'true' }}
56+
id: detect
57+
run: |
58+
has_label() {
59+
labels=$(printf '%s' "$LABELS" | jq -r '.[]')
60+
for l in $labels; do
61+
if [ "$l" = "$1" ]; then
62+
return 0
63+
fi
64+
done
65+
return 1
66+
}
67+
68+
target="$(echo "$CHERRY_PICK_LABEL" | sed 's|cherry-pick/||')"
69+
if has_label "cherry-pick-done/$target"; then
70+
echo "Skipping $label ($target) because already cherry-picked"
71+
echo "target-branch=" >> $GITHUB_OUTPUT
72+
return 0
73+
fi
74+
75+
echo "Need to create cherry-pick for $label ($target)"
76+
echo "target-branch=$target" >> $GITHUB_OUTPUT
77+
env:
78+
CHERRY_PICK_LABEL: ${{ inputs.label-added }}
79+
LABELS: ${{ inputs.all-labels-json }}
80+
81+
- if: ${{ steps.detect.outputs.target-branch != '' }}
82+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5
83+
with:
84+
token: ${{ secrets.token }}
85+
persist-credentials: true
86+
fetch-depth: 0
87+
88+
- if: ${{ steps.detect.outputs.target-branch != '' }}
89+
run: |
90+
git config --global user.email "$EMAIL"
91+
git config --global user.name "$NAME"
92+
env:
93+
EMAIL: ${{ inputs.git-user-email }}
94+
NAME: ${{ inputs.git-user-name }}
95+
96+
- if: ${{ steps.detect.outputs.target-branch != '' }}
97+
uses: .github/actions/backport
98+
id: backport
99+
with:
100+
pull-request: ${{ inputs.pr-number }}
101+
target-branch: ${{ steps.detect.outputs.target-branch }}
102+
repository: ${{ github.repository }}
103+
is-draft: false
104+
env:
105+
GH_TOKEN: ${{ secrets.token }}
106+
107+
- if: ${{ steps.detect.outputs.target-branch != '' }}
108+
run: |
109+
gh pr edit $PR_NUMBER -R "$REPO" --add-label "cherry-pick-done/$TARGET"
110+
gh pr comment $PR_NUMBER -R "$REPO" --body "Cherry-pick requested from @$GITHUB_ACTOR. Created the following cherry-pick PR: $BACKPORT_PR_URL. See run for more details at: $RUN_URL"
111+
env:
112+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
113+
BACKPORT_PR_URL: ${{ steps.backport.outputs.pr-url }}
114+
TARGET: ${{ steps.detect.outputs.target-branch }}
115+
GH_TOKEN: ${{ secrets.token }}
116+
PR_NUMBER: ${{ inputs.pr-number }}
117+
REPO: ${{ github.repository }}

0 commit comments

Comments
 (0)