mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-08-29 04:07:40 +00:00
* Support bare version shorthand in /backport comments Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> * Auto-rename changelog filename in backport PRs to match new PR number Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> * Copy kind/changelog-not-required label to backport PRs Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> * Ensure DCO signoff on every commit in backport PRs Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: kaovilai <11228024+kaovilai@users.noreply.github.com>
261 lines
11 KiB
YAML
261 lines
11 KiB
YAML
name: Backport merged pull request
|
|
|
|
# Automates cherry-picking merged PRs onto release branches.
|
|
#
|
|
# Pre-merge (open PR):
|
|
# An authorized /backport or /cherrypick comment adds one `backport <branch>`
|
|
# label per requested branch. These labels are then picked up automatically
|
|
# when the PR is merged (see the pull_request_target: closed trigger below).
|
|
#
|
|
# Post-merge (merged PR):
|
|
# - Label a PR with e.g. `backport release-1.17` before merging; the label
|
|
# triggers the backport automatically when the PR closes as merged.
|
|
# - Comment `/backport release-1.17` or `/cherrypick release-1.17` on an
|
|
# already-merged PR to create the backport PR immediately.
|
|
#
|
|
# In both cases multiple target branches can be space-delimited in a comment:
|
|
# /backport release-1.17 release-1.18
|
|
#
|
|
# As a shorthand, a bare release version (e.g. `1.17`) is automatically
|
|
# expanded to the corresponding `release-1.17` branch, so `/backport 1.17`
|
|
# and `/backport release-1.17` are equivalent. This works generically for
|
|
# any `X.Y` version, e.g. `/backport 1.18 1.19`.
|
|
#
|
|
# The cherry-picked commit(s) carry over the original PR's changelog file
|
|
# (changelogs/unreleased/<source_pr>-<user>), which no longer matches the
|
|
# backport PR's own number. After the backport PR is created, its changelog
|
|
# file is automatically renamed to <backport_pr>-<user> so that
|
|
# hack/changelog-check.sh passes and release notes cite the correct PR.
|
|
#
|
|
# If the source PR is labeled `kind/changelog-not-required` (i.e. it has no
|
|
# changelog file), that label is copied to the backport PR so it isn't
|
|
# flagged as missing a changelog either.
|
|
#
|
|
# Every commit on a backport branch (the cherry-picked commit(s), even from
|
|
# the original author, plus the changelog rename commit) is re-signed with
|
|
# the bot's Signed-off-by trailer via `git rebase --signoff`, so the DCO
|
|
# check always passes regardless of whether the original commit had one.
|
|
#
|
|
# See: https://github.com/velero-io/velero/issues/9603
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [closed]
|
|
issue_comment:
|
|
types: [created]
|
|
|
|
permissions: {}
|
|
|
|
# Shared condition for authorized /backport or /cherrypick comments.
|
|
# Used by both jobs below to avoid duplicating the gate logic.
|
|
env:
|
|
AUTHORIZED_COMMENT: >-
|
|
${{
|
|
github.event_name == 'issue_comment' &&
|
|
github.event.issue.pull_request != '' &&
|
|
github.event.comment.user.id != 97796249 &&
|
|
contains(
|
|
fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'),
|
|
github.event.comment.author_association
|
|
) &&
|
|
(
|
|
startsWith(github.event.comment.body, '/backport') ||
|
|
startsWith(github.event.comment.body, '/cherrypick')
|
|
)
|
|
}}
|
|
|
|
jobs:
|
|
# ── Pre-merge: convert a /backport or /cherrypick comment into labels ───────
|
|
# When the PR is still open the backport-action cannot run (it requires a
|
|
# merged commit). Instead, add one `backport <branch>` label per requested
|
|
# branch so that the post-merge job picks them up automatically on close.
|
|
label-for-backport:
|
|
name: Label PR for deferred backport
|
|
# Run only when an authorized command is posted on an *open* (unmerged) PR.
|
|
if: >
|
|
github.repository == 'velero-io/velero' &&
|
|
github.event_name == 'issue_comment' &&
|
|
github.event.issue.pull_request != '' &&
|
|
github.event.issue.state == 'open' &&
|
|
github.event.comment.user.id != 97796249 &&
|
|
contains(
|
|
fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'),
|
|
github.event.comment.author_association
|
|
) &&
|
|
(
|
|
startsWith(github.event.comment.body, '/backport') ||
|
|
startsWith(github.event.comment.body, '/cherrypick')
|
|
)
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
issues: write # apply labels to the PR (PRs share the issues API)
|
|
steps:
|
|
- name: Parse branches and apply labels
|
|
env:
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
PR_NUMBER: ${{ github.event.issue.number }}
|
|
run: |
|
|
# Extract branch names from the first line of the comment.
|
|
# Strip the /backport or /cherrypick prefix; what remains is a
|
|
# space-delimited list of target branch names.
|
|
line=$(printf '%s' "$COMMENT_BODY" | head -n1 | tr -d '\r')
|
|
branches=$(printf '%s' "$line" | sed -E 's#^/(backport|cherrypick)[[:space:]]*##')
|
|
|
|
if [ -z "$branches" ]; then
|
|
echo "No target branches specified in comment; nothing to label."
|
|
exit 0
|
|
fi
|
|
|
|
for branch in $branches; do
|
|
# Shorthand: a bare version like "1.17" expands to "release-1.17".
|
|
if [[ "$branch" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
|
branch="release-${branch}"
|
|
fi
|
|
label="backport ${branch}"
|
|
echo "Applying label: '${label}'"
|
|
# Create the label if it does not exist yet (idempotent).
|
|
gh label create "${label}" --repo "${REPO}" --color "0075ca" \
|
|
--description "Backport to ${branch}" 2>/dev/null || true
|
|
gh issue edit "${PR_NUMBER}" --repo "${REPO}" --add-label "${label}"
|
|
done
|
|
|
|
# ── Post-merge: create backport PRs ─────────────────────────────────────────
|
|
backport:
|
|
name: Backport pull request
|
|
# Exclude comments from the backport-action bot (user id 97796249) to prevent
|
|
# recursive triggers. The bot does not post /backport commands, so startsWith
|
|
# already blocks recursion; the id check is defense in depth.
|
|
if: >
|
|
github.repository == 'velero-io/velero' &&
|
|
(
|
|
(
|
|
github.event_name == 'pull_request_target' &&
|
|
github.event.pull_request.merged &&
|
|
contains(toJSON(github.event.pull_request.labels.*.name), '"backport ')
|
|
) || (
|
|
github.event_name == 'issue_comment' &&
|
|
github.event.issue.pull_request != '' &&
|
|
github.event.issue.state == 'closed' &&
|
|
github.event.comment.user.id != 97796249 &&
|
|
contains(
|
|
fromJSON('["OWNER", "MEMBER", "COLLABORATOR"]'),
|
|
github.event.comment.author_association
|
|
) &&
|
|
(
|
|
startsWith(github.event.comment.body, '/backport') ||
|
|
startsWith(github.event.comment.body, '/cherrypick')
|
|
)
|
|
)
|
|
)
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write # push backport branches and comment
|
|
pull-requests: write # open backport PRs
|
|
steps:
|
|
- name: Parse target branches from comment
|
|
id: parse
|
|
if: github.event_name == 'issue_comment'
|
|
env:
|
|
COMMENT_BODY: ${{ github.event.comment.body }}
|
|
run: |
|
|
# First line only; strip /backport or /cherrypick prefix.
|
|
# Remaining text is a space-delimited list of target branches
|
|
# (may be empty, falls back to labels).
|
|
line=$(printf '%s' "$COMMENT_BODY" | head -n1 | tr -d '\r')
|
|
branches=$(printf '%s' "$line" | sed -E 's#^/(backport|cherrypick)[[:space:]]*##')
|
|
|
|
normalized=""
|
|
for branch in $branches; do
|
|
# Shorthand: a bare version like "1.17" expands to "release-1.17".
|
|
if [[ "$branch" =~ ^[0-9]+\.[0-9]+$ ]]; then
|
|
branch="release-${branch}"
|
|
fi
|
|
normalized="${normalized}${normalized:+ }${branch}"
|
|
done
|
|
echo "branches=${normalized}" >> "$GITHUB_OUTPUT"
|
|
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Create backport pull requests
|
|
id: backport
|
|
# Pin to commit SHA: workflow has contents/pull-requests write.
|
|
uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6.0
|
|
with:
|
|
# Labels like `backport release-1.17` select the target branch.
|
|
label_pattern: '^backport ([^ ]+)$'
|
|
# Carry over `kind/changelog-not-required` from the source PR so
|
|
# backport PRs of changelog-exempt changes aren't flagged as missing one.
|
|
copy_labels_pattern: '^kind/changelog-not-required$'
|
|
# Prefer draft PRs with conflict markers over failing the job silently.
|
|
experimental: |
|
|
{
|
|
"conflict_resolution": "draft_commit_conflicts"
|
|
}
|
|
# Empty when triggered by merge labels; set when `/backport` or `/cherrypick` includes branches.
|
|
target_branches: ${{ steps.parse.outputs.branches }}
|
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
- name: Rename changelog file(s) and ensure DCO signoff
|
|
# The cherry-picked commit(s) still carry the source PR's changelog
|
|
# filename (e.g. changelogs/unreleased/9795-kaovilai), which no
|
|
# longer matches the new backport PR's number. Rename it on each
|
|
# created backport branch so hack/changelog-check.sh passes and the
|
|
# release notes cite the correct PR.
|
|
#
|
|
# Also ensure every commit on the backport branch passes the DCO
|
|
# check by re-signing it with the bot's Signed-off-by trailer via
|
|
# `git rebase --signoff`. This covers the cherry-picked commits
|
|
# (even when the original author's commit had no trailer) as well
|
|
# as the changelog rename commit added above; it preserves any
|
|
# existing Signed-off-by trailers rather than replacing them.
|
|
if: steps.backport.outputs.created_pull_numbers != ''
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
REPO: ${{ github.repository }}
|
|
SOURCE_PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number }}
|
|
CREATED_PR_NUMBERS: ${{ steps.backport.outputs.created_pull_numbers }}
|
|
run: |
|
|
git config user.name "github-actions[bot]"
|
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
|
|
|
shopt -s nullglob
|
|
for new_pr in $CREATED_PR_NUMBERS; do
|
|
if [ "$new_pr" = "$SOURCE_PR_NUMBER" ]; then
|
|
continue
|
|
fi
|
|
|
|
branch=$(gh pr view "$new_pr" --repo "$REPO" --json headRefName -q .headRefName)
|
|
base_branch=$(gh pr view "$new_pr" --repo "$REPO" --json baseRefName -q .baseRefName)
|
|
git fetch origin "$branch" "$base_branch"
|
|
git checkout -B "$branch" "origin/${branch}"
|
|
|
|
files=(changelogs/unreleased/"${SOURCE_PR_NUMBER}"-*)
|
|
if [ ${#files[@]} -gt 0 ]; then
|
|
for old_file in "${files[@]}"; do
|
|
suffix=$(basename "$old_file" | sed -E "s/^${SOURCE_PR_NUMBER}-//")
|
|
new_file="changelogs/unreleased/${new_pr}-${suffix}"
|
|
if [ "$old_file" != "$new_file" ]; then
|
|
git mv "$old_file" "$new_file"
|
|
fi
|
|
done
|
|
if ! git diff --cached --quiet; then
|
|
git commit -m "Rename changelog to match backport PR #${new_pr}"
|
|
fi
|
|
else
|
|
echo "No changelog file for PR ${SOURCE_PR_NUMBER} found on ${branch}; skipping rename."
|
|
fi
|
|
|
|
# Add the bot's Signed-off-by trailer to every commit ahead of
|
|
# the target branch (cherry-picked commits + the rename commit).
|
|
if ! git rebase --signoff "origin/${base_branch}"; then
|
|
echo "::error::git rebase --signoff failed for PR #${new_pr} on branch ${branch}; aborting rebase, branch left unchanged." >&2
|
|
git rebase --abort
|
|
exit 1
|
|
fi
|
|
git push --force-with-lease origin "HEAD:${branch}"
|
|
done
|