Add backport function to release tools (#19568)

* Add backport function

* delete backport action
This commit is contained in:
Travis Plunk
2023-06-14 11:19:59 -07:00
committed by Aditya Patwardhan
parent f135ba4604
commit 6f37176bb9
4 changed files with 177 additions and 239 deletions
-61
View File
@@ -1,61 +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
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@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
-20
View File
@@ -1,20 +0,0 @@
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%
/cc %cc_users%
runs:
using: 'node12'
main: 'index.js'
-157
View File
@@ -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();
+177 -1
View File
@@ -630,4 +630,180 @@ function Update-PsVersionInCode
}
}
Export-ModuleMember -Function Get-ChangeLog, Get-NewOfficalPackage, Update-PsVersionInCode
##############################
#.SYNOPSIS
# Test if the GithubCli is in the path
##############################
function Test-GitHubCli {
$gitHubCli = Get-Command -Name 'gh' -ErrorAction SilentlyContinue
if ($gitHubCli) {
return $true
} else {
return $false
}
}
##############################
#.SYNOPSIS
# Test if the GithubCli is the required version
##############################
function Test-GitHubCliVersion {
param(
[Parameter(Mandatory)]
[System.Management.Automation.SemanticVersion]
$RequiredVersion
)
[System.Management.Automation.SemanticVersion] $version = gh --version | ForEach-Object {
if ($_ -match ' (\d+\.\d+\.\d+) ') {
$matches[1]
}
}
if ($version -ge $RequiredVersion) {
return $true
} else {
return $false
}
}
##############################
#.SYNOPSIS
# Gets a report of Backport PRs
#
#.PARAMETER Triage state
# The triage states of the PR. Consider, Approved or Done
#
#.PARAMETER Version
# The version of PowerShell the backport is targeting. 7.0, 7.2, 7.3, etc
#
#.PARAMETER Web
# A switch to open all the PRs in the browser
#
##############################
function Get-PRBackportReport {
param(
[ValidateSet('Consider', 'Approved', 'Done')]
[String] $TriageState = 'Approved',
[ValidatePattern('^\d+\.\d+$')]
[string] $Version,
[switch] $Web
)
if (!(Test-GitHubCli)) {
throw "GitHub CLI is not installed. Please install it from https://cli.github.com/"
}
$requiredVersion = '2.17'
if (!(Test-GitHubCliVersion -RequiredVersion $requiredVersion)) {
throw "Please upgrade the GitHub CLI to version $requiredVersion. Please install it from https://cli.github.com/"
}
if (!(gh auth status 2>&1 | Select-String 'logged in')){
throw "Please login to GitHub CLI using 'gh auth login'"
}
$prs = gh pr list --state merged --label "Backport-$Version.x-$TriageState" --json title,number,mergeCommit,mergedAt |
ConvertFrom-Json |
ForEach-Object {
[PScustomObject]@{
CommitId = $_.mergeCommit.oid
Number = $_.number
Title = $_.title
MergedAt = $_.mergedAt
}
} | Sort-Object -Property MergedAt
if ($Web) {
$prs | ForEach-Object {
gh pr view $_.Number --web
}
} else {
$prs
}
}
# 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 <postfix>
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