diff --git a/bin/cml/pr.js b/bin/cml/pr.js index 2c867ee3e..4a6d0201b 100755 --- a/bin/cml/pr.js +++ b/bin/cml/pr.js @@ -20,9 +20,10 @@ exports.builder = (yargs) => description: 'Output in markdown format [](url).' }, autoMerge: { - type: 'boolean', - description: - 'Mark the PR/MR for automatic merging after tests pass (unsupported by Bitbucket).' + type: 'string', + choices: ['merge', 'rebase', 'squash'], + coerce: (val) => (val === '' ? 'merge' : val), + description: 'Mark the PR/MR for automatic merging after tests pass.' }, remote: { type: 'string', diff --git a/bin/cml/pr.test.js b/bin/cml/pr.test.js index cda536051..2ee6f8a4d 100644 --- a/bin/cml/pr.test.js +++ b/bin/cml/pr.test.js @@ -15,8 +15,8 @@ describe('CML e2e', () => { --log Maximum log level [string] [choices: \\"error\\", \\"warn\\", \\"info\\", \\"debug\\"] [default: \\"info\\"] --md Output in markdown format [](url). [boolean] - --auto-merge Mark the PR/MR for automatic merging after tests pass - (unsupported by Bitbucket). [boolean] + --auto-merge Mark the PR/MR for automatic merging after tests pass. + [string] [choices: \\"merge\\", \\"rebase\\", \\"squash\\"] --remote Sets git remote. [string] [default: \\"origin\\"] --user-email Sets git user email. [string] [default: \\"olivaw@iterative.ai\\"] --user-name Sets git user name. [string] [default: \\"Olivaw[bot]\\"] diff --git a/src/drivers/bitbucket_cloud.js b/src/drivers/bitbucket_cloud.js index ce6b103f9..070418e03 100644 --- a/src/drivers/bitbucket_cloud.js +++ b/src/drivers/bitbucket_cloud.js @@ -128,7 +128,7 @@ class BitbucketCloud { } } }); - const endpoint = `/repositories/${projectPath}/pullrequests`; + const endpoint = `/repositories/${projectPath}/pullrequests/`; const { id, links: { @@ -136,24 +136,37 @@ class BitbucketCloud { } } = await this.request({ method: 'POST', - endpoint: `${endpoint}/`, + endpoint, body }); - if (autoMerge) { - winston.warn( - 'Auto-merge is unsupported by Bitbucket Cloud; see https://jira.atlassian.com/browse/BCLOUD-14286. Trying to merge immediately...' - ); - await this.request({ - method: 'POST', - endpoint: `${endpoint}/${id}/merge`, - body - }); - } - + if (autoMerge) + await this.prAutoMerge({ pullRequestId: id, mergeMode: autoMerge }); return href; } + async prAutoMerge({ pullRequestId, mergeMode, mergeMessage = undefined }) { + winston.warn( + 'Auto-merge is unsupported by Bitbucket Cloud; see https://jira.atlassian.com/browse/BCLOUD-14286. Trying to merge immediately...' + ); + const { projectPath } = this; + const endpoint = `/repositories/${projectPath}/pullrequests/${pullRequestId}/merge`; + const body = JSON.stringify({ + merge_strategy: { + merge: 'merge_commit', + rebase: 'fast_forward', + squash: 'squash' + }[mergeMode], + close_source_branch: true, + message: mergeMessage + }); + await this.request({ + method: 'POST', + endpoint, + body + }); + } + async prCommentCreate(opts = {}) { const { projectPath } = this; const { report, prNumber } = opts; diff --git a/src/drivers/github.js b/src/drivers/github.js index e35d036e4..147ee98bc 100644 --- a/src/drivers/github.js +++ b/src/drivers/github.js @@ -353,7 +353,7 @@ class Github { const { pulls } = octokit(this.token, this.repo); const { - data: { html_url: htmlUrl, node_id: nodeId, number } + data: { html_url: htmlUrl, number } } = await pulls.create({ owner, repo, @@ -363,17 +363,12 @@ class Github { body }); - if (autoMerge) { - try { - await this.prAutoMerge({ pullRequestId: nodeId, base }); - } catch ({ message }) { - winston.warn( - `Failed to enable auto-merge: ${message}. Trying to merge immediately...` - ); - await pulls.merge({ owner, repo, pull_number: number }); - } - } - + if (autoMerge) + await this.prAutoMerge({ + pullRequestId: number, + mergeMode: autoMerge, + base + }); return htmlUrl; } @@ -403,45 +398,74 @@ class Github { * @param {{ pullRequestId: number, base: string }} param0 * @returns {Promise} */ - async prAutoMerge({ pullRequestId, base }) { + async prAutoMerge({ + pullRequestId, + mergeMode, + mergeMessage = undefined, + base + }) { const octo = octokit(this.token, this.repo); const graphql = withCustomRequest(octo.request); - + const { owner, repo } = this.ownerRepo(); + const [commitHeadline, commitBody] = + mergeMessage === undefined ? [] : mergeMessage.split(/\n\n(.*)/s); + const { + data: { node_id: nodeId } + } = await octo.pulls.get({ owner, repo, pull_number: pullRequestId }); try { await graphql( ` - mutation autoMerge($pullRequestId: ID!) { + mutation autoMerge( + $pullRequestId: ID! + $mergeMethod: PullRequestMergeMethod + $commitHeadline: String + $commitBody: String + ) { enablePullRequestAutoMerge( - input: { pullRequestId: $pullRequestId } + input: { + pullRequestId: $pullRequestId + mergeMethod: $mergeMethod + commitHeadline: $commitHeadline + commitBody: $commitBody + } ) { clientMutationId } } `, { - pullRequestId + pullRequestId: nodeId, + mergeMethod: mergeMode.toUpperCase(), + commitHeadline, + commitBody } ); - } catch (error) { + } catch (err) { if ( - error.message.includes("Can't enable auto-merge for this pull request") - ) { - const { owner, repo } = this.ownerRepo(); - const settingsUrl = `https://github.com/${owner}/${repo}/settings`; - - const isProtected = await this.isProtected({ branch: base }); - if (!isProtected) { - throw new Error( - `Enabling Auto-Merge failed. Please set up branch protection and add "required status checks" for branch '${base}': ${settingsUrl}/branches` - ); - } + !err.message.includes("Can't enable auto-merge for this pull request") + ) + throw err; + + const settingsUrl = `https://github.com/${owner}/${repo}/settings`; - throw new Error( - `Enabling Auto-Merge failed. Enable the feature in your repository settings: ${settingsUrl}#merge_types_auto_merge` + if (!(await this.isProtected({ branch: base }))) { + winston.warn( + `Failed to enable auto-merge: Set up branch protection and add "required status checks" for branch '${base}': ${settingsUrl}/branches. Trying to merge immediately...` + ); + } else { + winston.warn( + `Failed to enable auto-merge: Enable the feature in your repository settings: ${settingsUrl}#merge_types_auto_merge. Trying to merge immediately...` ); } - throw error; + await octo.pulls.merge({ + owner, + repo, + pull_number: pullRequestId, + merge_method: mergeMode, + commit_title: commitHeadline, + commit_message: commitBody + }); } } diff --git a/src/drivers/gitlab.js b/src/drivers/gitlab.js index 0ac4b9ebb..a392ff536 100644 --- a/src/drivers/gitlab.js +++ b/src/drivers/gitlab.js @@ -272,41 +272,49 @@ class Gitlab { body }); - if (autoMerge) { - try { - await this.prAutoMerge({ mergeRequestId: iid }); - } catch ({ message }) { - winston.warn( - `Failed to enable auto-merge: ${message}. Trying to merge immediately...` - ); - await this.prAutoMerge({ - mergeRequestId: iid, - whenPipelineSucceeds: false - }); - } - } - + if (autoMerge) + await this.prAutoMerge({ pullRequestId: iid, mergeMode: autoMerge }); return url; } /** - * @param {{ mergeRequestId: string }} param0 + * @param {{ pullRequestId: string }} param0 * @returns {Promise} */ - async prAutoMerge({ mergeRequestId, whenPipelineSucceeds = true }) { + async prAutoMerge({ pullRequestId, mergeMode, mergeMessage = undefined }) { + if (mergeMode === 'rebase') + throw new Error(`Rebase auto-merge mode not implemented for GitLab`); + const projectPath = await this.projectPath(); - const endpoint = `/projects/${projectPath}/merge_requests/${mergeRequestId}/merge`; + const endpoint = `/projects/${projectPath}/merge_requests/${pullRequestId}/merge`; const body = new URLSearchParams(); - body.append('merge_when_pipeline_succeeds', whenPipelineSucceeds); + body.set('merge_when_pipeline_succeeds', true); + body.set('squash', mergeMode === 'squash'); + if (mergeMessage !== undefined) + body.set(`${mergeMode}_commit_message`, mergeMessage); - await backOff(() => - this.request({ - endpoint, - method: 'PUT', - body - }) - ); + try { + await backOff(() => + this.request({ + endpoint, + method: 'PUT', + body + }) + ); + } catch ({ message }) { + winston.warn( + `Failed to enable auto-merge: ${message}. Trying to merge immediately...` + ); + body.set('merge_when_pipeline_succeeds', false); + await backOff(() => + this.request({ + endpoint, + method: 'PUT', + body + }) + ); + } } async prCommentCreate(opts = {}) { diff --git a/test b/test new file mode 160000 index 000000000..7b3beabd3 --- /dev/null +++ b/test @@ -0,0 +1 @@ +Subproject commit 7b3beabd329b603cffd09f6b81f326727e17ec25