diff --git a/Dockerfile b/Dockerfile index 9b64eaea..f0c8b605 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/PHPCI/Controller/BuildController.php b/PHPCI/Controller/BuildController.php index 1dff5a15..8a12a1ef 100644 --- a/PHPCI/Controller/BuildController.php +++ b/PHPCI/Controller/BuildController.php @@ -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; diff --git a/PHPCI/Controller/WebhookController.php b/PHPCI/Controller/WebhookController.php index 9a9d143b..fb0e4417 100644 --- a/PHPCI/Controller/WebhookController.php +++ b/PHPCI/Controller/WebhookController.php @@ -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; + } } diff --git a/PHPCI/Model/Base/BuildBase.php b/PHPCI/Model/Base/BuildBase.php index 569c9d6b..97b427b5 100644 --- a/PHPCI/Model/Base/BuildBase.php +++ b/PHPCI/Model/Base/BuildBase.php @@ -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. * diff --git a/PHPCI/Model/Build.php b/PHPCI/Model/Build.php index 174126fa..6b0386ac 100644 --- a/PHPCI/Model/Build.php +++ b/PHPCI/Model/Build.php @@ -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; + } } diff --git a/PHPCI/Model/Build/GithubBuild.php b/PHPCI/Model/Build/GithubBuild.php index f358355a..0b1852d8 100644 --- a/PHPCI/Model/Build/GithubBuild.php +++ b/PHPCI/Model/Build/GithubBuild.php @@ -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; + } } diff --git a/PHPCI/Model/Build/RemoteGitBuild.php b/PHPCI/Model/Build/RemoteGitBuild.php index 9c7c6a26..055d75d8 100644 --- a/PHPCI/Model/Build/RemoteGitBuild.php +++ b/PHPCI/Model/Build/RemoteGitBuild.php @@ -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 diff --git a/PHPCI/Store/BuildStore.php b/PHPCI/Store/BuildStore.php index e419c9cc..469e7ec2 100644 --- a/PHPCI/Store/BuildStore.php +++ b/PHPCI/Store/BuildStore.php @@ -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); diff --git a/PHPCI/View/BuildsTable.phtml b/PHPCI/View/BuildsTable.phtml index da67e087..4fb5fa91 100644 --- a/PHPCI/View/BuildsTable.phtml +++ b/PHPCI/View/BuildsTable.phtml @@ -61,21 +61,7 @@ switch($build->getStatus())