Skip to content

Commit cf0ba10

Browse files
authored
Add cml-send-comment --update (#581)
* Fix minor typo in cml-send-comment.js * Add GitHub support for cml-send-comment --update * Add BitBucket support for cml-send comment --update * Increase end to end test timeouts * Emit a warning when Pull Request Commit Links is not installed * Alias console.log to print after monkey-patching * Rename fullEndpoint to url * Use more destructuring on bitbucket_cloud.js * Use destructuring on github.js * Add GitLab error for comment updates * Use destructuring as a workaround to optional chaining (#596) * Require the Pull Request Commit Links API to work * BB comment spacing
1 parent 1c6fb1f commit cf0ba10

File tree

9 files changed

+136
-56
lines changed

9 files changed

+136
-56
lines changed

bin/cml-send-comment.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env node
22

3+
const print = console.log;
34
console.log = console.error;
45

56
const fs = require('fs').promises;
@@ -11,7 +12,7 @@ const run = async (opts) => {
1112
const path = opts._[0];
1213
const report = await fs.readFile(path, 'utf-8');
1314
const cml = new CML(opts);
14-
await cml.commentCreate({ ...opts, report });
15+
print(await cml.commentCreate({ ...opts, report }));
1516
};
1617

1718
const opts = yargs
@@ -23,6 +24,11 @@ const opts = yargs
2324
'Commit SHA linked to this comment. Defaults to HEAD.'
2425
)
2526
.alias('commit-sha', 'head-sha')
27+
.boolean('update')
28+
.describe(
29+
'update',
30+
'Update the last CML comment (if any) instead of creating a new one'
31+
)
2632
.boolean('rm-watermark')
2733
.describe(
2834
'rm-watermark',
@@ -36,10 +42,10 @@ const opts = yargs
3642
.default('token')
3743
.describe(
3844
'token',
39-
'Personal access token to be used. If not specified in extracted from ENV REPO_TOKEN.'
45+
'Personal access token to be used. If not specified is extracted from ENV REPO_TOKEN.'
4046
)
4147
.default('driver')
42-
.choices('driver', ['github', 'gitlab'])
48+
.choices('driver', ['github', 'gitlab', 'bitbucket'])
4349
.describe('driver', 'If not specify it infers it from the ENV.')
4450
.help('h')
4551
.demand(1).argv;

bin/cml-send-comment.test.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,17 @@ describe('Comment integration tests', () => {
2121
Options:
2222
--version Show version number [boolean]
2323
--commit-sha, --head-sha Commit SHA linked to this comment. Defaults to HEAD.
24+
--update Update the last CML comment (if any) instead of
25+
creating a new one [boolean]
2426
--rm-watermark Avoid watermark. CML needs a watermark to be able to
2527
distinguish CML reports from other comments in order
2628
to provide extra functionality. [boolean]
2729
--repo Specifies the repo to be used. If not specified is
2830
extracted from the CI ENV.
2931
--token Personal access token to be used. If not specified
30-
in extracted from ENV REPO_TOKEN.
32+
is extracted from ENV REPO_TOKEN.
3133
--driver If not specify it infers it from the ENV.
32-
[choices: \\"github\\", \\"gitlab\\"]
34+
[choices: \\"github\\", \\"gitlab\\", \\"bitbucket\\"]
3335
-h Show help [boolean]"
3436
`);
3537
});

src/cml.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,11 @@ class CML {
9696
const {
9797
report: userReport,
9898
commitSha = await this.headSha(),
99-
rmWatermark
99+
rmWatermark,
100+
update
100101
} = opts;
102+
if (rmWatermark && update)
103+
throw new Error('watermarks are mandatory for updateable comments');
101104
const watermark = rmWatermark
102105
? ''
103106
: ' \n\n ![CML watermark](https://raw.githubusercontent.com/iterative/cml/master/assets/watermark.svg)';
@@ -106,7 +109,8 @@ class CML {
106109
return await getDriver(this).commentCreate({
107110
...opts,
108111
report,
109-
commitSha
112+
commitSha,
113+
watermark
110114
});
111115
}
112116

src/cml.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const CML = require('../src/cml').default;
22

3+
jest.setTimeout(40000);
34
describe('Github tests', () => {
45
const OLD_ENV = process.env;
56

src/drivers/bitbucket_cloud.js

Lines changed: 77 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,67 @@ class BitBucketCloud {
2222

2323
async commentCreate(opts = {}) {
2424
const { projectPath } = this;
25-
const { commitSha, report } = opts;
26-
27-
// Make a comment in the commit
28-
const commitEndpoint = `/repositories/${projectPath}/commit/${commitSha}/comments/`;
29-
const commitBody = JSON.stringify({ content: { raw: report } });
30-
const commitOutput = await this.request({
31-
endpoint: commitEndpoint,
32-
method: 'POST',
33-
body: commitBody
34-
});
25+
const { commitSha, report, update, watermark } = opts;
3526

3627
// Check for a corresponding PR. If it exists, also put the comment there.
37-
const getPrEndpt = `/repositories/${projectPath}/commit/${commitSha}/pullrequests`;
38-
const { values: prs } = await this.request({ endpoint: getPrEndpt });
28+
let prs;
29+
try {
30+
const getPrEndpoint = `/repositories/${projectPath}/commit/${commitSha}/pullrequests`;
31+
prs = await this.paginatedRequest({ endpoint: getPrEndpoint });
32+
} catch (err) {
33+
if (err.message === 'Not Found Resource not found')
34+
err.message =
35+
"Click 'Go to pull request' on any commit details page to enable this API";
36+
throw err;
37+
}
3938

4039
if (prs && prs.length) {
4140
for (const pr of prs) {
42-
try {
43-
// Append a watermark to the report with a link to the commit
44-
const commitLink = commitSha.substr(0, 7);
45-
const longReport = `${commitLink} \n${report}`;
46-
const prBody = JSON.stringify({ content: { raw: longReport } });
47-
48-
// Write a comment on the PR
49-
const prEndpoint = `/repositories/${projectPath}/pullrequests/${pr.id}/comments`;
50-
await this.request({
51-
endpoint: prEndpoint,
52-
method: 'POST',
53-
body: prBody
54-
});
55-
} catch (err) {
56-
console.debug(err.message);
57-
}
41+
// Append a watermark to the report with a link to the commit
42+
const commitLink = commitSha.substr(0, 7);
43+
const longReport = `${commitLink}\n\n${report}`;
44+
const prBody = JSON.stringify({ content: { raw: longReport } });
45+
46+
// Write a comment on the PR
47+
const prEndpoint = `/repositories/${projectPath}/pullrequests/${pr.id}/comments/`;
48+
const existingPr = (
49+
await this.paginatedRequest({ endpoint: prEndpoint, method: 'GET' })
50+
)
51+
.filter((comment) => {
52+
const { content: { raw = '' } = {} } = comment;
53+
return raw.endsWith(watermark);
54+
})
55+
.sort((first, second) => first.id < second.id)
56+
.pop();
57+
await this.request({
58+
endpoint: prEndpoint + (update && existingPr ? existingPr.id : ''),
59+
method: update && existingPr ? 'PUT' : 'POST',
60+
body: prBody
61+
});
5862
}
5963
}
6064

61-
return commitOutput;
65+
const commitEndpoint = `/repositories/${projectPath}/commit/${commitSha}/comments/`;
66+
67+
const existingCommmit = (
68+
await this.paginatedRequest({ endpoint: commitEndpoint, method: 'GET' })
69+
)
70+
.filter((comment) => {
71+
const { content: { raw = '' } = {} } = comment;
72+
return raw.endsWith(watermark);
73+
})
74+
.sort((first, second) => first.id < second.id)
75+
.pop();
76+
77+
return (
78+
await this.request({
79+
endpoint:
80+
commitEndpoint +
81+
(update && existingCommmit ? existingCommmit.id : ''),
82+
method: update && existingCommmit ? 'PUT' : 'POST',
83+
body: JSON.stringify({ content: { raw: report } })
84+
})
85+
).links.html.href;
6286
}
6387

6488
async checkCreate() {
@@ -150,15 +174,18 @@ class BitBucketCloud {
150174

151175
async request(opts = {}) {
152176
const { token, api } = this;
153-
const { endpoint, method = 'GET', body } = opts;
154-
155-
if (!endpoint) throw new Error('BitBucket Cloud API endpoint not found');
177+
const { url, endpoint, method = 'GET', body } = opts;
178+
if (!(url || endpoint))
179+
throw new Error('BitBucket Cloud API endpoint not found');
156180
const headers = {
157181
'Content-Type': 'application/json',
158182
Authorization: 'Basic ' + `${token}`
159183
};
160-
const url = `${api}${endpoint}`;
161-
const response = await fetch(url, { method, headers, body });
184+
const response = await fetch(url || `${api}${endpoint}`, {
185+
method,
186+
headers,
187+
body
188+
});
162189

163190
if (response.status > 300) {
164191
const {
@@ -170,6 +197,22 @@ class BitBucketCloud {
170197
return await response.json();
171198
}
172199

200+
async paginatedRequest(opts = {}) {
201+
const { method = 'GET', body } = opts;
202+
const { next, values } = await this.request(opts);
203+
204+
if (next) {
205+
const nextValues = await this.paginatedRequest({
206+
url: next,
207+
method,
208+
body
209+
});
210+
values.push(...nextValues);
211+
}
212+
213+
return values;
214+
}
215+
173216
get sha() {
174217
return BITBUCKET_COMMIT;
175218
}

src/drivers/bitbucket_cloud.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
jest.setTimeout(20000);
1+
jest.setTimeout(120000);
22
const BitBucketCloud = require('./bitbucket_cloud');
33
const {
44
TEST_BBCLOUD_TOKEN: TOKEN,

src/drivers/github.js

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -70,18 +70,40 @@ class Github {
7070
}
7171

7272
async commentCreate(opts = {}) {
73-
const { report: body, commitSha } = opts;
74-
75-
const { url: commitUrl } = await octokit(
76-
this.token,
77-
this.repo
78-
).repos.createCommitComment({
79-
...ownerRepo({ uri: this.repo }),
80-
body,
81-
commit_sha: commitSha
82-
});
83-
84-
return commitUrl;
73+
const { report: body, commitSha, update, watermark } = opts;
74+
75+
const { paginate, repos } = octokit(this.token, this.repo);
76+
77+
const existing = Object.values(
78+
await paginate(repos.listCommentsForCommit, {
79+
...ownerRepo({ uri: this.repo }),
80+
commit_sha: commitSha
81+
})
82+
)
83+
.filter((comment) => {
84+
const { body = '' } = comment;
85+
return body.endsWith(watermark);
86+
})
87+
.sort((first, second) => first.id < second.id)
88+
.pop();
89+
90+
if (update && existing) {
91+
return (
92+
await repos.updateCommitComment({
93+
...ownerRepo({ uri: this.repo }),
94+
comment_id: existing.id,
95+
body
96+
})
97+
).data.html_url;
98+
} else {
99+
return (
100+
await repos.createCommitComment({
101+
...ownerRepo({ uri: this.repo }),
102+
commit_sha: commitSha,
103+
body
104+
})
105+
).data.html_url;
106+
}
85107
}
86108

87109
async checkCreate(opts = {}) {

src/drivers/github.test.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
jest.setTimeout(20000);
1+
jest.setTimeout(40000);
22

33
const GithubClient = require('./github');
44

src/drivers/gitlab.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ class Gitlab {
7171
}
7272

7373
async commentCreate(opts = {}) {
74-
const { commitSha, report } = opts;
74+
const { commitSha, report, update } = opts;
75+
76+
if (update) throw new Error('GitLab does not support comment updates!');
7577

7678
const projectPath = await this.projectPath();
7779
const endpoint = `/projects/${projectPath}/repository/commits/${commitSha}/comments`;

0 commit comments

Comments
 (0)