diff --git a/lib/github-comment.js b/lib/github-comment.js index 221bc195..53075604 100644 --- a/lib/github-comment.js +++ b/lib/github-comment.js @@ -14,3 +14,16 @@ exports.createPrComment = function createPrComment ({ owner, repo, number, logge } }) } + +exports.editPrComment = function createPrComment ({ owner, repo, logger }, commentId, body) { + githubClient.issues.editComment({ + owner, + repo, + id: commentId, + body + }, (err) => { + if (err) { + logger.error(err, 'Error while editing comment on GitHub') + } + }) +} diff --git a/lib/push-jenkins-update.js b/lib/push-jenkins-update.js index 25bd1965..0c7fe822 100644 --- a/lib/push-jenkins-update.js +++ b/lib/push-jenkins-update.js @@ -3,7 +3,8 @@ const url = require('url') const githubClient = require('./github-client') -const { createPrComment } = require('./github-comment') +const { createPrComment, editPrComment } = require('./github-comment') +const botUsername = require('./bot-username') function pushStarted (options, build, cb) { const pr = findPrInRef(build.ref) @@ -15,7 +16,19 @@ function pushStarted (options, build, cb) { const optsWithPr = Object.assign({ pr }, options) if (build.identifier === 'node-test-pull-request' && build.status === 'pending') { - createPrComment(Object.assign({ number: pr }, options), `CI: ${build.url}`) + findExistingCiComment(optsWithPr, logger, (err, existingComment) => { + if (err) { + logger.error(err, 'Got error while retrieving GitHub comments for PR') + return + } + + const body = `CI: ${build.url}` + if (existingComment === undefined) { + createPrComment(Object.assign({ number: pr }, options), body) + } else { + editPrComment(options, existingComment.id, `${existingComment.body}\n${body}`) + } + }) } findLatestCommitInPr(optsWithPr, (err, latestCommit) => { @@ -118,6 +131,21 @@ function createGhStatus (options, logger, cb) { }) } +function findExistingCiComment (options, logger, cb) { + githubClient.issues.getComments({ + owner: options.owner, + repo: options.repo, + number: options.pr + }, (err, response) => { + if (err) { + return cb(err) + } + + const comments = response.data.reverse() + cb(null, comments.find((comment) => comment.body.startsWith('CI: ') && comment.user.login === botUsername)) + }) +} + function validate (payload) { const isString = (param) => typeof (payload[param]) === 'string' return ['identifier', 'status', 'message', 'commit', 'url'].every(isString) diff --git a/test/_fixtures/authenticated-user.json b/test/_fixtures/authenticated-user.json new file mode 100644 index 00000000..b9aeb143 --- /dev/null +++ b/test/_fixtures/authenticated-user.json @@ -0,0 +1,33 @@ +{ + "login": "nodejs-github-bot", + "id": 18269663, + "node_id": "MDQ6VXNlcjE4MjY5NjYz", + "avatar_url": "https://avatars1.githubusercontent.com/u/18269663?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/nodejs-github-bot", + "html_url": "https://github.com/nodejs-github-bot", + "followers_url": "https://api.github.com/users/nodejs-github-bot/followers", + "following_url": "https://api.github.com/users/nodejs-github-bot/following{/other_user}", + "gists_url": "https://api.github.com/users/nodejs-github-bot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/nodejs-github-bot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/nodejs-github-bot/subscriptions", + "organizations_url": "https://api.github.com/users/nodejs-github-bot/orgs", + "repos_url": "https://api.github.com/users/nodejs-github-bot/repos", + "events_url": "https://api.github.com/users/nodejs-github-bot/events{/privacy}", + "received_events_url": "https://api.github.com/users/nodejs-github-bot/received_events", + "type": "User", + "site_admin": false, + "name": "Node.js GitHub Bot", + "company": null, + "blog": "https://github.com/nodejs/github-bot", + "location": null, + "email": null, + "hireable": null, + "bio": null, + "public_repos": 1, + "public_gists": 0, + "followers": 21, + "following": 0, + "created_at": "2016-04-04T18:31:48Z", + "updated_at": "2018-11-21T02:22:03Z" +} diff --git a/test/_fixtures/pull-request-comments-with-ci-comment.json b/test/_fixtures/pull-request-comments-with-ci-comment.json new file mode 100644 index 00000000..226fc143 --- /dev/null +++ b/test/_fixtures/pull-request-comments-with-ci-comment.json @@ -0,0 +1,126 @@ +[ + { + "url": "https://api.github.com/repos/nodejs/node/issues/comments/475182143", + "html_url": "https://github.com/nodejs/node/pull/26837#issuecomment-475182143", + "issue_url": "https://api.github.com/repos/nodejs/node/issues/26837", + "id": 475182143, + "node_id": "MDEyOklzc3VlQ29tbWVudDQ3NTE4MjE0Mw==", + "user": { + "login": "nodejs-github-bot", + "id": 18269663, + "node_id": "MDQ6VXNlcjE4MjY5NjYz", + "avatar_url": "https://avatars1.githubusercontent.com/u/18269663?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/nodejs-github-bot", + "html_url": "https://github.com/nodejs-github-bot", + "followers_url": "https://api.github.com/users/nodejs-github-bot/followers", + "following_url": "https://api.github.com/users/nodejs-github-bot/following{/other_user}", + "gists_url": "https://api.github.com/users/nodejs-github-bot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/nodejs-github-bot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/nodejs-github-bot/subscriptions", + "organizations_url": "https://api.github.com/users/nodejs-github-bot/orgs", + "repos_url": "https://api.github.com/users/nodejs-github-bot/repos", + "events_url": "https://api.github.com/users/nodejs-github-bot/events{/privacy}", + "received_events_url": "https://api.github.com/users/nodejs-github-bot/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2019-03-21T10:42:26Z", + "updated_at": "2019-03-21T10:42:26Z", + "author_association": "MEMBER", + "body": "@addaleax build started: https://ci.nodejs.org/blue/organizations/jenkins/node-test-pull-request-lite-pipeline/detail/node-test-pull-request-lite-pipeline/2998/pipeline" + }, + { + "url": "https://api.github.com/repos/nodejs/node/issues/comments/475368357", + "html_url": "https://github.com/nodejs/node/pull/26837#issuecomment-475368357", + "issue_url": "https://api.github.com/repos/nodejs/node/issues/26837", + "id": 475368357, + "node_id": "MDEyOklzc3VlQ29tbWVudDQ3NTM2ODM1Nw==", + "user": { + "login": "addaleax", + "id": 899444, + "node_id": "MDQ6VXNlcjg5OTQ0NA==", + "avatar_url": "https://avatars2.githubusercontent.com/u/899444?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/addaleax", + "html_url": "https://github.com/addaleax", + "followers_url": "https://api.github.com/users/addaleax/followers", + "following_url": "https://api.github.com/users/addaleax/following{/other_user}", + "gists_url": "https://api.github.com/users/addaleax/gists{/gist_id}", + "starred_url": "https://api.github.com/users/addaleax/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/addaleax/subscriptions", + "organizations_url": "https://api.github.com/users/addaleax/orgs", + "repos_url": "https://api.github.com/users/addaleax/repos", + "events_url": "https://api.github.com/users/addaleax/events{/privacy}", + "received_events_url": "https://api.github.com/users/addaleax/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2019-03-21T19:20:30Z", + "updated_at": "2019-03-21T19:20:30Z", + "author_association": "MEMBER", + "body": "CI: https://ci.nodejs.org/job/node-test-pull-request/21737/" + }, + { + "url": "https://api.github.com/repos/nodejs/node/issues/comments/475385809", + "html_url": "https://github.com/nodejs/node/pull/26837#issuecomment-475385809", + "issue_url": "https://api.github.com/repos/nodejs/node/issues/26837", + "id": 475385809, + "node_id": "MDEyOklzc3VlQ29tbWVudDQ3NTM4NTgwOQ==", + "user": { + "login": "addaleax", + "id": 899444, + "node_id": "MDQ6VXNlcjg5OTQ0NA==", + "avatar_url": "https://avatars2.githubusercontent.com/u/899444?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/addaleax", + "html_url": "https://github.com/addaleax", + "followers_url": "https://api.github.com/users/addaleax/followers", + "following_url": "https://api.github.com/users/addaleax/following{/other_user}", + "gists_url": "https://api.github.com/users/addaleax/gists{/gist_id}", + "starred_url": "https://api.github.com/users/addaleax/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/addaleax/subscriptions", + "organizations_url": "https://api.github.com/users/addaleax/orgs", + "repos_url": "https://api.github.com/users/addaleax/repos", + "events_url": "https://api.github.com/users/addaleax/events{/privacy}", + "received_events_url": "https://api.github.com/users/addaleax/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2019-03-21T20:16:21Z", + "updated_at": "2019-03-21T20:16:21Z", + "author_association": "MEMBER", + "body": "Resume CI: https://ci.nodejs.org/job/node-test-pull-request/21739/" + }, + { + "url": "https://api.github.com/repos/nodejs/node/issues/comments/476584580", + "html_url": "https://github.com/nodejs/node/pull/26837#issuecomment-476584580", + "issue_url": "https://api.github.com/repos/nodejs/node/issues/26837", + "id": 476584580, + "node_id": "MDEyOklzc3VlQ29tbWVudDQ3NjU4NDU4MA==", + "user": { + "login": "nodejs-github-bot", + "id": 18269663, + "node_id": "MDQ6VXNlcjE4MjY5NjYz", + "avatar_url": "https://avatars1.githubusercontent.com/u/18269663?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/nodejs-github-bot", + "html_url": "https://github.com/nodejs-github-bot", + "followers_url": "https://api.github.com/users/nodejs-github-bot/followers", + "following_url": "https://api.github.com/users/nodejs-github-bot/following{/other_user}", + "gists_url": "https://api.github.com/users/nodejs-github-bot/gists{/gist_id}", + "starred_url": "https://api.github.com/users/nodejs-github-bot/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/nodejs-github-bot/subscriptions", + "organizations_url": "https://api.github.com/users/nodejs-github-bot/orgs", + "repos_url": "https://api.github.com/users/nodejs-github-bot/repos", + "events_url": "https://api.github.com/users/nodejs-github-bot/events{/privacy}", + "received_events_url": "https://api.github.com/users/nodejs-github-bot/received_events", + "type": "User", + "site_admin": false + }, + "created_at": "2019-03-26T11:25:56Z", + "updated_at": "2019-03-26T11:25:56Z", + "author_association": "MEMBER", + "body": "CI: https://ci.nodejs.org/job/node-test-pull-request/21904/" + } +] diff --git a/test/integration/push-jenkins-update.test.js b/test/integration/push-jenkins-update.test.js index 7184d81a..7a77a5f2 100644 --- a/test/integration/push-jenkins-update.test.js +++ b/test/integration/push-jenkins-update.test.js @@ -104,12 +104,18 @@ tap.test('Forwards payload provided in incoming POST to GitHub status API', (t) tap.test('Posts a CI comment in the related PR when Jenkins build is named node-test-pull-request', (t) => { const fixture = readFixture('jenkins-test-pull-request-success-payload.json') - const commentScope = nock('https://api.github.com') + const createCommentScope = nock('https://api.github.com') .filteringPath(ignoreQueryParams) .post('/repos/nodejs/node/issues/12345/comments', { body: 'CI: https://ci.nodejs.org/job/node-test-pull-request/21633/' }) .reply(200) + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get('/repos/nodejs/node/issues/12345/comments') + .reply(200, []) + // we don't care about asserting the scopes below, just want to stop the requests from actually being sent + setupGetBotUsernameMock() setupGetCommitsMock('node') nock('https://api.github.com') .filteringPath(ignoreQueryParams) @@ -123,7 +129,43 @@ tap.test('Posts a CI comment in the related PR when Jenkins build is named node- .send(fixture) .expect(201) .end((err, res) => { - commentScope.done() + createCommentScope.done() + t.equal(err, null) + }) +}) + +tap.test('Edits existing CI comment when bot has posted a CI comment before', (t) => { + const incomingReqFixture = readFixture('jenkins-test-pull-request-success-payload.json') + const existingCommentsFixture = readFixture('pull-request-comments-with-ci-comment.json') + + const editCommentScope = nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .patch('/repos/nodejs/node/issues/comments/476584580', { + body: `CI: https://ci.nodejs.org/job/node-test-pull-request/21904/\nCI: https://ci.nodejs.org/job/node-test-pull-request/21633/` + }) + .reply(200) + + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get('/repos/nodejs/node/issues/12345/comments') + .reply(200, existingCommentsFixture) + + // we don't care about asserting the scopes below, just want to stop the requests from actually being sent + setupGetBotUsernameMock() + setupGetCommitsMock('node') + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .post('/repos/nodejs/node/statuses/8a5fec2a6bade91e544a30314d7cf21f8a200de1') + .reply(201) + + t.plan(1) + + supertest(app) + .post('/node/jenkins/start') + .send(incomingReqFixture) + .expect(201) + .end((err, res) => { + editCommentScope.done() t.equal(err, null) }) }) @@ -205,6 +247,13 @@ function setupGetCommitsMock (repoName) { .reply(200, commitsResponse) } +function setupGetBotUsernameMock () { + nock('https://api.github.com') + .filteringPath(ignoreQueryParams) + .get('/user') + .reply(200, readFixture('authenticated-user.json')) +} + function ignoreQueryParams (pathAndQuery) { return url.parse(pathAndQuery, true).pathname }