Merge branch 'master' into phplint-output-block

* master:
  Moving to new .phpci.yml filename
  Fix
  Making PHPMD happy.
  Fixings
  Fixing new PHPCS errors.
  Docblock fixes
  Making duplicate builds work again.
  Adding support for beanstalkd-based workers.
  Update Build.php
  More composer.json cleanup
  Fixing composer.json
  Adding support to check for .phpci.yml so the file can be 'hidden' as with other CI systems such as Travis, StyleCI and Codeclimate
  Fixing Codeception plugin PHPCS errors.
  Updating PHPCI to send more detailed commit statuses, for @REBELinBLUE.
  PHP CS Fixer - fix name Closed #1054
  Fixes notice in github builds
  Fixes "Undefined index: login_token".
  Parsing variables in the code coverage output directory for PHPUnit
  Parsing variables in the Wipe plugin
  Removed log output so that it matches the other plugins which don't pollute the build log, and to prevent issues with the log output not being escaped
This commit is contained in:
Stephen Ball 2015-10-06 11:27:34 +01:00
commit 181a855985
21 changed files with 802 additions and 417 deletions

View file

@ -36,38 +36,42 @@ class BuildFactory
/**
* Takes a generic build and returns a type-specific build model.
* @param Build $base The build from which to get a more specific build type.
* @param Build $build The build from which to get a more specific build type.
* @return Build
*/
public static function getBuild(Build $base)
public static function getBuild(Build $build)
{
switch($base->getProject()->getType())
{
case 'remote':
$type = 'RemoteGitBuild';
break;
case 'local':
$type = 'LocalBuild';
break;
case 'github':
$type = 'GithubBuild';
break;
case 'bitbucket':
$type = 'BitbucketBuild';
break;
case 'gitlab':
$type = 'GitlabBuild';
break;
case 'hg':
$type = 'MercurialBuild';
break;
case 'svn':
$type = 'SubversionBuild';
break;
$project = $build->getProject();
if (!empty($project)) {
switch ($project->getType()) {
case 'remote':
$type = 'RemoteGitBuild';
break;
case 'local':
$type = 'LocalBuild';
break;
case 'github':
$type = 'GithubBuild';
break;
case 'bitbucket':
$type = 'BitbucketBuild';
break;
case 'gitlab':
$type = 'GitlabBuild';
break;
case 'hg':
$type = 'MercurialBuild';
break;
case 'svn':
$type = 'SubversionBuild';
break;
}
$class = '\\PHPCI\\Model\\Build\\' . $type;
$build = new $class($build->getDataArray());
}
$type = '\\PHPCI\\Model\\Build\\' . $type;
return new $type($base->getDataArray());
return $build;
}
}

View file

@ -0,0 +1,80 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2015, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Command;
use b8\Config;
use Monolog\Logger;
use PHPCI\Logging\OutputLogHandler;
use PHPCI\Worker\BuildWorker;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
/**
* Worker Command - Starts the BuildWorker, which pulls jobs from beanstalkd
* @author Dan Cryer <dan@block8.co.uk>
* @package PHPCI
* @subpackage Console
*/
class WorkerCommand extends Command
{
/**
* @var OutputInterface
*/
protected $output;
/**
* @var Logger
*/
protected $logger;
/**
* @param \Monolog\Logger $logger
* @param string $name
*/
public function __construct(Logger $logger, $name = null)
{
parent::__construct($name);
$this->logger = $logger;
}
protected function configure()
{
$this
->setName('phpci:worker')
->setDescription('Runs the PHPCI build worker.');
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$this->output = $output;
// For verbose mode we want to output all informational and above
// messages to the symphony output interface.
if ($input->hasOption('verbose') && $input->getOption('verbose')) {
$this->logger->pushHandler(
new OutputLogHandler($this->output, Logger::INFO)
);
}
$config = Config::getInstance()->get('phpci.worker', []);
if (empty($config['host']) || empty($config['queue'])) {
$error = 'The worker is not configured. You must set a host and queue in your config.yml file.';
throw new \Exception($error);
}
$worker = new BuildWorker($config['host'], $config['queue']);
$worker->setLogger($this->logger);
$worker->setMaxJobs(Config::getInstance()->get('phpci.worker.max_jobs', -1));
$worker->startWorker();
}
}

View file

@ -44,7 +44,7 @@ class SessionController extends \PHPCI\Controller
if ($this->request->getMethod() == 'POST') {
$token = $this->getParam('token');
if ($token === null || $token !== $_SESSION['login_token']) {
if (!isset($token, $_SESSION['login_token']) || $token !== $_SESSION['login_token']) {
$isLoginFailure = true;
} else {
unset($_SESSION['login_token']);

View file

@ -364,10 +364,6 @@ class WebhookController extends \b8\Controller
// If not, create a new build job for it:
$build = $this->buildService->createBuild($project, $commitId, $branch, $committer, $commitMessage, $extra);
$build = BuildFactory::getBuild($build);
// Send a status postback if the build type provides one:
$build->sendStatusPostback();
return array('status' => 'ok', 'buildID' => $build->getID());
}

View file

@ -71,7 +71,7 @@ class MailerFactory
} else {
// Check defaults
switch($configName) {
switch ($configName) {
case 'smtp_address':
return "localhost";
case 'default_mailto_address':

View file

@ -99,8 +99,13 @@ class Build extends BuildBase
{
$build_config = null;
// Try .phpci.yml
if (is_file($buildPath . '/.phpci.yml')) {
$build_config = file_get_contents($buildPath . '/.phpci.yml');
}
// Try phpci.yml first:
if (is_file($buildPath . '/phpci.yml')) {
if (empty($build_config) && is_file($buildPath . '/phpci.yml')) {
$build_config = file_get_contents($buildPath . '/phpci.yml');
}
@ -228,7 +233,7 @@ class Build extends BuildBase
if (!$this->getId()) {
return null;
}
return PHPCI_BUILD_ROOT_DIR . $this->getId();
return PHPCI_BUILD_ROOT_DIR . $this->getId() . '_' . substr(md5(microtime(true)), 0, 5);
}
/**

View file

@ -45,39 +45,52 @@ class GithubBuild extends RemoteGitBuild
{
$token = \b8\Config::getInstance()->get('phpci.github.token');
if (empty($token)) {
if (empty($token) || empty($this->data['id'])) {
return;
}
$project = $this->getProject();
if (empty($project)) {
return;
}
$url = 'https://api.github.com/repos/'.$project->getReference().'/statuses/'.$this->getCommitId();
$http = new \b8\HttpClient();
switch($this->getStatus())
{
switch ($this->getStatus()) {
case 0:
case 1:
$status = 'pending';
$description = 'PHPCI build running.';
break;
case 2:
$status = 'success';
$description = 'PHPCI build passed.';
break;
case 3:
$status = 'failure';
$description = 'PHPCI build failed.';
break;
default:
$status = 'error';
$description = 'PHPCI build failed to complete.';
break;
}
$phpciUrl = \b8\Config::getInstance()->get('phpci.url');
$params = array( 'state' => $status,
'target_url' => $phpciUrl . '/build/view/' . $this->getId());
$params = array(
'state' => $status,
'target_url' => $phpciUrl . '/build/view/' . $this->getId(),
'description' => $description,
'context' => 'PHPCI',
);
$headers = array(
'Authorization: token ' . $token,
'Content-Type: application/x-www-form-urlencoded'
);
);
$http->setHeaders($headers);
$http->request('POST', $url, json_encode($params));
@ -105,10 +118,14 @@ class GithubBuild extends RemoteGitBuild
{
$rtn = parent::getCommitMessage($this->data['commit_message']);
$reference = $this->getProject()->getReference();
$commitLink = '<a target="_blank" href="https://github.com/' . $reference . '/issues/$1">#$1</a>';
$rtn = preg_replace('/\#([0-9]+)/', $commitLink, $rtn);
$rtn = preg_replace('/\@([a-zA-Z0-9_]+)/', '<a target="_blank" href="https://github.com/$1">@$1</a>', $rtn);
$project = $this->getProject();
if (!is_null($project)) {
$reference = $project->getReference();
$commitLink = '<a target="_blank" href="https://github.com/' . $reference . '/issues/$1">#$1</a>';
$rtn = preg_replace('/\#([0-9]+)/', $commitLink, $rtn);
$rtn = preg_replace('/\@([a-zA-Z0-9_]+)/', '<a target="_blank" href="https://github.com/$1">@$1</a>', $rtn);
}
return $rtn;
}
@ -221,6 +238,6 @@ class GithubBuild extends RemoteGitBuild
$helper = new Diff();
$lines = $helper->getLinePositions($diff);
return $lines[$line];
return isset($lines[$line]) ? $lines[$line] : null;
}
}

View file

@ -130,6 +130,7 @@ class Codeception implements \PHPCI\Plugin, \PHPCI\ZeroConfigPlugin
}
$cmd = 'cd "%s" && ' . $codecept . ' run -c "%s" --xml ' . $this->args;
if (IS_WIN) {
$cmd = 'cd /d "%s" && ' . $codecept . ' run -c "%s" --xml ' . $this->args;
}
@ -137,14 +138,12 @@ class Codeception implements \PHPCI\Plugin, \PHPCI\ZeroConfigPlugin
$configPath = $this->phpci->buildPath . $configPath;
$success = $this->phpci->executeCommand($cmd, $this->phpci->buildPath, $configPath);
$this->phpci->log(
'Codeception XML path: '. $this->phpci->buildPath . $this->path . 'report.xml',
Loglevel::DEBUG
);
$this->phpci->log(
'Codeception XML path: '. $this->phpci->buildPath . $this->path . 'report.xml',
Loglevel::DEBUG
);
$xml = file_get_contents($this->phpci->buildPath . $this->path . 'report.xml', false);
$xml = file_get_contents($this->phpci->buildPath . $this->path . 'report.xml', false);
$parser = new Parser($this->phpci, $xml);
$output = $parser->parse();
@ -157,7 +156,6 @@ class Codeception implements \PHPCI\Plugin, \PHPCI\ZeroConfigPlugin
$this->build->storeMeta('codeception-meta', $meta);
$this->build->storeMeta('codeception-data', $output);
$this->build->storeMeta('codeception-errors', $parser->getTotalFailures());
$this->phpci->logExecOutput(true);
return $success;

View file

@ -69,8 +69,7 @@ class PackageBuild implements \PHPCI\Plugin
}
foreach ($this->format as $format) {
switch($format)
{
switch ($format) {
case 'tar':
$cmd = 'tar cfz "%s/%s.tar.gz" ./*';
break;

View file

@ -14,7 +14,7 @@ use PHPCI\Helper\Lang;
use PHPCI\Model\Build;
/**
* PHP CS Fixer - Works with the PHP CS Fixer for testing coding standards.
* PHP CS Fixer - Works with the PHP Coding Standards Fixer for testing coding standards.
* @author Gabriel Baker <gabriel@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Plugins

View file

@ -133,7 +133,7 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
}
if (isset($options['coverage'])) {
$this->coverage = " --coverage-html {$options['coverage']} ";
$this->coverage = ' --coverage-html ' . $this->phpci->interpolate($options['coverage']) . ' ';
}
}

View file

@ -151,6 +151,7 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
$ignores = $this->ignore;
$ignores[] = 'phpci.yml';
$ignores[] = '.phpci.yml';
foreach ($iterator as $file) {
$filePath = $file->getRealPath();
@ -188,7 +189,6 @@ class TechnicalDebt implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
$content = trim($allLines[$lineNumber - 1]);
$errorCount++;
$this->phpci->log("Found $search on line $lineNumber of $file:\n$content");
$fileName = str_replace($this->directory, '', $file);
$data[] = array(

View file

@ -43,7 +43,7 @@ class Wipe implements \PHPCI\Plugin
$path = $phpci->buildPath;
$this->phpci = $phpci;
$this->build = $build;
$this->directory = isset($options['directory']) ? $options['directory'] : $path;
$this->directory = isset($options['directory']) ? $this->phpci->interpolate($options['directory']) : $path;
}
/**

View file

@ -47,7 +47,7 @@ class Factory
*/
public static function createProcessControl()
{
switch(true) {
switch (true) {
case PosixProcessControl::isAvailable():
return new PosixProcessControl();

View file

@ -9,6 +9,10 @@
namespace PHPCI\Service;
use b8\Config;
use Pheanstalk\Pheanstalk;
use Pheanstalk\PheanstalkInterface;
use PHPCI\BuildFactory;
use PHPCI\Helper\Lang;
use PHPCI\Model\Build;
use PHPCI\Model\Project;
@ -81,7 +85,13 @@ class BuildService
$build->setExtra(json_encode($extra));
}
return $this->buildStore->save($build);
$build = $this->buildStore->save($build);
$build = BuildFactory::getBuild($build);
$build->sendStatusPostback();
$this->addBuildToQueue($build);
return $build;
}
/**
@ -104,7 +114,13 @@ class BuildService
$build->setCreated(new \DateTime());
$build->setStatus(0);
return $this->buildStore->save($build);
$build = $this->buildStore->save($build);
$build = BuildFactory::getBuild($build);
$build->sendStatusPostback();
$this->addBuildToQueue($build);
return $build;
}
/**
@ -117,4 +133,40 @@ class BuildService
$build->removeBuildDirectory();
return $this->buildStore->delete($build);
}
/**
* Takes a build and puts it into the queue to be run (if using a queue)
* @param Build $build
*/
public function addBuildToQueue(Build $build)
{
$buildId = $build->getId();
if (empty($buildId)) {
return;
}
$config = Config::getInstance();
$settings = $config->get('phpci.worker', []);
if (!empty($settings['host']) && !empty($settings['queue'])) {
$jobData = array(
'build_id' => $build->getId(),
);
if ($config->get('using_custom_file')) {
$jobData['config'] = $config->getArray();
}
$pheanstalk = new Pheanstalk($settings['host']);
$pheanstalk->useTube($settings['queue']);
$pheanstalk->put(
json_encode($jobData),
PheanstalkInterface::DEFAULT_PRIORITY,
PheanstalkInterface::DEFAULT_DELAY,
$config->get('phpci.worker.job_timeout', 600)
);
}
}
}

View file

@ -0,0 +1,163 @@
<?php
namespace PHPCI\Worker;
use b8\Config;
use b8\Database;
use b8\Store\Factory;
use Monolog\Logger;
use Pheanstalk\Pheanstalk;
use PHPCI\Builder;
use PHPCI\BuildFactory;
use PHPCI\Logging\BuildDBLogHandler;
use PHPCI\Model\Build;
/**
* Class BuildWorker
* @package PHPCI\Worker
*/
class BuildWorker
{
/**
* If this variable changes to false, the worker will stop after the current build.
* @var bool
*/
protected $run = true;
/**
* The maximum number of jobs this worker should run before exiting.
* Use -1 for no limit.
* @var int
*/
protected $maxJobs = -1;
/**
* The logger for builds to use.
* @var \Monolog\Logger
*/
protected $logger;
/**
* beanstalkd host
* @var string
*/
protected $host;
/**
* beanstalkd queue to watch
* @var string
*/
protected $queue;
/**
* @param $host
* @param $queue
*/
public function __construct($host, $queue)
{
$this->host = $host;
$this->queue = $queue;
}
/**
* @param int $maxJobs
*/
public function setMaxJobs($maxJobs = -1)
{
$this->maxJobs = $maxJobs;
}
/**
* @param Logger $logger
*/
public function setLogger(Logger $logger)
{
$this->logger = $logger;
}
/**
* Start the worker.
*/
public function startWorker()
{
$pheanstalk = new Pheanstalk($this->host);
$pheanstalk->watch($this->queue);
$buildStore = Factory::getStore('Build');
$jobs = 0;
while ($this->run) {
// Get a job from the queue:
$job = $pheanstalk->reserve();
// Make sure we don't run more than maxJobs jobs on this worker:
$jobs++;
if ($this->maxJobs != -1 && $this->maxJobs <= $jobs) {
$this->run = false;
}
// Get the job data and run the job:
$jobData = json_decode($job->getData(), true);
$this->logger->addInfo('Received build #'.$jobData['build_id'].' from Beanstalkd');
// If the job comes with config data, reset our config and database connections
// and then make sure we kill the worker afterwards:
if (!empty($jobData['config'])) {
$this->logger->addDebug('Using job-specific config.');
$currentConfig = Config::getInstance()->getArray();
$config = new Config($jobData['config']);
Database::reset($config);
}
$build = BuildFactory::getBuildById($jobData['build_id']);
if (empty($build)) {
$this->logger->addWarning('Build #' . $jobData['build_id'] . ' does not exist in the database.');
$pheanstalk->delete($job);
}
try {
// Logging relevant to this build should be stored
// against the build itself.
$buildDbLog = new BuildDBLogHandler($build, Logger::INFO);
$this->logger->pushHandler($buildDbLog);
$builder = new Builder($build, $this->logger);
$builder->execute();
// After execution we no longer want to record the information
// back to this specific build so the handler should be removed.
$this->logger->popHandler($buildDbLog);
} catch (\PDOException $ex) {
// If we've caught a PDO Exception, it is probably not the fault of the build, but of a failed
// connection or similar. Release the job and kill the worker.
$this->run = false;
$pheanstalk->release($job);
} catch (\Exception $ex) {
$build->setStatus(Build::STATUS_FAILED);
$build->setFinished(new \DateTime());
$build->setLog($build->getLog() . PHP_EOL . PHP_EOL . $ex->getMessage());
$buildStore->save($build);
$build->sendStatusPostback();
}
// Reset the config back to how it was prior to running this job:
if (!empty($currentConfig)) {
$config = new Config($currentConfig);
Database::reset($config);
}
// Delete the job when we're done:
$pheanstalk->delete($job);
}
}
/**
* Stops the worker after the current build.
*/
public function stopWorker()
{
$this->run = false;
}
}

View file

@ -17,9 +17,11 @@ if (empty($timezone)) {
$configFile = dirname(__FILE__) . '/PHPCI/config.yml';
$configEnv = getenv('phpci_config_file');
$usingCustomConfigFile = false;
if (!empty($configEnv) && file_exists($configEnv)) {
$configFile = $configEnv;
$usingCustomConfigFile = true;
}
// If we don't have a config file at all, fail at this point and tell the user to install:
@ -53,6 +55,7 @@ $conf = array();
$conf['b8']['app']['namespace'] = 'PHPCI';
$conf['b8']['app']['default_controller'] = 'Home';
$conf['b8']['view']['path'] = dirname(__FILE__) . '/PHPCI/View/';
$conf['using_custom_file'] = $usingCustomConfigFile;
$config = new b8\Config($conf);

View file

@ -48,11 +48,7 @@
"pimple/pimple": "~1.1",
"robmorgan/phinx": "~0.4",
"sensiolabs/ansi-to-html": "~1.1",
"jakub-onderka/php-parallel-lint": "0.8.*"
},
"autoload-dev": {
"psr-4": { "Tests\\PHPCI\\": "Tests/PHPCI" }
"pda/pheanstalk": "~3.1"
},
"require-dev": {
@ -60,7 +56,8 @@
"phpmd/phpmd": "~2.0",
"squizlabs/php_codesniffer": "~2.3",
"block8/php-docblock-checker": "~1.0",
"phploc/phploc": "~2.0"
"phploc/phploc": "~2.0",
"jakub-onderka/php-parallel-lint": "0.8.*"
},
"suggest": {
@ -69,7 +66,7 @@
"sebastian/phpcpd": "PHP Copy/Paste Detector",
"squizlabs/php_codesniffer": "PHP Code Sniffer",
"phpspec/phpspec": "PHP Spec",
"fabpot/php-cs-fixer": "PHP Code Sniffer Fixer",
"fabpot/php-cs-fixer": "PHP Coding Standards Fixer",
"phploc/phploc": "PHP Lines of Code",
"atoum/atoum": "Atoum",
"jakub-onderka/php-parallel-lint": "Parallel Linting Tool",

761
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@ use PHPCI\Command\DaemonCommand;
use PHPCI\Command\PollCommand;
use PHPCI\Command\CreateAdminCommand;
use PHPCI\Command\CreateBuildCommand;
use PHPCI\Command\WorkerCommand;
use PHPCI\Service\BuildService;
use Symfony\Component\Console\Application;
use b8\Store\Factory;
@ -36,5 +37,6 @@ $application->add(new DaemonCommand($loggerConfig->getFor('DaemonCommand')));
$application->add(new PollCommand($loggerConfig->getFor('PollCommand')));
$application->add(new CreateAdminCommand(Factory::getStore('User')));
$application->add(new CreateBuildCommand(Factory::getStore('Project'), new BuildService(Factory::getStore('Build'))));
$application->add(new WorkerCommand($loggerConfig->getFor('WorkerCommand')));
$application->run();