Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add auto-merge feature for GitHub and GitLab #903

Merged
merged 15 commits into from
Mar 16, 2022
5 changes: 5 additions & 0 deletions bin/cml/pr.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ exports.builder = (yargs) =>
type: 'boolean',
description: 'Output in markdown format [](url).'
},
autoMerge: {
type: 'boolean',
description:
'Mark the PR/MR for automatic merging after tests pass (unsupported by Bitbucket).'
},
remote: {
type: 'string',
default: GIT_REMOTE,
Expand Down
2 changes: 2 additions & 0 deletions bin/cml/pr.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +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]
--remote Sets git remote. [string] [default: \\"origin\\"]
--user-email Sets git user email. [string] [default: \\"[email protected]\\"]
--user-name Sets git user name. [string] [default: \\"Olivaw[bot]\\"]
Expand Down
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,12 @@
"@actions/github": "^4.0.0",
"@npcz/magic": "^1.3.12",
"@octokit/core": "^3.5.1",
"@octokit/graphql": "^4.8.0",
casperdcl marked this conversation as resolved.
Show resolved Hide resolved
"@octokit/plugin-throttling": "^3.5.2",
"@octokit/rest": "18.0.0",
"colors": "1.4.0",
"ec2-spot-notification": "^2.0.3",
"exponential-backoff": "^3.1.0",
"form-data": "^3.0.1",
"fs-extra": "^9.1.0",
"git-url-parse": "^11.6.0",
Expand Down
12 changes: 9 additions & 3 deletions src/cml.js
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,8 @@ class CML {
const {
remote = GIT_REMOTE,
globs = ['dvc.lock', '.gitignore'],
md
md,
autoMerge
} = opts;

await this.ci(opts);
Expand Down Expand Up @@ -392,7 +393,11 @@ class CML {
await exec(`git checkout -B ${target} ${sha}`);
await exec(`git checkout -b ${source}`);
await exec(`git add ${paths.join(' ')}`);
await exec(`git commit -m "CML PR for ${shaShort} [skip ci]"`);
let commitMessage = `CML PR for ${shaShort}`;
if (!autoMerge) {
commitMessage += ' [skip ci]';
}
await exec(`git commit -m "${commitMessage}"`);
await exec(`git push --set-upstream ${remote} ${source}`);
}

Expand All @@ -405,7 +410,8 @@ Automated commits for ${this.repo}/commit/${sha} created by CML.
source,
target,
title,
description
description,
autoMerge
});

return renderPr(url);
Expand Down
8 changes: 7 additions & 1 deletion src/drivers/bitbucket_cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,13 @@ class BitbucketCloud {

async prCreate(opts = {}) {
const { projectPath } = this;
const { source, target, title, description } = opts;
const { source, target, title, description, autoMerge } = opts;

if (autoMerge) {
throw new Error(
'Auto-merging is unsupported by Bitbucket Cloud. See https://jira.atlassian.com/browse/BCLOUD-14286'
);
}

const body = JSON.stringify({
title,
Expand Down
83 changes: 81 additions & 2 deletions src/drivers/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fetch = require('node-fetch');

const github = require('@actions/github');
const { Octokit } = require('@octokit/rest');
const { withCustomRequest } = require('@octokit/graphql');
const { throttling } = require('@octokit/plugin-throttling');
const tar = require('tar');
const ProxyAgent = require('proxy-agent');
Expand Down Expand Up @@ -313,12 +314,18 @@ class Github {
}

async prCreate(opts = {}) {
const { source: head, target: base, title, description: body } = opts;
const {
source: head,
target: base,
title,
description: body,
autoMerge
} = opts;
const { owner, repo } = ownerRepo({ uri: this.repo });
const { pulls } = octokit(this.token, this.repo);

const {
data: { html_url: htmlUrl }
data: { html_url: htmlUrl, node_id: nodeId }
} = await pulls.create({
owner,
repo,
Expand All @@ -328,9 +335,81 @@ class Github {
body
});

if (autoMerge) {
await this.prAutoMerge({ pullRequestId: nodeId, base });
}

return htmlUrl;
}

/**
* @param {string} branch
* @returns {Promise<boolean>}
*/
async isProtected(branch) {
0x2b3bfa0 marked this conversation as resolved.
Show resolved Hide resolved
const octo = octokit(this.token, this.repo);
const { owner, repo } = this.ownerRepo();
try {
await octo.repos.getBranchProtection({
branch,
owner,
repo
});
return true;
} catch (error) {
if (error.message === 'Branch not protected') {
return false;
}
throw error;
}
}

/**
* @param {{ pullRequestId: number, base: string }} param0
* @returns {Promise<void>}
*/
async prAutoMerge({ pullRequestId, base }) {
const octo = octokit(this.token, this.repo);
const graphql = withCustomRequest(octo.request);

try {
await graphql(
`
mutation autoMerge($pullRequestId: ID!) {
enablePullRequestAutoMerge(
input: { pullRequestId: $pullRequestId }
) {
clientMutationId
}
}
`,
{
pullRequestId
}
);
} catch (error) {
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(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`
);
}

throw new Error(
`Enabling Auto-Merge failed. Enable the feature in your repository settings: ${settingsUrl}#merge_types_auto_merge`
);
}

throw error;
}
}

async prCommentCreate(opts = {}) {
const { report: body, prNumber } = opts;
const { owner, repo } = ownerRepo({ uri: this.repo });
Expand Down
29 changes: 27 additions & 2 deletions src/drivers/gitlab.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const fs = require('fs').promises;
const fse = require('fs-extra');
const { resolve } = require('path');
const ProxyAgent = require('proxy-agent');
const { backOff } = require('exponential-backoff');

const { fetchUploadData, download, exec } = require('../utils');

Expand Down Expand Up @@ -235,7 +236,7 @@ class Gitlab {

async prCreate(opts = {}) {
const projectPath = await this.projectPath();
const { source, target, title, description } = opts;
const { source, target, title, description, autoMerge } = opts;

const endpoint = `/projects/${projectPath}/merge_requests`;
const body = new URLSearchParams();
Expand All @@ -244,15 +245,39 @@ class Gitlab {
body.append('title', title);
body.append('description', description);

const { web_url: url } = await this.request({
const { web_url: url, iid } = await this.request({
endpoint,
method: 'POST',
body
});

if (autoMerge) {
await this.prAutoMerge({ mergeRequestId: iid });
}

return url;
}

/**
* @param {{ mergeRequestId: string }} param0
* @returns {Promise<void>}
*/
async prAutoMerge({ mergeRequestId }) {
const projectPath = await this.projectPath();

const endpoint = `/projects/${projectPath}/merge_requests/${mergeRequestId}/merge`;
const body = new URLSearchParams();
body.append('merge_when_pipeline_succeeds', true);
casperdcl marked this conversation as resolved.
Show resolved Hide resolved

await backOff(() =>
this.request({
endpoint,
method: 'PUT',
body
})
);
}

async prCommentCreate(opts = {}) {
const projectPath = await this.projectPath();
const { report, prNumber } = opts;
Expand Down
5 changes: 2 additions & 3 deletions src/drivers/gitlab.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,10 @@ describe('Non Enviromental tests', () => {

test('Runner token', async () => {
const output = await client.runnerToken();

expect(output.length).toBe(20);
expect(output.length >= 20).toBe(true);
});

test.skip('updateGitConfig', async () => {
test('updateGitConfig', async () => {
const client = new GitlabClient({
repo: 'https://gitlab.com/test/test',
token: 'dXNlcjpwYXNz'
Expand Down