@@ -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+
3240runs :
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