-
Notifications
You must be signed in to change notification settings - Fork 10.5k
Add github action for backporting PRs #32777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| name: Backport PR to branch | ||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
|
|
||
| jobs: | ||
| backport: | ||
| if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/backport to') | ||
| runs-on: ubuntu-20.04 | ||
| steps: | ||
| - name: Extract backport target branch | ||
| uses: actions/github-script@v3 | ||
| id: target-branch-extractor | ||
| with: | ||
| result-encoding: string | ||
| script: | | ||
| if (context.eventName !== "issue_comment") throw "Error: This action only works on issue_comment events."; | ||
|
|
||
| // extract the target branch name from the trigger phrase containing these characters: a-z, A-Z, digits, forward slash, dot, hyphen, underscore | ||
| const regex = /\/backport to ([a-zA-Z\d\/\.\-\_]+)/; | ||
| target_branch = regex.exec(context.payload.comment.body); | ||
| if (target_branch == null) throw "Error: No backport branch found in the trigger phrase."; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggest enforcing Nit: could just ignore poorly-formed comments i.e. anything that doesn't match the full regular expression. |
||
|
|
||
| return target_branch[1]; | ||
| - name: Post backport started comment to pull request | ||
| uses: actions/github-script@v3 | ||
| with: | ||
| script: | | ||
| const backport_start_body = `Started backporting to ${{ steps.target-branch-extractor.outputs.result }}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${process.env.GITHUB_RUN_ID}`; | ||
| await github.issues.createComment({ | ||
| issue_number: context.issue.number, | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| body: backport_start_body | ||
| }); | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v2 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Run backport | ||
| uses: ./eng/actions/backport | ||
| with: | ||
| target_branch: ${{ steps.target-branch-extractor.outputs.result }} | ||
| auth_token: ${{ secrets.GITHUB_TOKEN }} | ||
| pr_description_template: | | ||
| Backport of #%source_pr_number% to %target_branch% | ||
|
|
||
| /cc %cc_users% | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Am I correct Separately, this action doesn't check group membership until fairly late. What is the visible failure if someone w/o rights to create branches in dotnet/aspnetcore adds a |
||
|
|
||
| ## Customer Impact | ||
|
|
||
| ## Testing | ||
|
|
||
| ## Risk | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| name: 'PR Backporter' | ||
| description: 'Backports a pull request to a branch using the "/backport to <branch>" comment' | ||
| inputs: | ||
| target_branch: | ||
| description: 'Backport target branch.' | ||
| auth_token: | ||
| description: 'The token used to authenticate to GitHub.' | ||
| pr_title_template: | ||
| description: 'The template used for the PR title. Special placeholder tokens that will be replaced with a value: %target_branch%, %source_pr_title%, %source_pr_number%, %cc_users%.' | ||
| default: '[%target_branch%] %source_pr_title%' | ||
| pr_description_template: | ||
| description: 'The template used for the PR description. Special placeholder tokens that will be replaced with a value: %target_branch%, %source_pr_title%, %source_pr_number%, %cc_users%.' | ||
| default: | | ||
| Backport of #%source_pr_number% to %target_branch% | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If possible, would appreciate the final commit hash in this comment |
||
|
|
||
| /cc %cc_users% | ||
|
|
||
| runs: | ||
| using: 'node12' | ||
| main: 'index.js' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| function BackportException(message, postToGitHub = true) { | ||
| this.message = message; | ||
| this.postToGitHub = postToGitHub; | ||
| } | ||
|
|
||
| async function run() { | ||
| const util = require("util"); | ||
| const jsExec = util.promisify(require("child_process").exec); | ||
|
|
||
| console.log("Installing npm dependencies"); | ||
| const { stdout, stderr } = await jsExec("npm install @actions/core @actions/github @actions/exec"); | ||
| console.log("npm-install stderr:\n\n" + stderr); | ||
| console.log("npm-install stdout:\n\n" + stdout); | ||
| console.log("Finished installing npm dependencies"); | ||
|
|
||
| const core = require("@actions/core"); | ||
| const github = require("@actions/github"); | ||
| const exec = require("@actions/exec"); | ||
|
|
||
| const repo_owner = github.context.payload.repository.owner.login; | ||
| const repo_name = github.context.payload.repository.name; | ||
| const pr_number = github.context.payload.issue.number; | ||
| const comment_user = github.context.payload.comment.user.login; | ||
|
|
||
| let octokit = github.getOctokit(core.getInput("auth_token", { required: true })); | ||
| let target_branch = core.getInput("target_branch", { required: true }); | ||
|
|
||
| try { | ||
| // verify the comment user is a repo collaborator | ||
| try { | ||
| await octokit.repos.checkCollaborator({ | ||
| owner: repo_owner, | ||
| repo: repo_name, | ||
| username: comment_user | ||
| }); | ||
| console.log(`Verified ${comment_user} is a repo collaborator.`); | ||
| } catch { | ||
| throw new BackportException(`Error: @${comment_user} is not a repo collaborator, backporting is not allowed.`); | ||
| } | ||
|
|
||
| try { await exec.exec(`git ls-remote --exit-code --heads origin ${target_branch}`) } catch { throw new BackportException(`Error: The specified backport target branch ${target_branch} wasn't found in the repo.`); } | ||
| console.log(`Backport target branch: ${target_branch}`); | ||
|
|
||
| console.log("Applying backport patch"); | ||
|
|
||
| await exec.exec(`git checkout ${target_branch}`); | ||
| await exec.exec(`git clean -xdff`); | ||
|
|
||
| // configure git | ||
| await exec.exec(`git config user.name "github-actions"`); | ||
| await exec.exec(`git config user.email "github-actions@github.com"`); | ||
|
|
||
| // create temporary backport branch | ||
| const temp_branch = `backport/pr-${pr_number}-to-${target_branch}`; | ||
| await exec.exec(`git checkout -b ${temp_branch}`); | ||
|
|
||
| // skip opening PR if the branch already exists on the origin remote since that means it was opened | ||
| // by an earlier backport and force pushing to the branch updates the existing PR | ||
| let should_open_pull_request = true; | ||
| try { | ||
| await exec.exec(`git ls-remote --exit-code --heads origin ${temp_branch}`); | ||
| should_open_pull_request = false; | ||
| } catch { } | ||
|
|
||
| // download and apply patch | ||
| await exec.exec(`curl -sSL "${github.context.payload.issue.pull_request.patch_url}" --output changes.patch`); | ||
|
|
||
| const git_am_command = "git am --3way --ignore-whitespace --keep-non-patch changes.patch"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand using the patch allows for back-porting an in-progress PR but
Do the available APIs support determining the final commit or commits when a PR is merged❔ |
||
| let git_am_output = `$ ${git_am_command}\n\n`; | ||
| let git_am_failed = false; | ||
| try { | ||
| await exec.exec(git_am_command, [], { | ||
| listeners: { | ||
| stdout: function stdout(data) { git_am_output += data; }, | ||
| stderr: function stderr(data) { git_am_output += data; } | ||
| } | ||
| }); | ||
| } catch (error) { | ||
| git_am_output += error; | ||
| git_am_failed = true; | ||
| } | ||
|
|
||
| if (git_am_failed) { | ||
| const git_am_failed_body = `@${github.context.payload.comment.user.login} backporting to ${target_branch} failed, the patch most likely resulted in conflicts:\n\n\`\`\`shell\n${git_am_output}\n\`\`\`\n\nPlease backport manually!`; | ||
| await octokit.issues.createComment({ | ||
| owner: repo_owner, | ||
| repo: repo_name, | ||
| issue_number: pr_number, | ||
| body: git_am_failed_body | ||
| }); | ||
| throw new BackportException("Error: git am failed, most likely due to a merge conflict.", false); | ||
| } | ||
| else { | ||
| // push the temp branch to the repository | ||
| await exec.exec(`git push --force --set-upstream origin HEAD:${temp_branch}`); | ||
| } | ||
|
|
||
| if (!should_open_pull_request) { | ||
| console.log("Backport temp branch already exists, skipping opening a PR."); | ||
| return; | ||
| } | ||
|
|
||
| // prepate the GitHub PR details | ||
| let backport_pr_title = core.getInput("pr_title_template"); | ||
| let backport_pr_description = core.getInput("pr_description_template"); | ||
|
|
||
| // get users to cc (append PR author if different from user who issued the backport command) | ||
| let cc_users = `@${comment_user}`; | ||
| if (comment_user != github.context.payload.issue.user.login) cc_users += ` @${github.context.payload.issue.user.login}`; | ||
|
|
||
| // replace the special placeholder tokens with values | ||
| backport_pr_title = backport_pr_title | ||
| .replace(/%target_branch%/g, target_branch) | ||
| .replace(/%source_pr_title%/g, github.context.payload.issue.title) | ||
| .replace(/%source_pr_number%/g, github.context.payload.issue.number) | ||
| .replace(/%cc_users%/g, cc_users); | ||
|
|
||
| backport_pr_description = backport_pr_description | ||
| .replace(/%target_branch%/g, target_branch) | ||
| .replace(/%source_pr_title%/g, github.context.payload.issue.title) | ||
| .replace(/%source_pr_number%/g, github.context.payload.issue.number) | ||
| .replace(/%cc_users%/g, cc_users); | ||
|
|
||
| // open the GitHub PR | ||
| await octokit.pulls.create({ | ||
| owner: repo_owner, | ||
| repo: repo_name, | ||
| title: backport_pr_title, | ||
| body: backport_pr_description, | ||
| head: temp_branch, | ||
| base: target_branch | ||
| }); | ||
|
|
||
| console.log("Successfully opened the GitHub PR."); | ||
| } catch (error) { | ||
|
|
||
| core.setFailed(error); | ||
|
|
||
| if (error.postToGitHub === undefined || error.postToGitHub == true) { | ||
| // post failure to GitHub comment | ||
| const unknown_error_body = `@${comment_user} an error occurred while backporting to ${target_branch}, please check the run log for details!\n\n${error.message}`; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Where would users find the "run log"❔ |
||
| await octokit.issues.createComment({ | ||
| owner: repo_owner, | ||
| repo: repo_name, | ||
| issue_number: pr_number, | ||
| body: unknown_error_body | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| run(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we confirm the PR has been merged before allowing backports❔