Skip to content

Commit 35e7b41

Browse files
authored
Merge pull request #29 from screwdriver-cd/addWebhook
feat(379): addWebhook to attach a webhook URL to a repo
2 parents c52156e + c1d7fd3 commit 35e7b41

File tree

4 files changed

+549
-65
lines changed

4 files changed

+549
-65
lines changed

index.js

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
/* eslint no-underscore-dangle: ["error", { "allowAfterThis": true }] */
2+
13
'use strict';
24

35
const Fusebox = require('circuit-fuses');
@@ -22,6 +24,35 @@ const STATE_MAP = {
2224
FAILURE: 'FAILED',
2325
ABORTED: 'STOPPED'
2426
};
27+
const WEBHOOK_PAGE_SIZE = 30;
28+
29+
/**
30+
* Check the status code of the server's response.
31+
*
32+
* If there was an error encountered with the request, this will format a human-readable
33+
* error message.
34+
* @method checkResponseError
35+
* @param {HTTPResponse} response HTTP Response from `request` call
36+
* @param {Number} response.statusCode HTTP status code of the HTTP request
37+
* @param {String} [response.body.error.message] Error message from the server
38+
* @param {String} [response.body.error.detail.required] Error resolution message
39+
* @return {Promise} Resolves when no error encountered.
40+
* Rejects when status code is non-200
41+
*/
42+
function checkResponseError(response) {
43+
if (response.statusCode >= 200 && response.statusCode < 300) {
44+
return;
45+
}
46+
47+
const errorMessage = hoek.reach(response, 'body.error.message', {
48+
default: `SCM service unavailable (${response.statusCode}).`
49+
});
50+
const errorReason = hoek.reach(response, 'body.error.detail.required', {
51+
default: JSON.stringify(response.body)
52+
});
53+
54+
throw new Error(`${errorMessage} Reason "${errorReason}"`);
55+
}
2556

2657
/**
2758
* Get repo information
@@ -78,6 +109,121 @@ class BitbucketScm extends Scm {
78109
this.breaker = new Fusebox(request, this.config.fusebox);
79110
}
80111

112+
/**
113+
* Look for a specific webhook that is attached to a repo.
114+
*
115+
* Searches through the webhook pages until the given webhook URL is found. If nothing is found, this will
116+
* return nothing. If a status response of non-200 is encountered, the chain is rejected with the
117+
* HTTP operation and the status code received.
118+
* @method _findWebhook
119+
* @param {Object} config
120+
* @param {Number} config.page pagination: page number to search next. 1-index.
121+
* @param {String} config.repoId The bitbucket repo ID (e.g., "username/repoSlug")
122+
* @param {String} config.token Admin Oauth2 token for the repo
123+
* @param {String} config.url url for webhook notifications
124+
* @return {Promise} Resolves to a webhook information payload
125+
*/
126+
_findWebhook(config) {
127+
return this.breaker.runCommand({
128+
json: true,
129+
login_type: 'oauth2',
130+
method: 'GET',
131+
oauth_access_token: config.token,
132+
url: `${API_URL_V2}/repositories/${config.repoId}/hooks?pagelen=30&page=${config.page}`
133+
}).then((response) => {
134+
checkResponseError(response);
135+
136+
const hooks = response.body;
137+
const result = hooks.values.find(webhook => webhook.url === config.url);
138+
139+
if (!result && hooks.size >= WEBHOOK_PAGE_SIZE) {
140+
return this._findWebhook({
141+
page: config.page + 1,
142+
repoId: config.repoId,
143+
token: config.token,
144+
url: config.url
145+
});
146+
}
147+
148+
return result;
149+
});
150+
}
151+
152+
/**
153+
* Creates and updates the webhook that is attached to a repo.
154+
*
155+
* By default, it creates a new webhook. If given a webhook payload, it will instead update the webhook to
156+
* ensure the correct settings are in place. If a status response of non-200 is encountered, the chain is
157+
* rejected with the HTTP operation and the status code received.
158+
* @method _createWebhook
159+
* @param {Object} config
160+
* @param {Object} [config.hookInfo] Information about an existing webhook
161+
* @param {String} config.repoId Bitbucket repo ID (e.g., "username/repoSlug")
162+
* @param {String} config.token Admin Oauth2 token for the repo
163+
* @param {String} config.url url to create for webhook notifications
164+
* @return {Promise} Resolves when complete
165+
*/
166+
_createWebhook(config) {
167+
const params = {
168+
body: {
169+
description: 'Screwdriver-CD build trigger',
170+
url: config.url,
171+
active: true,
172+
events: [
173+
'repo:push',
174+
'pullrequest:created',
175+
'pullrequest:fulfilled',
176+
'pullrequest:rejected',
177+
'pullrequest:updated'
178+
]
179+
},
180+
json: true,
181+
login_type: 'oauth2',
182+
method: 'POST',
183+
oauth_access_token: config.token,
184+
url: `${API_URL_V2}/repositories/${config.repoId}/hooks`
185+
};
186+
187+
if (config.hookInfo) {
188+
params.url = `${params.url}/${config.hookInfo.uuid}`;
189+
params.method = 'PUT';
190+
}
191+
192+
return this.breaker.runCommand(params)
193+
.then(checkResponseError);
194+
}
195+
196+
/**
197+
* Adds the Screwdriver webhook to the Bitbucket repository
198+
*
199+
* By default, it will attach the webhook to the repository. If the webhook URL already exists, then it
200+
* is instead updated.
201+
* @method _addWebhook
202+
* @param {Object} config
203+
* @param {String} config.scmUri The SCM URI to add the webhook to
204+
* @param {String} config.token Oauth2 token to authenticate with Bitbucket
205+
* @param {String} config.url The URL to use for webhook notifications
206+
* @return {Promise} Resolves upon success
207+
*/
208+
_addWebhook(config) {
209+
const repoInfo = getScmUriParts(config.scmUri);
210+
211+
return this._findWebhook({
212+
page: 1,
213+
repoId: repoInfo.repoId,
214+
token: config.token,
215+
url: config.url
216+
})
217+
.then(hookInfo =>
218+
this._createWebhook({
219+
hookInfo,
220+
repoId: repoInfo.repoId,
221+
token: config.token,
222+
url: config.url
223+
})
224+
);
225+
}
226+
81227
/**
82228
* Parse the url for a repo for the specific source control
83229
* @method parseUrl

test/data/commands.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"name": "sd-checkout-code",
3-
"command": "echo Cloning https://bitbucket.org/screwdriver-cd/scm-bitbucket, on branch master && git clone --quiet --progress --branch master https://bitbucket.org/screwdriver-cd/scm-bitbucket $SD_SOURCE_DIR && echo Reset to SHA 40171b678527 && git reset --hard 40171b678527 && echo Setting user name and user email && git config user.name sd-buildbot && git config user.email dev-null@screwdriver.cd"
3+
"command": "echo Cloning https://hostName/orgName/repoName, on branch branchName && git clone --quiet --progress --branch branchName https://hostName/orgName/repoName $SD_SOURCE_DIR && echo Reset to SHA shaValue && git reset --hard shaValue && echo Setting user name and user email && git config user.name sd-buildbot && git config user.email dev-null@screwdriver.cd"
44
}

test/data/prCommands.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
22
"name": "sd-checkout-code",
3-
"command": "echo Cloning https://bitbucket.org/screwdriver-cd/scm-bitbucket, on branch master && git clone --quiet --progress --branch master https://bitbucket.org/screwdriver-cd/scm-bitbucket $SD_SOURCE_DIR && echo Reset to SHA master && git reset --hard master && echo Setting user name and user email && git config user.name sd-buildbot && git config user.email dev-null@screwdriver.cd && echo Fetching PR and merging with master && git fetch origin prBranch && git merge 40171b678527"
3+
"command": "echo Cloning https://hostName/orgName/repoName, on branch branchName && git clone --quiet --progress --branch branchName https://hostName/orgName/repoName $SD_SOURCE_DIR && echo Reset to SHA branchName && git reset --hard branchName && echo Setting user name and user email && git config user.name sd-buildbot && git config user.email dev-null@screwdriver.cd && echo Fetching PR and merging with branchName && git fetch origin prBranch && git merge shaValue"
44
}

0 commit comments

Comments
 (0)