Skip to content

Cherry-pick 78b3ad1fa44a2133c6345283fcad4fa8903f2fe5 from master to 3.0 #15

Cherry-pick 78b3ad1fa44a2133c6345283fcad4fa8903f2fe5 from master to 3.0

Cherry-pick 78b3ad1fa44a2133c6345283fcad4fa8903f2fe5 from master to 3.0 #15

Workflow file for this run

# Manually cherry-pick a commit from one branch into another and open a PR.
#
# Trigger it from the Actions tab (workflow_dispatch). Fill in the source branch,
# target branch, and the commit SHA, then run. The job:
# 1. fetches the target branch
# 2. validates that the commit exists on the source branch
# 3. creates a throwaway `cherry-pick-<sha>-to-<branch>` branch off the target
# 4. cherry-picks the commit onto it and pushes the branch
# 5. opens a PR from that branch into the target branch
#
# The PR is NOT auto-merged; merge it manually after review.
#
# The branch push and PR creation authenticate with the repository secret
# `CHERRY_PICK_PAT` (a personal access token owned by a trusted member). This makes
# the PR author a real collaborator instead of github-actions[bot], so CI runs
# on the new PR without requiring "Approve workflows to run". Create a PAT with
# `repo` scope (or fine-grained `contents: write` + `pull-requests: write`,
# scoped to this repository) and store it as `CHERRY_PICK_PAT`.
name: Cherry-pick Commit
run-name: Cherry-pick ${{ inputs.commit_id }} from ${{ inputs.from_branch }} to ${{ inputs.to_branch }}
on:
workflow_dispatch:
inputs:
from_branch:
description: 'Source branch (the commit is cherry-picked FROM this branch)'
required: true
type: choice
default: master
options:
- master
- 2.6
- 3.0
to_branch:
description: 'Target branch (the commit is cherry-picked INTO this branch)'
required: true
type: choice
default: 3.0
options:
- master
- 2.6
- 3.0
commit_id:
description: 'Full or abbreviated commit SHA to cherry-pick'
required: true
type: string
# Only one run per (from_branch, to_branch, commit_id) at a time, so two dispatches (or a
# rerun) for the same combo cannot race on the shared remote cherry-pick branch. A duplicate
# dispatch cancels the stale run; different combos still run in parallel.
concurrency:
group: cherry-pick-${{ github.event.inputs.from_branch }}-${{ github.event.inputs.to_branch }}-${{ github.event.inputs.commit_id }}
cancel-in-progress: true
# Least privilege: pushes and PR creation use the PAT (CHERRY_PICK_PAT); the
# GITHUB_TOKEN is only used by actions/checkout and local git ops, so read is enough.
permissions:
contents: read
jobs:
cherry-pick:
name: Cherry-pick and open PR
runs-on: ubuntu-latest
steps:
- name: Checkout target branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.to_branch }}
fetch-depth: 0
# do not persist the GITHUB_TOKEN extraheader into .git/config: it would take
# precedence over the PAT in the push URL and cause a 403 (contents: read)
persist-credentials: false
- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
- name: Validate inputs
run: |
if [ "$FROM" = "$TO" ]; then
echo "::error::from_branch and to_branch must be different"
exit 1
fi
git fetch origin "$FROM"
if ! git rev-parse --verify --quiet "$COMMIT^{commit}" >/dev/null; then
echo "::error::commit '$COMMIT' does not exist"
exit 1
fi
if ! git merge-base --is-ancestor "$COMMIT" "origin/$FROM"; then
echo "::error::commit '$COMMIT' is not on branch '$FROM'"
exit 1
fi
# HEAD is already checked out to the target branch, so use it instead of a
# remote-tracking ref (origin/$TO) which may not exist locally.
if git merge-base --is-ancestor "$COMMIT" HEAD; then
echo "::error::commit '$COMMIT' is already on branch '$TO'"
exit 1
fi
FULL_SHA=$(git rev-parse "$COMMIT")
# use git's --grep filter and capture the output: a `grep -q` early-exit would be
# killed by SIGPIPE under pipefail and misreport a match as failure.
if [ -n "$(git log HEAD --format=%B --grep="cherry picked from commit ${FULL_SHA}")" ]; then
echo "::error::commit '$COMMIT' was already cherry-picked onto '$TO' by a previous run"
exit 1
fi
env:
FROM: ${{ github.event.inputs.from_branch }}
TO: ${{ github.event.inputs.to_branch }}
COMMIT: ${{ github.event.inputs.commit_id }}
- name: Create cherry-pick branch
id: branch
run: |
SHORT_SHA=$(git rev-parse --short "$COMMIT")
# unique per run (GITHUB_RUN_ID): the plain name would be predictable, and deleting
# it would risk removing an unrelated branch that shares the name. Each run gets its
# own branch, so no remote deletion is needed. HEAD is already the target branch tip.
BRANCH="cherry-pick-${SHORT_SHA}-to-${TO}-${RUN_ID}"
git checkout -b "$BRANCH"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "short_sha=$SHORT_SHA" >> "$GITHUB_OUTPUT"
env:
TO: ${{ github.event.inputs.to_branch }}
COMMIT: ${{ github.event.inputs.commit_id }}
RUN_ID: ${{ github.run_id }}
- name: Cherry-pick the commit
run: |
PARENT_COUNT=$(git rev-list --parents -n 1 "$COMMIT" | awk '{print NF}')
if [ "$PARENT_COUNT" -gt 2 ]; then
echo "::warning::commit '$COMMIT' is a merge commit; cherry-picking with -m 1 (diff against first parent)"
CHERRY_PICK_ARGS="-x -m 1"
else
CHERRY_PICK_ARGS="-x"
fi
if ! git cherry-pick ${CHERRY_PICK_ARGS} "$COMMIT" 2>/tmp/cherry_pick_err; then
# capture instead of `git ls-files -u | grep -q .` to avoid SIGPIPE/pipefail
# misreporting once the conflict list exceeds the pipe buffer.
if [ -n "$(git ls-files -u)" ]; then
echo "::error::cherry-pick of '$COMMIT' has conflicts; a manual backport is required"
elif git diff --quiet; then
echo "::error::commit '$COMMIT' appears to already be applied on '$TO' (cherry-pick produced an empty change)"
else
echo "::error::cherry-pick of '$COMMIT' failed (neither a conflict nor an empty change)"
fi
cat /tmp/cherry_pick_err
git cherry-pick --abort 2>/dev/null || true
exit 1
fi
env:
COMMIT: ${{ github.event.inputs.commit_id }}
TO: ${{ github.event.inputs.to_branch }}
- name: Sign off for DCO
run: |
# `if` is a condition context, so the non-zero exit of grep when the
# sign-off is missing does not trip `set -e`/pipefail.
# If the source commit already carries the author's own DCO attestation,
# pass it through unchanged: the cherry-pick preserves the author, so the
# sign-off still matches and the workflow adds nothing.
AUTHOR_SIGN_OFF="Signed-off-by: $(git show -s --format='%an <%ae>' HEAD)"
if git log -1 --format='%B' | grep -Fq "$AUTHOR_SIGN_OFF"; then
exit 0
fi
# Otherwise the source commit was never DCO-signed. The cherry-picked commit
# is a NEW commit created by this workflow, so signing it as the original
# author would be an attestation they never made. Instead the operator
# (github.actor, who triggered the backport) attests DCO for this commit,
# and the git committer identity is aligned to the sign-off so DCO's
# author-or-committer rule matches.
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
git log -1 --format='%B' > /tmp/msg
printf '\nSigned-off-by: %s <%s@users.noreply.github.com>\n' "$GITHUB_ACTOR" "$GITHUB_ACTOR" >> /tmp/msg
git commit --amend -F /tmp/msg
- name: Push branch
env:
BRANCH: ${{ steps.branch.outputs.branch }}
PUSH_TOKEN: ${{ secrets.CHERRY_PICK_PAT }}
run: |
if [ -z "$PUSH_TOKEN" ]; then
echo "::error::secrets.CHERRY_PICK_PAT is not configured; add a PAT with 'repo' scope (contents + PR write) and store it as CHERRY_PICK_PAT"
exit 1
fi
# push with the tokenized URL inline so the PAT is never written into .git/config
git push "https://x-access-token:${PUSH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "$BRANCH"
- name: Create pull request
env:
GH_TOKEN: ${{ secrets.CHERRY_PICK_PAT }}
BRANCH: ${{ steps.branch.outputs.branch }}
SHORT_SHA: ${{ steps.branch.outputs.short_sha }}
FROM: ${{ github.event.inputs.from_branch }}
TO: ${{ github.event.inputs.to_branch }}
COMMIT: ${{ github.event.inputs.commit_id }}
run: |
if [ -z "$GH_TOKEN" ]; then
echo "::error::secrets.CHERRY_PICK_PAT is not configured; add a PAT with 'repo' scope (contents + PR write) and store it as CHERRY_PICK_PAT"
exit 1
fi
COMMIT_TITLE=$(git show -s --format=%s "$COMMIT")
COMMIT_BODY=$(git show -s --format=%b "$COMMIT")
TITLE="[cherry-pick] ${COMMIT_TITLE}"
BODY_FILE=$(mktemp)
printf 'Cherry-picks commit `%s` (%s) from `%s` to `%s`.\n\n' "$COMMIT" "$SHORT_SHA" "$FROM" "$TO" > "$BODY_FILE"
printf 'Created by the cherry-pick workflow — please review and merge manually.\n' >> "$BODY_FILE"
if [ -n "$COMMIT_BODY" ]; then
printf '\n%s\n' "$COMMIT_BODY" >> "$BODY_FILE"
fi
# Re-dispatching the same cherry-pick while a previous PR is still open must not
# create a duplicate: the head branch is unique per run, so match an existing
# open PR by title + base instead. awk reads all input (no early-exit pipe).
EXISTING_URL=$(gh pr list --state open --base "$TO" --json url,title \
--jq '.[] | [.url, .title] | @tsv' \
| awk -F '\t' -v t="$TITLE" '$2 == t { found=$1 } END { print found }')
if [ -n "$EXISTING_URL" ]; then
echo "An open cherry-pick PR already exists: $EXISTING_URL"
elif ! gh pr create --base "$TO" --head "$BRANCH" --title "$TITLE" --body-file "$BODY_FILE" 2>/tmp/pr_err; then
echo "::error::Failed to create pull request"
cat /tmp/pr_err
exit 1
else
echo "Pull request created."
fi