Fix backport PR description #1
Workflow file for this run
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: Extract PR id | ||
uses: actions/github-script@v7 | ||
id: pr-id-extractor | ||
with: | ||
result-encoding: string | ||
script: | | ||
return context.issue.number; | ||
- name: Calculate backport branch name | ||
uses: actions/github-script@v7 | ||
id: backport-branch-name-extractor | ||
with: | ||
result-encoding: string | ||
script: | | ||
return `backport/${{ steps.pr-id-extractor.outputs.result }}/to/${{ steps.target-branch-extractor.outputs.result }}`; | ||
<<<<<<< Updated upstream | ||
======= | ||
- name: Calculate backport PR title | ||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 | ||
id: backport-pr-title-extractor | ||
env: | ||
BACKPORT_PR_TITLE_TEMPLATE: ${{ inputs.pr_title_template }} | ||
with: | ||
result-encoding: string | ||
script: | | ||
// replace the special placeholder tokens with values | ||
const { BACKPORT_PR_TITLE_TEMPLATE } = process.env | ||
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}'; | ||
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) | ||
return backport_pr_title; | ||
- name: Calculate backport PR description | ||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7 | ||
id: backport-pr-description-extractor | ||
env: | ||
BACKPORT_PR_DESCRIPTION_TEMPLATE: ${{ inputs.pr_description_template }} | ||
with: | ||
result-encoding: string | ||
script: | | ||
// get users to cc (append PR author if different from user who issued the backport command) | ||
const comment_user = context.payload.comment.user.login; | ||
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_DESCRIPTION_TEMPLATE } = process.env | ||
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}'; | ||
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); | ||
return backport_pr_description; | ||
>>>>>>> Stashed changes | ||
- 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`); | ||
// download and apply patch | ||
await exec.exec(`curl -sSL "${context.payload.issue.pull_request.patch_url}" --output changes.patch`); | ||
// create temporary backport branch | ||
// const temp_branch = `backport/pr-${pr_number}-to-${target_branch}`; | ||
// await exec.exec(`git checkout -b ${temp_branch}`); | ||
// configure git | ||
await exec.exec(`git config user.name "github-actions"`); | ||
await exec.exec(`git config user.email "[email protected]"`); | ||
let 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; | ||
} | ||
let failed_count = 0; | ||
while (git_am_failed) { | ||
failed_count++; | ||
if (failed_count >= 5) { | ||
await github.rest.issues.createComment({ | ||
owner: repo_owner, | ||
repo: repo_name, | ||
issue_number: pr_number, | ||
body: "Potential infinite loop guard hit. Stopping" | ||
}); | ||
return; | ||
} | ||
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\n**NOTE:** A PR will be created, but needs to be revised manually!**`; | ||
await github.rest.issues.createComment({ | ||
owner: repo_owner, | ||
repo: repo_name, | ||
issue_number: pr_number, | ||
body: git_am_failed_body | ||
}); | ||
await exec.exec(`git add .`); | ||
await exec.exec(`git restore --staged changes.patch`); | ||
git_am_command = "git am --continue"; | ||
git_am_output = `$ ${git_am_command}\n\n`; | ||
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; | ||
} | ||
} | ||
await exec.exec(`rm changes.patch`); | ||
// 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 | ||
}); | ||
} | ||
- uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7 | ||
with: | ||
token: ${{ secrets.BACKPORT_MACHINE_USER_PAT }} | ||
push-to-fork: youssef-backport-bot/testfx | ||
branch: ${{ steps.backport-branch-name-extractor.outputs.result }} | ||
- 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" | ||
}); |