Skip to content
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

Copy backport GH logic locally and call it #2775

Merged
merged 1 commit into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
259 changes: 259 additions & 0 deletions .github/workflows/backport-base.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
on:
workflow_call:
inputs:
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%.'
required: false
type: string
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%.'
required: false
type: string
default: |
Backport of #%source_pr_number% to %target_branch%

/cc %cc_users%
repository_owners:
description: 'A comma-separated list of repository owners where the workflow will run. Defaults to "dotnet,microsoft".'
required: false
type: string
default: 'dotnet,microsoft'

jobs:
cleanup:
if: ${{ contains(format('{0},', inputs.repository_owners), format('{0},', github.repository_owner)) && github.event_name == 'schedule' }}
runs-on: ubuntu-latest
permissions:
actions: write
steps:
- name: Cleanup workflow runs
uses: actions/github-script@v7
with:
script: |
const repo_owner = context.payload.repository.owner.login;
const repo_name = context.payload.repository.name;

// look up workflow from current run
const currentWorkflowRun = await github.rest.actions.getWorkflowRun({
owner: repo_owner,
repo: repo_name,
run_id: context.runId
});

// get runs which are 'completed' (other candidate values of status field are e.g. 'queued' and 'in_progress')
for await (const response of github.paginate.iterator(
github.rest.actions.listWorkflowRuns, {
owner: repo_owner,
repo: repo_name,
workflow_id: currentWorkflowRun.data.workflow_id,
status: 'completed'
}
)) {
// delete each run
for (const run of response.data) {
console.log(`Deleting workflow run ${run.id}`);
await github.rest.actions.deleteWorkflowRun({
owner: repo_owner,
repo: repo_name,
run_id: run.id
});
}
}

run_backport:
if: ${{ contains(format('{0},', inputs.repository_owners), format('{0},', github.repository_owner)) && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/backport to') }}
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
steps:
- name: Extract backport target branch
uses: actions/github-script@v7
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: Unlock comments if PR is locked
uses: actions/github-script@v7
if: ${{ github.event.issue.locked == true }}
with:
script: |
console.log(`Unlocking locked PR #${context.issue.number}.`);
await github.rest.issues.unlock({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
});
- name: Post backport started comment to pull request
uses: actions/github-script@v7
with:
script: |
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const backport_start_body = `Started backporting to ${target_branch}: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await github.rest.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@v4
with:
fetch-depth: 0
- name: Run backport
uses: actions/github-script@v7
env:
BACKPORT_PR_TITLE_TEMPLATE: ${{ inputs.pr_title_template }}
BACKPORT_PR_DESCRIPTION_TEMPLATE: ${{ inputs.pr_description_template }}
with:
script: |
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
const repo_owner = context.payload.repository.owner.login;
const repo_name = context.payload.repository.name;
const pr_number = context.payload.issue.number;
const comment_user = context.payload.comment.user.login;

try {
// verify the comment user is a repo collaborator
try {
await github.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 Error(`Error: @${comment_user} is not a repo collaborator, backporting is not allowed. If you're a collaborator please make sure your ${repo_owner} team membership visibility is set to Public on https://github.com/orgs/${repo_owner}/people?query=${comment_user}`);
}

try { await exec.exec(`git ls-remote --exit-code --heads origin ${target_branch}`) } catch { throw new Error(`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 "${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 = `@${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 github.rest.issues.createComment({
owner: repo_owner,
repo: repo_name,
issue_number: pr_number,
body: git_am_failed_body
});
throw new Error("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;
}

// prepare the GitHub PR details

// get users to cc (append PR author if different from user who issued the backport command)
let cc_users = `@${comment_user}`;
if (comment_user != context.payload.issue.user.login) cc_users += ` @${context.payload.issue.user.login}`;

// replace the special placeholder tokens with values
const { BACKPORT_PR_TITLE_TEMPLATE, BACKPORT_PR_DESCRIPTION_TEMPLATE } = process.env

const backport_pr_title = BACKPORT_PR_TITLE_TEMPLATE
.replace(/%target_branch%/g, target_branch)
.replace(/%source_pr_title%/g, context.payload.issue.title)
.replace(/%source_pr_number%/g, context.payload.issue.number)
.replace(/%cc_users%/g, cc_users);

const backport_pr_description = BACKPORT_PR_DESCRIPTION_TEMPLATE
.replace(/%target_branch%/g, target_branch)
.replace(/%source_pr_title%/g, context.payload.issue.title)
.replace(/%source_pr_number%/g, context.payload.issue.number)
.replace(/%cc_users%/g, cc_users);

// open the GitHub PR
await github.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);

// 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 github.rest.issues.createComment({
owner: repo_owner,
repo: repo_name,
issue_number: pr_number,
body: unknown_error_body
});
}

- name: Re-lock PR comments
uses: actions/github-script@v7
if: ${{ github.event.issue.locked == true && (success() || failure()) }}
with:
script: |
console.log(`Locking previously locked PR #${context.issue.number} again.`);
await github.rest.issues.lock({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
lock_reason: "resolved"
});
2 changes: 1 addition & 1 deletion .github/workflows/backport.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:
jobs:
backport:
if: ${{ contains(github.event.comment.body, '/backport to') || github.event_name == 'schedule' }}
uses: dotnet/arcade/.github/workflows/backport-base.yml@main
uses: microsoft/testfx/.github/workflows/backport-base.yml@main
with:
pr_description_template: |
Backport of #%source_pr_number% to %target_branch%
Expand Down