|
| 1 | +// Licensed to the .NET Foundation under one or more agreements. |
| 2 | +// The .NET Foundation licenses this file to you under the MIT license. |
| 3 | +// from https://github.com/dotnet/runtime/blob/main/eng/actions/backport/index.js |
| 4 | + |
| 5 | +function BackportException(message, postToGitHub = true) { |
| 6 | + this.message = message; |
| 7 | + this.postToGitHub = postToGitHub; |
| 8 | + } |
| 9 | + |
| 10 | + async function run() { |
| 11 | + const util = require("util"); |
| 12 | + const jsExec = util.promisify(require("child_process").exec); |
| 13 | + |
| 14 | + console.log("Installing npm dependencies"); |
| 15 | + const { stdout, stderr } = await jsExec("npm install @actions/core @actions/github @actions/exec"); |
| 16 | + console.log("npm-install stderr:\n\n" + stderr); |
| 17 | + console.log("npm-install stdout:\n\n" + stdout); |
| 18 | + console.log("Finished installing npm dependencies"); |
| 19 | + |
| 20 | + const core = require("@actions/core"); |
| 21 | + const github = require("@actions/github"); |
| 22 | + const exec = require("@actions/exec"); |
| 23 | + |
| 24 | + const repo_owner = github.context.payload.repository.owner.login; |
| 25 | + const repo_name = github.context.payload.repository.name; |
| 26 | + const pr_number = github.context.payload.issue.number; |
| 27 | + const comment_user = github.context.payload.comment.user.login; |
| 28 | + |
| 29 | + let octokit = github.getOctokit(core.getInput("auth_token", { required: true })); |
| 30 | + let target_branch = core.getInput("target_branch", { required: true }); |
| 31 | + |
| 32 | + try { |
| 33 | + // verify the comment user is a repo collaborator |
| 34 | + try { |
| 35 | + await octokit.rest.repos.checkCollaborator({ |
| 36 | + owner: repo_owner, |
| 37 | + repo: repo_name, |
| 38 | + username: comment_user |
| 39 | + }); |
| 40 | + console.log(`Verified ${comment_user} is a repo collaborator.`); |
| 41 | + } catch (error) { |
| 42 | + console.log(error); |
| 43 | + throw new BackportException(`Error: @${comment_user} is not a repo collaborator, backporting is not allowed. If you're a collaborator please make sure your Microsoft team membership visibility is set to Public on https://github.com/orgs/microsoft/people?query=${comment_user}`); |
| 44 | + } |
| 45 | + |
| 46 | + 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.`); } |
| 47 | + console.log(`Backport target branch: ${target_branch}`); |
| 48 | + |
| 49 | + console.log("Applying backport patch"); |
| 50 | + |
| 51 | + await exec.exec(`git checkout ${target_branch}`); |
| 52 | + await exec.exec(`git clean -xdff`); |
| 53 | + |
| 54 | + // configure git |
| 55 | + await exec.exec(`git config user.name "github-actions"`); |
| 56 | + await exec.exec(`git config user.email "[email protected]"`); |
| 57 | + |
| 58 | + // create temporary backport branch |
| 59 | + const temp_branch = `backport/pr-${pr_number}-to-${target_branch}`; |
| 60 | + await exec.exec(`git checkout -b ${temp_branch}`); |
| 61 | + |
| 62 | + // skip opening PR if the branch already exists on the origin remote since that means it was opened |
| 63 | + // by an earlier backport and force pushing to the branch updates the existing PR |
| 64 | + let should_open_pull_request = true; |
| 65 | + try { |
| 66 | + await exec.exec(`git ls-remote --exit-code --heads origin ${temp_branch}`); |
| 67 | + should_open_pull_request = false; |
| 68 | + } catch { } |
| 69 | + |
| 70 | + // download and apply patch |
| 71 | + await exec.exec(`curl -sSL "${github.context.payload.issue.pull_request.patch_url}" --output changes.patch`); |
| 72 | + |
| 73 | + const git_am_command = "git am --3way --ignore-whitespace --keep-non-patch changes.patch"; |
| 74 | + let git_am_output = `$ ${git_am_command}\n\n`; |
| 75 | + let git_am_failed = false; |
| 76 | + try { |
| 77 | + await exec.exec(git_am_command, [], { |
| 78 | + listeners: { |
| 79 | + stdout: function stdout(data) { git_am_output += data; }, |
| 80 | + stderr: function stderr(data) { git_am_output += data; } |
| 81 | + } |
| 82 | + }); |
| 83 | + } catch (error) { |
| 84 | + git_am_output += error; |
| 85 | + git_am_failed = true; |
| 86 | + } |
| 87 | + |
| 88 | + if (git_am_failed) { |
| 89 | + 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!`; |
| 90 | + await octokit.rest.issues.createComment({ |
| 91 | + owner: repo_owner, |
| 92 | + repo: repo_name, |
| 93 | + issue_number: pr_number, |
| 94 | + body: git_am_failed_body |
| 95 | + }); |
| 96 | + throw new BackportException("Error: git am failed, most likely due to a merge conflict.", false); |
| 97 | + } |
| 98 | + else { |
| 99 | + // push the temp branch to the repository |
| 100 | + await exec.exec(`git push --force --set-upstream origin HEAD:${temp_branch}`); |
| 101 | + } |
| 102 | + |
| 103 | + if (!should_open_pull_request) { |
| 104 | + console.log("Backport temp branch already exists, skipping opening a PR."); |
| 105 | + return; |
| 106 | + } |
| 107 | + |
| 108 | + // prepate the GitHub PR details |
| 109 | + let backport_pr_title = core.getInput("pr_title_template"); |
| 110 | + let backport_pr_description = core.getInput("pr_description_template"); |
| 111 | + |
| 112 | + // get users to cc (append PR author if different from user who issued the backport command) |
| 113 | + let cc_users = `@${comment_user}`; |
| 114 | + if (comment_user != github.context.payload.issue.user.login) cc_users += ` @${github.context.payload.issue.user.login}`; |
| 115 | + |
| 116 | + // replace the special placeholder tokens with values |
| 117 | + backport_pr_title = backport_pr_title |
| 118 | + .replace(/%target_branch%/g, target_branch) |
| 119 | + .replace(/%source_pr_title%/g, github.context.payload.issue.title) |
| 120 | + .replace(/%source_pr_number%/g, github.context.payload.issue.number) |
| 121 | + .replace(/%cc_users%/g, cc_users); |
| 122 | + |
| 123 | + backport_pr_description = backport_pr_description |
| 124 | + .replace(/%target_branch%/g, target_branch) |
| 125 | + .replace(/%source_pr_title%/g, github.context.payload.issue.title) |
| 126 | + .replace(/%source_pr_number%/g, github.context.payload.issue.number) |
| 127 | + .replace(/%cc_users%/g, cc_users); |
| 128 | + |
| 129 | + // open the GitHub PR |
| 130 | + await octokit.rest.pulls.create({ |
| 131 | + owner: repo_owner, |
| 132 | + repo: repo_name, |
| 133 | + title: backport_pr_title, |
| 134 | + body: backport_pr_description, |
| 135 | + head: temp_branch, |
| 136 | + base: target_branch |
| 137 | + }); |
| 138 | + |
| 139 | + console.log("Successfully opened the GitHub PR."); |
| 140 | + } catch (error) { |
| 141 | + |
| 142 | + core.setFailed(error); |
| 143 | + |
| 144 | + if (error.postToGitHub === undefined || error.postToGitHub == true) { |
| 145 | + // post failure to GitHub comment |
| 146 | + 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}`; |
| 147 | + await octokit.rest.issues.createComment({ |
| 148 | + owner: repo_owner, |
| 149 | + repo: repo_name, |
| 150 | + issue_number: pr_number, |
| 151 | + body: unknown_error_body |
| 152 | + }); |
| 153 | + } |
| 154 | + } |
| 155 | + } |
| 156 | + |
| 157 | + run(); |
0 commit comments