|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* PHPCI - Continuous Integration for PHP. |
|
4
|
|
|
* |
|
5
|
|
|
* @copyright Copyright 2014-2015, Block 8 Limited. |
|
6
|
|
|
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md |
|
7
|
|
|
* |
|
8
|
|
|
* @link https://www.phptesting.org/ |
|
9
|
|
|
*/ |
|
10
|
|
|
|
|
11
|
|
|
namespace PHPCI\Controller; |
|
12
|
|
|
|
|
13
|
|
|
use b8; |
|
14
|
|
|
use b8\Store; |
|
15
|
|
|
use Exception; |
|
16
|
|
|
use PHPCI\Model\Project; |
|
17
|
|
|
use PHPCI\Service\BuildService; |
|
18
|
|
|
use PHPCI\Store\BuildStore; |
|
19
|
|
|
use PHPCI\Store\ProjectStore; |
|
20
|
|
|
|
|
21
|
|
|
/** |
|
22
|
|
|
* Webhook Controller - Processes webhook pings from BitBucket, Github, Gitlab, etc. |
|
23
|
|
|
* |
|
24
|
|
|
* @author Dan Cryer <[email protected]> |
|
25
|
|
|
* @author Sami Tikka <[email protected]> |
|
26
|
|
|
* @author Alex Russell <[email protected]> |
|
27
|
|
|
* @author Guillaume Perréal <[email protected]> |
|
28
|
|
|
*/ |
|
29
|
|
|
class WebhookController extends \b8\Controller |
|
30
|
|
|
{ |
|
31
|
|
|
/** |
|
32
|
|
|
* @var BuildStore |
|
33
|
|
|
*/ |
|
34
|
|
|
protected $buildStore; |
|
35
|
|
|
|
|
36
|
|
|
/** |
|
37
|
|
|
* @var ProjectStore |
|
38
|
|
|
*/ |
|
39
|
|
|
protected $projectStore; |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* @var BuildService |
|
43
|
|
|
*/ |
|
44
|
|
|
protected $buildService; |
|
45
|
|
|
|
|
46
|
|
|
/** |
|
47
|
|
|
* Initialise the controller, set up stores and services. |
|
48
|
|
|
*/ |
|
49
|
|
|
public function init() |
|
50
|
|
|
{ |
|
51
|
|
|
$this->buildStore = Store\Factory::getStore('Build'); |
|
52
|
|
|
$this->projectStore = Store\Factory::getStore('Project'); |
|
53
|
|
|
$this->buildService = new BuildService($this->buildStore); |
|
|
|
|
|
|
54
|
|
|
} |
|
55
|
|
|
|
|
56
|
|
|
/** Handle the action, Ensuring to return a JsonResponse. |
|
57
|
|
|
* @param string $action |
|
58
|
|
|
* @param mixed $actionParams |
|
59
|
|
|
* |
|
60
|
|
|
* @return \b8\Http\Response |
|
61
|
|
|
*/ |
|
62
|
|
|
public function handleAction($action, $actionParams) |
|
63
|
|
|
{ |
|
64
|
|
|
$response = new b8\Http\Response\JsonResponse(); |
|
65
|
|
|
try { |
|
66
|
|
|
$data = parent::handleAction($action, $actionParams); |
|
67
|
|
|
if (isset($data['responseCode'])) { |
|
68
|
|
|
$response->setResponseCode($data['responseCode']); |
|
69
|
|
|
unset($data['responseCode']); |
|
70
|
|
|
} |
|
71
|
|
|
$response->setContent($data); |
|
72
|
|
|
} catch (Exception $ex) { |
|
73
|
|
|
$response->setResponseCode(500); |
|
74
|
|
|
$response->setContent(array('status' => 'failed', 'error' => $ex->getMessage())); |
|
75
|
|
|
} |
|
76
|
|
|
|
|
77
|
|
|
return $response; |
|
|
|
|
|
|
78
|
|
|
} |
|
79
|
|
|
|
|
80
|
|
|
/** |
|
81
|
|
|
* Called by Bitbucket POST service. |
|
82
|
|
|
*/ |
|
83
|
|
|
public function bitbucket($projectId) |
|
84
|
|
|
{ |
|
85
|
|
|
$project = $this->fetchProject($projectId, 'bitbucket'); |
|
86
|
|
|
$payload = json_decode($this->getParam('payload'), true); |
|
87
|
|
|
|
|
88
|
|
|
$results = array(); |
|
89
|
|
|
$status = 'failed'; |
|
90
|
|
|
foreach ($payload['commits'] as $commit) { |
|
91
|
|
|
try { |
|
92
|
|
|
$email = $commit['raw_author']; |
|
93
|
|
|
$email = substr($email, 0, strpos($email, '>')); |
|
94
|
|
|
$email = substr($email, strpos($email, '<') + 1); |
|
95
|
|
|
|
|
96
|
|
|
$results[$commit['raw_node']] = $this->createBuild( |
|
97
|
|
|
$project, |
|
|
|
|
|
|
98
|
|
|
$commit['raw_node'], |
|
99
|
|
|
$commit['branch'], |
|
100
|
|
|
$email, |
|
101
|
|
|
$commit['message'] |
|
102
|
|
|
); |
|
103
|
|
|
$status = 'ok'; |
|
104
|
|
|
} catch (Exception $ex) { |
|
105
|
|
|
$results[$commit['raw_node']] = array('status' => 'failed', 'error' => $ex->getMessage()); |
|
106
|
|
|
} |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
return array('status' => $status, 'commits' => $results); |
|
110
|
|
|
} |
|
111
|
|
|
|
|
112
|
|
|
/** |
|
113
|
|
|
* Called by POSTing to /webhook/git/<project_id>?branch=<branch>&commit=<commit>. |
|
114
|
|
|
* |
|
115
|
|
|
* @param string $projectId |
|
116
|
|
|
*/ |
|
117
|
|
|
public function git($projectId) |
|
118
|
|
|
{ |
|
119
|
|
|
$project = $this->fetchProject($projectId, array('local', 'remote')); |
|
120
|
|
|
$branch = $this->getParam('branch', $project->getBranch()); |
|
121
|
|
|
$commit = $this->getParam('commit'); |
|
122
|
|
|
$commitMessage = $this->getParam('message'); |
|
123
|
|
|
$committer = $this->getParam('committer'); |
|
124
|
|
|
|
|
125
|
|
|
return $this->createBuild($project, $commit, $branch, $committer, $commitMessage); |
|
|
|
|
|
|
126
|
|
|
} |
|
127
|
|
|
|
|
128
|
|
|
/** |
|
129
|
|
|
* Called by Github Webhooks:. |
|
130
|
|
|
*/ |
|
131
|
|
|
public function github($projectId) |
|
|
|
|
|
|
132
|
|
|
{ |
|
133
|
|
|
$project = $this->fetchProject($projectId, 'github'); |
|
134
|
|
|
|
|
135
|
|
|
switch ($_SERVER['CONTENT_TYPE']) { |
|
136
|
|
|
case 'application/json': |
|
137
|
|
|
$payload = json_decode(file_get_contents('php://input'), true); |
|
138
|
|
|
break; |
|
139
|
|
|
case 'application/x-www-form-urlencoded': |
|
140
|
|
|
$payload = json_decode($this->getParam('payload'), true); |
|
141
|
|
|
break; |
|
142
|
|
|
default: |
|
143
|
|
|
return array('status' => 'failed', 'error' => 'Content type not supported.', 'responseCode' => 401); |
|
144
|
|
|
} |
|
145
|
|
|
|
|
146
|
|
|
// Handle Pull Request web hooks: |
|
147
|
|
|
if (array_key_exists('pull_request', $payload)) { |
|
148
|
|
|
return $this->githubPullRequest($project, $payload); |
|
|
|
|
|
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
|
// Handle Push web hooks: |
|
152
|
|
|
if (array_key_exists('commits', $payload)) { |
|
153
|
|
|
return $this->githubCommitRequest($project, $payload); |
|
|
|
|
|
|
154
|
|
|
} |
|
155
|
|
|
|
|
156
|
|
|
return array('status' => 'ignored', 'message' => 'Unusable payload.'); |
|
157
|
|
|
} |
|
158
|
|
|
|
|
159
|
|
|
/** |
|
160
|
|
|
* Handle the payload when Github sends a commit webhook. |
|
161
|
|
|
* |
|
162
|
|
|
* @param Project $project |
|
163
|
|
|
* @param array $payload |
|
164
|
|
|
* @param b8\Http\Response\JsonResponse $response |
|
|
|
|
|
|
165
|
|
|
* |
|
166
|
|
|
* @return b8\Http\Response\JsonResponse |
|
167
|
|
|
*/ |
|
168
|
|
|
protected function githubCommitRequest(Project $project, array $payload) |
|
169
|
|
|
{ |
|
170
|
|
|
// Github sends a payload when you close a pull request with a |
|
171
|
|
|
// non-existent commit. We don't want this. |
|
172
|
|
|
if (array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') { |
|
173
|
|
|
return array('status' => 'ignored'); |
|
174
|
|
|
} |
|
175
|
|
|
|
|
176
|
|
|
if (isset($payload['commits']) && is_array($payload['commits'])) { |
|
177
|
|
|
// If we have a list of commits, then add them all as builds to be tested: |
|
178
|
|
|
|
|
179
|
|
|
$results = array(); |
|
180
|
|
|
$status = 'failed'; |
|
181
|
|
|
foreach ($payload['commits'] as $commit) { |
|
182
|
|
|
if (!$commit['distinct']) { |
|
183
|
|
|
$results[$commit['id']] = array('status' => 'ignored'); |
|
184
|
|
|
continue; |
|
185
|
|
|
} |
|
186
|
|
|
|
|
187
|
|
|
try { |
|
188
|
|
|
$branch = str_replace('refs/heads/', '', $payload['ref']); |
|
189
|
|
|
$committer = $commit['committer']['email']; |
|
190
|
|
|
$results[$commit['id']] = $this->createBuild( |
|
191
|
|
|
$project, |
|
192
|
|
|
$commit['id'], |
|
193
|
|
|
$branch, |
|
194
|
|
|
$committer, |
|
195
|
|
|
$commit['message'] |
|
196
|
|
|
); |
|
197
|
|
|
$status = 'ok'; |
|
198
|
|
|
} catch (Exception $ex) { |
|
199
|
|
|
$results[$commit['id']] = array('status' => 'failed', 'error' => $ex->getMessage()); |
|
200
|
|
|
} |
|
201
|
|
|
} |
|
202
|
|
|
|
|
203
|
|
|
return array('status' => $status, 'commits' => $results); |
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
if (substr($payload['ref'], 0, 10) == 'refs/tags/') { |
|
207
|
|
|
// If we don't, but we're dealing with a tag, add that instead: |
|
208
|
|
|
$branch = str_replace('refs/tags/', 'Tag: ', $payload['ref']); |
|
209
|
|
|
$committer = $payload['pusher']['email']; |
|
210
|
|
|
$message = $payload['head_commit']['message']; |
|
211
|
|
|
|
|
212
|
|
|
return $this->createBuild($project, $payload['after'], $branch, $committer, $message); |
|
213
|
|
|
} |
|
214
|
|
|
|
|
215
|
|
|
return array('status' => 'ignored', 'message' => 'Unusable payload.'); |
|
216
|
|
|
} |
|
217
|
|
|
|
|
218
|
|
|
/** |
|
219
|
|
|
* Handle the payload when Github sends a Pull Request webhook. |
|
220
|
|
|
* |
|
221
|
|
|
* @param Project $project |
|
222
|
|
|
* @param array $payload |
|
223
|
|
|
*/ |
|
224
|
|
|
protected function githubPullRequest(Project $project, array $payload) |
|
225
|
|
|
{ |
|
226
|
|
|
// We only want to know about open pull requests: |
|
227
|
|
|
if (!in_array($payload['action'], array('opened', 'synchronize', 'reopened'))) { |
|
228
|
|
|
return array('status' => 'ok'); |
|
229
|
|
|
} |
|
230
|
|
|
|
|
231
|
|
|
$headers = array(); |
|
232
|
|
|
$token = \b8\Config::getInstance()->get('phpci.github.token'); |
|
233
|
|
|
|
|
234
|
|
|
if (!empty($token)) { |
|
235
|
|
|
$headers[] = 'Authorization: token '.$token; |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
$url = $payload['pull_request']['commits_url']; |
|
239
|
|
|
$http = new \b8\HttpClient(); |
|
240
|
|
|
$http->setHeaders($headers); |
|
241
|
|
|
$response = $http->get($url); |
|
242
|
|
|
|
|
243
|
|
|
// Check we got a success response: |
|
244
|
|
|
if (!$response['success']) { |
|
245
|
|
|
throw new Exception('Could not get commits, failed API request.'); |
|
246
|
|
|
} |
|
247
|
|
|
|
|
248
|
|
|
$results = array(); |
|
249
|
|
|
$status = 'failed'; |
|
250
|
|
|
foreach ($response['body'] as $commit) { |
|
251
|
|
|
// Skip all but the current HEAD commit ID: |
|
252
|
|
|
$id = $commit['sha']; |
|
253
|
|
|
if ($id != $payload['pull_request']['head']['sha']) { |
|
254
|
|
|
$results[$id] = array('status' => 'ignored', 'message' => 'not branch head'); |
|
255
|
|
|
continue; |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
try { |
|
259
|
|
|
$branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']); |
|
260
|
|
|
$committer = $commit['commit']['author']['email']; |
|
261
|
|
|
$message = $commit['commit']['message']; |
|
262
|
|
|
|
|
263
|
|
|
$remoteUrlKey = $payload['pull_request']['head']['repo']['private'] ? 'ssh_url' : 'clone_url'; |
|
264
|
|
|
|
|
265
|
|
|
$extra = array( |
|
266
|
|
|
'build_type' => 'pull_request', |
|
267
|
|
|
'pull_request_id' => $payload['pull_request']['id'], |
|
268
|
|
|
'pull_request_number' => $payload['number'], |
|
269
|
|
|
'remote_branch' => $payload['pull_request']['head']['ref'], |
|
270
|
|
|
'remote_url' => $payload['pull_request']['head']['repo'][$remoteUrlKey], |
|
271
|
|
|
); |
|
272
|
|
|
|
|
273
|
|
|
$results[$id] = $this->createBuild($project, $id, $branch, $committer, $message, $extra); |
|
274
|
|
|
$status = 'ok'; |
|
275
|
|
|
} catch (Exception $ex) { |
|
276
|
|
|
$results[$id] = array('status' => 'failed', 'error' => $ex->getMessage()); |
|
277
|
|
|
} |
|
278
|
|
|
} |
|
279
|
|
|
|
|
280
|
|
|
return array('status' => $status, 'commits' => $results); |
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
/** |
|
284
|
|
|
* Called by Gitlab Webhooks:. |
|
285
|
|
|
*/ |
|
286
|
|
|
public function gitlab($projectId) |
|
287
|
|
|
{ |
|
288
|
|
|
$project = $this->fetchProject($projectId, 'gitlab'); |
|
289
|
|
|
|
|
290
|
|
|
$payloadString = file_get_contents('php://input'); |
|
291
|
|
|
$payload = json_decode($payloadString, true); |
|
292
|
|
|
|
|
293
|
|
|
// build on merge request events |
|
294
|
|
|
if (isset($payload['object_kind']) && $payload['object_kind'] == 'merge_request') { |
|
295
|
|
|
$attributes = $payload['object_attributes']; |
|
296
|
|
|
if ($attributes['state'] == 'opened' || $attributes['state'] == 'reopened') { |
|
297
|
|
|
$branch = $attributes['source_branch']; |
|
298
|
|
|
$commit = $attributes['last_commit']; |
|
299
|
|
|
$committer = $commit['author']['email']; |
|
300
|
|
|
|
|
301
|
|
|
return $this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']); |
|
|
|
|
|
|
302
|
|
|
} |
|
303
|
|
|
} |
|
304
|
|
|
|
|
305
|
|
|
// build on push events |
|
306
|
|
|
if (isset($payload['commits']) && is_array($payload['commits'])) { |
|
307
|
|
|
// If we have a list of commits, then add them all as builds to be tested: |
|
308
|
|
|
|
|
309
|
|
|
$results = array(); |
|
310
|
|
|
$status = 'failed'; |
|
311
|
|
|
foreach ($payload['commits'] as $commit) { |
|
312
|
|
|
try { |
|
313
|
|
|
$branch = str_replace('refs/heads/', '', $payload['ref']); |
|
314
|
|
|
$committer = $commit['author']['email']; |
|
315
|
|
|
$results[$commit['id']] = $this->createBuild( |
|
316
|
|
|
$project, |
|
|
|
|
|
|
317
|
|
|
$commit['id'], |
|
318
|
|
|
$branch, |
|
319
|
|
|
$committer, |
|
320
|
|
|
$commit['message'] |
|
321
|
|
|
); |
|
322
|
|
|
$status = 'ok'; |
|
323
|
|
|
} catch (Exception $ex) { |
|
324
|
|
|
$results[$commit['id']] = array('status' => 'failed', 'error' => $ex->getMessage()); |
|
325
|
|
|
} |
|
326
|
|
|
} |
|
327
|
|
|
|
|
328
|
|
|
return array('status' => $status, 'commits' => $results); |
|
329
|
|
|
} |
|
330
|
|
|
|
|
331
|
|
|
return array('status' => 'ignored', 'message' => 'Unusable payload.'); |
|
332
|
|
|
} |
|
333
|
|
|
|
|
334
|
|
|
/** |
|
335
|
|
|
* Wrapper for creating a new build. |
|
336
|
|
|
* |
|
337
|
|
|
* @param Project $project |
|
338
|
|
|
* @param string $commitId |
|
339
|
|
|
* @param string $branch |
|
340
|
|
|
* @param string $committer |
|
341
|
|
|
* @param string $commitMessage |
|
342
|
|
|
* @param array $extra |
|
343
|
|
|
* |
|
344
|
|
|
* @return array |
|
345
|
|
|
* |
|
346
|
|
|
* @throws Exception |
|
347
|
|
|
*/ |
|
348
|
|
|
protected function createBuild( |
|
349
|
|
|
Project $project, |
|
350
|
|
|
$commitId, |
|
351
|
|
|
$branch, |
|
352
|
|
|
$committer, |
|
353
|
|
|
$commitMessage, |
|
354
|
|
|
array $extra = null |
|
355
|
|
|
) { |
|
356
|
|
|
// Check if a build already exists for this commit ID: |
|
357
|
|
|
$builds = $this->buildStore->getByProjectAndCommit($project->getId(), $commitId); |
|
358
|
|
|
|
|
359
|
|
|
if ($builds['count']) { |
|
360
|
|
|
return array( |
|
361
|
|
|
'status' => 'ignored', |
|
362
|
|
|
'message' => sprintf('Duplicate of build #%d', $builds['items'][0]->getId()), |
|
363
|
|
|
); |
|
364
|
|
|
} |
|
365
|
|
|
|
|
366
|
|
|
// If not, create a new build job for it: |
|
367
|
|
|
$build = $this->buildService->createBuild($project, $commitId, $branch, $committer, $commitMessage, $extra); |
|
|
|
|
|
|
368
|
|
|
|
|
369
|
|
|
return array('status' => 'ok', 'buildID' => $build->getID()); |
|
370
|
|
|
} |
|
371
|
|
|
|
|
372
|
|
|
/** |
|
373
|
|
|
* Fetch a project and check its type. |
|
374
|
|
|
* |
|
375
|
|
|
* @param int $projectId |
|
376
|
|
|
* @param array|string $expectedType |
|
377
|
|
|
* |
|
378
|
|
|
* @return Project |
|
379
|
|
|
* |
|
380
|
|
|
* @throws Exception If the project does not exist or is not of the expected type. |
|
381
|
|
|
*/ |
|
382
|
|
|
protected function fetchProject($projectId, $expectedType) |
|
383
|
|
|
{ |
|
384
|
|
|
$project = $this->projectStore->getById($projectId); |
|
385
|
|
|
|
|
386
|
|
|
if (empty($projectId)) { |
|
387
|
|
|
throw new Exception('Project does not exist: '.$projectId); |
|
388
|
|
|
} |
|
389
|
|
|
|
|
390
|
|
|
if (is_array($expectedType) |
|
391
|
|
|
? !in_array($project->getType(), $expectedType) |
|
392
|
|
|
: $project->getType() !== $expectedType |
|
393
|
|
|
) { |
|
394
|
|
|
throw new Exception('Wrong project type: '.$project->getType()); |
|
395
|
|
|
} |
|
396
|
|
|
|
|
397
|
|
|
return $project; |
|
398
|
|
|
} |
|
399
|
|
|
} |
|
400
|
|
|
|
This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.
Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.