Merge remote-tracking branch 'upstream/master'

This commit is contained in:
a.cianfarani 2013-07-26 09:45:36 +02:00
commit e77a5a75fb
30 changed files with 1016 additions and 42 deletions

View file

@ -10,6 +10,7 @@
namespace PHPCI;
use b8;
use b8\Registry;
use b8\Http\Response\RedirectResponse;
use b8\View;
@ -33,8 +34,11 @@ class Application extends b8\Application
$sessionAction = ($this->controllerName == 'Session' && in_array($this->action, array('login', 'logout')));
$externalAction = in_array($this->controllerName, array('Bitbucket', 'Github', 'BuildStatus'));
$skipValidation = ($externalAction || $sessionAction);
if($skipValidation || $this->validateSession()) {
if ( !empty($_SESSION['user']) ) {
Registry::getInstance()->set('user', $_SESSION['user']);
}
parent::handleRequest();
}
@ -43,7 +47,7 @@ class Application extends b8\Application
$view->content = $this->response->getContent();
$this->response->setContent($view->render());
}
return $this->response;
}

View file

@ -28,6 +28,9 @@ class BuildFactory
{
switch($base->getProject()->getType())
{
case 'remote':
$type = 'RemoteGitBuild';
break;
case 'local':
$type = 'LocalBuild';
break;

View file

@ -116,6 +116,16 @@ class Builder
return isset($this->config[$key]) ? $this->config[$key] : null;
}
/**
* Access a variable from the config.yml
* @param $key
* @return mixed
*/
public function getSystemConfig($key)
{
return \b8\Registry::getInstance()->get($key);
}
/**
* Access the build.
* @param Build
@ -125,6 +135,22 @@ class Builder
return $this->build;
}
/**
* @return string The title of the project being built.
*/
public function getBuildProjectTitle() {
return $this->getBuild()->getProject()->getTitle();
}
/**
* Indicates if the build has passed or failed.
* @return bool
*/
public function getSuccessStatus()
{
return $this->success;
}
/**
* Run the active build.
*/

View file

@ -0,0 +1,122 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
* nohup PHPCI_DIR/console phpci:start-daemon > /dev/null 2>&1 &
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link http://www.phptesting.org/
*/
namespace PHPCI\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use b8\Store\Factory;
use PHPCI\Builder;
use PHPCI\BuildFactory;
/**
* Daemon that loops and call the run-command.
* @author Gabriel Baker <gabriel.baker@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Console
*/
class DaemonCommand extends Command
{
protected function configure()
{
$this
->setName('phpci:daemon')
->setDescription('Initiates the daemon to run commands.')
->addArgument(
'state',
InputArgument::REQUIRED,
'start|stop|status'
);
}
/**
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$state = $input->getArgument('state');
switch ($state) {
case 'start':
$this->startDaemon();
break;
case 'stop':
$this->stopDaemon();
break;
case 'status':
$this->statusDaemon();
break;
default:
echo "Not a valid choice, please use start stop or status";
break;
}
}
protected function startDaemon()
{
if ( file_exists(PHPCI_DIR.'/daemon/daemon.pid') ) {
echo "Already started\n";
return "alreadystarted";
}
$logfile = PHPCI_DIR."/daemon/daemon.log";
$cmd = "nohup %s/daemonise phpci:daemonise > %s 2>&1 &";
$command = sprintf($cmd, PHPCI_DIR, $logfile);
exec($command);
}
protected function stopDaemon()
{
if ( !file_exists(PHPCI_DIR.'/daemon/daemon.pid') ) {
echo "Not started\n";
return "notstarted";
}
$cmd = "kill $(cat %s/daemon/daemon.pid)";
$command = sprintf($cmd, PHPCI_DIR);
exec($command);
unlink(PHPCI_DIR.'/daemon/daemon.pid');
}
protected function statusDaemon()
{
if ( !file_exists(PHPCI_DIR.'/daemon/daemon.pid') ) {
echo "Not running\n";
return "notrunning";
}
$pid = trim(file_get_contents(PHPCI_DIR.'/daemon/daemon.pid'));
$pidcheck = sprintf("/proc/%s", $pid);
if ( is_dir($pidcheck) ) {
echo "Running\n";
return "running";
}
unlink(PHPCI_DIR.'/daemon/daemon.pid');
echo "Not running\n";
return "notrunning";
}
/**
* Called when log entries are made in Builder / the plugins.
* @see \PHPCI\Builder::log()
*/
public function logCallback($log)
{
$this->output->writeln($log);
}
}

View file

@ -0,0 +1,76 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
* nohup PHPCI_DIR/console phpci:start-daemon > /dev/null 2>&1 &
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link http://www.phptesting.org/
*/
namespace PHPCI\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use b8\Store\Factory;
use PHPCI\Builder;
use PHPCI\BuildFactory;
/**
* Daemon that loops and call the run-command.
* @author Gabriel Baker <gabriel.baker@autonomicpilot.co.uk>
* @package PHPCI
* @subpackage Console
*/
class DaemoniseCommand extends Command
{
protected function configure()
{
$this
->setName('phpci:daemonise')
->setDescription('Starts the daemon to run commands.');
}
/**
* Loops through running.
*/
protected function execute(InputInterface $input, OutputInterface $output)
{
$cmd = "echo %s > '%s/daemon/daemon.pid'";
$command = sprintf($cmd, getmypid(), PHPCI_DIR);
exec($command);
$this->run = true;
$this->sleep = 0;
$runner = new RunCommand;
while ($this->run) {
try {
$buildCount = $runner->execute($input, $output);
} catch (\Exception $e) {
var_dump($e);
}
if (0 == $buildCount && $this->sleep < 15) {
$this->sleep++;
} else if (1 < $this->sleep) {
$this->sleep--;
}
echo '.'.(0 === $buildCount?'':'build');
sleep($this->sleep);
}
}
/**
* Called when log entries are made in Builder / the plugins.
* @see \PHPCI\Builder::log()
*/
public function logCallback($log)
{
$this->output->writeln($log);
}
}

View file

@ -48,6 +48,14 @@ class InstallCommand extends Command
$conf['phpci']['github']['id'] = $this->ask('(Optional) Github Application ID: ', true);
$conf['phpci']['github']['secret'] = $this->ask('(Optional) Github Application Secret: ', true);
$conf['phpci']['email_settings']['smtp_address'] = $this->ask('(Optional) Smtp server address: ', true);
$conf['phpci']['email_settings']['smtp_port'] = $this->ask('(Optional) Smtp port: ', true);
$conf['phpci']['email_settings']['smtp_encryption'] = $this->ask('(Optional) Smtp encryption: ', true);
$conf['phpci']['email_settings']['smtp_username'] = $this->ask('(Optional) Smtp Username: ', true);
$conf['phpci']['email_settings']['smtp_password'] = $this->ask('(Optional) Smtp Password: ', true);
$conf['phpci']['email_settings']['from_address'] = $this->ask('(Optional) Email address to send from: ', true);
$conf['phpci']['email_settings']['default_mailto_address'] = $this->ask('(Optional) Default address to email notifications to: ', true);
$dbUser = $conf['b8']['database']['username'];
$dbPass = $conf['b8']['database']['password'];
$dbHost = $conf['b8']['database']['servers']['write'];

View file

@ -42,18 +42,23 @@ class RunCommand extends Command
$store = Factory::getStore('Build');
$result = $store->getByStatus(0);
$builds = 0;
foreach ($result['items'] as $build) {
$builds++;
$build = BuildFactory::getBuild($build);
if ($input->getOption('verbose')) {
$builder = new Builder($build, array($this, 'logCallback'));
} else {
$builder = new Builder($build);
}
$builder->execute();
}
return $builds;
}
/**

View file

@ -79,6 +79,7 @@ class BuildController extends \PHPCI\Controller
$build = $this->_buildStore->save($build);
header('Location: '.PHPCI_URL.'build/view/' . $build->getId());
exit;
}
/**
@ -86,14 +87,15 @@ class BuildController extends \PHPCI\Controller
*/
public function delete($buildId)
{
if (!Registry::getInstance()->get('user')->getIsAdmin()) {
if (empty($_SESSION['user']) || !$_SESSION['user']->getIsAdmin()) {
throw new \Exception('You do not have permission to do that.');
}
$build = $this->_buildStore->getById($buildId);
$this->_buildStore->delete($build);
header('Location: '.PHPCI_URL.'project/view/' . $build->getProjectId());
exit;
}
/**

View file

@ -33,7 +33,7 @@ class IndexController extends \PHPCI\Controller
$projects = $this->_projectStore->getWhere(array(), 50, 0, array(), array('title' => 'ASC'));
$summary = $this->_buildStore->getBuildSummary();
$summaryView = new b8\View('BuildsTable');
$summaryView = new b8\View('SummaryTable');
$summaryView->builds = $summary['items'];
$this->view->builds = $this->getLatestBuildsHtml();

View file

@ -63,6 +63,7 @@ class ProjectController extends \PHPCI\Controller
$build = $this->_buildStore->save($build);
header('Location: '.PHPCI_URL.'build/view/' . $build->getId());
exit;
}
/**
@ -78,6 +79,7 @@ class ProjectController extends \PHPCI\Controller
$this->_projectStore->delete($project);
header('Location: '.PHPCI_URL);
exit;
}
/**
@ -252,12 +254,13 @@ class ProjectController extends \PHPCI\Controller
'choose' => 'Select repository type...',
'github' => 'Github',
'bitbucket' => 'Bitbucket',
'remote' => 'Remote URL',
'local' => 'Local Path'
);
$field = new Form\Element\Select('type');
$field->setRequired(true);
$field->setPattern('^(github|bitbucket|local)');
$field->setPattern('^(github|bitbucket|remote|local)');
$field->setOptions($options);
$field->setLabel('Where is your project hosted?');
$field->setClass('span4');
@ -275,6 +278,11 @@ class ProjectController extends \PHPCI\Controller
$type = $values['type'];
switch($type) {
case 'remote':
if (!preg_match('/^(git|https?):\/\//', $val)) {
throw new \Exception('Repository URL must be start with git://, http:// or https://.');
}
break;
case 'local':
if (!is_dir($val)) {
throw new \Exception('The path you specified does not exist.');
@ -282,7 +290,7 @@ class ProjectController extends \PHPCI\Controller
break;
case 'github':
case 'bitbucket':
if (!preg_match('/^[a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-]+$/', $val)) {
if (!preg_match('/^[a-zA-Z0-9_\-]+\/[a-zA-Z0-9_\-\.]+$/', $val)) {
throw new \Exception('Repository name must be in the format "owner/repo".');
}
break;

View file

@ -25,11 +25,22 @@ class LocalBuild extends Build
* Create a working copy by cloning, copying, or similar.
*/
public function createWorkingCopy(Builder $builder, $buildPath)
{
{
$reference = $this->getProject()->getReference();
$reference = substr($reference, -1) == '/' ? substr($reference, 0, -1) : $reference;
$buildPath = substr($buildPath, 0, -1);
$yamlParser = new YamlParser();
if(is_file($reference.'/config')) {
//We're probably looing at a bare repository. We'll open the config and check
$gitConfig = parse_ini_file($reference.'/config',TRUE);
if($gitConfig["core"]["bare"]) {
// Looks like we're right. We need to extract the archive!
$guid = uniqid();
$builder->executeCommand('mkdir "/tmp/%s" && git --git-dir="%s" archive master | tar -x -C "/tmp/%s"', $guid, $reference, $guid);
$reference = '/tmp/'.$guid;
}
}
if (!is_file($reference . '/phpci.yml')) {
$builder->logFailure('Project does not contain a phpci.yml file.');

View file

@ -19,12 +19,15 @@ use Symfony\Component\Yaml\Parser as YamlParser;
* @package PHPCI
* @subpackage Core
*/
abstract class RemoteGitBuild extends Build
class RemoteGitBuild extends Build
{
/**
* Get the URL to be used to clone this remote repository.
*/
abstract protected function getCloneUrl();
protected function getCloneUrl()
{
return $this->getProject()->getReference();
}
/**
* Create a working copy by cloning, copying, or similar.

51
PHPCI/Plugin/Atoum.php Normal file
View file

@ -0,0 +1,51 @@
<?php
namespace PHPCI\Plugin;
class Atoum implements \PHPCI\Plugin
{
private $args;
private $config;
private $directory;
private $executable;
public function __construct(\PHPCI\Builder $phpci, array $options = array())
{
$this->phpci = $phpci;
if (isset($options['executable'])) {
$this->executable = $options['executable'];
}
else {
$this->executable = './vendor/bin/atoum';
}
if (isset($options['args'])) {
$this->args = $options['args'];
}
if (isset($options['config'])) {
$this->config = $options['config'];
}
if (isset($options['directory'])) {
$this->directory = $options['directory'];
}
}
public function execute()
{
$cmd = $this->phpci->buildPath . DIRECTORY_SEPARATOR . $this->executable;
if ($this->args !== null) {
$cmd .= " {$this->args}";
}
if ($this->config !== null) {
$cmd .= " -c '{$this->config}'";
}
if ($this->directory !== null) {
$cmd .= " -d '{$this->directory}'";
}
return $this->phpci->executeCommand($cmd);
}
}

View file

@ -19,6 +19,7 @@ class Composer implements \PHPCI\Plugin
{
protected $directory;
protected $action;
protected $preferDist;
protected $phpci;
public function __construct(\PHPCI\Builder $phpci, array $options = array())
@ -27,6 +28,7 @@ class Composer implements \PHPCI\Plugin
$this->phpci = $phpci;
$this->directory = isset($options['directory']) ? $path . '/' . $options['directory'] : $path;
$this->action = isset($options['action']) ? $options['action'] : 'update';
$this->preferDist = isset($options['prefer_dist']) ? $options['prefer_dist'] : true;
}
/**
@ -34,7 +36,7 @@ class Composer implements \PHPCI\Plugin
*/
public function execute()
{
$cmd = PHPCI_DIR . 'composer.phar --prefer-dist --working-dir="%s" %s';
$cmd = PHPCI_DIR . 'composer.phar '. ($this->preferDist ? '--prefer-dist' : null) .' --working-dir="%s" %s';
return $this->phpci->executeCommand($cmd, $this->directory, $this->action);
}
}

194
PHPCI/Plugin/Email.php Normal file
View file

@ -0,0 +1,194 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link http://www.phptesting.org/
*/
namespace PHPCI\Plugin;
/**
* Email Plugin - Provides simple email capability to PHPCI.
* @author Steve Brazier <meadsteve@gmail.com>
* @package PHPCI
* @subpackage Plugins
*/
class Email implements \PHPCI\Plugin
{
/**
* @var \PHPCI\Builder
*/
protected $phpci;
/**
* @var array
*/
protected $options;
/**
* @var array
*/
protected $emailConfig;
/**
* @var \Swift_Mailer
*/
protected $mailer;
public function __construct(\PHPCI\Builder $phpci,
array $options = array(),
\Swift_Mailer $mailer = null)
{
$phpCiSettings = $phpci->getSystemConfig('phpci');
$this->phpci = $phpci;
$this->options = $options;
$this->emailConfig = isset($phpCiSettings['email_settings']) ? $phpCiSettings['email_settings'] : array();
// Either a mailer will have been passed in or we load from the
// config.
if ($mailer === null) {
$this->loadSwiftMailerFromConfig();
}
else {
$this->mailer = $mailer;
}
}
/**
* Connects to MySQL and runs a specified set of queries.
*/
public function execute()
{
$addresses = $this->getEmailAddresses();
// Without some email addresses in the yml file then we
// can't do anything.
if (count($addresses) == 0) {
return false;
}
$sendFailures = array();
$subjectTemplate = "PHPCI - %s - %s";
$projectName = $this->phpci->getBuildProjectTitle();
$logText = $this->phpci->getBuild()->getLog();
if($this->phpci->getSuccessStatus()) {
$sendFailures = $this->sendSeparateEmails(
$addresses,
sprintf($subjectTemplate, $projectName, "Passing Build"),
sprintf("Log Output: <br><pre>%s</pre>", $logText)
);
}
else {
$sendFailures = $this->sendSeparateEmails(
$addresses,
sprintf($subjectTemplate, $projectName, "Failing Build"),
sprintf("Log Output: <br><pre>%s</pre>", $logText)
);
}
// This is a success if we've not failed to send anything.
$this->phpci->log(sprintf(
"%d emails sent",
(count($addresses) - count($sendFailures)))
);
$this->phpci->log(sprintf(
"%d emails failed to send",
count($sendFailures))
);
return (count($sendFailures) == 0);
}
/**
* @param array|string $toAddresses Array or single address to send to
* @param string $subject Email subject
* @param string $body Email body
* @return array Array of failed addresses
*/
public function sendEmail($toAddresses, $subject, $body)
{
$message = \Swift_Message::newInstance($subject)
->setFrom($this->getMailConfig('from_address'))
->setTo($toAddresses)
->setBody($body)
->setContentType("text/html");
$failedAddresses = array();
$this->mailer->send($message, $failedAddresses);
return $failedAddresses;
}
public function sendSeparateEmails(array $toAddresses, $subject, $body)
{
$failures = array();
foreach($toAddresses as $address) {
$newFailures = $this->sendEmail($address, $subject, $body);
foreach($newFailures as $failure) {
$failures[] = $failure;
}
}
return $failures;
}
protected function loadSwiftMailerFromConfig()
{
/** @var \Swift_SmtpTransport $transport */
$transport = \Swift_SmtpTransport::newInstance(
$this->getMailConfig('smtp_address'),
$this->getMailConfig('smtp_port'),
$this->getMailConfig('smtp_encryption')
);
$transport->setUsername($this->getMailConfig('smtp_username'));
$transport->setPassword($this->getMailConfig('smtp_password'));
$this->mailer = \Swift_Mailer::newInstance($transport);
}
protected function getMailConfig($configName)
{
if (isset($this->emailConfig[$configName])
&& $this->emailConfig[$configName] != "")
{
return $this->emailConfig[$configName];
}
// Check defaults
else {
switch($configName) {
case 'smtp_address':
return "localhost";
case 'default_mailto_address':
return null;
case 'smtp_port':
return '25';
case 'smtp_encryption':
return null;
case 'from_address':
return "notifications-ci@phptesting.org";
default:
return "";
}
}
}
protected function getEmailAddresses()
{
$addresses = array();
if (isset($this->options['addresses'])) {
foreach ($this->options['addresses'] as $address) {
$addresses[] = $address;
}
}
if (isset($this->options['default_mailto_address'])) {
$addresses[] = $this->options['default_mailto_address'];
return $addresses;
}
return $addresses;
}
}

View file

@ -18,10 +18,16 @@ namespace PHPCI\Plugin;
class PhpMessDetector implements \PHPCI\Plugin
{
protected $directory;
/**
* Array of PHPMD rules. Possible values: codesize, unusedcode, naming, design, controversial
* @var array
*/
protected $rules;
public function __construct(\PHPCI\Builder $phpci, array $options = array())
{
$this->phpci = $phpci;
$this->rules = isset($options['rules']) ? (array)$options['rules'] : array('codesize', 'unusedcode', 'naming');
}
/**
@ -35,7 +41,7 @@ class PhpMessDetector implements \PHPCI\Plugin
$ignore = ' --exclude ' . implode(',', $this->phpci->ignore);
}
$cmd = PHPCI_BIN_DIR . 'phpmd "%s" text codesize,unusedcode,naming %s';
return $this->phpci->executeCommand($cmd, $this->phpci->buildPath, $ignore);
$cmd = PHPCI_BIN_DIR . 'phpmd "%s" text %s %s';
return $this->phpci->executeCommand($cmd, $this->phpci->buildPath, implode(',', $this->rules), $ignore);
}
}

View file

@ -31,7 +31,7 @@ class BuildStore extends BuildStoreBase
$count = 0;
}
$query = 'SELECT b.* FROM build b LEFT JOIN project p on p.id = b.project_id GROUP BY b.project_id ORDER BY p.title ASC, b.id DESC';
$query = 'SELECT b.* FROM build b LEFT JOIN project p on p.id = b.project_id ORDER BY p.title ASC, b.id DESC';
$stmt = \b8\Database::getConnection('read')->prepare($query);
if ($stmt->execute()) {

View file

@ -13,7 +13,7 @@
<li class="nav-header">Options</li>
<li><a href="<?= PHPCI_URL ?>build/rebuild/<?php print $build->getId(); ?>"><i class="icon-cog"></i> Rebuild</a></li>
<?php if($this->User()->getIsAdmin()): ?>
<li><a href="javascript:confirmDelete('<?= PHPCI_URL ?>build/delete/<?php print $build->getId(); ?>')"><i class="icon-trash"></i> Delete Build</a></li>
<li><a href="#" id="delete-build"><i class="icon-trash"></i> Delete Build</a></li>
<?php endif; ?>
</ul>
</div>
@ -73,8 +73,12 @@
}, 10000);
<?php endif; ?>
$(document).ready(function()
{
$(function() {
updateBuildView(window.initial);
$('#delete-build').on('click', function (e) {
e.preventDefault();
confirmDelete("<?= PHPCI_URL ?>build/delete/<?php print $build->getId(); ?>");
});
});
</script>

View file

@ -11,21 +11,26 @@ switch($build->getStatus())
{
case 0:
$cls = 'info';
$subcls = 'info';
$status = 'Pending';
break;
case 1:
$cls = 'warning';
$subcls = 'warning';
$status = 'Running';
break;
case 2:
$cls = 'success';
$subcls = 'success';
$status = 'Success';
break;
case 3:
$cls = 'error';
$subcls = 'important';
$status = 'Failed';
break;
}
@ -35,7 +40,31 @@ switch($build->getStatus())
<td><a href="<?= PHPCI_URL ?>project/view/<?php print $build->getProjectId(); ?>"><?php print $build->getProject()->getTitle(); ?></a></td>
<td><a href="<?php print $build->getCommitLink(); ?>"><?php print $build->getCommitId(); ?></a></td>
<td><a href="<?php print $build->getBranchLink(); ?>"><?php print $build->getBranch(); ?></a></td>
<td><?php print $status; ?></td>
<td>
<?php
$plugins = json_decode($build->getPlugins(), true);
if ( !is_array($plugins) ) {
$plugins = array();
}
if ( 0 === count($plugins) ) {
?>
<span class='label label-<?= $subcls ?>'>
<?= $status ?>
</span>
<?php
}
?>
<?php
foreach($plugins as $plugin => $pluginstatus):
$subcls = $pluginstatus?'label label-success':'label label-important';
?>
<span class='<?= $subcls ?>'>
<?= ucwords(str_replace('_', ' ', $plugin)) ?>
</span>
<?php endforeach; ?>
<br style='clear:both;' />
</td>
<td>
<div class="btn-group">
<a class="btn" href="<?= PHPCI_URL ?>build/view/<?php print $build->getId(); ?>">View</a>

View file

@ -44,11 +44,11 @@
<table class="table table-striped table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Health</th>
<th>Project</th>
<th>Commit</th>
<th>Branch</th>
<th>Status</th>
<th>Last Success</th>
<th>Last Failure</th>
<th>Success/Failures</th>
<th style="width: 1%"></th>
</tr>
</thead>

View file

@ -14,13 +14,15 @@
<?php if($this->User()->getIsAdmin()): ?>
<li><a href="<?= PHPCI_URL ?>project/edit/<?php print $project->getId(); ?>"><i class="icon-edit"></i> Edit Project</a></li>
<li><a href="javascript:confirmDelete('<?= PHPCI_URL ?>project/delete/<?php print $project->getId(); ?>')"><i class="icon-trash"></i> Delete Project</a></li>
<li><a href="#" id="delete-project"><i class="icon-trash"></i> Delete Project</a></li>
<?php endif; ?>
</ul>
</div>
<br>
<p class="alert alert-info">To automatically build this project when new commits are pushed, add the URL below
<?php if (in_array($project->getType(), array('github', 'bitbucket'))): ?>
<br>
<p class="alert alert-info">To automatically build this project when new commits are pushed, add the URL below
<?php endif; ?>
<?php
switch($project->getType())
@ -61,14 +63,14 @@
$pages = ceil($total / 10);
$pages = $pages == 0 ? 1 : $pages;
print '<li class="'.($page == 1 ? 'disabled' : '').'"><a href="<?= PHPCI_URL ?>project/view/'.$project->getId().'?p='.($page == 1 ? '1' : $page - 1).'">&laquo;</a></li>';
print '<li class="'.($page == 1 ? 'disabled' : '').'"><a href="' . PHPCI_URL . 'project/view/'.$project->getId().'?p='.($page == 1 ? '1' : $page - 1).'">&laquo;</a></li>';
for($i = 1; $i <= $pages; $i++)
{
print '<li><a href="<?= PHPCI_URL ?>project/view/' . $project->getId() . '?p=' . $i . '">' . $i . '</a></li>';
print '<li><a href="' . PHPCI_URL . 'project/view/' . $project->getId() . '?p=' . $i . '">' . $i . '</a></li>';
}
print '<li class="'.($page == $pages ? 'disabled' : '').'"><a href="<?= PHPCI_URL ?>project/view/'.$project->getId().'?p='.($page == $pages ? $pages : $page + 1).'">&raquo;</a></li>';
print '<li class="'.($page == $pages ? 'disabled' : '').'"><a href="' . PHPCI_URL . 'project/view/'.$project->getId().'?p='.($page == $pages ? $pages : $page + 1).'">&raquo;</a></li>';
print '</ul></div>';
@ -78,9 +80,16 @@
<?php if($page == 1): ?>
<script>
setInterval(function()
{
$('#latest-builds').load('<?= PHPCI_URL ?>project/builds/<?php print $project->getId(); ?>');
}, 10000);
setInterval(function()
{
$('#latest-builds').load('<?= PHPCI_URL ?>project/builds/<?php print $project->getId(); ?>');
}, 10000);
$(function() {
$('#delete-project').on('click', function (e) {
e.preventDefault();
confirmDelete("<?= PHPCI_URL ?>project/delete/<?php print $project->getId(); ?>");
});
})
</script>
<?php endif; ?>
<?php endif; ?>

View file

@ -0,0 +1,114 @@
<?php
// echo "<pre>";
// var_dump($builds);
// echo "</pre>";
$maxbuildcount = 5;
$projects = array();
$prevBuild = null;
$health = false;
foreach($builds as $build):
if ($build->getStatus() < 2) {
continue;
}
if ( is_null($prevBuild) || $build->getProjectId() !== $prevBuild->getProjectId() ) {
$health = false;
$projects[$build->getProjectId()]['count'] = 0;
$projects[$build->getProjectId()]['health'] = 0;
$projects[$build->getProjectId()]['successes'] = 0;
$projects[$build->getProjectId()]['failures'] = 0;
$projects[$build->getProjectId()]['lastbuildstatus'] = (int)$build->getStatus();
}
if (
!is_null($prevBuild) &&
$projects[$build->getProjectId()]['count'] >= $maxbuildcount &&
$build->getProjectId() === $prevBuild->getProjectId()
) {
$projects[$build->getProjectId()]['count']++;
continue;
}
switch ((int)$build->getStatus()) {
case 2:
$projects[$build->getProjectId()]['health']++;
$projects[$build->getProjectId()]['successes']++;
if ( empty($projects[$build->getProjectId()]['lastsuccess']) ) {
$projects[$build->getProjectId()]['lastsuccess'] = $build;
}
break;
case 3:
$projects[$build->getProjectId()]['health']--;
$projects[$build->getProjectId()]['failures']++;
if ( empty($projects[$build->getProjectId()]['lastfailure']) ) {
$projects[$build->getProjectId()]['lastfailure'] = $build;
}
break;
}
$projects[$build->getProjectId()]['count']++;
$projects[$build->getProjectId()]['projectname'] = $build->getProject()->getTitle();
$prevBuild = $build;
endforeach;
foreach($projects as $projectId => $project):
switch($project['lastbuildstatus'])
{
case 0:
$cls = 'info';
$status = 'Pending';
break;
case 1:
$cls = 'warning';
$status = 'Running';
break;
case 2:
$cls = 'success';
$status = 'Success';
break;
case 3:
$cls = 'error';
$status = 'Failed';
break;
}
$health = ($project['health'] < 0 ? 'Stormy': ($project['health'] < 5? 'Overcast': 'Sunny'));
$subcls = ($project['health'] < 0 ? 'important': ($project['health'] < 5? 'warning': 'success'));
?>
<tr class="<?php print $cls; ?>">
<td>
<span class='label label-<?= $subcls ?>'>
<?= $health ?>
</span>
</td>
<td><a href='<?= PHPCI_URL ?>project/view/<?= $projectId ?>'><?= $project['projectname'] ?></a></td>
<td>
<?php if (empty($project['lastsuccess'])) {
echo "Never";
} else { ?>
<a href='<?= PHPCI_URL ?>build/view/<?= $project['lastsuccess']->getId() ?>'>
<?= $project['lastsuccess']->getStarted()->format("Y-m-d H:i:s") ?>
</a>
<?php } ?>
</td>
<td>
<?php if (empty($project['lastfailure'])) {
echo "Never";
} else { ?>
<a href='<?= PHPCI_URL ?>build/view/<?= $project['lastfailure']->getId() ?>'>
<?= $project['lastfailure']->getStarted()->format("Y-m-d H:i:s") ?>
</a>
<?php } ?>
</td>
<td><?= $project['successes'] ?>/<?= $project['failures'] ?></td>
<td><a class="btn" href='<?= PHPCI_URL ?>project/build/<?= $projectId ?>'>build</a></td>
</tr>
<?php endforeach; ?>

View file

@ -17,7 +17,7 @@
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="<?= PHPCI_URL ?>">PHPCI <span class="badge badge-important" style="position: relative; top: -3px; margin-left: 10px">1.0 Alpha</span></a>
<a class="brand" href="<?= PHPCI_URL ?>">PHPCI <span class="badge badge-important" style="position: relative; top: -3px; margin-left: 10px">1.0.0 Beta</span></a>
<ul class="nav pull-right">
<li><a href="<?= PHPCI_URL ?>session/logout">Log out</a></li>
@ -35,4 +35,4 @@
<?php print $content; ?>
</div>
</body>
</html>
</html>

View file

@ -3,14 +3,14 @@ PHPCI
PHPCI is a free and open source continuous integration tool specifically designed for PHP. We've built it with simplicity in mind, so whilst it doesn't do *everything* Jenkins can do, it is a breeze to set up and use.
_**Please be aware that this is a brand new project, in an alpha state, so there will be bugs and missing features.**_
_**Please be aware that PHPCI is a beta-release project, so whilst it is very stable, there may be bugs and/or missing features.**_
**Current Build Status**
![Build Status](http://phpci.block8.net/build-status/image/2)
##What it does:
* Clones your project from Github, Bitbucket or a local path (support for standard remote Git repositories coming soon.)
* Clones your project from Github, Bitbucket or a local path
* Allows you to set up and tear down test databases.
* Installs your project's Composer dependencies.
* Runs through any combination of the following plugins:
@ -28,7 +28,6 @@ _**Please be aware that this is a brand new project, in an alpha state, so there
* Multiple testing workers.
* Install PEAR or PECL extensions.
* Deployments.
* Success / Failure emails.
##Installing PHPCI:
####Pre-requisites:

View file

@ -0,0 +1,256 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link http://www.phptesting.org/
*/
namespace PHPCI\Plugin\Tests;
use PHPCI\Plugin\Email as EmailPlugin;
define('PHPCI_BIN_DIR', "FAKEPHPCIBIN");
/**
* Unit test for the PHPUnit plugin.
* @author meadsteve
*/
class EmailTest extends \PHPUnit_Framework_TestCase
{
/**
* @var EmailPlugin $testedPhpUnit
*/
protected $testedEmailPlugin;
/**
* @var \PHPUnit_Framework_MockObject_MockObject $mockCiBuilder
*/
protected $mockCiBuilder;
/**
* @var \PHPUnit_Framework_MockObject_MockObject $mockMailer
*/
protected $mockMailer;
/**
* @var \PHPUnit_Framework_MockObject_MockObject $mockMailer
*/
protected $mockBuild;
public function setUp()
{
$this->mockBuild = $this->getMock(
'\PHPCI\Model\Build',
array('getLog'),
array(),
"mockBuild",
false
);
$this->mockBuild->expects($this->any())
->method('getLog')
->will($this->returnValue("Build Log"));
$this->mockCiBuilder = $this->getMock(
'\PHPCI\Builder',
array('getSystemConfig',
'getBuildProjectTitle',
'getBuild',
'log'),
array(),
"mockBuilder",
false
);
$this->mockCiBuilder->buildPath = "/";
$this->mockCiBuilder->expects($this->any())
->method('getSystemConfig')
->with('phpci')
->will($this->returnValue(array(
'email_settings' => array(
'from_address' => "test-from-address@example.com"
)
)));
$this->mockCiBuilder->expects($this->any())
->method('getBuildProjectTitle')
->will($this->returnValue('Test-Project'));
$this->mockCiBuilder->expects($this->any())
->method('getBuild')
->will($this->returnValue($this->mockBuild));
$this->mockMailer = $this->getMock(
'\Swift_Mailer',
array('send'),
array(),
"mockMailer",
false
);
$this->loadEmailPluginWithOptions();
}
protected function loadEmailPluginWithOptions($arrOptions = array())
{
$this->testedEmailPlugin = new EmailPlugin(
$this->mockCiBuilder,
$arrOptions,
$this->mockMailer
);
}
/**
* @covers PHPUnit::execute
*/
public function testExecute_ReturnsFalseWithoutArgs()
{
$returnValue = $this->testedEmailPlugin->execute();
// As no addresses will have been mailed as non are configured.
$expectedReturn = false;
$this->assertEquals($expectedReturn, $returnValue);
}
/**
* @covers PHPUnit::execute
*/
public function testExecute_BuildsBasicEmails()
{
$this->loadEmailPluginWithOptions(array(
'addresses' => array('test-receiver@example.com')
));
/** @var \Swift_Message $actualMail */
$actualMail = null;
$this->catchMailPassedToSend($actualMail);
$returnValue = $this->testedEmailPlugin->execute();
$expectedReturn = true;
$this->assertSystemMail(
'test-receiver@example.com',
'test-from-address@example.com',
"Log Output: <br><pre>Build Log</pre>",
"PHPCI - Test-Project - Passing Build",
$actualMail
);
$this->assertEquals($expectedReturn, $returnValue);
}
/**
* @covers PHPUnit::sendEmail
*/
public function testSendEmail_CallsMailerSend()
{
$this->mockMailer->expects($this->once())
->method('send');
$this->testedEmailPlugin->sendEmail("test@email.com", "hello", "body");
}
/**
* @covers PHPUnit::sendEmail
*/
public function testSendEmail_BuildsAMessageObject()
{
$subject = "Test mail";
$body = "Message Body";
$toAddress = "test@example.com";
$this->mockMailer->expects($this->once())
->method('send')
->with($this->isInstanceOf('\Swift_Message'), $this->anything());
$this->testedEmailPlugin->sendEmail($toAddress, $subject, $body);
}
/**
* @covers PHPUnit::sendEmail
*/
public function testSendEmail_BuildsExpectedMessage()
{
$subject = "Test mail";
$body = "Message Body";
$toAddress = "test@example.com";
$expectedMessage = \Swift_Message::newInstance($subject)
->setFrom('test-from-address@example.com')
->setTo($toAddress)
->setBody($body);
/** @var \Swift_Message $actualMail */
$actualMail = null;
$this->catchMailPassedToSend($actualMail);
$this->testedEmailPlugin->sendEmail($toAddress, $subject, $body);
$this->assertSystemMail(
$toAddress,
'test-from-address@example.com',
$body,
$subject,
$actualMail
);
}
/**
* @param \Swift_Message $actualMail passed by ref and populated with
* the message object the mock mailer
* receives.
*/
protected function catchMailPassedToSend(&$actualMail)
{
$this->mockMailer->expects($this->once())
->method('send')
->will(
$this->returnCallback(
function ($passedMail) use (&$actualMail) {
$actualMail = $passedMail;
return array();
}
)
);
}
/**
* Asserts that the actual mail object is populated as expected.
*
* @param string $expectedToAddress
* @param $expectedFromAddress
* @param string $expectedBody
* @param string $expectedSubject
* @param \Swift_Message $actualMail
*/
protected function assertSystemMail($expectedToAddress,
$expectedFromAddress,
$expectedBody,
$expectedSubject,
$actualMail)
{
if (! ($actualMail instanceof \Swift_Message)) {
$type = is_object($actualMail) ? get_class($actualMail) : gettype($actualMail);
throw new \Exception("Expected Swift_Message got " . $type);
}
$this->assertEquals(
array($expectedFromAddress => null),
$actualMail->getFrom()
);
$this->assertEquals(
array($expectedToAddress => null),
$actualMail->getTo()
);
$this->assertEquals(
$expectedBody,
$actualMail->getBody()
);
$this->assertEquals(
$expectedSubject,
$actualMail->getSubject()
);
}
}

View file

@ -14,6 +14,20 @@ body
padding: 10px;
}
td .label { margin-right: 5px; }
.success-message {
background-color: #4F8A10;
}
.error-message {
background-color: #FF4747;
}
#latest-builds td {
vertical-align: middle;
}
.widget-title, .modal-header, .table th, div.dataTables_wrapper .ui-widget-header, .ui-dialog .ui-dialog-titlebar {
background-color: #efefef;
background-image: -webkit-gradient(linear, 0 0%, 0 100%, from(#fdfdfd), to(#eaeaea));
@ -61,7 +75,7 @@ body
background: url('/assets/img/icon-build-running.png') no-repeat top left;
}
h3
h3
{
border-bottom: 1px solid #f0f0f0;
margin-top: 0;

View file

@ -32,6 +32,7 @@
"phpspec/phpspec" : "2.*",
"symfony/yaml" : "2.2.x-dev",
"symfony/console" : "2.2.*",
"fabpot/php-cs-fixer" : "0.3.*@dev"
"fabpot/php-cs-fixer" : "0.3.*@dev",
"swiftmailer/swiftmailer" : "v5.0.0"
}
}

View file

@ -10,6 +10,7 @@
define('PHPCI_BIN_DIR', dirname(__FILE__) . '/vendor/bin/');
define('PHPCI_DIR', dirname(__FILE__) . '/');
define('ENABLE_SHELL_PLUGIN', false);
// If this is the first time ./console has been run, we probably don't have Composer or any of our dependencies yet.
// So we need to install and run Composer.
@ -28,10 +29,12 @@ require('bootstrap.php');
use PHPCI\Command\RunCommand;
use PHPCI\Command\GenerateCommand;
use PHPCI\Command\InstallCommand;
use PHPCI\Command\DaemonCommand;
use Symfony\Component\Console\Application;
$application = new Application();
$application->add(new RunCommand);
$application->add(new InstallCommand);
$application->add(new GenerateCommand);
$application->add(new DaemonCommand);
$application->run();

2
daemon/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

22
daemonise Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env php
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link http://www.phptesting.org/
*/
define('PHPCI_BIN_DIR', dirname(__FILE__) . '/vendor/bin/');
define('PHPCI_DIR', dirname(__FILE__) . '/');
define('ENABLE_SHELL_PLUGIN', false);
require('bootstrap.php');
use PHPCI\Command\DaemoniseCommand;
use Symfony\Component\Console\Application;
$application = new Application();
$application->add(new DaemoniseCommand);
$application->run();