Skip to content

Fix backport PR description #1

Fix backport PR description

Fix backport PR description #1

Workflow file for this run

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

Check failure on line 103 in .github/workflows/backport-base.yml

View workflow run for this annotation

GitHub Actions / .github/workflows/backport-base.yml

Invalid workflow file

You have an error in your yaml syntax on line 103
=======
- 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"
});