Merge pull request #414 from Block8/dc/build-pull-requests

Allow PHPCI to test Github Pull Requests
This commit is contained in:
Dan Cryer 2014-05-12 16:43:49 +01:00
commit 7bfc4712c7
9 changed files with 250 additions and 132 deletions

View file

@ -9,6 +9,10 @@ RUN apt-get update
# Install PHP:
RUN apt-get install -qy git-core php5-common php5-cli php5-curl php5-imap php5-mcrypt php5-mysqlnd
# Give Git some fake user details to prevent it asking when trying to test merges:
RUN git config --global user.name "PHPCI"
RUN git config --global user.email "hello@php.ci"
ADD ./ /phpci
CMD /phpci/daemonise phpci:daemonise

View file

@ -105,7 +105,6 @@ class BuildController extends \PHPCI\Controller
$data = array();
$data['status'] = (int)$build->getStatus();
$data['log'] = $this->cleanLog($build->getLog());
$data['plugins'] = json_decode($build->getPlugins(), true);
$data['created'] = !is_null($build->getCreated()) ? $build->getCreated()->format('Y-m-d H:i:s') : null;
$data['started'] = !is_null($build->getStarted()) ? $build->getStarted()->format('Y-m-d H:i:s') : null;
$data['finished'] = !is_null($build->getFinished()) ? $build->getFinished()->format('Y-m-d H:i:s') : null;

View file

@ -11,6 +11,7 @@ namespace PHPCI\Controller;
use b8;
use b8\Store;
use PHPCI\BuildFactory;
use PHPCI\Model\Build;
/**
@ -42,21 +43,16 @@ class WebhookController extends \PHPCI\Controller
foreach ($payload['commits'] as $commit) {
try {
$email = $commit['raw_author'];
$email = substr($email, 0, strpos($email, '>'));
$email = substr($email, strpos($email, '<') + 1);
$build = new Build();
$build->setProjectId($project);
$build->setCommitId($commit['raw_node']);
$build->setCommitterEmail($email);
$build->setStatus(Build::STATUS_NEW);
$build->setLog('');
$build->setCreated(new \DateTime());
$build->setBranch($commit['branch']);
$build->setCommitMessage($commit['message']);
$this->buildStore->save($build);
$this->createBuild($project, $commit['raw_node'], $commit['branch'], $email, $commit['message']);
} catch (\Exception $ex) {
header('HTTP/1.1 500 Internal Server Error');
header('Ex: ' . $ex->getMessage());
die('FAIL');
}
}
@ -74,36 +70,22 @@ class WebhookController extends \PHPCI\Controller
$commit = $this->getParam('commit');
try {
$build = new Build();
$build->setProjectId($project);
if ($branch !== null && trim($branch) !== '') {
$build->setBranch($branch);
} else {
$build->setBranch('master');
if (empty($branch)) {
$branch = 'master';
}
if ($commit !== null && trim($commit) !== '') {
$build->setCommitId($commit);
if (empty($commit)) {
$commit = null;
}
$build->setStatus(Build::STATUS_NEW);
$build->setLog('');
$build->setCreated(new \DateTime());
$this->createBuild($project, $commit, $branch, null, null);
} catch (\Exception $ex) {
header('HTTP/1.1 400 Bad Request');
header('Ex: ' . $ex->getMessage());
die('FAIL');
}
try {
$this->buildStore->save($build); /** bugfix: Errors with PHPCI GitHub hook #296 */
} catch (\Exception $ex) {
header('HTTP/1.1 500 Internal Server Error');
header('Ex: ' . $ex->getMessage());
die('FAIL');
}
die('OK');
}
@ -112,11 +94,27 @@ class WebhookController extends \PHPCI\Controller
*/
public function github($project)
{
$payload = json_decode($this->getParam('payload'), true);
$payload = json_decode($this->getParam('payload'), true);
// Handle Pull Request web hooks:
if (array_key_exists('pull_request', $payload)) {
return $this->githubPullRequest($project, $payload);
}
// Handle Push web hooks:
if (array_key_exists('commits', $payload)) {
return $this->githubCommitRequest($project, $payload);
}
header('HTTP/1.1 200 OK');
die('This request type is not supported, this is not an error.');
}
protected function githubCommitRequest($project, array $payload)
{
// Github sends a payload when you close a pull request with a
// non-existant commit. We don't want this.
if ($payload['after'] === '0000000000000000000000000000000000000000') {
if (array_key_exists('after', $payload) && $payload['after'] === '0000000000000000000000000000000000000000') {
die('OK');
}
@ -130,32 +128,16 @@ class WebhookController extends \PHPCI\Controller
continue;
}
$build = new Build();
$build->setProjectId($project);
$build->setCommitId($commit['id']);
$build->setStatus(Build::STATUS_NEW);
$build->setLog('');
$build->setCreated(new \DateTime());
$build->setBranch(str_replace('refs/heads/', '', $payload['ref']));
$build->setCommitterEmail($commit['committer']['email']);
$build->setCommitMessage($commit['message']);
$build = $this->buildStore->save($build);
$build->sendStatusPostback();
$branch = str_replace('refs/heads/', '', $payload['ref']);
$committer = $commit['committer']['email'];
$this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']);
}
} elseif (substr($payload['ref'], 0, 10) == 'refs/tags/') {
// If we don't, but we're dealing with a tag, add that instead:
$build = new Build();
$build->setProjectId($project);
$build->setCommitId($payload['after']);
$build->setStatus(Build::STATUS_NEW);
$build->setLog('');
$build->setCreated(new \DateTime());
$build->setBranch(str_replace('refs/tags/', 'Tag: ', $payload['ref']));
$build->setCommitterEmail($payload['pusher']['email']);
$build->setCommitMessage($payload['head_commit']['message']);
$build = $this->buildStore->save($build);
$build->sendStatusPostback();
$branch = str_replace('refs/tags/', 'Tag: ', $payload['ref']);
$committer = $payload['pusher']['email'];
$message = $payload['head_commit']['message'];
$this->createBuild($project, $payload['after'], $branch, $committer, $message);
}
} catch (\Exception $ex) {
@ -167,6 +149,57 @@ class WebhookController extends \PHPCI\Controller
die('OK');
}
protected function githubPullRequest($projectId, array $payload)
{
// We only want to know about open pull requests:
if (!in_array($payload['action'], array('opened', 'synchronize', 'reopened'))) {
die('OK');
}
try {
$headers = array();
$token = \b8\Config::getInstance()->get('phpci.github.token');
if (!empty($token)) {
$headers[] = 'Authorization: token ' . $token;
}
$url = $payload['pull_request']['commits_url'];
$http = new \b8\HttpClient();
$http->setHeaders($headers);
$response = $http->get($url);
// Check we got a success response:
if (!$response['success']) {
header('HTTP/1.1 500 Internal Server Error');
header('Ex: Could not get commits, failed API request.');
die('FAIL');
}
foreach ($response['body'] as $commit) {
$branch = str_replace('refs/heads/', '', $payload['pull_request']['base']['ref']);
$committer = $commit['commit']['author']['email'];
$message = $commit['commit']['message'];
$extra = array(
'build_type' => 'pull_request',
'pull_request_id' => $payload['pull_request']['id'],
'pull_request_number' => $payload['number'],
'remote_branch' => $payload['pull_request']['head']['ref'],
'remote_url' => $payload['pull_request']['head']['repo']['clone_url'],
);
$this->createBuild($projectId, $commit['sha'], $branch, $committer, $message, $extra);
}
} catch (\Exception $ex) {
header('HTTP/1.1 500 Internal Server Error');
header('Ex: ' . $ex->getMessage());
die('FAIL');
}
die('OK');
}
/**
* Called by Gitlab Webhooks:
*/
@ -181,17 +214,9 @@ class WebhookController extends \PHPCI\Controller
// If we have a list of commits, then add them all as builds to be tested:
foreach ($payload['commits'] as $commit) {
$build = new Build();
$build->setProjectId($project);
$build->setCommitId($commit['id']);
$build->setStatus(Build::STATUS_NEW);
$build->setLog('');
$build->setCreated(new \DateTime());
$build->setBranch(str_replace('refs/heads/', '', $payload['ref']));
$build->setCommitterEmail($commit['author']['email']);
$build->setCommitMessage($commit['message']);
$build = $this->buildStore->save($build);
$build->sendStatusPostback();
$branch = str_replace('refs/heads/', '', $payload['ref']);
$committer = $commit['author']['email'];
$this->createBuild($project, $commit['id'], $branch, $committer, $commit['message']);
}
}
@ -203,4 +228,36 @@ class WebhookController extends \PHPCI\Controller
die('OK');
}
protected function createBuild($projectId, $commitId, $branch, $committer, $commitMessage, $extra = null)
{
// Check if a build already exists for this commit ID:
$builds = $this->buildStore->getByProjectAndCommit($projectId, $commitId);
if ($builds['count']) {
return true;
}
// If not, create a new build job for it:
$build = new Build();
$build->setProjectId($projectId);
$build->setCommitId($commitId);
$build->setStatus(Build::STATUS_NEW);
$build->setLog('');
$build->setCreated(new \DateTime());
$build->setBranch($branch);
$build->setCommitterEmail($committer);
$build->setCommitMessage($commitMessage);
if (!is_null($extra)) {
$build->setExtra(json_encode($extra));
}
$build = BuildFactory::getBuild($this->buildStore->save($build));
// Send a status postback if the build type provides one:
$build->sendStatusPostback();
return true;
}
}

View file

@ -42,9 +42,9 @@ class BuildBase extends Model
'created' => null,
'started' => null,
'finished' => null,
'plugins' => null,
'committer_email' => null,
'commit_message' => null,
'extra' => null,
);
/**
@ -61,9 +61,9 @@ class BuildBase extends Model
'created' => 'getCreated',
'started' => 'getStarted',
'finished' => 'getFinished',
'plugins' => 'getPlugins',
'committer_email' => 'getCommitterEmail',
'commit_message' => 'getCommitMessage',
'extra' => 'getExtra',
// Foreign key getters:
'Project' => 'getProject',
@ -83,9 +83,9 @@ class BuildBase extends Model
'created' => 'setCreated',
'started' => 'setStarted',
'finished' => 'setFinished',
'plugins' => 'setPlugins',
'committer_email' => 'setCommitterEmail',
'commit_message' => 'setCommitMessage',
'extra' => 'setExtra',
// Foreign key setters:
'Project' => 'setProject',
@ -143,11 +143,6 @@ class BuildBase extends Model
'nullable' => true,
'default' => null,
),
'plugins' => array(
'type' => 'text',
'nullable' => true,
'default' => null,
),
'committer_email' => array(
'type' => 'varchar',
'length' => 512,
@ -159,6 +154,11 @@ class BuildBase extends Model
'nullable' => true,
'default' => null,
),
'extra' => array(
'type' => 'longtext',
'nullable' => true,
'default' => null,
),
);
/**
@ -303,18 +303,6 @@ class BuildBase extends Model
return $rtn;
}
/**
* Get the value of Plugins / plugins.
*
* @return string
*/
public function getPlugins()
{
$rtn = $this->data['plugins'];
return $rtn;
}
/**
* Get the value of CommitterEmail / committer_email.
*
@ -339,6 +327,18 @@ class BuildBase extends Model
return $rtn;
}
/**
* Get the value of Extra / extra.
*
* @return string
*/
public function getExtra()
{
$rtn = $this->data['extra'];
return $rtn;
}
/**
* Set the value of Id / id.
*
@ -509,24 +509,6 @@ class BuildBase extends Model
$this->_setModified('finished');
}
/**
* Set the value of Plugins / plugins.
*
* @param $value string
*/
public function setPlugins($value)
{
$this->_validateString('Plugins', $value);
if ($this->data['plugins'] === $value) {
return;
}
$this->data['plugins'] = $value;
$this->_setModified('plugins');
}
/**
* Set the value of CommitterEmail / committer_email.
*
@ -563,6 +545,24 @@ class BuildBase extends Model
$this->_setModified('commit_message');
}
/**
* Set the value of Extra / extra.
*
* @param $value string
*/
public function setExtra($value)
{
$this->_validateString('Extra', $value);
if ($this->data['extra'] === $value) {
return;
}
$this->data['extra'] = $value;
$this->_setModified('extra');
}
/**
* Get the Project model for this Build by Id.
*

View file

@ -164,4 +164,19 @@ class Build extends BuildBase
{
return null;
}
public function getExtra($key = null)
{
$data = json_decode($this->data['extra'], true);
if (is_null($key)) {
$rtn = $data;
} elseif (isset($data[$key])) {
$rtn = $data[$key];
} else {
$rtn = null;
}
return $rtn;
}
}

View file

@ -9,6 +9,7 @@
namespace PHPCI\Model\Build;
use PHPCI\Builder;
use PHPCI\Model\Build\RemoteGitBuild;
/**
@ -115,4 +116,29 @@ class GithubBuild extends RemoteGitBuild
return $link;
}
protected function postCloneSetup(Builder $builder, $cloneTo)
{
$buildType = $this->getExtra('build_type');
$success = true;
try {
if (!empty($buildType) && $buildType == 'pull_request') {
$remoteUrl = $this->getExtra('remote_url');
$remoteBranch = $this->getExtra('remote_branch');
$cmd = 'cd "%s" && git checkout -b phpci/' . $this->getId() . ' %s && git pull -q --no-edit %s %s';
$success = $builder->executeCommand($cmd, $cloneTo, $this->getBranch(), $remoteUrl, $remoteBranch);
}
} catch (\Exception $ex) {
$success = false;
}
if ($success) {
$success = parent::postCloneSetup($builder, $cloneTo);
}
return $success;
}
}

View file

@ -65,12 +65,8 @@ class RemoteGitBuild extends Build
$cmd .= ' -b %s %s "%s"';
$success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo);
if (!empty($commit) && $commit != 'Manual') {
$cmd = 'cd "%s" && git checkout %s';
if (IS_WIN) {
$cmd = 'cd /d "%s" && git checkout %s';
}
$builder->executeCommand($cmd, $cloneTo, $this->getCommitId());
if ($success) {
$success = $this->postCloneSetup($builder, $cloneTo);
}
return $success;
@ -104,15 +100,8 @@ class RemoteGitBuild extends Build
$success = $builder->executeCommand($cmd, $this->getBranch(), $this->getCloneUrl(), $cloneTo);
// Checkout a specific commit if we need to:
$commit = $this->getCommitId();
if (!empty($commit) && $commit != 'Manual') {
$cmd = 'cd "%s" && git checkout %s';
if (IS_WIN) {
$cmd = 'cd /d "%s" && git checkout %s';
}
$builder->executeCommand($cmd, $cloneTo, $this->getCommitId());
if ($success) {
$success = $this->postCloneSetup($builder, $cloneTo);
}
// Remove the key file and git wrapper:
@ -122,6 +111,24 @@ class RemoteGitBuild extends Build
return $success;
}
protected function postCloneSetup(Builder $builder, $cloneTo)
{
$success = true;
$commit = $this->getCommitId();
if (!empty($commit) && $commit != 'Manual') {
$cmd = 'cd "%s" && git checkout %s';
if (IS_WIN) {
$cmd = 'cd /d "%s" && git checkout %s';
}
$success = $builder->executeCommand($cmd, $cloneTo, $this->getCommitId());
}
return $success;
}
/**
* Create an SSH key file on disk for this build.
* @param $cloneTo

View file

@ -9,6 +9,8 @@
namespace PHPCI\Store;
use b8\Database;
use PHPCI\Model\Build;
use PHPCI\Store\Base\BuildStoreBase;
/**
@ -22,7 +24,7 @@ class BuildStore extends BuildStoreBase
public function getLatestBuilds($projectId)
{
$query = 'SELECT * FROM build WHERE project_id = :pid ORDER BY id DESC LIMIT 5';
$stmt = \b8\Database::getConnection('read')->prepare($query);
$stmt = Database::getConnection('read')->prepare($query);
$stmt->bindValue(':pid', $projectId);
if ($stmt->execute()) {
@ -39,6 +41,28 @@ class BuildStore extends BuildStoreBase
}
}
public function getByProjectAndCommit($projectId, $commitId)
{
$query = 'SELECT * FROM `build` WHERE `project_id` = :project_id AND `commit_id` = :commit_id';
$stmt = Database::getConnection('read')->prepare($query);
$stmt->bindValue(':project_id', $projectId);
$stmt->bindValue(':commit_id', $commitId);
if ($stmt->execute()) {
$res = $stmt->fetchAll(\PDO::FETCH_ASSOC);
$map = function ($item) {
return new Build($item);
};
$rtn = array_map($map, $res);
return array('items' => $rtn, 'count' => count($rtn));
} else {
return array('items' => array(), 'count' => 0);
}
}
public function getMeta($key, $projectId, $buildId = null, $numResults = 1)
{
$select = '`build_id`, `meta_key`, `meta_value`';
@ -46,7 +70,7 @@ class BuildStore extends BuildStoreBase
$where = '`meta_key` = :key AND `project_id` = :projectId ' . $and;
$query = 'SELECT '.$select.' FROM `build_meta` WHERE '.$where.' ORDER BY id DESC LIMIT :numResults';
$stmt = \b8\Database::getConnection('read')->prepare($query);
$stmt = Database::getConnection('read')->prepare($query);
$stmt->bindValue(':key', $key, \PDO::PARAM_STR);
$stmt->bindValue(':projectId', (int)$projectId, \PDO::PARAM_INT);
$stmt->bindValue(':buildId', (int)$buildId, \PDO::PARAM_INT);
@ -77,7 +101,7 @@ class BuildStore extends BuildStoreBase
$cols = '`project_id`, `build_id`, `meta_key`, `meta_value`';
$query = 'REPLACE INTO build_meta ('.$cols.') VALUES (:projectId, :buildId, :key, :value)';
$stmt = \b8\Database::getConnection('read')->prepare($query);
$stmt = Database::getConnection('read')->prepare($query);
$stmt->bindValue(':key', $key, \PDO::PARAM_STR);
$stmt->bindValue(':projectId', (int)$projectId, \PDO::PARAM_INT);
$stmt->bindValue(':buildId', (int)$buildId, \PDO::PARAM_INT);

View file

@ -61,21 +61,7 @@ switch($build->getStatus())
<td><a href="<?php print $build->getBranchLink(); ?>"><?php print $build->getBranch(); ?></a></td>
<td>
<?php
$plugins = json_decode($build->getPlugins(), true);
if ( !is_array($plugins) ) {
$plugins = array();
}
if ( 0 === count($plugins) ) {
?> <span class='label label-<?php echo $subcls ?>'><?php echo $status ?></span> <?php
}
?>
<?php
foreach($plugins as $plugin => $pluginstatus):
$subcls = $pluginstatus?'label label-success':'label label-danger';
?> <span class='<?php echo $subcls ?>'><?php print $this->Build()->formatPluginName($plugin); ?></span> <?php endforeach; ?>
<br style='clear:both;' />
<span class='label label-<?php echo $subcls ?>'><?php echo $status ?></span>
</td>
<td>
<div class="btn-group">