1+ /* eslint no-underscore-dangle: ["error", { "allowAfterThis": true }] */
2+
13'use strict' ;
24
35const 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
0 commit comments