From cbe18db47bf0491f0f1ba5fa9d0e6676c0db90cc Mon Sep 17 00:00:00 2001 From: Travis Plunk Date: Wed, 26 Apr 2023 09:47:53 -0700 Subject: [PATCH] Add backport function to release tools (#19568) * Add backport function * delete backport action --- .github/workflows/backport.yml | 66 ------------- tools/actions/backport/action.yml | 20 ---- tools/actions/backport/index.js | 157 ------------------------------ tools/releaseTools.psm1 | 84 +++++++++++++++- 4 files changed, 83 insertions(+), 244 deletions(-) delete mode 100644 .github/workflows/backport.yml delete mode 100644 tools/actions/backport/action.yml delete mode 100644 tools/actions/backport/index.js diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml deleted file mode 100644 index 0dd8b33105..0000000000 --- a/.github/workflows/backport.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Backport PR to branch -on: - issue_comment: - types: [created] - -permissions: - contents: write - issues: write - pull-requests: write - -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@v6 - 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."; - - return target_branch[1]; - - name: Post backport started comment to pull request - uses: actions/github-script@v6 - continue-on-error: true - 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}`; - console.log(`issue_number: ${context.issue.number}`); - console.log(`owner: ${context.repo.owner}`); - console.log(`repo: ${context.repo.repo}`); - console.log(`body: ${backport_start_body}`); - 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@v3 - with: - fetch-depth: 0 - - name: Run backport - uses: ./tools/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% - - ## Customer Impact - - ## Testing - - - [ ] For any change that affects the release process, please work with a maintainer to come up with a plan to test this. - - ## Risk diff --git a/tools/actions/backport/action.yml b/tools/actions/backport/action.yml deleted file mode 100644 index e596f1dd58..0000000000 --- a/tools/actions/backport/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -name: 'PR Backporter' -description: 'Backports a pull request to a branch using the "/backport to " 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% - - /cc %cc_users% - -runs: - using: 'node12' - main: 'index.js' diff --git a/tools/actions/backport/index.js b/tools/actions/backport/index.js deleted file mode 100644 index 88d348f167..0000000000 --- a/tools/actions/backport/index.js +++ /dev/null @@ -1,157 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// from https://github.com/dotnet/runtime/blob/main/eng/actions/backport/index.js - -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.rest.repos.checkCollaborator({ - owner: repo_owner, - repo: repo_name, - username: comment_user - }); - console.log(`Verified ${comment_user} is a repo collaborator.`); - } catch (error) { - console.log(error); - 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}`); - } - - 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"; - 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.rest.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.rest.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}`; - await octokit.rest.issues.createComment({ - owner: repo_owner, - repo: repo_name, - issue_number: pr_number, - body: unknown_error_body - }); - } - } - } - - run(); diff --git a/tools/releaseTools.psm1 b/tools/releaseTools.psm1 index 65c6fc54ed..f156a8e104 100644 --- a/tools/releaseTools.psm1 +++ b/tools/releaseTools.psm1 @@ -724,4 +724,86 @@ function Get-PRBackportReport { } } -Export-ModuleMember -Function Get-ChangeLog, Get-NewOfficalPackage, Update-PsVersionInCode, Get-PRBackportReport +# Backports a PR +# requires: +# * a remote called upstream pointing to powershell/powershell +# * the github cli installed and authenticated +# Usage: +# Invoke-PRBackport -PRNumber 1234 -Target release/v7.0.1 +# To overwrite a local branch add -Overwrite +# To add an postfix to the branch name use -BranchPostFix +function Invoke-PRBackport { + [cmdletbinding(SupportsShouldProcess, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory)] + [string] + $PrNumber, + + [Parameter(Mandatory)] + [ValidateScript({$_ -match '^release/v\d+\.\d+\.\d+'})] + [string] + $Target, + + [switch] + $Overwrite, + + [string] + $BranchPostFix + ) + function script:Invoke-NativeCommand { + param( + [scriptblock] $ScriptBlock + ) + &$ScriptBlock + if ($LASTEXITCODE -ne 0) { + throw "$ScriptBlock fail with $LASTEXITCODE" + } + } + $ErrorActionPreference = 'stop' + + $pr = gh pr view $PrNumber --json 'mergeCommit,state,title' | ConvertFrom-Json + + $commitId = $pr.mergeCommit.oid + $state = $pr.state + $originaltitle = $pr.title + $backportTitle = "[$Target]$originalTitle" + + Write-Verbose -Verbose "commitId: $commitId; state: $state" + Write-Verbose -Verbose "title:$backportTitle" + + if ($state -ne 'MERGED') { + throw "PR is not merged ($state)" + } + + $upstream = $null + $upstreamName = 'powershell/powershell' + $upstream = Invoke-NativeCommand { git remote -v } | Where-Object { $_ -match "^upstream.*$upstreamName.*fetch" } + + if (!$upstream) { + throw "Please create an upstream remote that points to $upstreamName" + } + + Invoke-NativeCommand { git fetch upstream $Target } + + $switch = '-c' + if ($Overwrite) { + $switch = '-C' + } + + $branchName = "backport-$PrNumber" + if ($BranchPostFix) { + $branchName += "-$BranchPostFix" + } + + if ($PSCmdlet.ShouldProcess("Create branch $branchName from upstream/$Target")) { + Invoke-NativeCommand { git switch upstream/$Target $switch $branchName } + } + + Invoke-NativeCommand { git cherry-pick $commitId } + + if ($PSCmdlet.ShouldProcess("Create the PR")) { + gh pr create --base $Target --title $backportTitle --body "Backport #$PrNumber" + } +} + +Export-ModuleMember -Function Get-ChangeLog, Get-NewOfficalPackage, Update-PsVersionInCode, Get-PRBackportReport, Invoke-PRBackport