From a7e1a6c81245bf6502990b1a961180386321ec85 Mon Sep 17 00:00:00 2001
From: Doug Bunting <6431421+dougbu@users.noreply.github.com>
Date: Mon, 15 Nov 2021 17:10:15 -0800
Subject: [PATCH] [release/5.0] Grant GitHub actions explicit permissions -
ends up w/ same .github/workflows/ and eng/actions/ content as in #38421 -
like [main#38420 but remove `runtime-sync` and _add_ `backport` actions
---
.github/workflows/ReportDiff.ps1 | 29 ------
.github/workflows/backport.yml | 59 +++++++++++
.github/workflows/runtime-sync.yml | 72 -------------
eng/actions/backport/action.yml | 20 ++++
eng/actions/backport/index.js | 156 +++++++++++++++++++++++++++++
5 files changed, 235 insertions(+), 101 deletions(-)
delete mode 100644 .github/workflows/ReportDiff.ps1
create mode 100644 .github/workflows/backport.yml
delete mode 100644 .github/workflows/runtime-sync.yml
create mode 100644 eng/actions/backport/action.yml
create mode 100644 eng/actions/backport/index.js
diff --git a/.github/workflows/ReportDiff.ps1 b/.github/workflows/ReportDiff.ps1
deleted file mode 100644
index 2093a9dd863d..000000000000
--- a/.github/workflows/ReportDiff.ps1
+++ /dev/null
@@ -1,29 +0,0 @@
-# Check the code is in sync
-$changed = (select-string "nothing to commit" artifacts\status.txt).count -eq 0
-if (-not $changed) { return $changed }
-# Check if tracking issue is open/closed
-$Headers = @{ Authorization = 'token {0}' -f $ENV:GITHUB_TOKEN; };
-$result = Invoke-RestMethod -Uri $issue
-if ($result.state -eq "closed") {
- $json = "{ `"state`": `"open`" }"
- $result = Invoke-RestMethod -Method PATCH -Headers $Headers -Uri $issue -Body $json
-}
-# Add a comment
-$status = [IO.File]::ReadAllText("artifacts\status.txt")
-$diff = [IO.File]::ReadAllText("artifacts\diff.txt")
-$body = @"
-The shared code is out of sync.
-
- The Diff
-
-``````
-$status
-$diff
-``````
-
-
-"@
-$json = ConvertTo-Json -InputObject @{ 'body' = $body }
-$issue = $issue + '/comments'
-$result = Invoke-RestMethod -Method POST -Headers $Headers -Uri $issue -Body $json
-return $changed
\ No newline at end of file
diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml
new file mode 100644
index 000000000000..99ebe3b43d0c
--- /dev/null
+++ b/.github/workflows/backport.yml
@@ -0,0 +1,59 @@
+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@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.";
+
+ 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%
+
+ ## Customer Impact
+
+ ## Testing
+
+ ## Risk
diff --git a/.github/workflows/runtime-sync.yml b/.github/workflows/runtime-sync.yml
deleted file mode 100644
index 03e7e8f9d6ba..000000000000
--- a/.github/workflows/runtime-sync.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-name: AspNetCore-Runtime Code Sync
-on:
- # Manual run
- workflow_dispatch:
- schedule:
- # * is a special character in YAML so you have to quote this string
- # https://help.github.com/en/actions/automating-your-workflow-with-github-actions/events-that-trigger-workflows#scheduled-events-schedule
- # Once per day at midnight PST (8 UTC)
- - cron: '0 8 * * *'
-
-jobs:
- compare_repos:
- # Comment out this line to test the scripts in a fork
- if: github.repository == 'dotnet/aspnetcore'
- name: Compare the shared code in the AspNetCore and Runtime repos and notify if they're out of sync.
- runs-on: windows-latest
- steps:
- - name: Checkout aspnetcore
- uses: actions/checkout@v2.0.0
- with:
- # Test this script using changes in a fork
- repository: 'dotnet/aspnetcore'
- path: aspnetcore
- ref: release/5.0
- - name: Checkout runtime
- uses: actions/checkout@v2.0.0
- with:
- # Test this script using changes in a fork
- repository: 'dotnet/runtime'
- path: runtime
- ref: release/5.0
- - name: Copy
- shell: cmd
- working-directory: .\runtime\src\libraries\Common\src\System\Net\Http\aspnetcore\
- env:
- ASPNETCORE_REPO: d:\a\aspnetcore\aspnetcore\aspnetcore\
- run: CopyToAspNetCore.cmd
- - name: Diff
- shell: cmd
- working-directory: .\aspnetcore\
- run: |
- mkdir ..\artifacts
- git status > ..\artifacts\status.txt
- git diff > ..\artifacts\diff.txt
- - uses: actions/upload-artifact@v1
- with:
- name: results
- path: artifacts
- - name: Check and Notify
- id: check
- shell: pwsh
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: |
- # Test this script using an issue in the local forked repo
- $issue = 'https://api.github.com/repos/dotnet/aspnetcore/issues/18943'
- $changed = .\aspnetcore\.github\workflows\ReportDiff.ps1
- echo "::set-output name=changed::$changed"
- - name: Send PR
- if: steps.check.outputs.changed == 'true'
- # https://github.com/marketplace/actions/create-pull-request
- uses: peter-evans/create-pull-request@v2
- with:
- token: ${{ secrets.GITHUB_TOKEN }}
- path: .\aspnetcore
- commit-message: 'Sync shared code from runtime'
- title: 'Sync shared code from runtime'
- body: 'This PR was automatically generated to sync shared code changes from runtime. Fixes #18943'
- labels: area-servers
- base: release/5.0
- branch: github-action/sync-runtime
- branch-suffix: timestamp
diff --git a/eng/actions/backport/action.yml b/eng/actions/backport/action.yml
new file mode 100644
index 000000000000..e596f1dd5865
--- /dev/null
+++ b/eng/actions/backport/action.yml
@@ -0,0 +1,20 @@
+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/eng/actions/backport/index.js b/eng/actions/backport/index.js
new file mode 100644
index 000000000000..49a2c7401088
--- /dev/null
+++ b/eng/actions/backport/index.js
@@ -0,0 +1,156 @@
+// 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.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.`);
+ }
+
+ 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();