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

test-cml-pr #487

Merged
merged 1 commit into from
Apr 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions bin/cml-pr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env node

const print = console.log;
console.log = console.error;

const yargs = require('yargs');
const decamelize = require('decamelize-keys');

const CML = require('../src/cml');

const run = async (opts) => {
const globs = opts._.length ? opts._ : undefined;
const cml = new CML(opts);
print(await cml.pr_create({ ...opts, globs }));
};

const opts = decamelize(
yargs
.usage('Usage: $0 <path to markdown file>')
.describe('md', 'Output in markdown format [](url).')
.boolean('md')
.default('repo')
.describe(
'repo',
'Specifies the repo to be used. If not specified is extracted from the CI ENV.'
)
.default('token')
.describe(
'token',
'Personal access token to be used. If not specified in extracted from ENV repo_token.'
)
.default('driver')
.choices('driver', ['github', 'gitlab'])
.describe('driver', 'If not specify it infers it from the ENV.')
.help('h').argv
);

run(opts).catch((e) => {
console.error(e);
process.exit(1);
});
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"cml-publish": "bin/cml-publish.js",
"cml-tensorboard-dev": "bin/cml-tensorboard-dev.js",
"cml-runner": "bin/cml-runner.js",
"cml-cloud-runner-entrypoint": "bin/cml-runner.js"
"cml-cloud-runner-entrypoint": "bin/cml-runner.js",
"test-cml-pr": "bin/cml-pr.js"
},
"scripts": {
"lintfix": "eslint --fix ./",
Expand Down
85 changes: 85 additions & 0 deletions src/cml.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { execSync } = require('child_process');
const git_url_parse = require('git-url-parse');
const strip_auth = require('strip-url-auth');
const git = require('simple-git/promise')('./');
const globby = require('globby');

const Gitlab = require('./drivers/gitlab');
const Github = require('./drivers/github');
Expand Down Expand Up @@ -234,6 +236,89 @@ class CML {
}
}

async pr_create(opts = {}) {
const { globs = ['dvc.lock', '.gitignore'], md } = opts;

const { files } = await git.status();
if (!files.length) {
console.log('No files changed. Nothing to do.');
return;
}

const driver = get_driver(this);
const paths = (await globby(globs)).filter((path) =>
files.map((item) => item.path).includes(path)
);

const render_pr = (url) => {
if (md)
return `[CML's ${
this.driver === 'gitlab' ? 'Merge' : 'Pull'
} Request](${url})`;
return url;
};

const sha = await exec(`git rev-parse HEAD`);
const sha_short = sha.substr(0, 7);
let target = await exec(`git branch --show-current`);
if (!target) {
if (this.driver === 'gitlab') {
target = await exec('echo $CI_BUILD_REF_NAME');
}
}
const source = `${target}-cmlpr-${sha_short}`;

await exec(`git fetch origin`);

const branch_exists = (await exec(`git branch -r`)).includes(source);
if (branch_exists) {
const prs = await driver.prs();
const { url } =
prs.find((pr) => pr.source === source && pr.target === target) || {};

if (url) return render_pr(url);
} else {
try {
await exec(`git config --local user.email "[email protected]"`);
await exec(`git config --local user.name "cml-bot"`);
await exec('git config advice.addIgnoredFile false');

if (this.driver !== 'github') {
const repo = new URL(this.repo);
repo.password = this.token;
repo.username = driver.user_name;

await exec(`git remote rm origin`);
await exec(`git remote add origin "${repo.toString()}.git"`);
}

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 [skip ci]"`);
await exec(`git push --set-upstream origin ${source}`);
await exec(`git checkout -B ${target} ${sha}`);
} catch (err) {
await exec(`git checkout -B ${target} ${sha}`);
throw err;
}
}

const title = `CML commits ${target} ${sha_short}`;
const description = `
Automated commits for ${this.repo}/commit/${sha} created by CML.
`;

const url = await driver.pr_create({
source,
target,
title,
description
});

return render_pr(url);
}

log_error(e) {
console.error(e.message);
}
Expand Down
52 changes: 52 additions & 0 deletions src/drivers/github.js
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,58 @@ class Github {
)
.map((runner) => ({ id: runner.id, name: runner.name }));
}

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

const {
data: { url }
} = await pulls.create({
owner,
repo,
head,
base,
title,
body
});

return url;
}

async prs(opts = {}) {
const { state = 'open' } = opts;
const { owner, repo } = owner_repo({ uri: this.repo });
const { pulls } = octokit(this.token, this.repo);

const { data: prs } = await pulls.list({
owner,
repo,
state
});

return prs.map((pr) => {
const {
url,
head: { ref: source },
base: { ref: target }
} = pr;
return {
url,
source,
target
};
});
}

get user_email() {
return '[email protected]';
}

get user_name() {
return 'GitHub Action';
}
}

module.exports = Github;
43 changes: 42 additions & 1 deletion src/drivers/gitlab.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const { resolve } = require('path');

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

const { IN_DOCKER } = process.env;
const { IN_DOCKER, GITLAB_USER_EMAIL, GITLAB_USER_NAME } = process.env;
const API_VER = 'v4';
class Gitlab {
constructor(opts = {}) {
Expand Down Expand Up @@ -180,6 +180,39 @@ class Gitlab {
return runners.map((runner) => ({ id: runner.id, name: runner.name }));
}

async pr_create(opts = {}) {
const { project_path } = this;
const { source, target, title, description } = opts;

const endpoint = `/projects/${project_path}/merge_requests`;
const body = new URLSearchParams();
body.append('source_branch', source);
body.append('target_branch', target);
body.append('title', title);
body.append('description', description);

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

return web_url;
}

async prs(opts = {}) {
const { project_path } = this;
const { state = 'opened' } = opts;

const endpoint = `/projects/${project_path}/merge_requests?state=${state}`;
const prs = await this.request({ endpoint, method: 'GET' });

return prs.map((pr) => {
const { web_url: url, source_branch: source, target_branch: target } = pr;
return {
url,
source,
target
};
});
}

async request(opts = {}) {
const { token } = this;
const { endpoint, method = 'GET', body, raw } = opts;
Expand All @@ -198,6 +231,14 @@ class Gitlab {

return await response.json();
}

get user_email() {
return GITLAB_USER_EMAIL;
}

get user_name() {
return GITLAB_USER_NAME;
}
}

module.exports = Gitlab;