1 changed files with 228 additions and 0 deletions
@ -0,0 +1,228 @@
@@ -0,0 +1,228 @@
|
||||
# Based on: https://github.com/dotnet/arcade/blob/main/.github/workflows/backport-base.yml |
||||
# Ref: https://github.com/dotnet/arcade/blob/7cd1cae2edfb3895f5f0ee577131e0214ed8af1a/.github/workflows/backport-base.yml |
||||
|
||||
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: |
||||
# Add cleanup job |
||||
# cleanup: |
||||
# uses: dotnet/arcade/.github/workflows/scheduled-action-cleanup-base.yml@main |
||||
# with: |
||||
# repository_owners: ${{ inputs.repository_owners }} |
||||
|
||||
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 --empty=keep --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" |
||||
}); |
||||
Loading…
Reference in new issue