Skip to content

Conversation

karthikvetrivel
Copy link
Contributor

Usage

Add a /cherry-pick comment to your PR specifying target release branches:

/cherry-pick release-25.3
/cherry-pick release-24.9

When to add the comment:

  • Before merge: Bot acknowledges and automatically backports after PR is merged
  • After merge: Bot immediately starts backporting

What Happens

  1. ✅ Bot creates a new PR for each target branch
  2. 🏷️ Applies labels: backport + auto-backport (clean) or needs-manual-resolution (conflicts)
  3. 💬 Posts a comment with link to each backport PR

Example

Example of a PR where you specify the target release branch:

unknown

Example of new PR opened:

Screenshot 2025-10-08 at 1 46 32 PM

Conflict Handling

If cherry-pick has conflicts:

  • PR is created in draft mode with needs-manual-resolution label
  • Follow instructions in the backport PR description to resolve conflicts

Branch Naming

Release branches must follow the pattern: release-X.Y

Examples: release-25.3, release-24.9.1, release-23.9

Example

Original PR #1234 merged to main
  ↓
/cherry-pick release-25.3 comment
  ↓
Bot creates PR #1235: [release-25.3] Original PR title

Copy link
Contributor

@cdesiniotis cdesiniotis left a comment

Choose a reason for hiding this comment

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

Made a quick first pass. This is a great start!

let branches = [];

// Get PR number
const prNumber = context.payload.pull_request?.number || context.payload.issue?.number;
Copy link
Contributor

Choose a reason for hiding this comment

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

Question -- is grabbing a github issue number (the RHS of the conditional statement) necessary / correct here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, I believe so. I found that when the workflow is triggered by someone commenting / cherry-pick on a PR, GitHub's API doesn't populate context.payload.pull_request. Instead, the PR information is in context.payload.issue. The fallback ensures we get the PR number in both trigger scenarios.

Comment on lines 56 to 57
const bodyMatches = prBody.matchAll(/\/cherry-pick\s+(release-[\d.]+)/g);
branches.push(...Array.from(bodyMatches, m => m[1]));
Copy link
Contributor

Choose a reason for hiding this comment

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

I am not quite following this. Could you explain what this is doing and why searching the PR body is required? From a quick glance I would assume that searching through the PR comments, as is done below, would suffice.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I added this in the case that the PR author knows upfront which branches need the backport and wants to declare it in the PR description, rather than needing to add a separate comment later. If you don't believe that this is a useful case, I can remove it--let me know


- name: Configure git
run: |
git config user.name "nvidia-bot"
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: what about a more descriptive git username? nv-cherrypick-bot?

Copy link
Contributor Author

@karthikvetrivel karthikvetrivel Oct 8, 2025

Choose a reason for hiding this comment

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

Agreed, will update.

EDIT: updated

owner: context.repo.owner,
repo: context.repo.repo,
comment_id: context.payload.comment.id,
content: 'eyes'
Copy link
Contributor

Choose a reason for hiding this comment

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

👀

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes! I was following the convention of some other fun workflow bots and add the eyes reaction to the comment

execSync(`git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });

// Cherry-pick the merge commit
core.info(`Cherry-picking commit ${mergeCommitSha}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

@tariq1890 this will cherry-pick the merge commits as opposed to the individual commits that are part of the PR. This differs from our current process of backporting individual commits (not merge commits). Do you have any objections to backporting the merge commits instead?

Copy link
Contributor

Choose a reason for hiding this comment

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

I strongly prefer the backport of the individual commits over merge commits. It is way more UI friendly when comparing git branches

Copy link
Contributor

Choose a reason for hiding this comment

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

Can't we retrieve the commits from a PR ID?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can look into this, it should be possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks @karthikvetrivel !

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Updated the PR message too now to reflect showing each individual commit.
Screenshot 2025-10-08 at 4 42 36 PM

Copy link
Collaborator

@ArangoGutierrez ArangoGutierrez left a comment

Choose a reason for hiding this comment

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

Awesome initiative! left some comments

Copy link
Collaborator

Choose a reason for hiding this comment

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

Let's rename the file to a more proper name, like cherrypick.yml or something

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated!

# See the License for the specific language governing permissions and
# limitations under the License.

name: Backport merged pull request
Copy link
Collaborator

Choose a reason for hiding this comment

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

Cherry-Pick, all our workflow files follow a short name approach

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

# Run on merged PRs OR on /cherry-pick comments
if: |
(github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) ||
(github.event_name == 'issue_comment' && github.event.issue.pull_request && startsWith(github.event.comment.body, '/cherry-pick'))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Does this mean that the cherry-pick will happen before the PR is approved and merged?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No. The PR must be merged before doing any cherry-picks (lines 118-144). If someone comments /cherry-pick on an unmerged PR, the bot just acknowledges it with 👀 and says it will backport after merge. The actual cherry-pick only happens once the PR is merged.

Generally, It's a convenience add—you can say "this needs to go to release-X.Y" while reviewing, and it'll happen when the PR merges.

BRANCHES_JSON: ${{ steps.extract-branches.outputs.result }}
with:
script: |
const branches = JSON.parse(process.env.BRANCHES_JSON);
Copy link
Contributor

@tariq1890 tariq1890 Oct 8, 2025

Choose a reason for hiding this comment

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

This JS script has a lot of lines. It's better to move them to a file instead of keeping them inline in the yaml file

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed, moved to seperated Javascript files.

@karthikvetrivel karthikvetrivel force-pushed the ci/integrate-cherry-pick-bot branch from 32320fc to e1ae960 Compare October 8, 2025 20:45
@karthikvetrivel
Copy link
Contributor Author

Screenshot 2025-10-08 at 4 53 41 PM

@tariq1890 Updated the backport bot to cherry-pick individual commits instead of merge commits. Tested with a 2-commit PR to verify it handles multiple commits correctly.

@@ -0,0 +1,222 @@
module.exports = async ({ github, context, core }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Can we have a Licence Header

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Comment on lines 221 to 222


Copy link
Collaborator

Choose a reason for hiding this comment

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

Double end of file line

@@ -0,0 +1,42 @@
module.exports = async ({ github, context, core }) => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Licence header

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Comment on lines 66 to 67


Copy link
Collaborator

Choose a reason for hiding this comment

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

Double end of file line

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Copy link
Collaborator

@ArangoGutierrez ArangoGutierrez left a comment

Choose a reason for hiding this comment

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

Not a JS expert, just a few comments from what I can remember from my early days with JS


- name: Backport to release branches
id: backport
uses: actions/github-script@v7
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
uses: actions/github-script@v7
uses: actions/github-script@v8

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

// Create backport branch from target release branch
core.info(`Creating branch ${backportBranch} from ${targetBranch}`);
execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { stdio: 'inherit' });
execSync(`git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
execSync(`git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });
execSync(`git checkout ${backportBranch} || git checkout -b ${backportBranch} ${targetBranch}`, { stdio: 'inherit' });```

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

if (context.payload.pull_request?.body) {
const prBody = context.payload.pull_request.body;
// Enforce release-X.Y or release-X.Y.Z
const bodyMatches = prBody.matchAll(/\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/g);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const bodyMatches = prBody.matchAll(/\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/g);
// Strict ASCII, anchored; allow X.Y or X.Y.Z
const bodyMatches = prBody.matchAll(/^\/cherry-pick\s+(release-\d+\.\d+(?:\.\d+)?)/gmi);

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

1. Review the conflicts in the "Files changed" tab
2. Check out this branch locally: \`git fetch origin ${backportBranch} && git checkout ${backportBranch}\`
3. Resolve conflicts manually
4. Push the resolution: \`git push origin ${backportBranch}\`
Copy link
Member

Choose a reason for hiding this comment

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

nit: Resolving the conflicts will most likely require a force push:

Suggested change
4. Push the resolution: \`git push origin ${backportBranch}\`
4. Push the resolution: \`git push -f origin ${backportBranch}\`

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

git push origin ${backportBranch}
\`\`\`
</details>`
: `🤖 **Automated backport of #${prNumber} to \`${targetBranch}\`**
Copy link
Member

Choose a reason for hiding this comment

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

nit: I understand that this may be idiomatic, but looking for this : to differentiate between the two contents is not ideal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated.

Copy link
Member

@elezar elezar left a comment

Choose a reason for hiding this comment

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

Thanks @karthikvetrivel. This is great.

Is this something that we want to publish as an action at some point, or what options do we have to share this to other repos we own?

Maybe as a follow up: What does the landscape of available actions for doing this look like?

@ArangoGutierrez
Copy link
Collaborator

Thanks @karthikvetrivel. This is great.

Is this something that we want to publish as an action at some point, or what options do we have to share this to other repos we own?

++ To this, since @karthikvetrivel implementation is already in JS, we could have this JS code at k8s-test-infra and from there export it as a GitHub Action so we can reuse in all our repos

@karthikvetrivel
Copy link
Contributor Author

@ArangoGutierrez @elezar thanks for the review! Regarding rollout, I see a few options.

  1. As @ArangoGutierrez mentioned, we can host as reusable workflow in k8s-test-infra. Other repos can call it with something like this:
jobs: 
  cherrypick:
    uses: NVIDIA/k8s-test-infra/.github/workflows/cherrypick.yml@main

These seems the simplest to me.

  1. Package as a composite action in k8s-test-infra. Repos would use it as a step:
  - uses: NVIDIA/k8s-test-infra/.github/actions/backport@main
    with:
      github-token: ${{ secrets.GITHUB_TOKEN }}

This gives repos more flexibility to add custom steps before/after, but our bot needs full job isolation for git operations (script will fail if initial starting state is incorrect), so option 1 seems better.

  1. Build as a full JavaScript action with @vercel/ncc and publish to GitHub Marketplace. This would only makes sense if we want to open-source it for the broader community as it adds to build/release complexity.

  2. Keep copy-paste approach (current state). Works but means updating multiple repos when we improve the logic.

It seems like option 1 is the best to me but would to leave hear your thoughts.

1. Review the conflicts in the "Files changed" tab
2. Check out this branch locally: \`git fetch origin ${backportBranch} && git checkout ${backportBranch}\`
3. Resolve conflicts manually
4. Push the resolution: \`git push -f origin ${backportBranch}\`
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: just to be ultra-safe, I would recommend --force-with-lease instead of a plain -f.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point, resolved.

# Resolve conflicts in your editor
git add .
git commit
git push -f origin ${backportBranch}
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated as well.

@karthikvetrivel karthikvetrivel force-pushed the ci/integrate-cherry-pick-bot branch from 7dbcd48 to 87e9703 Compare October 9, 2025 20:04
@ArangoGutierrez
Copy link
Collaborator

@ArangoGutierrez @elezar thanks for the review! Regarding rollout, I see a few options.

  1. As @ArangoGutierrez mentioned, we can host as reusable workflow in k8s-test-infra. Other repos can call it with something like this:
jobs: 
  cherrypick:
    uses: NVIDIA/k8s-test-infra/.github/workflows/cherrypick.yml@main

These seems the simplest to me.

  1. Package as a composite action in k8s-test-infra. Repos would use it as a step:
  - uses: NVIDIA/k8s-test-infra/.github/actions/backport@main
    with:
      github-token: ${{ secrets.GITHUB_TOKEN }}

This gives repos more flexibility to add custom steps before/after, but our bot needs full job isolation for git operations (script will fail if initial starting state is incorrect), so option 1 seems better.

  1. Build as a full JavaScript action with @vercel/ncc and publish to GitHub Marketplace. This would only makes sense if we want to open-source it for the broader community as it adds to build/release complexity.
  2. Keep copy-paste approach (current state). Works but means updating multiple repos when we improve the logic.

It seems like option 1 is the best to me but would to leave hear your thoughts.

As you recommend option 1, lets go with that.

I think we can finish the review here and once we all agree with the implementation, we can talk about a centralized GitHub action so other repos can benefit from this tool

@karthikvetrivel
Copy link
Contributor Author

@ArangoGutierrez Thanks!

@elezar @cdesiniotis @tariq1890 what do you think about the current plan? As next steps, we can:

  1. Close this PR w/o merging once everyone is satisfied with the outlined functionality
  2. Centralize backport workflow as a reusable component in k8s-test-infra & discuss target repos we'd like to have the action
  3. Create PRs in target repos for initiating the action.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants