Skip to content

Commit

Permalink
add PR/MR comments (#776)
Browse files Browse the repository at this point in the history
Co-authored-by: Casper da Costa-Luis <[email protected]>
  • Loading branch information
DavidGOrtega and casperdcl authored Oct 27, 2021
1 parent 2ed8e4d commit 0958298
Show file tree
Hide file tree
Showing 9 changed files with 390 additions and 121 deletions.
5 changes: 5 additions & 0 deletions bin/cml/send-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ exports.handler = async (opts) => {
exports.builder = (yargs) =>
yargs.env('CML_SEND_COMMENT').options(
kebabcaseKeys({
pr: {
type: 'boolean',
description:
'Post to an existing PR/MR associated with the specified commit'
},
commitSha: {
type: 'string',
alias: 'head-sha',
Expand Down
2 changes: 2 additions & 0 deletions bin/cml/send-comment.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Options:
--version Show version number [boolean]
--log Maximum log level
[string] [choices: \\"error\\", \\"warn\\", \\"info\\", \\"debug\\"] [default: \\"info\\"]
--pr Post to an existing PR/MR associated with the
specified commit [boolean]
--commit-sha, --head-sha Commit SHA linked to this comment. Defaults to HEAD.
[string]
--update Update the last CML comment (if any) instead of
Expand Down
72 changes: 62 additions & 10 deletions src/cml.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,11 @@ const inferToken = () => {

const inferDriver = (opts = {}) => {
const { repo } = opts;
if (repo && repo.includes('github.com')) return GITHUB;
if (repo && repo.includes('gitlab.com')) return GITLAB;
if (repo && repo.includes('bitbucket.com')) return BB;
if (repo) {
if (repo.includes('github.com')) return GITHUB;
if (repo.includes('gitlab.com')) return GITLAB;
if (/bitbucket\.(com|org)/.test(repo)) return BB;
}

if (GITHUB_REPOSITORY) return GITHUB;
if (CI_PROJECT_URL) return GITLAB;
Expand Down Expand Up @@ -95,20 +97,70 @@ class CML {
report: userReport,
commitSha = await this.headSha(),
rmWatermark,
update
update,
pr
} = opts;

if (rmWatermark && update)
throw new Error('watermarks are mandatory for updateable comments');

const watermark = rmWatermark
? ''
: ' \n\n ![CML watermark](https://raw.githubusercontent.com/iterative/cml/master/assets/watermark.svg)';
const report = `${userReport}${watermark}`;
: '![CML watermark](https://raw.githubusercontent.com/iterative/cml/master/assets/watermark.svg)';

const report = `${userReport}\n\n${watermark}`;
const drv = getDriver(this);

let comment;
const updatableComment = (comments) => {
return comments.reverse().find(({ body }) => {
return body.includes('watermark.svg');
});
};

if (pr || this.driver === 'bitbucket') {
let commentUrl;

const longReport = `${commitSha.substr(0, 7)}\n\n${report}`;
const [commitPr] = await drv.commitPrs({ commitSha });
const { url } = commitPr;

if (!url) throw new Error(`PR for commit sha "${commitSha}" not found`);

const [prNumber] = url.split('/').slice(-1);

if (update)
comment = updatableComment(await drv.prComments({ prNumber }));

if (update && comment) {
commentUrl = await drv.prCommentUpdate({
report: longReport,
id: comment.id,
prNumber
});
} else
commentUrl = await drv.prCommentCreate({
report: longReport,
prNumber
});

if (this.driver !== 'bitbucket') return commentUrl;
}

if (update)
comment = updatableComment(await drv.commitComments({ commitSha }));

if (update && comment) {
return await drv.commentUpdate({
report,
id: comment.id,
commitSha
});
}

return await getDriver(this).commentCreate({
...opts,
return await drv.commentCreate({
report,
commitSha,
watermark
commitSha
});
}

Expand Down
197 changes: 123 additions & 74 deletions src/drivers/bitbucket_cloud.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,69 +23,71 @@ class BitbucketCloud {

async commentCreate(opts = {}) {
const { projectPath } = this;
const { commitSha, report, update, watermark } = opts;
const { commitSha, report } = opts;

// Check for a corresponding PR. If it exists, also put the comment there.
let prs;
try {
const getPrEndpoint = `/repositories/${projectPath}/commit/${commitSha}/pullrequests`;
prs = await this.paginatedRequest({ endpoint: getPrEndpoint });
} catch (err) {
if (err.message === 'Not Found Resource not found')
err.message =
"Click 'Go to pull request' on any commit details page to enable this API";
throw err;
}

if (prs && prs.length) {
for (const pr of prs) {
// Append a watermark to the report with a link to the commit
const commitLink = commitSha.substr(0, 7);
const longReport = `${commitLink}\n\n${report}`;
const prBody = JSON.stringify({ content: { raw: longReport } });

// Write a comment on the PR
const prEndpoint = `/repositories/${projectPath}/pullrequests/${pr.id}/comments/`;
const existingPr = (
await this.paginatedRequest({ endpoint: prEndpoint, method: 'GET' })
)
.filter((comment) => {
const { content: { raw = '' } = {} } = comment;
return raw.endsWith(watermark);
})
.sort((first, second) => first.id < second.id)
.pop();
await this.request({
endpoint: prEndpoint + (update && existingPr ? existingPr.id : ''),
method: update && existingPr ? 'PUT' : 'POST',
body: prBody
});
}
}

const commitEndpoint = `/repositories/${projectPath}/commit/${commitSha}/comments/`;

const existingCommmit = (
await this.paginatedRequest({ endpoint: commitEndpoint, method: 'GET' })
)
.filter((comment) => {
const { content: { raw = '' } = {} } = comment;
return raw.endsWith(watermark);
const endpoint = `/repositories/${projectPath}/commit/${commitSha}/comments/`;
return (
await this.request({
endpoint,
method: 'POST',
body: JSON.stringify({ content: { raw: report } })
})
.sort((first, second) => first.id < second.id)
.pop();
).links.html.href;
}

async commentUpdate(opts = {}) {
const { projectPath } = this;
const { commitSha, report, id } = opts;

const endpoint = `/repositories/${projectPath}/commit/${commitSha}/comments/${id}`;
return (
await this.request({
endpoint:
commitEndpoint +
(update && existingCommmit ? existingCommmit.id : ''),
method: update && existingCommmit ? 'PUT' : 'POST',
endpoint,
method: 'PUT',
body: JSON.stringify({ content: { raw: report } })
})
).links.html.href;
}

async commitComments(opts = {}) {
const { projectPath } = this;
const { commitSha } = opts;

const endpoint = `/repositories/${projectPath}/commit/${commitSha}/comments/`;

return (await this.paginatedRequest({ endpoint, method: 'GET' })).map(
({ id, content: { raw: body = '' } = {} }) => {
return { id, body };
}
);
}

async commitPrs(opts = {}) {
const { projectPath } = this;
const { commitSha, state = 'OPEN' } = opts;

try {
const endpoint = `/repositories/${projectPath}/commit/${commitSha}/pullrequests?state=${state}`;
const prs = await this.paginatedRequest({ endpoint });

return prs.map((pr) => {
const {
links: {
html: { href: url }
}
} = pr;
return {
url
};
});
} catch (err) {
if (err.message === 'Not Found Resource not found')
err.message =
"Click 'Go to pull request' on any commit details page to enable this API";
throw err;
}
}

async checkCreate() {
throw new Error('Bitbucket Cloud does not support check!');
}
Expand Down Expand Up @@ -142,31 +144,78 @@ class BitbucketCloud {
return href;
}

async prs(opts = {}) {
async prCommentCreate(opts = {}) {
const { projectPath } = this;
const { state = 'OPEN' } = opts;
const { report, prNumber } = opts;

const endpoint = `/repositories/${projectPath}/pullrequests?state=${state}`;
const { values: prs } = await this.request({ endpoint });
const endpoint = `/repositories/${projectPath}/pullrequests/${prNumber}/comments/`;
const output = await this.request({
endpoint,
method: 'POST',
body: JSON.stringify({ content: { raw: report } })
});

return prs.map((pr) => {
const {
links: {
html: { href: url }
},
source: {
branch: { name: source }
},
destination: {
branch: { name: target }
}
} = pr;
return {
url,
source,
target
};
return output.links.self.href;
}

async prCommentUpdate(opts = {}) {
const { projectPath } = this;
const { report, prNumber, id } = opts;

const endpoint = `/repositories/${projectPath}/pullrequests/${prNumber}/comments/${id}`;
const output = await this.request({
endpoint,
method: 'PUT',
body: JSON.stringify({ content: { raw: report } })
});

return output.links.self.href;
}

async prComments(opts = {}) {
const { projectPath } = this;
const { prNumber } = opts;

const endpoint = `/repositories/${projectPath}/pullrequests/${prNumber}/comments/`;
return (await this.paginatedRequest({ endpoint, method: 'GET' })).map(
({ id, content: { raw: body = '' } = {} }) => {
return { id, body };
}
);
}

async prs(opts = {}) {
const { projectPath } = this;
const { state = 'OPEN' } = opts;

try {
const endpoint = `/repositories/${projectPath}/pullrequests?state=${state}`;
const prs = await this.paginatedRequest({ endpoint });

return prs.map((pr) => {
const {
links: {
html: { href: url }
},
source: {
branch: { name: source }
},
destination: {
branch: { name: target }
}
} = pr;
return {
url,
source,
target
};
});
} catch (err) {
if (err.message === 'Not Found Resource not found')
err.message =
"Click 'Go to pull request' on any commit details page to enable this API";
throw err;
}
}

async pipelineRestart(opts = {}) {
Expand Down
3 changes: 2 additions & 1 deletion src/drivers/bitbucket_cloud.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ describe('Non Enviromental tests', () => {
test('Comment', async () => {
const report = '## Test comment';
const commitSha = SHA;
const url = await client.commentCreate({ report, commitSha });

await client.commentCreate({ report, commitSha });
expect(url.startsWith('https://')).toBe(true);
});

test('Check', async () => {
Expand Down
Loading

19 comments on commit 0958298

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test Comment

CML watermark

Please sign in to comment.