Skip to content

Commit a03cefe

Browse files
committed
feat(actions/create-and-merge-pull-request): add outputs for final ref and sha
Signed-off-by: Emilien Escalle <emilien.escalle@escemi.com>
1 parent a0c7eac commit a03cefe

1 file changed

Lines changed: 131 additions & 60 deletions

File tree

actions/create-and-merge-pull-request/action.yml

Lines changed: 131 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,14 @@ inputs:
2929
description: "The commit message for the pull request"
3030
required: true
3131

32+
outputs:
33+
merged-ref:
34+
description: "The fully qualified ref updated by the merge."
35+
value: ${{ steps.merge-pull-request.outputs.merged-ref }}
36+
merged-sha:
37+
description: "The commit SHA at the merged ref after the merge completes."
38+
value: ${{ steps.merge-pull-request.outputs.merged-sha }}
39+
3240
runs:
3341
using: "composite"
3442
steps:
@@ -67,84 +75,147 @@ runs:
6775
author: ${{ steps.github-actions-bot-user.outputs.name }} <${{ steps.github-actions-bot-user.outputs.email }}>
6876
committer: ${{ steps.github-actions-bot-user.outputs.name }} <${{ steps.github-actions-bot-user.outputs.email }}>
6977

70-
- id: wait-for-pull-request-mergeable-by-admin
78+
- id: merge-pull-request
79+
name: Merge pull request
7180
if: steps.create-pull-request.outputs.pull-request-number && steps.create-pull-request.outputs.pull-request-operation != 'closed'
7281
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
7382
env:
83+
GH_TOKEN: ${{ inputs.github-token }}
7484
PULL_REQUEST_NUMBER: ${{ steps.create-pull-request.outputs.pull-request-number }}
7585
with:
7686
github-token: ${{ inputs.github-token }}
7787
script: |
78-
let attempts = 0;
7988
const maxAttempts = 10;
89+
const requiredWorkflowsError = 'Required workflow';
90+
const repository = `${context.repo.owner}/${context.repo.repo}`;
91+
const getRetryDelayMs = attempt => Math.min(1000 * (2 ** attempt), 10000);
92+
93+
const sleep = delayMs => new Promise(resolve => setTimeout(resolve, delayMs));
94+
95+
const waitFor = async ({
96+
run,
97+
isComplete,
98+
shouldRetry = () => true,
99+
onAttempt = () => {},
100+
getRetryMessage,
101+
getFailureMessage,
102+
getTimeoutMessage,
103+
}) => {
104+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
105+
const result = await run();
106+
107+
onAttempt(result);
108+
109+
if (isComplete(result)) {
110+
return result;
111+
}
112+
113+
if (!shouldRetry(result)) {
114+
throw new Error(getFailureMessage(result));
115+
}
116+
117+
const retryDelayMs = getRetryDelayMs(attempt);
118+
core.debug(getRetryMessage(result, retryDelayMs));
119+
await sleep(retryDelayMs);
120+
}
80121
81-
const pullNumberRaw = process.env.PULL_REQUEST_NUMBER ?? '';
82-
if (!/^[0-9]+$/.test(pullNumberRaw)) {
83-
throw new Error(`Invalid pull request number: ${pullNumberRaw}`);
84-
}
85-
const pullNumber = Number.parseInt(pullNumberRaw, 10);
122+
throw new Error(getTimeoutMessage());
123+
};
86124
87-
while (attempts < maxAttempts) {
88-
const { data: { mergeable, mergeable_state } } = await github.rest.pulls.get({
125+
const parsePullRequestNumber = pullNumberRaw => {
126+
if (!/^[0-9]+$/.test(pullNumberRaw)) {
127+
throw new Error(`Invalid pull request number: ${pullNumberRaw}`);
128+
}
129+
130+
return Number.parseInt(pullNumberRaw, 10);
131+
};
132+
133+
const getPullRequest = async pullNumber => {
134+
const { data: pullRequest } = await github.rest.pulls.get({
89135
owner: context.repo.owner,
90136
repo: context.repo.repo,
91137
pull_number: pullNumber,
92138
});
93139
94-
if (mergeable === true) {
95-
core.setOutput('is-mergeable', true);
96-
return;
97-
}
98-
99-
core.debug(`Pull request is not mergeable, mergeable_state: ${mergeable_state}`);
140+
return pullRequest;
141+
};
142+
143+
const waitForMergeablePullRequest = pullNumber =>
144+
waitFor({
145+
run: () => getPullRequest(pullNumber),
146+
isComplete: pullRequest => pullRequest.mergeable === true,
147+
onAttempt: pullRequest => {
148+
if (pullRequest.mergeable !== true) {
149+
core.debug(`Pull request is not mergeable, mergeable_state: ${pullRequest.mergeable_state}`);
150+
}
151+
},
152+
getRetryMessage: (_pullRequest, retryDelayMs) => `Retrying mergeability check in ${retryDelayMs}ms...`,
153+
getFailureMessage: () => `Pull request #${pullNumber} cannot be retried because it is not mergeable.`,
154+
getTimeoutMessage: () => `Pull request #${pullNumber} is not mergeable after ${maxAttempts} attempts`,
155+
});
100156
101-
await new Promise(resolve => setTimeout(resolve, 5000));
102-
attempts++;
103-
}
157+
const mergePullRequest = async pullNumber => {
158+
core.debug(`Merging pull request #${pullNumber} for repository ${repository}...`);
159+
160+
const { exitCode, stdout, stderr } = await exec.getExecOutput(
161+
'gh',
162+
[
163+
'pr',
164+
'merge',
165+
'-R',
166+
repository,
167+
'--rebase',
168+
'--admin',
169+
String(pullNumber),
170+
],
171+
{
172+
env: process.env,
173+
ignoreReturnCode: true,
174+
silent: true,
175+
},
176+
);
177+
178+
return {
179+
mergeExitCode: exitCode,
180+
mergeOutputs: [stdout, stderr].filter(Boolean).join('\n').trim(),
181+
};
182+
};
183+
184+
const verifyMergedPullRequest = async (pullNumber, mergeOutputs) => {
185+
const pullRequest = await waitFor({
186+
run: () => getPullRequest(pullNumber),
187+
isComplete: currentPullRequest => Boolean(
188+
currentPullRequest.merged
189+
&& currentPullRequest.base.ref
190+
&& currentPullRequest.merge_commit_sha,
191+
),
192+
getRetryMessage: (_pullRequest, retryDelayMs) => `Pull request merge has not been reflected by the REST API yet, retrying in ${retryDelayMs}ms...`,
193+
getFailureMessage: () => `Pull request merge verification stopped before completion: ${mergeOutputs}`,
194+
getTimeoutMessage: () => `Pull request merge succeeded but timed out while waiting for REST API verification: ${mergeOutputs}`,
195+
});
104196
105-
core.error('Pull request is not mergeable');
197+
core.setOutput('merged-ref', `refs/heads/${pullRequest.base.ref}`);
198+
core.setOutput('merged-sha', pullRequest.merge_commit_sha);
199+
};
200+
201+
const pullNumber = parsePullRequestNumber(process.env.PULL_REQUEST_NUMBER ?? '');
202+
203+
await waitForMergeablePullRequest(pullNumber);
204+
205+
const { mergeOutputs } = await waitFor({
206+
run: () => mergePullRequest(pullNumber),
207+
isComplete: ({ mergeExitCode }) => mergeExitCode === 0,
208+
shouldRetry: ({ mergeOutputs }) => mergeOutputs.includes(requiredWorkflowsError),
209+
onAttempt: ({ mergeExitCode, mergeOutputs }) => {
210+
core.debug(`Merge outputs: ${mergeOutputs}`);
211+
core.debug(`Merge exit code: ${mergeExitCode}`);
212+
},
213+
getRetryMessage: (_result, retryDelayMs) => `Pull request is not mergeable yet because some of required workflow check issues, retrying in ${retryDelayMs}ms...`,
214+
getFailureMessage: ({ mergeOutputs }) => `Failed to merge pull request: ${mergeOutputs}`,
215+
getTimeoutMessage: () => `Failed to merge pull request after ${maxAttempts} attempts`,
216+
});
106217
107-
- name: Merge pull request
108-
if: steps.wait-for-pull-request-mergeable-by-admin.outputs.is-mergeable
109-
shell: bash
110-
env:
111-
GH_TOKEN: ${{ inputs.github-token }}
112-
PULL_REQUEST_NUMBER: ${{ steps.create-pull-request.outputs.pull-request-number }}
113-
run: |
114-
set +e
115-
116-
if ! [[ "$PULL_REQUEST_NUMBER" =~ ^[0-9]+$ ]]; then
117-
echo "::error::Invalid pull request number: $PULL_REQUEST_NUMBER"
118-
exit 1
119-
fi
120-
121-
ATTEMPTS=0
122-
MAX_ATTEMPTS=10
123-
REQUIRED_WORKFLOWS_ERROR="Required workflow"
124-
125-
while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
126-
echo "::debug::Merging pull request #${PULL_REQUEST_NUMBER} for repository ${{ github.repository }}..."
127-
MERGE_OUTPUTS=$(gh pr merge -R "${{ github.repository }}" --rebase --admin "${PULL_REQUEST_NUMBER}" 2>&1)
128-
MERGE_EXIT_CODE=$?
129-
echo "::debug::Merge outputs: $MERGE_OUTPUTS"
130-
echo "::debug::Merge exit code: $MERGE_EXIT_CODE"
131-
132-
if [ "$MERGE_EXIT_CODE" = "0" ]; then
133-
exit 0
134-
fi
135-
136-
if [[ "$MERGE_OUTPUTS" != *"$REQUIRED_WORKFLOWS_ERROR"* ]]; then
137-
echo "::error::Failed to merge pull request: $MERGE_OUTPUTS"
138-
exit 1
139-
fi
140-
141-
echo "::debug::Pull request is not mergeable yet because some of required workflow check issues, retrying in 5 seconds..."
142-
sleep 5
143-
ATTEMPTS=$((ATTEMPTS+1))
144-
done
145-
146-
echo "::error::Failed to merge pull request after $MAX_ATTEMPTS attempts: $MERGE_OUTPUTS"
147-
exit 1
218+
await verifyMergedPullRequest(pullNumber, mergeOutputs);
148219
149220
- name: Cleanup sibling actions
150221
if: ${{ always() }}

0 commit comments

Comments
 (0)