Skip to content

Commit f11f9fc

Browse files
authored
Add conflict resolution extension point for PR backport workflow (#16237)
1 parent 904bfd1 commit f11f9fc

File tree

1 file changed

+78
-28
lines changed

1 file changed

+78
-28
lines changed

.github/workflows/backport-base.yml

Lines changed: 78 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ on:
2424
required: false
2525
type: string
2626
default: ''
27+
conflict_resolution_command:
28+
description: >-
29+
Optional shell command to attempt automatic conflict resolution after a failed git am.
30+
If the command eliminates merge conflicts, the backport proceeds automatically.
31+
Do not run git commands from this parameter as they might have unintended side effects.
32+
DO NOT PASS UNTRUSTED INPUT TO THIS PARAMETER.
33+
required: false
34+
type: string
2735

2836
jobs:
2937
cleanup:
@@ -69,7 +77,8 @@ jobs:
6977
with:
7078
script: |
7179
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
72-
const backport_start_body = `Started backporting to _${target_branch}_: https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
80+
const workflow_run_url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
81+
const backport_start_body = `Started backporting to \`${target_branch}\` ([link to workflow run](${workflow_run_url}))`;
7382
await github.rest.issues.createComment({
7483
issue_number: context.issue.number,
7584
owner: context.repo.owner,
@@ -86,13 +95,29 @@ jobs:
8695
BACKPORT_PR_TITLE_TEMPLATE: ${{ inputs.pr_title_template }}
8796
BACKPORT_PR_DESCRIPTION_TEMPLATE: ${{ inputs.pr_description_template }}
8897
ADDITIONAL_GIT_AM_SWITCHES: ${{ inputs.additional_git_am_switches }}
98+
CONFLICT_RESOLUTION_COMMAND: ${{ inputs.conflict_resolution_command }}
8999
with:
90100
script: |
91101
const target_branch = '${{ steps.target-branch-extractor.outputs.result }}';
92102
const repo_owner = context.payload.repository.owner.login;
93103
const repo_name = context.payload.repository.name;
94104
const pr_number = context.payload.issue.number;
95105
const comment_user = context.payload.comment.user.login;
106+
const workflow_run_url = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
107+
108+
const wrap_in_code_block = (language, content) => `\`\`\`${language}\n${content}\n\`\`\``;
109+
const wrap_in_details_block = (summary, content) => `<details>\n<summary>${summary}</summary>\n\n${content}\n</details>`;
110+
111+
// Post a comment on the PR and return the comment URL
112+
async function postComment(body) {
113+
const { data: comment } = await github.rest.issues.createComment({
114+
owner: repo_owner,
115+
repo: repo_name,
116+
issue_number: pr_number,
117+
body
118+
});
119+
return comment.html_url;
120+
}
96121
97122
try {
98123
// verify the comment user is a repo collaborator
@@ -133,13 +158,14 @@ jobs:
133158
} catch { }
134159
135160
// download and apply patch
136-
await exec.exec(`curl -sSL "${context.payload.issue.pull_request.patch_url}" --output changes.patch`);
161+
const patch_file = 'changes.patch';
162+
await exec.exec(`curl -sSL "${context.payload.issue.pull_request.patch_url}" --output ${patch_file}`);
137163
138164
const additional_switches = process.env.ADDITIONAL_GIT_AM_SWITCHES?.trim() || '';
139165
const base_switches = '--3way --empty=keep --ignore-whitespace --keep-non-patch';
140166
const git_am_command = additional_switches
141-
? `git am ${base_switches} ${additional_switches} changes.patch`
142-
: `git am ${base_switches} changes.patch`;
167+
? `git am ${base_switches} ${additional_switches} ${patch_file}`
168+
: `git am ${base_switches} ${patch_file}`;
143169
let git_am_output = `$ ${git_am_command}\n\n`;
144170
let git_am_failed = false;
145171
try {
@@ -155,21 +181,52 @@ jobs:
155181
}
156182
157183
if (git_am_failed) {
158-
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!`;
159-
await github.rest.issues.createComment({
160-
owner: repo_owner,
161-
repo: repo_name,
162-
issue_number: pr_number,
163-
body: git_am_failed_body
164-
});
165-
core.setFailed("Error: git am failed, most likely due to a merge conflict.");
166-
return;
167-
}
168-
else {
169-
// push the temp branch to the repository
170-
await exec.exec(`git push --force --set-upstream origin HEAD:${temp_branch}`);
184+
const resolution_command = process.env.CONFLICT_RESOLUTION_COMMAND || '';
185+
186+
// If no resolution command supplied, fail immediately
187+
if (resolution_command.trim().length === 0) {
188+
const details = `${wrap_in_code_block('console', git_am_output)}\n[Link to workflow output](${workflow_run_url})`;
189+
const git_am_failed_body = `@${comment_user} backporting to \`${target_branch}\` failed, the patch most likely resulted in conflicts. Please backport manually!\n${wrap_in_details_block('git am output', details)}`;
190+
postComment(git_am_failed_body);
191+
core.setFailed("git am failed, most likely due to a merge conflict.");
192+
return;
193+
}
194+
195+
console.log(`git am failed; attempting in-session conflict resolution via provided command: ${resolution_command}`);
196+
197+
// Run user-provided resolution command
198+
// Ignore return code to capture stdout/stderr
199+
const resolution_result = await exec.getExecOutput(`bash -c "${resolution_command}"`, [], { ignoreReturnCode: true });
200+
if (resolution_result.exitCode !== 0) {
201+
const details = `\`${resolution_command}\` stderr:\n${wrap_in_code_block('console', resolution_result.stderr)}\n[Link to workflow output](${workflow_run_url})`;
202+
const resolution_failed_body = `@${comment_user} backporting to \`${target_branch}\` failed during automated conflict resolution. Please backport manually!\n${wrap_in_details_block('Error details', details)}`;
203+
postComment(resolution_failed_body);
204+
core.setFailed(`Automated conflict resolution command exited with code ${resolution_result.exitCode}`);
205+
return;
206+
}
207+
208+
// Stage changes (excluding patch file)
209+
await exec.exec(`git add -A`);
210+
await exec.exec(`git reset HEAD ${patch_file}`);
211+
212+
// Check for remaining conflicts
213+
const diff_command = 'git diff --name-only --diff-filter=U';
214+
const diff_result = await exec.getExecOutput(diff_command);
215+
if (diff_result.stdout.trim().length !== 0) {
216+
const details = `${wrap_in_code_block('console', diff_result.stdout)}\n[Link to workflow output](${workflow_run_url})`;
217+
const conflicts_body = `@${comment_user} backporting to \`${target_branch}\` failed. Automated conflict resolution did not resolve all conflicts. Please backport manually!\n${wrap_in_details_block(`${diff_command} output`, details)}`;
218+
postComment(conflicts_body);
219+
core.setFailed(`Automated conflict resolution did not resolve all conflicts.`);
220+
return;
221+
}
222+
223+
console.log('Automated conflict resolution resolved all merge conflicts. Continuing.');
224+
await exec.exec('git am --continue');
171225
}
172226
227+
// push the temp branch to the repository
228+
await exec.exec(`git push --force --set-upstream origin HEAD:${temp_branch}`);
229+
173230
if (!should_open_pull_request) {
174231
console.log("Backport temp branch already exists, skipping opening a PR.");
175232
return;
@@ -210,19 +267,12 @@ jobs:
210267
211268
console.log("Successfully opened the GitHub PR.");
212269
} catch (error) {
213-
270+
const body = `@${comment_user} an error occurred while backporting to \`${target_branch}\`. See the [workflow output](${workflow_run_url}) for details.`;
271+
const comment_url = await postComment(body);
272+
console.log(`Posted comment: ${comment_url}`);
214273
core.setFailed(error);
215-
216-
// post failure to GitHub comment
217-
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}`;
218-
await github.rest.issues.createComment({
219-
owner: repo_owner,
220-
repo: repo_name,
221-
issue_number: pr_number,
222-
body: unknown_error_body
223-
});
224274
}
225-
275+
226276
- name: Re-lock PR comments
227277
uses: actions/github-script@v7
228278
if: ${{ github.event.issue.locked == true && (success() || failure()) }}

0 commit comments

Comments
 (0)