├── .gitignore
├── web
├── images
│ ├── hr.png
│ ├── failed.png
│ ├── header.png
│ ├── nobuild.png
│ ├── success.png
│ └── sensio-labs-product.png
├── .htaccess
├── index.php
└── css
│ └── sismo.css
├── src
├── templates
│ ├── error.twig
│ ├── ccmonitor.twig.xml
│ ├── layout.twig
│ ├── projects.twig
│ └── project.twig
├── Sismo
│ ├── BuildException.php
│ ├── SSHProject.php
│ ├── Contrib
│ │ ├── GoogleTalkNotifier.php
│ │ ├── JoliNotifier.php
│ │ ├── KDENotifier.php
│ │ ├── LoggerNotifier.php
│ │ ├── XmppNotifier.php
│ │ ├── TwitterNotifier.php
│ │ ├── CrossFingerNotifier.php
│ │ ├── IrcNotifier.php
│ │ ├── GrowlNotifier.php
│ │ ├── AndroidPushC2DMNotifier.php
│ │ └── GithubNotifier.php
│ ├── Notifier
│ │ ├── Notifier.php
│ │ ├── DBusNotifier.php
│ │ ├── MailNotifier.php
│ │ └── GrowlNotifier.php
│ ├── GithubProject.php
│ ├── BitbucketProject.php
│ ├── Storage
│ │ ├── StorageInterface.php
│ │ ├── Storage.php
│ │ └── PdoStorage.php
│ ├── Sismo.php
│ ├── Builder.php
│ ├── Commit.php
│ └── Project.php
├── controllers.php
├── app.php
└── console.php
├── .travis.yml
├── sismo
├── tests
├── Sismo
│ └── Tests
│ │ ├── BuildExceptionTest.php
│ │ ├── SSHProjectTest.php
│ │ ├── Notifier
│ │ └── NotifierTest.php
│ │ ├── Contrib
│ │ ├── LoggerNotifierTest.php
│ │ └── CrossFingerNotifierTest.php
│ │ ├── Storage
│ │ ├── PdoStorageTest.php
│ │ └── StorageTest.php
│ │ ├── GithubProjectTest.php
│ │ ├── BitbucketProjectTest.php
│ │ ├── CommitTest.php
│ │ ├── ProjectTest.php
│ │ └── SismoTest.php
├── appTest.php
├── consoleTest.php
└── controllersTest.php
├── phpunit.xml.dist
├── LICENSE
├── composer.json
├── README.rst
└── compile
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | composer.phar
3 | vendor
4 |
--------------------------------------------------------------------------------
/web/images/hr.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/hr.png
--------------------------------------------------------------------------------
/web/images/failed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/failed.png
--------------------------------------------------------------------------------
/web/images/header.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/header.png
--------------------------------------------------------------------------------
/web/images/nobuild.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/nobuild.png
--------------------------------------------------------------------------------
/web/images/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/success.png
--------------------------------------------------------------------------------
/web/images/sensio-labs-product.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FriendsOfPHP/Sismo/HEAD/web/images/sensio-labs-product.png
--------------------------------------------------------------------------------
/web/.htaccess:
--------------------------------------------------------------------------------
1 | #SetEnv SISMO_DATA_PATH "/path/to/data"
2 | #SetEnv SISMO_CONFIG_PATH "/path/to/config"
3 |
4 | RewriteEngine On
5 |
6 | RewriteCond %{REQUEST_FILENAME} !-f
7 | RewriteRule ^(.*)$ index.php [QSA,L]
8 |
--------------------------------------------------------------------------------
/src/templates/error.twig:
--------------------------------------------------------------------------------
1 | {% extends 'layout.twig' %}
2 |
3 | {% block title 'Error' %}
4 |
5 | {% block content %}
6 |
Hmmm, looks like something went wrong
7 |
8 | {{ error ? error|replace({"\n": '
' })|raw : 'An error occurred' }}
9 | {% endblock %}
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | sudo: false
4 |
5 | php:
6 | - 5.3
7 | - 5.4
8 | - 5.5
9 | - 5.6
10 | - 7.0
11 |
12 | matrix:
13 | fast_finish: true
14 | allow_failures:
15 | - php: 7.0
16 |
17 | install:
18 | - travis_retry composer install --no-interaction --prefer-source
19 |
20 | script:
21 | - phpunit --verbose
22 |
--------------------------------------------------------------------------------
/web/index.php:
--------------------------------------------------------------------------------
1 | run();
12 |
--------------------------------------------------------------------------------
/sismo:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
16 |
--------------------------------------------------------------------------------
/src/Sismo/BuildException.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | /**
15 | * Thrown for any problem during a build.
16 | *
17 | * @author Fabien Potencier
18 | */
19 | class BuildException extends \Exception
20 | {
21 | }
22 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/BuildExceptionTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\BuildException;
15 |
16 | class BuildExceptionTest extends \PHPUnit_Framework_TestCase
17 | {
18 | public function test()
19 | {
20 | $this->assertInstanceOf('\Exception', new BuildException());
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/templates/ccmonitor.twig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | {% for project in projects %}
4 | {% set commit = project.getLatestCommit %}
5 |
14 | {% endfor %}
15 |
16 |
--------------------------------------------------------------------------------
/src/Sismo/SSHProject.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | /**
15 | * Describes a project accessible via SSH.
16 | *
17 | * @author Toni Uebernickel
18 | */
19 | class SSHProject extends Project
20 | {
21 | /**
22 | * Sets the project repository URL.
23 | *
24 | * @param string $url The project repository URL
25 | */
26 | public function setRepository($url)
27 | {
28 | $this->repository = $url;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
14 |
15 |
16 | ./tests/
17 |
18 |
19 |
20 |
21 |
22 | ./src/
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/templates/layout.twig:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {% block title '' %} | Sismo
6 |
7 |
8 |
9 |
10 | {% block content '' %}
11 |
12 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/templates/projects.twig:
--------------------------------------------------------------------------------
1 | {% extends 'layout.twig' %}
2 |
3 | {% block title 'Projects' %}
4 |
5 | {% block content %}
6 | {% if not projects %}
7 | No project yet.
8 | {% else %}
9 |
20 | {% endif %}
21 | {% endblock %}
22 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/GoogleTalkNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | // @codeCoverageIgnoreStart
15 | /**
16 | * Notifies builds via a Google Talk server.
17 | *
18 | * This notifier needs the XMPPHP library to be required in your configuration.
19 | *
20 | * require '/path/to/XMPPHP/XMPP.php';
21 | *
22 | * Download it at http://code.google.com/p/xmpphp
23 | *
24 | * @author Fabien Potencier
25 | */
26 | class GoogleTalkNotifier extends XmppNotifier
27 | {
28 | public function __construct($username, $password, $recipient, $format = '[%STATUS%] %name% %short_sha% -- %message% by %author%')
29 | {
30 | parent::__construct('talk.google.com', 5222, 'gmail.com', $username, $password, $recipient, $format);
31 | }
32 | }
33 | // @codeCoverageIgnoreEnd
34 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/SSHProjectTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\SSHProject;
15 |
16 | class SSHProjectTest extends \PHPUnit_Framework_TestCase
17 | {
18 | public function sshRepositoryProvider()
19 | {
20 | return array(
21 | array('git@github.com:twigphp/Twig.git'),
22 | array('git@git.assembla.com:Twig.git'),
23 | array('ssh://git@git.example.com:Twig.git'),
24 | );
25 | }
26 |
27 | /**
28 | * @dataProvider sshRepositoryProvider
29 | */
30 | public function testSetRepository($repository)
31 | {
32 | $project = new SSHProject('Project');
33 | $project->setRepository($repository);
34 |
35 | $this->assertEquals($repository, $project->getRepository());
36 | $this->assertEquals('master', $project->getBranch());
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2009-2013 Fabien Potencier
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is furnished
8 | to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
20 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "sismo/sismo",
3 | "type": "silex-application",
4 | "description": "Sismo is a personal continuous integration server.",
5 | "keywords": ["sismo", "continuous integration"],
6 | "homepage": "http://sismo.sensiolabs.org",
7 | "license": "MIT",
8 | "authors": [
9 | {
10 | "name": "Fabien Potencier",
11 | "email": "fabpot@symfony.com"
12 | },
13 | {
14 | "name": "Sismo Community",
15 | "homepage": "https://github.com/FriendsOfPhp/sismo/contributors"
16 | }
17 | ],
18 | "require": {
19 | "php": ">=5.3.3",
20 | "ext-sqlite3": "*",
21 | "silex/silex": "~1.0",
22 | "symfony/console": "~2.3",
23 | "symfony/twig-bridge": "~2.2",
24 | "symfony/process": "~2.2",
25 | "symfony/filesystem": "~2.2",
26 | "symfony/finder": "~2.2",
27 | "sensiolabs/ansi-to-html": "~1.0"
28 | },
29 | "require-dev": {
30 | "phpunit/phpunit": "3.7.*",
31 | "symfony/browser-kit": "~2.2",
32 | "symfony/css-selector": "~2.2.0",
33 | "symfony/dom-crawler": "~2.2.0",
34 | "symfony/class-loader": "~2.2"
35 | },
36 | "suggest": {
37 | "jolicode/jolinotif": "Use the best notifier available on your system (Mac, Linux or Windows)",
38 | "monolog/monolog": "Use Monolog handlers as notifiers"
39 | },
40 | "autoload": {
41 | "psr-0": { "Sismo": "src/" }
42 | },
43 | "bin": ["sismo"]
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/Notifier/NotifierTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests\Notifier;
13 |
14 | use Sismo\Notifier\Notifier;
15 | use Sismo\Commit;
16 | use Sismo\Project;
17 |
18 | class NotifierTest extends \PHPUnit_Framework_TestCase
19 | {
20 | public function testFormat()
21 | {
22 | $notifier = $this->getMock('Sismo\Notifier\Notifier');
23 | $r = new \ReflectionObject($notifier);
24 | $m = $r->getMethod('format');
25 | $m->setAccessible(true);
26 |
27 | $project = new Project('Twig');
28 | $commit = new Commit($project, '123456');
29 | $commit->setAuthor('Fabien');
30 | $commit->setMessage('Foo');
31 |
32 | $this->assertEquals('twig', $m->invoke($notifier, '%slug%', $commit));
33 | $this->assertEquals('Twig', $m->invoke($notifier, '%name%', $commit));
34 | $this->assertEquals('building', $m->invoke($notifier, '%status%', $commit));
35 | $this->assertEquals('building', $m->invoke($notifier, '%status_code%', $commit));
36 | $this->assertEquals('123456', $m->invoke($notifier, '%sha%', $commit));
37 | $this->assertEquals('Fabien', $m->invoke($notifier, '%author%', $commit));
38 | $this->assertEquals('Foo', $m->invoke($notifier, '%message%', $commit));
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/Contrib/LoggerNotifierTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests\Contrib;
13 |
14 | use Sismo\Commit;
15 | use Sismo\Contrib\LoggerNotifier;
16 | use Sismo\Project;
17 |
18 | class LoggerNotifierTest extends \PHPUnit_Framework_TestCase
19 | {
20 | public function testNotify()
21 | {
22 | $project = new Project('Twig');
23 | $successCommit = new Commit($project, '123455');
24 | $successCommit->setAuthor('Fabien');
25 | $successCommit->setMessage('Bar');
26 | $successCommit->setStatusCode('success');
27 |
28 | $failedCommit = new Commit($project, '123456');
29 | $failedCommit->setAuthor('Fabien');
30 | $failedCommit->setMessage('Foo');
31 | $failedCommit->setStatusCode('failed');
32 |
33 | $project->setCommits(array(
34 | $failedCommit,
35 | $successCommit,
36 | ));
37 |
38 | $logger = $this->getMock('Psr\Log\NullLogger');
39 | $logger->expects($this->once())->method('info');
40 | $logger->expects($this->once())->method('critical');
41 |
42 | $notifier = new LoggerNotifier($logger);
43 |
44 | //notify success commit
45 | $notifier->notify($successCommit);
46 | //notify failed commit
47 | $notifier->notify($failedCommit);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/Sismo/Notifier/Notifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Notifier;
13 |
14 | use Sismo\Commit;
15 |
16 | /**
17 | * Base class for notifiers.
18 | *
19 | * @author Fabien Potencier
20 | */
21 | abstract class Notifier
22 | {
23 | /**
24 | * Notifies a commit.
25 | *
26 | * @param Commit $commit A Commit instance
27 | */
28 | abstract public function notify(Commit $commit);
29 |
30 | protected function format($format, Commit $commit)
31 | {
32 | return strtr($format, $this->getPlaceholders($commit));
33 | }
34 |
35 | protected function getPlaceholders(Commit $commit)
36 | {
37 | $project = $commit->getProject();
38 |
39 | return array(
40 | '%slug%' => $project->getSlug(),
41 | '%name%' => $project->getName(),
42 | '%status%' => $commit->getStatus(),
43 | '%status_code%' => $commit->getStatusCode(),
44 | '%STATUS%' => strtoupper($commit->getStatus()),
45 | '%sha%' => $commit->getSha(),
46 | '%short_sha%' => $commit->getShortSha(),
47 | '%author%' => $commit->getAuthor(),
48 | '%message%' => $commit->getMessage(),
49 | '%output%' => $commit->getOutput(),
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/JoliNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Joli\JoliNotif\Notification;
15 | use Joli\JoliNotif\NotifierFactory;
16 | use Sismo\Commit;
17 | use Sismo\Notifier\Notifier;
18 |
19 | // @codeCoverageIgnoreStart
20 | /**
21 | * Notifies builds via the best notifier available on the system (Mac, Linux or Windows).
22 | *
23 | * @author Loïck Piera
24 | */
25 | class JoliNotifier extends Notifier
26 | {
27 | private $format;
28 | private $notifier;
29 |
30 | public function __construct($format = "[%STATUS%]\n%message%\n%author%")
31 | {
32 | if (!class_exists('Joli\JoliNotif\NotifierFactory')) {
33 | throw new \RuntimeException('This notifier requires the package jolicode/jolinotif to be installed');
34 | }
35 |
36 | $this->format = $format;
37 | $this->notifier = NotifierFactory::create();
38 | }
39 |
40 | public function notify(Commit $commit)
41 | {
42 | if (!$this->notifier) {
43 | return;
44 | }
45 |
46 | $notification = new Notification();
47 | $notification->setTitle($commit->getProject()->getName());
48 | $notification->setBody($this->format($this->format, $commit));
49 |
50 | $this->notifier->send($notification);
51 | }
52 | }
53 | // @codeCoverageIgnoreEnd
54 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/KDENotifier.php:
--------------------------------------------------------------------------------
1 |
21 | */
22 | class KDENotifier extends Notifier
23 | {
24 | protected $titleFormat;
25 | protected $messageFormat;
26 |
27 | public function __construct($titleFormat = '', $messageFormat = '')
28 | {
29 | $this->titleFormat = $titleFormat;
30 | $this->messageFormat = $messageFormat;
31 | }
32 |
33 | public function notify(Commit $commit)
34 | {
35 | // first, try with the kdialog program
36 | $process = new Process(sprintf('kdialog --title "%s" --passivepopup "%s" 5', $this->format($this->titleFormat, $commit), $this->format($this->messageFormat, $commit)));
37 | $process->setTimeout(2);
38 | $process->run();
39 | if ($process->isSuccessful()) {
40 | return;
41 | }
42 |
43 | // then, try knotify
44 | $process = new Process(sprintf('dcop knotify default notify eventname "%s" "%s" "" "" 16 2 ', $this->format($this->titleFormat, $commit), $this->format($this->messageFormat, $commit)));
45 | $process->setTimeout(2);
46 | $process->run();
47 | if ($process->isSuccessful()) {
48 | return;
49 | }
50 | }
51 | }
52 | // @codeCoverageIgnoreEnd
53 |
--------------------------------------------------------------------------------
/src/Sismo/Notifier/DBusNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Notifier;
13 |
14 | use Symfony\Component\Process\Process;
15 | use Sismo\Commit;
16 |
17 | // @codeCoverageIgnoreStart
18 | /**
19 | * Notifies builds via DBus.
20 | *
21 | * @author Fabien Potencier
22 | */
23 | class DBusNotifier extends Notifier
24 | {
25 | public function __construct($format = "[%STATUS%]\n%message%\n%author%")
26 | {
27 | $this->format = $format;
28 | }
29 |
30 | public function notify(Commit $commit)
31 | {
32 | // first, try with the notify-send program
33 | $process = new Process(sprintf('notify-send "%s" "%s"', $commit->getProject()->getName(), $this->format($this->format, $commit)));
34 | $process->setTimeout(2);
35 | $process->run();
36 | if ($process->isSuccessful()) {
37 | return;
38 | }
39 |
40 | // then, try dbus-send?
41 | $process = new Process(sprintf('dbus-send --print-reply --dest=org.freedesktop.Notifications /org/freedesktop/Notifications org.freedesktop.Notifications.Notify string:"sismo" int32:0 string:"" string:"%s" string:"%s" array:string:"" dict:string:"" int32:-1', $commit->getProject()->getName(), $this->format($this->format, $commit)));
42 | $process->setTimeout(2);
43 | $process->run();
44 | if ($process->isSuccessful()) {
45 | return;
46 | }
47 | }
48 | }
49 | // @codeCoverageIgnoreEnd
50 |
--------------------------------------------------------------------------------
/src/Sismo/GithubProject.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | use Symfony\Component\Process\Process;
15 |
16 | /**
17 | * Describes a project hosted on Github.
18 | *
19 | * @author Fabien Potencier
20 | */
21 | class GithubProject extends Project
22 | {
23 | public function setRepository($url)
24 | {
25 | parent::setRepository($url);
26 |
27 | if (file_exists($this->getRepository())) {
28 | $process = new Process('git remote -v', $this->getRepository());
29 | $process->run();
30 | foreach (explode("\n", $process->getOutput()) as $line) {
31 | $parts = explode("\t", $line);
32 | if ('origin' == $parts[0] && preg_match('#(?:\:|/|@)github.com(?:\:|/)(.*?)/(.*?)\.git#', $parts[1], $matches)) {
33 | $this->setUrlPattern(sprintf('https://github.com/%s/%s/commit/%%commit%%', $matches[1], $matches[2]));
34 |
35 | break;
36 | }
37 | }
38 | } elseif (preg_match('#^[a-z0-9_-]+/[a-z0-9_-]+$#i', $this->getRepository())) {
39 | $this->setUrlPattern(sprintf('https://github.com/%s/commit/%%commit%%', $this->getRepository()));
40 | parent::setRepository(sprintf('https://github.com/%s.git', $this->getRepository()));
41 | } else {
42 | throw new \InvalidArgumentException(sprintf('URL "%s" does not look like a Github repository.', $this->getRepository()));
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/LoggerNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Psr\Log\LoggerInterface;
15 | use Sismo\Commit;
16 | use Sismo\Notifier\Notifier;
17 |
18 | /**
19 | * Notifies builds via Monolog or any other PSR logger.
20 | * Logger must have info log level.
21 | *
22 | * Here is a usage example of Monolog with Slack handler:
23 | *
24 | * $message = <<
43 | */
44 | class LoggerNotifier extends Notifier
45 | {
46 | protected $logger;
47 | protected $messageFormat;
48 |
49 | /**
50 | * Constructor.
51 | *
52 | * @param LoggerInterface $logger
53 | * @param string $messageFormat
54 | */
55 | public function __construct(LoggerInterface $logger, $messageFormat = '')
56 | {
57 | $this->logger = $logger;
58 | $this->messageFormat = $messageFormat;
59 | }
60 |
61 | /**
62 | * @inherit
63 | */
64 | public function notify(Commit $commit)
65 | {
66 | $message = $this->format($this->messageFormat, $commit);
67 |
68 | return ($commit->getStatusCode() == 'success') ? $this->logger->info($message) : $this->logger->critical($message);
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Sismo/BitbucketProject.php:
--------------------------------------------------------------------------------
1 |
6 | *
7 | * This source file is subject to the MIT license that is bundled
8 | * with this source code in the file LICENSE.
9 | */
10 |
11 | namespace Sismo;
12 |
13 | use Symfony\Component\Process\Process;
14 |
15 | /**
16 | * Describes a project hosted on BitBucket.
17 | *
18 | * @author Micah Breedlove
19 | * @author Fabien Potencier
20 | */
21 | class BitbucketProject extends Project
22 | {
23 | public function setRepository($url)
24 | {
25 | parent::setRepository($url);
26 |
27 | if (file_exists($this->getRepository())) {
28 | $process = new Process('git remote -v', $this->getRepository());
29 | $process->run();
30 | foreach (explode("\n", $process->getOutput()) as $line) {
31 | $parts = explode("\t", $line);
32 | if ('origin' == $parts[0] && preg_match('#(?:\:|/|@)bitbucket.org(?:\:|/)(.*?)/(.*?)\.git#', $parts[1], $matches)) {
33 | $this->setUrlPattern(sprintf('https://bitbucket.org/%s/%s/commits/%%commit%%', $matches[1], $matches[2]));
34 |
35 | break;
36 | }
37 | }
38 | } elseif (preg_match('#^[a-z0-9_.-]+/[a-z0-9_.-]+$#i', $this->getRepository())) {
39 | $repo = preg_split('/\//', $this->getRepository());
40 |
41 | $this->setUrlPattern(sprintf('https://bitbucket.org/%s/commits/%%commit%%', $this->getRepository()));
42 | $this->setBitBucketRepository(sprintf('git@bitbucket.org:/%s.git', $this->getRepository()));
43 | } else {
44 | throw new \InvalidArgumentException(sprintf('URL "%s" does not look like a BitBucket repository.', $this->getRepository()));
45 | }
46 | }
47 |
48 | public function setBitBucketRepository($url)
49 | {
50 | $this->repository = $url;
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/XmppNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Sismo\Notifier\Notifier;
15 | use Sismo\Commit;
16 |
17 | // @codeCoverageIgnoreStart
18 | /**
19 | * Notifies builds via a XMPP server.
20 | *
21 | * This notifier needs the XMPPHP library to be required in your configuration.
22 | *
23 | * require '/path/to/XMPPHP/XMPP.php';
24 | *
25 | * Download it at http://code.google.com/p/xmpphp
26 | *
27 | * @author Fabien Potencier
28 | */
29 | class XmppNotifier extends Notifier
30 | {
31 | private $format;
32 | private $host;
33 | private $port;
34 | private $username;
35 | private $password;
36 | private $server;
37 | private $recipient;
38 |
39 | public function __construct($host, $port, $server, $username, $password, $recipient, $format = '[%STATUS%] %name% %short_sha% -- %message% by %author%')
40 | {
41 | $this->host = $host;
42 | $this->port = $port;
43 | $this->server = $server;
44 | $this->username = $username;
45 | $this->password = $password;
46 | $this->recipient = $recipient;
47 | $this->format = $format;
48 | }
49 |
50 | public function notify(Commit $commit)
51 | {
52 | $old = error_reporting(0);
53 | $conn = new \XMPPHP_XMPP($this->host, $this->port, $this->username, $this->password, 'sismo', $this->server);
54 | $conn->connect();
55 | $conn->processUntil('session_start');
56 | $conn->presence();
57 | foreach (explode(',', $this->recipient) as $user) {
58 | $conn->message($user, $this->format($this->format, $commit));
59 | }
60 | $conn->disconnect();
61 | error_reporting($old);
62 | }
63 | }
64 | // @codeCoverageIgnoreEnd
65 |
--------------------------------------------------------------------------------
/src/templates/project.twig:
--------------------------------------------------------------------------------
1 | {% extends 'layout.twig' %}
2 |
3 | {% from _self import commit_block %}
4 |
5 | {% block title 'Project ' ~ project %}
6 |
7 | {% block content %}
8 |
9 |
10 |
16 |
17 | {% if not commit %}
18 | Never built yet.
19 | {% else %}
20 | {{ commit_block(commit) }}
21 |
22 | {% if commit.isbuilt %}
23 |
24 |
{{ commit.output ? ansi_to_html.convert(commit.output)|raw : 'No output' }}
25 |
26 | {% endif %}
27 |
28 | {% if commits is defined and commits %}
29 | Builds History
30 |
31 | {% for commit in commits %}
32 | - {{ commit_block(commit) }}
33 | {% endfor %}
34 |
35 | {% endif %}
36 | {% endif %}
37 | {% endblock %}
38 |
39 | {% macro commit_block(commit) %}
40 | {{ _self.commit_link(commit) }}
41 |
42 | {{ commit.message }}
43 | ¶
44 |
45 |
46 | by {{ commit.author }} on {{ commit.date|date('j M Y H:i') }}
47 |
48 | {% endmacro %}
49 |
50 | {% macro commit_link(commit) %}
51 | {% if not commit.project.urlpattern %}
52 | #{{ commit.shortsha }} {{ commit.status }}
53 | {% else %}
54 | #{{ commit.shortsha }} {{ commit.status }}
55 | {% endif %}
56 | {% endmacro %}
57 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/TwitterNotifier.php:
--------------------------------------------------------------------------------
1 |
37 | */
38 | class TwitterNotifier extends Notifier
39 | {
40 | protected $consumerKey;
41 | protected $consumerSecret;
42 | protected $accessToken;
43 | protected $accessTokenSecret;
44 | protected $messageFormat;
45 |
46 | public function __construct($consumerKey, $consumerSecret, $accessToken, $accessTokenSecret, $messageFormat = "[%STATUS%]\n%message%\n%author%")
47 | {
48 | $this->consumerKey = $consumerKey;
49 | $this->consumerSecret = $consumerSecret;
50 | $this->accessToken = $accessToken;
51 | $this->accessTokenSecret = $accessTokenSecret;
52 | $this->messageFormat = $messageFormat;
53 | }
54 |
55 | public function notify(Commit $commit)
56 | {
57 | $conn = new \TwitterOAuth($this->consumerKey, $this->consumerSecret, $this->accessToken, $this->accessTokenSecret);
58 | $content = $conn->get('account/verify_credentials');
59 | $conn->post('statuses/update', array('status' => $this->format($this->messageFormat, $commit)));
60 | }
61 | }
62 | // @codeCoverageIgnoreEnd
63 |
--------------------------------------------------------------------------------
/src/Sismo/Storage/StorageInterface.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Storage;
13 |
14 | use Sismo\Project;
15 | use Sismo\Commit;
16 |
17 | /**
18 | * Stores projects and builds information.
19 | *
20 | * @author Toni Uebernickel
21 | */
22 | interface StorageInterface
23 | {
24 | /**
25 | * Retrieves a commit out of a project.
26 | *
27 | * @throws \RuntimeException
28 | *
29 | * @param Project $project The project this commit is part of.
30 | * @param string $sha The hash of the commit to retrieve.
31 | *
32 | * @return Commit
33 | */
34 | public function getCommit(Project $project, $sha);
35 |
36 | /**
37 | * Initiate, create and save a new commit.
38 | *
39 | * @param Project $project The project of the new commit.
40 | * @param string $sha The hash of the commit.
41 | * @param string $author The name of the author of the new commit.
42 | * @param \DateTime $date The date the new commit was created originally (e.g. by external resources).
43 | * @param string $message The commit message.
44 | *
45 | * @return Commit The newly created commit.
46 | */
47 | public function initCommit(Project $project, $sha, $author, \DateTime $date, $message);
48 |
49 | /**
50 | * Create or update the information of a project.
51 | *
52 | * If the project is already available, the information of the existing project will be updated.
53 | *
54 | * @param Project $project The project to create or update.
55 | */
56 | public function updateProject(Project $project);
57 |
58 | /**
59 | * Update the commits information.
60 | *
61 | * The commit is identified by its sha hash.
62 | *
63 | * @param Commit $commit
64 | */
65 | public function updateCommit(Commit $commit);
66 |
67 | /**
68 | * Shutdown the storage and all of its external resources.
69 | */
70 | public function close();
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/Storage/PdoStorageTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests\Storage;
13 |
14 | use Sismo\Storage\PdoStorage;
15 |
16 | require_once __DIR__.'/StorageTest.php';
17 |
18 | class PdoStorageTest extends StorageTest
19 | {
20 | private $db;
21 | private $run = false;
22 |
23 | public function setUp()
24 | {
25 | // Skip checks once everything is fine.
26 | if (!$this->run) {
27 | if (!class_exists('\PDO')) {
28 | $this->markTestSkipped('PDO is not available.');
29 | }
30 |
31 | if (!in_array('sqlite', \PDO::getAvailableDrivers())) {
32 | $this->markTestSkipped('SQLite PDO driver is not available.');
33 | }
34 |
35 | $this->run = true;
36 | }
37 |
38 | $app = require __DIR__.'/../../../../src/app.php';
39 |
40 | // sqlite with file is tested by StorageTest, so we use memory here
41 | $this->db = new \PDO('sqlite::memory:');
42 | $this->db->exec($app['db.schema']);
43 | }
44 |
45 | public function testStaticCreate()
46 | {
47 | $this->assertInstanceOf('\Sismo\Storage\PdoStorage', PdoStorage::create('sqlite::memory:'));
48 | }
49 |
50 | public function testInitExistingCommit()
51 | {
52 | $project = $this->getProject();
53 |
54 | $storage = $this->getStorage();
55 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo');
56 | $this->assertInstanceOf('\Sismo\Commit', $commit);
57 |
58 | $commit2 = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo-amended');
59 | $this->assertInstanceOf('\Sismo\Commit', $commit2);
60 |
61 | $this->assertEquals('foo-amended', $storage->getCommit($project, '7d78d5')->getMessage());
62 | }
63 |
64 | public function tearDown()
65 | {
66 | unset($this->db);
67 | }
68 |
69 | protected function getStorage()
70 | {
71 | return new PdoStorage($this->db);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/CrossFingerNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Sismo\Commit;
15 | use Sismo\Notifier\Notifier;
16 |
17 | /**
18 | * A cross finger notifier.
19 | * Launch notification on an array of Notifier instance
20 | * only if the commit needs it.
21 | *
22 | * @author Tugdual Saunier
23 | */
24 | class CrossFingerNotifier extends Notifier
25 | {
26 | protected $notifiers;
27 |
28 | /**
29 | * Constructor
30 | *
31 | * @param array|Notifier $notifiers An array or a single Notifier instance
32 | */
33 | public function __construct($notifiers = array())
34 | {
35 | if (!is_array($notifiers)) {
36 | $notifiers = array($notifiers);
37 | }
38 |
39 | foreach ($notifiers as $notifier) {
40 | if (!$notifier instanceof Notifier) {
41 | throw new \InvalidArgumentException("Only Sismo\Notifier instance supported");
42 | }
43 |
44 | $this->notifiers[] = $notifier;
45 | }
46 | }
47 |
48 | /**
49 | * Notifies a commit.
50 | *
51 | * @param Commit $commit Then Commit instance
52 | *
53 | * @return bool whether notification has been sent or not
54 | */
55 | public function notify(Commit $commit)
56 | {
57 | if ($this->commitNeedNotification($commit)) {
58 | foreach ($this->notifiers as $notifier) {
59 | $notifier->notify($commit);
60 | }
61 |
62 | return true;
63 | }
64 |
65 | return false;
66 | }
67 |
68 | /**
69 | * Determines if a build needs to be notify
70 | * based on his status and his predecessor's one
71 | *
72 | * @param Commit $commit The commit to analyse
73 | *
74 | * @return bool whether the commit need notification or not
75 | */
76 | protected function commitNeedNotification(Commit $commit)
77 | {
78 | if (!$commit->isSuccessful()) {
79 | return true;
80 | }
81 |
82 | //getProject()->getLatestCommit() actually contains the previous build
83 | $previousCommit = $commit->getProject()->getLatestCommit();
84 |
85 | return !$previousCommit || $previousCommit->getStatusCode() != $commit->getStatusCode();
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/GithubProjectTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\GithubProject;
15 | use Symfony\Component\Filesystem\Filesystem;
16 | use Symfony\Component\Process\Process;
17 |
18 | class GithubProjectTest extends \PHPUnit_Framework_TestCase
19 | {
20 | public function testSetRepository()
21 | {
22 | $project = new GithubProject('Twig', 'twigphp/Twig');
23 | $this->assertEquals('master', $project->getBranch());
24 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository());
25 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern());
26 |
27 | $project = new GithubProject('Twig', 'twigphp/Twig@foo');
28 | $this->assertEquals('foo', $project->getBranch());
29 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository());
30 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern());
31 | }
32 |
33 | public function localRepositoryProvider()
34 | {
35 | return array(
36 | array('https://github.com/twigphp/Twig.git'),
37 | array('git@github.com:twigphp/Twig.git'),
38 | );
39 | }
40 |
41 | /**
42 | * @dataProvider localRepositoryProvider
43 | */
44 | public function testSetRepositoryLocal($url)
45 | {
46 | $fs = new Filesystem();
47 | $repository = sys_get_temp_dir().'/sismo/twigphp/Twig';
48 | $fs->remove($repository);
49 | $fs->mkdir($repository);
50 |
51 | $process = new Process('git init && git remote add origin '.$url, $repository);
52 | $process->run();
53 |
54 | $project = new GithubProject('Twig', $repository);
55 | $this->assertEquals('master', $project->getBranch());
56 | $this->assertEquals($repository, $project->getRepository());
57 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern());
58 |
59 | $fs->remove($repository);
60 | }
61 |
62 | /**
63 | * @expectedException \InvalidArgumentException
64 | */
65 | public function testSetRepositoryThrowsAnExceptionIfRepositoryIsNotAGithubOne()
66 | {
67 | $project = new GithubProject('Twig', 'twigphp/Twig/foobar');
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/controllers.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | use Symfony\Component\HttpFoundation\Response;
13 | use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
14 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
15 |
16 | $app->get('/', function () use ($app) {
17 | return $app['twig']->render('projects.twig', array('projects' => $app['sismo']->getProjects()));
18 | })->bind('projects');
19 |
20 | $app->get('/{slug}', function ($slug) use ($app) {
21 | if (!$app['sismo']->hasProject($slug)) {
22 | throw new NotFoundHttpException(sprintf('Project "%s" not found.', $slug));
23 | }
24 |
25 | $project = $app['sismo']->getProject($slug);
26 | $commits = $project->getCommits();
27 | $latest = array_shift($commits);
28 |
29 | return $app['twig']->render('project.twig', array(
30 | 'project' => $project,
31 | 'commit' => $latest,
32 | 'commits' => $commits,
33 | ));
34 | })->bind('project');
35 |
36 | $app->get('/dashboard/cctray.xml', function () use ($app) {
37 | $content = $app['twig']->render('ccmonitor.twig.xml', array('projects' => $app['sismo']->getProjects()));
38 |
39 | return new Response($content, 200, array('content-type' => 'text/xml'));
40 | })->bind('ccmonitor');
41 |
42 | $app->get('/{slug}/{sha}', function ($slug, $sha) use ($app) {
43 | if (!$app['sismo']->hasProject($slug)) {
44 | throw new NotFoundHttpException(sprintf('Project "%s" not found.', $slug));
45 | }
46 |
47 | $project = $app['sismo']->getProject($slug);
48 |
49 | if (!$commit = $app['storage']->getCommit($project, $sha)) {
50 | throw new NotFoundHttpException(sprintf('Commit "%s" for project "%s" not found.', $sha, $slug));
51 | }
52 |
53 | return $app['twig']->render('project.twig', array(
54 | 'project' => $project,
55 | 'commit' => $commit,
56 | ));
57 | })->bind('commit');
58 |
59 | $app->post('/{slug}/build/{token}', function ($slug, $token) use ($app) {
60 | // Boot sismo
61 | $app['sismo'];
62 |
63 | if (!$server_token = $app['build.token']) {
64 | throw new NotFoundHttpException('Not found.');
65 | }
66 | if ($token != $server_token) {
67 | throw new AccessDeniedHttpException();
68 | }
69 | if (!$app['sismo']->hasProject($slug)) {
70 | throw new NotFoundHttpException(sprintf('Project "%s" not found.', $slug));
71 | }
72 |
73 | $project = $app['sismo']->getProject($slug);
74 | $app['sismo']->build($project);
75 |
76 | return sprintf('Triggered build for project "%s".', $slug);
77 | })->bind('build');
78 |
--------------------------------------------------------------------------------
/tests/appTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | use Symfony\Component\Filesystem\Filesystem;
13 | use Sismo\Project;
14 |
15 | class appTest extends \PHPUnit_Framework_TestCase
16 | {
17 | private $app;
18 |
19 | public function setUp()
20 | {
21 | $this->app = require __DIR__.'/../src/app.php';
22 |
23 | $this->baseDir = sys_get_temp_dir().'/sismo';
24 | $fs = new Filesystem();
25 | $fs->mkdir($this->baseDir);
26 | $this->app['data.path'] = $this->baseDir.'/db';
27 | $this->app['config.file'] = $this->baseDir.'/config.php';
28 |
29 | // This file does not exist, so app will use default sqlite storage.
30 | $app['config.storage.file'] = $this->baseDir.'/storage.php';
31 |
32 | @unlink($this->app['db.path']);
33 | file_put_contents($app['config.file'], 'app['storage']->close();
41 |
42 | $fs = new Filesystem();
43 | $fs->remove($this->baseDir);
44 | }
45 |
46 | public function testServices()
47 | {
48 | $this->assertInstanceOf('SQLite3', $this->app['db']);
49 | $this->assertInstanceOf('Sismo\Storage\StorageInterface', $this->app['storage']);
50 | $this->assertInstanceOf('Sismo\Builder', $this->app['builder']);
51 | $this->assertInstanceOf('Sismo\Sismo', $this->app['sismo']);
52 | }
53 |
54 | public function testMissingGit()
55 | {
56 | $this->app['git.path'] = 'gitinvalidcommand';
57 |
58 | $this->setExpectedException('\RuntimeException');
59 | $builder = $this->app['builder']->init(new Project('foo'), null);
60 | }
61 |
62 | public function testMissingConfigFile()
63 | {
64 | $this->app['config.file'] = $this->baseDir.'/missing-config.php';
65 |
66 | $this->setExpectedException('\RuntimeException');
67 | $sismo = $this->app['sismo'];
68 | }
69 |
70 | public function invalidConfigProvider()
71 | {
72 | return array(
73 | array('app['config.file'], $config);
85 |
86 | $this->setExpectedException('\RuntimeException');
87 | $sismo = $this->app['sismo'];
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/Sismo/Notifier/MailNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Notifier;
13 |
14 | use Sismo\Commit;
15 |
16 | // @codeCoverageIgnoreStart
17 | /**
18 | * A base email notifier using the native mail() function.
19 | *
20 | * Here is a usage example:
21 | *
22 | * $subject = '[%status_code%] %name% (%short_sha%)';
23 | * $message = <<
39 | */
40 | class MailNotifier extends Notifier
41 | {
42 | protected $recipients;
43 | protected $subjectFormat;
44 | protected $messageFormat;
45 | protected $headers;
46 | protected $params;
47 |
48 | /**
49 | * Constructor.
50 | *
51 | * @param array|string $recipients
52 | * @param string $subjectFormat
53 | * @param string $messageFormat
54 | * @param string $headers Additional headers applied to the email.
55 | * @param string $params Additional params to be used on mail()
56 | */
57 | public function __construct($recipients, $subjectFormat = '', $messageFormat = '', $headers = '', $params = '')
58 | {
59 | $this->recipients = $recipients;
60 | $this->subjectFormat = $subjectFormat;
61 | $this->messageFormat = $messageFormat;
62 | $this->headers = $headers;
63 | $this->params = $params;
64 | }
65 |
66 | public function notify(Commit $commit)
67 | {
68 | $subject = $this->format($this->subjectFormat, $commit);
69 | $message = $this->format($this->messageFormat, $commit);
70 |
71 | return $this->sendEmail($this->recipients, $subject, $message, $this->headers, $this->params);
72 | }
73 |
74 | /**
75 | * Send the email.
76 | *
77 | * @param array|string $to
78 | * @param string $subject
79 | * @param string $message
80 | * @param string $headers Additional headers to send.
81 | * @param string $params Additional params for the mailer in use.
82 | *
83 | * @return bool Whether the mail has been sent.
84 | */
85 | protected function sendEmail($to, $subject, $message, $headers = '', $params = '')
86 | {
87 | if (is_array($to)) {
88 | $to = implode(',', $to);
89 | }
90 |
91 | return mail($to, $subject, $message, $headers, $params);
92 | }
93 | }
94 | // @codeCoverageIgnoreEnd
95 |
--------------------------------------------------------------------------------
/src/Sismo/Notifier/GrowlNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Notifier;
13 |
14 | use Sismo\Commit;
15 |
16 | // @codeCoverageIgnoreStart
17 | /**
18 | * Notifies builds via a Growl (Mac only).
19 | *
20 | * @author Fabien Potencier
21 | */
22 | class GrowlNotifier extends Notifier
23 | {
24 | private $application;
25 | private $address;
26 | private $notifications;
27 | private $password;
28 | private $port;
29 | private $registered;
30 | private $format;
31 |
32 | public function __construct($password, $application = 'sismo', $address = 'localhost', $format = "[%STATUS%]\n%message%\n%author%", $port = 9887)
33 | {
34 | $this->application = $application;
35 | $this->address = $address;
36 | $this->password = $password;
37 | $this->format = $format;
38 | $this->port = $port;
39 | $this->registered = false;
40 | $this->notifications = array(
41 | array('name' => 'Success', 'enabled' => true),
42 | array('name' => 'Fail', 'enabled' => true),
43 | );
44 | }
45 |
46 | public function notify(Commit $commit)
47 | {
48 | $this->register();
49 |
50 | return $this->doNotify($commit->isSuccessful() ? 'Success' : 'Fail', $commit->getProject()->getName(), $this->format($this->format, $commit));
51 | }
52 |
53 | private function register()
54 | {
55 | if (true === $this->registered) {
56 | return;
57 | }
58 |
59 | $this->registered = true;
60 | $data = '';
61 | $defaults = '';
62 | $nbDefaults = 0;
63 | foreach ($this->notifications as $i => $notification) {
64 | $data .= pack('n', strlen($notification['name'])).$notification['name'];
65 | if ($notification['enabled']) {
66 | $defaults .= pack('c', $i);
67 | ++$nbDefaults;
68 | }
69 | }
70 |
71 | // pack(Protocol version, type, app name, number of notifications to register)
72 | $data = pack('c2nc2', 1, 0, strlen($this->application), count($this->notifications), $nbDefaults).$this->application.$data.$defaults;
73 |
74 | $this->send($data);
75 | }
76 |
77 | private function doNotify($name, $title, $message)
78 | {
79 | // pack(protocol version, type, priority/sticky flags, notification name length, title length, message length. app name length)
80 | $data = pack('c2n5', 1, 1, 0, strlen($name), strlen($title), strlen($message), strlen($this->application)).$name.$title.$message.$this->application;
81 |
82 | $this->send($data);
83 | }
84 |
85 | private function send($data)
86 | {
87 | $data .= pack('H32', md5($data.$this->password));
88 |
89 | $fp = fsockopen('udp://'.$this->address, $this->port);
90 | fwrite($fp, $data);
91 | fclose($fp);
92 | }
93 | }
94 | // @codeCoverageIgnoreEnd
95 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/BitbucketProjectTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\BitbucketProject;
15 | use Symfony\Component\Filesystem\Filesystem;
16 | use Symfony\Component\Process\Process;
17 |
18 | class BitbucketProjectTest extends \PHPUnit_Framework_TestCase
19 | {
20 | /**
21 | * @dataProvider getRepositoryProvider
22 | */
23 | public function testThatItSetRepository($repositoryPath, $branch, $repository, $urlPattern)
24 | {
25 | $project = new BitbucketProject('Twig', $repositoryPath);
26 | $this->assertEquals($branch, $project->getBranch());
27 | $this->assertEquals($repository, $project->getRepository());
28 | $this->assertEquals($urlPattern, $project->getUrlPattern());
29 | }
30 |
31 | public function getRepositoryProvider()
32 | {
33 | return array(
34 | array('acme/Demo', 'master', 'git@bitbucket.org:/acme/Demo.git', 'https://bitbucket.org/acme/Demo/commits/%commit%'),
35 | array('acme/Demo@develop', 'develop', 'git@bitbucket.org:/acme/Demo.git', 'https://bitbucket.org/acme/Demo/commits/%commit%'),
36 | array('acme/no.no1', 'master', 'git@bitbucket.org:/acme/no.no1.git', 'https://bitbucket.org/acme/no.no1/commits/%commit%'),
37 | array('no.no/acme', 'master', 'git@bitbucket.org:/no.no/acme.git', 'https://bitbucket.org/no.no/acme/commits/%commit%'),
38 | array('acme/no_no1', 'master', 'git@bitbucket.org:/acme/no_no1.git', 'https://bitbucket.org/acme/no_no1/commits/%commit%'),
39 | array('acme/no-no1', 'master', 'git@bitbucket.org:/acme/no-no1.git', 'https://bitbucket.org/acme/no-no1/commits/%commit%'),
40 | );
41 | }
42 |
43 | /**
44 | * @expectedException \InvalidArgumentException
45 | */
46 | public function testSetRepositoryThrowsAnExceptionIfRepositoryIsNotAGithubOne()
47 | {
48 | $project = new BitbucketProject('Twig', 'twigphp/Twig/foobar');
49 | }
50 |
51 | public function localRepositoryProvider()
52 | {
53 | return array(
54 | array('https://bitbucket.org/atlassian/stash-example-plugin.git'),
55 | array('git@bitbucket.org:atlassian/stash-example-plugin.git'),
56 | );
57 | }
58 |
59 | /**
60 | * @dataProvider localRepositoryProvider
61 | */
62 | public function testSetRepositoryLocal($url)
63 | {
64 | $fs = new Filesystem();
65 | $repository = sys_get_temp_dir().'/sismo/atlassian/stash-example-plugin';
66 | $fs->remove($repository);
67 | $fs->mkdir($repository);
68 |
69 | $process = new Process('git init && git remote add origin '.$url, $repository);
70 | $process->run();
71 |
72 | $project = new BitbucketProject('Stash', $repository);
73 | $this->assertEquals('master', $project->getBranch());
74 | $this->assertEquals($repository, $project->getRepository());
75 | $this->assertEquals('https://bitbucket.org/atlassian/stash-example-plugin/commits/%commit%', $project->getUrlPattern());
76 |
77 | $fs->remove($repository);
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/Contrib/CrossFingerNotifierTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests\Contrib;
13 |
14 | use Sismo\Notifier\Notifier;
15 | use Sismo\Contrib\CrossFingerNotifier;
16 | use Sismo\Commit;
17 | use Sismo\Project;
18 |
19 | class CrossFingerNotifierTest extends \PHPUnit_Framework_TestCase
20 | {
21 | /**
22 | * @expectedException InvalidArgumentException
23 | */
24 | public function testConstruct()
25 | {
26 | $notifier = new CrossFingerNotifier(new \stdClass());
27 | }
28 |
29 | public function testCommitNeedNotification()
30 | {
31 | $notifier = $this->getMock('Sismo\Contrib\CrossFingerNotifier');
32 | $r = new \ReflectionObject($notifier);
33 | $m = $r->getMethod('commitNeedNotification');
34 | $m->setAccessible(true);
35 |
36 | $project = new Project('Twig');
37 | $commit = new Commit($project, '123456');
38 | $commit->setAuthor('Fabien');
39 | $commit->setMessage('Foo');
40 |
41 | $commit2 = new Commit($project, '123455');
42 | $commit2->setAuthor('Fabien');
43 | $commit2->setMessage('Bar');
44 | $commit2->setStatusCode('success');
45 |
46 | $commit3 = clone $commit2;
47 |
48 | //a failed commit should be notified
49 | $this->assertTrue($m->invoke($notifier, $commit));
50 |
51 | //a successful commit without predecessor should be notified
52 | $this->assertTrue($m->invoke($notifier, $commit2));
53 |
54 | $project->setCommits(array(
55 | $commit3,
56 | ));
57 | //a successful commit with a successful predecessor should NOT be notified
58 | $this->assertFalse($m->invoke($notifier, $commit2));
59 |
60 | $project->setCommits(array(
61 | $commit2,
62 | $commit3,
63 | ));
64 | //a failed commit with a successful predecessor should be notified
65 | $this->assertTrue($m->invoke($notifier, $commit));
66 | }
67 |
68 | public function testNotify()
69 | {
70 | $project = new Project('Twig');
71 | $failedCommit = new Commit($project, '123456');
72 | $failedCommit->setAuthor('Fabien');
73 | $failedCommit->setMessage('Foo');
74 |
75 | $successCommit = new Commit($project, '123455');
76 | $successCommit->setAuthor('Fabien');
77 | $successCommit->setMessage('Bar');
78 | $successCommit->setStatusCode('success');
79 |
80 | $baseNotifier = $this->getMock('Sismo\Notifier\Notifier');
81 | $baseNotifier->expects($this->once())
82 | ->method('notify')
83 | ->will($this->returnValue('foo'));
84 |
85 | $notifier = new CrossFingerNotifier(array($baseNotifier));
86 |
87 | //a failed commit should call notify on real notifier
88 | $this->assertTrue($notifier->notify($failedCommit));
89 |
90 | $project->setCommits(array(
91 | $successCommit,
92 | ));
93 | //a success commit should not call notify on real notifier
94 | $this->assertFalse($notifier->notify($successCommit));
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/IrcNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Sismo\Notifier\Notifier;
15 | use Sismo\Commit;
16 |
17 | /**
18 | * Delivers a connection via socket to the IRC server.
19 | * Extends Sismo\Notifier\Notifier for notifying users via Irc
20 | *
21 | * @package IRCBot
22 | * @subpackage Library
23 | * @author Daniel Siepmann
24 | * @author Brent Shaffer
25 | */
26 | class IrcNotifier extends Notifier
27 | {
28 | private $server;
29 | private $port;
30 | private $nick;
31 | private $channel;
32 | private $format;
33 |
34 | /**
35 | * Example:
36 | * // basic usage
37 | * $irc = new Sismo\Contrib\IrcNotifier('#mychannel');
38 | *
39 | * // more advanced usage
40 | * $irc = new Sismo\Contrib\IrcNotifier('#mychannel', 'sismo-bot', 'chat.mysite.com', '6668');
41 | */
42 | public function __construct($channel, $nick = 'Sismo', $server = 'irc.freenode.com', $port = 6667, $format = '[%STATUS%] %name% %short_sha% -- %message% by %author%')
43 | {
44 | $this->server = $server;
45 | $this->port = $port;
46 | $this->nick = $nick;
47 | $this->channel = $channel;
48 | $this->format = $format;
49 | }
50 |
51 | public function notify(Commit $commit)
52 | {
53 | $old = error_reporting(0);
54 | $this->connect();
55 | $channels = explode(',', $this->channel);
56 | $this->join($channels);
57 | foreach ($channels as $channel) {
58 | $this->say($channel, $this->format($this->format, $commit));
59 | }
60 | $this->disconnect();
61 | error_reporting($old);
62 | }
63 |
64 | private function say($channel, $message)
65 | {
66 | $this->sendData(sprintf('PRIVMSG %s :%s', $channel, $message));
67 | }
68 |
69 | private function connect()
70 | {
71 | $this->socket = fsockopen($this->server, $this->port);
72 | if (!$this->isConnected()) {
73 | throw new \RuntimeException('Unable to connect to server via fsockopen with server: "'.$this->server.'" and port: "'.$this->port.'".');
74 | }
75 | // USER username hostname servername :realname
76 | $this->sendData(sprintf('USER %s Sismo Sismo :%s', $this->nick, $this->nick));
77 | $this->sendData(sprintf('NICK %s', $this->nick));
78 | }
79 |
80 | private function disconnect()
81 | {
82 | if ($this->socket) {
83 | return fclose($this->socket);
84 | }
85 |
86 | return false;
87 | }
88 |
89 | private function sendData($data)
90 | {
91 | return fwrite($this->socket, $data."\r\n");
92 | }
93 |
94 | private function isConnected()
95 | {
96 | if (is_resource($this->socket)) {
97 | return true;
98 | }
99 |
100 | return false;
101 | }
102 |
103 | private function join($channel)
104 | {
105 | foreach ((array) $channel as $chan) {
106 | $this->sendData(sprintf('JOIN %s', $chan));
107 | }
108 | }
109 |
110 | public function __destruct()
111 | {
112 | $this->disconnect();
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/GrowlNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Sismo\Notifier\Notifier;
15 | use Sismo\Commit;
16 |
17 | require_once 'Net/Growl/Autoload.php';
18 |
19 | // @codeCoverageIgnoreStart
20 | /**
21 | * Notifies builds via a Growl (Mac or Windows, but not Linux)
22 | *
23 | * Requires PEAR::Net_Growl package
24 | *
25 | * @author Laurent Laville
26 | * @link http://growl.laurent-laville.org/
27 | * @link http://pear.php.net/package/Net_Growl
28 | */
29 | class GrowlNotifier extends Notifier
30 | {
31 | const NOTIFY_SUCCESS = 'Success';
32 | const NOTIFY_FAILURE = 'Fail';
33 |
34 | /**
35 | * Net_Growl instance
36 | * @var object
37 | */
38 | private $growl;
39 |
40 | /**
41 | * Message format with predefined place holders
42 | * @var string
43 | * @see Sismo\Notifier\Notifier::getPlaceholders() for known place holders
44 | */
45 | private $format;
46 |
47 | /**
48 | * Class constructor
49 | *
50 | * @param string $application (optional) Identify an application by a string
51 | * @param array $notifications (optional) Options to configure the
52 | * notification channels
53 | * @param string $password (optional) Password to protect your Growl client
54 | * for notification spamming
55 | * @param array $options (optional) Options to configure the Growl comm.
56 | * Choose either UDP or GNTP protocol,
57 | * host URL, and more ...
58 | */
59 | public function __construct($application = 'sismo', $notifications = array(),
60 | $password = '', $options = array()
61 | ) {
62 | $this->format = "[%STATUS%]\n%message%\n%author%";
63 |
64 | $notifications = array_merge(
65 | // default notifications (channels Success and Fail are enabled)
66 | array(
67 | self::NOTIFY_SUCCESS => array(),
68 | self::NOTIFY_FAILURE => array(),
69 | ),
70 | // custom notifications
71 | $notifications
72 | );
73 |
74 | $this->growl = \Net_Growl::singleton(
75 | $application, $notifications, $password, $options
76 | );
77 | }
78 |
79 | /**
80 | * Defines the new message format
81 | *
82 | * @param string $format The message format with predefined place holders
83 | *
84 | * @return $this
85 | * @see Sismo\Notifier\Notifier::getPlaceholders() for known place holders
86 | */
87 | public function setMessageFormat($format)
88 | {
89 | if (is_string($format) && !empty($format)) {
90 | $this->format = $format;
91 | }
92 |
93 | return $this;
94 | }
95 |
96 | /**
97 | * Notify a project commit
98 | *
99 | * @param Sismo\Commit $commit The latest project commit
100 | *
101 | * @return bool TRUE on a succesfull notification, FALSE on failure
102 | */
103 | public function notify(Commit $commit)
104 | {
105 | try {
106 | $this->growl->register();
107 |
108 | $name = $commit->isSuccessful()
109 | ? self::NOTIFY_SUCCESS : self::NOTIFY_FAILURE;
110 | $notifications = $this->growl->getApplication()->getGrowlNotifications();
111 |
112 | $this->growl->publish(
113 | $name,
114 | $commit->getProject()->getName(),
115 | $this->format($this->format, $commit),
116 | $notifications[$name]
117 | );
118 | } catch (\Net_Growl_Exception $e) {
119 | return false;
120 | }
121 |
122 | return true;
123 | }
124 | }
125 | // @codeCoverageIgnoreEnd
126 |
--------------------------------------------------------------------------------
/tests/consoleTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | use Sismo\Project;
13 | use Symfony\Component\Console\Tester\ApplicationTester;
14 | use Sismo\BuildException;
15 |
16 | class consoleTest extends \PHPUnit_Framework_TestCase
17 | {
18 | protected $app;
19 | protected $console;
20 | protected $baseDir;
21 |
22 | public function setUp()
23 | {
24 | $this->app = require __DIR__.'/../src/app.php';
25 |
26 | $this->app['sismo'] = $this->getMockBuilder('Sismo\Sismo')->disableOriginalConstructor()->getMock();
27 |
28 | $this->console = require __DIR__.'/../src/console.php';
29 | $this->console->setAutoExit(false);
30 | $this->console->setCatchExceptions(false);
31 | }
32 |
33 | public function testBuildForNonExistentProject()
34 | {
35 | $tester = new ApplicationTester($this->console);
36 |
37 | $this->assertEquals(1, $tester->run(array('command' => 'build', 'slug' => 'Twig')));
38 | $this->assertEquals('Project "Twig" does not exist.', trim($tester->getDisplay()));
39 | }
40 |
41 | public function testBuildForProject()
42 | {
43 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock();
44 |
45 | $this->app['sismo']->expects($this->once())->method('hasProject')->will($this->returnValue(true));
46 | $this->app['sismo']->expects($this->once())->method('getProject')->will($this->returnValue($project));
47 | $this->app['sismo']->expects($this->once())->method('build');
48 |
49 | $tester = new ApplicationTester($this->console);
50 |
51 | $this->assertEquals(0, $tester->run(array('command' => 'build', 'slug' => 'Twig')));
52 | $this->assertEquals('Building Project "" (into "d41d8c")', trim($tester->getDisplay()));
53 | }
54 |
55 | public function testBuildForProjects()
56 | {
57 | $project1 = $this->getMock('Sismo\Project', null, array('Twig'));
58 | $project2 = $this->getMock('Sismo\Project', null, array('Silex'));
59 |
60 | $this->app['sismo']->expects($this->once())->method('getProjects')->will($this->returnValue(array($project1, $project2)));
61 | $this->app['sismo']->expects($this->exactly(2))->method('build');
62 |
63 | $tester = new ApplicationTester($this->console);
64 |
65 | $this->assertEquals(0, $tester->run(array('command' => 'build')));
66 | $this->assertEquals('Building Project "Twig" (into "eb0a19")'.PHP_EOL.PHP_EOL.'Building Project "Silex" (into "eb0a19")', trim($tester->getDisplay()));
67 | }
68 |
69 | public function testBuildForProjectsWithBuildExceptions()
70 | {
71 | $project1 = $this->getMock('Sismo\Project', null, array('Twig'));
72 | $project2 = $this->getMock('Sismo\Project', null, array('Silex'));
73 |
74 | $this->app['sismo']->expects($this->once())->method('getProjects')->will($this->returnValue(array($project1, $project2)));
75 | $this->app['sismo']->expects($this->exactly(2))->method('build')->will($this->throwException(new BuildException()));
76 |
77 | $tester = new ApplicationTester($this->console);
78 | $this->assertEquals(1, $tester->run(array('command' => 'build')));
79 | }
80 |
81 | public function testVerboseBuildForProject()
82 | {
83 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock();
84 |
85 | $this->app['sismo']->expects($this->once())->method('hasProject')->will($this->returnValue(true));
86 | $this->app['sismo']->expects($this->once())->method('getProject')->will($this->returnValue($project));
87 | $this->app['sismo']->expects($this->once())->method('build');
88 |
89 | $tester = new ApplicationTester($this->console);
90 |
91 | $this->assertEquals(0, $tester->run(array('command' => 'build', 'slug' => 'Twig', '--verbose' => true)));
92 | $this->assertEquals('Building Project "" (into "d41d8c")', trim($tester->getDisplay()));
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/Storage/StorageTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests\Storage;
13 |
14 | use Sismo\Storage;
15 | use Sismo\Commit;
16 | use Sismo\Project;
17 |
18 | class StorageTest extends \PHPUnit_Framework_TestCase
19 | {
20 | /**
21 | * A SQLite3 reference.
22 | *
23 | * @var \SQLite3
24 | */
25 | private $db;
26 | private $path;
27 |
28 | public function setUp()
29 | {
30 | $app = require __DIR__.'/../../../../src/app.php';
31 |
32 | $this->path = sys_get_temp_dir().'/sismo.db';
33 | @unlink($this->path);
34 |
35 | $this->db = new \SQLite3($this->path);
36 | $this->db->busyTimeout(1000);
37 | $this->db->exec($app['db.schema']);
38 | }
39 |
40 | public function tearDown()
41 | {
42 | @unlink($this->path);
43 | }
44 |
45 | public function testInterfaceIsValid()
46 | {
47 | $project = $this->getProject();
48 |
49 | $storage = $this->getStorage();
50 |
51 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo');
52 | $this->assertInstanceOf('Sismo\Commit', $commit);
53 |
54 | $commit->setMessage('foo, amended with stuff');
55 | $storage->updateCommit($commit);
56 |
57 | $commit = $storage->getCommit($project, '7d78d5');
58 | $this->assertInstanceOf('Sismo\Commit', $commit);
59 | }
60 |
61 | public function testGetCommitReturnsFalseIfNotInDatabase()
62 | {
63 | $storage = $this->getStorage();
64 | $project = $this->getProject();
65 | $this->assertFalse($storage->getCommit($project, '7d78d5'));
66 | }
67 |
68 | public function testGetCommitReturnsCommitIfInDatabase()
69 | {
70 | $project = $this->getProject();
71 |
72 | $storage = $this->getStorage();
73 | $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo');
74 |
75 | $commit = $storage->getCommit($project, '7d78d5');
76 | $this->assertInstanceOf('Sismo\Commit', $commit);
77 | $this->assertEquals('7d78d5', $commit->getSha());
78 | }
79 |
80 | public function testUpdateCommit()
81 | {
82 | $project = $this->getProject();
83 |
84 | $storage = $this->getStorage();
85 | $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo');
86 |
87 | $commit = new Commit($project, '7d78d5');
88 | $commit->setOutput('foo');
89 | $commit->setStatusCode('success');
90 |
91 | $storage->updateCommit($commit);
92 |
93 | $this->assertEquals('foo', $commit->getOutput());
94 | $this->assertEquals('success', $commit->getStatusCode());
95 |
96 | $commit = $storage->getCommit($project, '7d78d5');
97 | $this->assertNotNull($commit->getBuildDate());
98 | }
99 |
100 | public function testUpdateProject()
101 | {
102 | $project = new Project('Twig');
103 |
104 | $storage = $this->getStorage();
105 |
106 | $storage->updateProject($project);
107 | $this->assertEquals(false, $project->isBuilding());
108 | $this->assertEquals(array(), $project->getCommits());
109 |
110 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo');
111 | $storage->updateProject($project);
112 | $this->assertEquals(true, $project->isBuilding());
113 | $this->assertEquals(array($commit), $project->getCommits());
114 | }
115 |
116 | protected function getProject()
117 | {
118 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock();
119 | $project->expects($this->any())->method('getSlug')->will($this->returnValue('twig'));
120 |
121 | return $project;
122 | }
123 |
124 | protected function getStorage()
125 | {
126 | return new Storage($this->db);
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/Sismo/Sismo.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | use Sismo\Storage\StorageInterface;
15 |
16 | /**
17 | * Main entry point for Sismo.
18 | *
19 | * @author Fabien Potencier
20 | */
21 | class Sismo
22 | {
23 | const VERSION = '1.0.0';
24 |
25 | const FORCE_BUILD = 1;
26 | const LOCAL_BUILD = 2;
27 | const SILENT_BUILD = 4;
28 |
29 | private $storage;
30 | private $builder;
31 | private $projects = array();
32 |
33 | /**
34 | * Constructor.
35 | *
36 | * @param StorageInterface $storage A StorageInterface instance
37 | * @param Builder $builder A Builder instance
38 | */
39 | public function __construct(StorageInterface $storage, Builder $builder)
40 | {
41 | $this->storage = $storage;
42 | $this->builder = $builder;
43 | }
44 |
45 | /**
46 | * Builds a project.
47 | *
48 | * @param Project $project A Project instance
49 | * @param string $revision The revision to build (or null for the latest revision)
50 | * @param integer $flags Flags (a combinaison of FORCE_BUILD, LOCAL_BUILD, and SILENT_BUILD)
51 | * @param mixed $callback A PHP callback
52 | */
53 | public function build(Project $project, $revision = null, $flags = 0, $callback = null)
54 | {
55 | // project already has a running build
56 | if ($project->isBuilding() && Sismo::FORCE_BUILD !== ($flags & Sismo::FORCE_BUILD)) {
57 | return;
58 | }
59 |
60 | $this->builder->init($project, $callback);
61 |
62 | list($sha, $author, $date, $message) = $this->builder->prepare($revision, Sismo::LOCAL_BUILD !== ($flags & Sismo::LOCAL_BUILD));
63 |
64 | $commit = $this->storage->getCommit($project, $sha);
65 |
66 | // commit has already been built
67 | if ($commit && $commit->isBuilt() && Sismo::FORCE_BUILD !== ($flags & Sismo::FORCE_BUILD)) {
68 | return;
69 | }
70 |
71 | $commit = $this->storage->initCommit($project, $sha, $author, \DateTime::createFromFormat('Y-m-d H:i:s O', $date), $message);
72 |
73 | $process = $this->builder->build();
74 |
75 | if (!$process->isSuccessful()) {
76 | $commit->setStatusCode('failed');
77 | $commit->setOutput(sprintf("\033[31mBuild failed\033[0m\n\n\033[33mOutput\033[0m\n%s\n\n\033[33m Error\033[0m%s", $process->getOutput(), $process->getErrorOutput()));
78 | } else {
79 | $commit->setStatusCode('success');
80 | $commit->setOutput($process->getOutput());
81 | }
82 |
83 | $this->storage->updateCommit($commit);
84 |
85 | if (Sismo::SILENT_BUILD !== ($flags & Sismo::SILENT_BUILD)) {
86 | foreach ($project->getNotifiers() as $notifier) {
87 | $notifier->notify($commit);
88 | }
89 | }
90 | }
91 |
92 | /**
93 | * Checks if Sismo knows about a given project.
94 | *
95 | * @param string $slug A project slug
96 | */
97 | public function hasProject($slug)
98 | {
99 | return isset($this->projects[$slug]);
100 | }
101 |
102 | /**
103 | * Gets a project.
104 | *
105 | * @param string $slug A project slug
106 | */
107 | public function getProject($slug)
108 | {
109 | if (!isset($this->projects[$slug])) {
110 | throw new \InvalidArgumentException(sprintf('Project "%s" does not exist.', $slug));
111 | }
112 |
113 | return $this->projects[$slug];
114 | }
115 |
116 | /**
117 | * Adds a project.
118 | *
119 | * @param Project $project A Project instance
120 | */
121 | public function addProject(Project $project)
122 | {
123 | $this->storage->updateProject($project);
124 |
125 | $this->projects[$project->getSlug()] = $project;
126 | }
127 |
128 | /**
129 | * Gets all projects.
130 | *
131 | * @return array An array of Project instances
132 | */
133 | public function getProjects()
134 | {
135 | return $this->projects;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/src/app.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | use Silex\Application;
13 | use Silex\Provider\TwigServiceProvider;
14 | use Silex\Provider\UrlGeneratorServiceProvider;
15 | use Sismo\Sismo;
16 | use Sismo\Project;
17 | use Sismo\Storage\Storage;
18 | use Sismo\Builder;
19 | use Symfony\Component\HttpFoundation\Response;
20 | use SensioLabs\AnsiConverter\AnsiToHtmlConverter;
21 |
22 | $app = new Application();
23 | $app->register(new UrlGeneratorServiceProvider());
24 | $app->register(new TwigServiceProvider(), array(
25 | 'twig.path' => __DIR__.'/templates',
26 | ));
27 | $app['twig'] = $app->share($app->extend('twig', function ($twig, $app) {
28 | $twig->setCache($app['twig.cache.path']);
29 | $twig->addGlobal('ansi_to_html', new AnsiToHtmlConverter());
30 |
31 | return $twig;
32 | }));
33 |
34 | $app['data.path'] = getenv('SISMO_DATA_PATH') ?: getenv('HOME').'/.sismo/data';
35 | $app['config.file'] = getenv('SISMO_CONFIG_PATH') ?: getenv('HOME').'/.sismo/config.php';
36 | $app['config.storage.file'] = getenv('SISMO_STORAGE_PATH') ?: getenv('HOME').'/.sismo/storage.php';
37 | $app['build.path'] = $app->share(function ($app) { return $app['data.path'].'/build'; });
38 | $app['db.path'] = $app->share(function ($app) {
39 | if (!is_dir($app['data.path'])) {
40 | mkdir($app['data.path'], 0777, true);
41 | }
42 |
43 | return $app['data.path'].'/sismo.db';
44 | });
45 | $app['build.token'] = getenv('SISMO_BUILD_TOKEN');
46 | $app['twig.cache.path'] = $app->share(function ($app) { return $app['data.path'].'/cache'; });
47 | $app['git.path'] = getenv('SISMO_GIT_PATH') ?: 'git';
48 | $app['git.cmds'] = array();
49 | $app['db.schema'] = <<share(function () use ($app) {
75 | $db = new \SQLite3($app['db.path']);
76 | $db->busyTimeout(1000);
77 | $db->exec($app['db.schema']);
78 |
79 | return $db;
80 | });
81 |
82 | $app['storage'] = $app->share(function () use ($app) {
83 | if (is_file($app['config.storage.file'])) {
84 | $storage = require $app['config.storage.file'];
85 | } else {
86 | $storage = new Storage($app['db']);
87 | }
88 |
89 | return $storage;
90 | });
91 |
92 | $app['builder'] = $app->share(function () use ($app) {
93 | return new Builder($app['build.path'], $app['git.path'], $app['git.cmds']);
94 | });
95 |
96 | $app['sismo'] = $app->share(function () use ($app) {
97 | $sismo = new Sismo($app['storage'], $app['builder']);
98 | if (!is_file($app['config.file'])) {
99 | throw new \RuntimeException(sprintf("Looks like you forgot to define your projects.\nSismo looked into \"%s\".", $app['config.file']));
100 | }
101 | $projects = require $app['config.file'];
102 |
103 | if (null === $projects) {
104 | throw new \RuntimeException(sprintf('The "%s" configuration file must return an array of Projects (returns null).', $app['config.file']));
105 | }
106 |
107 | if (!is_array($projects)) {
108 | throw new \RuntimeException(sprintf('The "%s" configuration file must return an array of Projects (returns a non-array).', $app['config.file']));
109 | }
110 |
111 | foreach ($projects as $project) {
112 | if (!$project instanceof Project) {
113 | throw new \RuntimeException(sprintf('The "%s" configuration file must return an array of Project instances.', $app['config.file']));
114 | }
115 |
116 | $sismo->addProject($project);
117 | }
118 |
119 | return $sismo;
120 | });
121 |
122 | $app->error(function (\Exception $e, $code) use ($app) {
123 | if ($app['debug']) {
124 | return;
125 | }
126 |
127 | $error = 404 == $code ? $e->getMessage() : null;
128 |
129 | return new Response($app['twig']->render('error.twig', array('error' => $error)), $code);
130 | });
131 |
132 | return $app;
133 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/CommitTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\Project;
15 | use Sismo\Commit;
16 |
17 | class CommitTest extends \PHPUnit_Framework_TestCase
18 | {
19 | public function testToString()
20 | {
21 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
22 | $this->assertEquals('Twig@7d78d5', (string) $commit);
23 | }
24 |
25 | public function testSha()
26 | {
27 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
28 | $this->assertEquals('7d78d5f7a8c039059046d6c5e1d7f66765bd91c7', $commit->getSha());
29 | $this->assertEquals('7d78d5', $commit->getShortSha());
30 | }
31 |
32 | public function testStatus()
33 | {
34 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
35 |
36 | $this->assertEquals('building', $commit->getStatusCode());
37 | $this->assertEquals('building', $commit->getStatus());
38 |
39 | $commit->setStatusCode('failed');
40 | $this->assertEquals('failed', $commit->getStatusCode());
41 | $this->assertEquals('failed', $commit->getStatus());
42 |
43 | $commit->setStatusCode('success');
44 | $this->assertEquals('success', $commit->getStatusCode());
45 | $this->assertEquals('succeeded', $commit->getStatus());
46 | }
47 |
48 | /**
49 | * @expectedException \InvalidArgumentException
50 | */
51 | public function testSetStatus()
52 | {
53 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
54 | $commit->setStatusCode('weird');
55 | }
56 |
57 | public function testIsBuilding()
58 | {
59 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
60 | $this->assertTrue($commit->isBuilding());
61 |
62 | $commit->setStatusCode('failed');
63 | $this->assertFalse($commit->isBuilding());
64 | }
65 |
66 | public function testIsBuilt()
67 | {
68 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
69 | $this->assertFalse($commit->isBuilt());
70 |
71 | $commit->setStatusCode('failed');
72 | $this->assertTrue($commit->isBuilt());
73 |
74 | $commit->setStatusCode('success');
75 | $this->assertTrue($commit->isBuilt());
76 | }
77 |
78 | public function testIsSuccessful()
79 | {
80 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
81 | $this->assertFalse($commit->isSuccessful());
82 |
83 | $commit->setStatusCode('failed');
84 | $this->assertFalse($commit->isSuccessful());
85 |
86 | $commit->setStatusCode('success');
87 | $this->assertTrue($commit->isSuccessful());
88 | }
89 |
90 | public function testOutput()
91 | {
92 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
93 | $commit->setOutput('foo');
94 | $this->assertEquals('foo', $commit->getOutput());
95 | }
96 |
97 | public function testMessage()
98 | {
99 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
100 | $commit->setMessage('foo');
101 | $this->assertEquals('foo', $commit->getMessage());
102 | }
103 |
104 | public function testProject()
105 | {
106 | $commit = new Commit($project = new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
107 | $this->assertEquals($project, $commit->getProject());
108 | }
109 |
110 | public function testAuthor()
111 | {
112 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
113 | $commit->setAuthor('foo');
114 | $this->assertEquals('foo', $commit->getAuthor());
115 | }
116 |
117 | public function testDate()
118 | {
119 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
120 | $commit->setDate($date = new \DateTime());
121 | $this->assertEquals($date, $commit->getDate());
122 | }
123 |
124 | public function testBuildDate()
125 | {
126 | $commit = new Commit(new Project('Twig'), '7d78d5f7a8c039059046d6c5e1d7f66765bd91c7');
127 | $commit->setBuildDate($date = new \DateTime());
128 | $this->assertEquals($date, $commit->getBuildDate());
129 | }
130 | }
131 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/AndroidPushC2DMNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Sismo\Commit;
15 |
16 | // @codeCoverageIgnoreStart
17 | /**
18 | * A base C2DM (Cloud 2 Device Messaging) function that will send a small push text message to an
19 | * Android device via Google service.
20 | *
21 | * Here is a usage example:
22 | *
23 | * $messageFormat = '[%status_code%] %name% (%short_sha%)';
24 | * $c2dmNotifier = new \Sismo\Notifier\AndroidPushC2DMNotifier('C2DM_PUSH_USER', 'C2DM_PUSH_PASSWORD',
25 | * 'DEVICE_REGISTRATION_ID', $messageFormat, 'SismoNotifier');
26 | *
27 | *
28 | * @author Michael Kliewe
29 | */
30 | class AndroidPushC2DMNotifier extends Notifier
31 | {
32 | protected $authCode;
33 | protected $deviceRegistrationId;
34 | protected $messageFormat;
35 | protected $msgType;
36 |
37 | /**
38 | * Constructor
39 | *
40 | * @param string $username
41 | * @param string $password
42 | * @param string $deviceRegistrationId
43 | * @param string $messageFormat
44 | * @param string $source
45 | * @param string $msgType
46 | * @param string $service
47 | */
48 | public function __construct($username, $password, $deviceRegistrationId, $messageFormat = '',
49 | $source = 'Company-AppName-Version', $msgType = 'SismoNotifierMsgType', $service = 'ac2dm')
50 | {
51 | $this->authCode = $this->getGoogleAuthCodeHelper($username, $password, $source, $service);
52 | $this->deviceRegistrationId = $deviceRegistrationId;
53 | $this->messageFormat = $messageFormat;
54 | $this->msgType = $msgType;
55 | }
56 |
57 | public function notify(Commit $commit)
58 | {
59 | $message = $this->format($this->messageFormat, $commit);
60 |
61 | $headers = array('Authorization: GoogleLogin auth='.$this->authCode);
62 | $data = array(
63 | 'registration_id' => $this->deviceRegistrationId,
64 | 'collapse_key' => $this->msgType,
65 | 'data.message' => $message,
66 | );
67 |
68 | $ch = curl_init();
69 |
70 | curl_setopt($ch, CURLOPT_URL, "https://android.apis.google.com/c2dm/send");
71 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
72 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
73 | curl_setopt($ch, CURLOPT_POST, true);
74 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
75 | curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
76 |
77 | $response = curl_exec($ch);
78 | curl_close($ch);
79 |
80 | // Check the response. If the exact error message is needed it can be parsed here
81 | $responseArray = preg_split('/=/', $response);
82 | if (!isset($responseArray[0]) || !isset($responseArray[1])) {
83 | return false;
84 | }
85 | if (strtolower($responseArray[0]) == 'error') {
86 | return false;
87 | }
88 |
89 | return true;
90 | }
91 |
92 | public function getGoogleAuthCodeHelper($username, $password, $source = 'Company-AppName-Version', $service = 'ac2dm')
93 | {
94 | $ch = curl_init();
95 | if (!$ch) {
96 | return false;
97 | }
98 |
99 | curl_setopt($ch, CURLOPT_URL, "https://www.google.com/accounts/ClientLogin");
100 | $postFields = "accountType=".urlencode('HOSTED_OR_GOOGLE')
101 | ."&Email=".urlencode($username)
102 | ."&Passwd=".urlencode($password)
103 | ."&source=".urlencode($source)
104 | ."&service=".urlencode($service);
105 | curl_setopt($ch, CURLOPT_HEADER, true);
106 | curl_setopt($ch, CURLOPT_POST, true);
107 | curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
108 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
109 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
110 | curl_setopt($ch, CURLOPT_FRESH_CONNECT, true);
111 | curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
112 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
113 |
114 | $response = curl_exec($ch);
115 |
116 | curl_close($ch);
117 |
118 | if (strpos($response, '200 OK') === false) {
119 | return false;
120 | }
121 |
122 | // find the auth code
123 | preg_match("/(Auth=)([\w|-]+)/", $response, $matches);
124 |
125 | if (!$matches[2]) {
126 | return false;
127 | }
128 |
129 | return $matches[2];
130 | }
131 | }
132 | // @codeCoverageIgnoreEnd
133 |
--------------------------------------------------------------------------------
/src/Sismo/Contrib/GithubNotifier.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Contrib;
13 |
14 | use Sismo\Commit;
15 | use Sismo\Notifier\Notifier;
16 |
17 | // @codeCoverageIgnoreStart
18 | /**
19 | * Class GithubNotifier
20 | * @package Sismo\Contrib
21 | */
22 | class GithubNotifier extends Notifier
23 | {
24 | /** @var string */
25 | protected $apikey;
26 |
27 | /** @var string */
28 | protected $host = 'https://api.github.com';
29 |
30 | /** @var string */
31 | protected $repo;
32 |
33 | /** @var string */
34 | protected $context = 'fabpot/sismo';
35 |
36 | /** @var string */
37 | protected $description = "Sismo";
38 |
39 | /** @var string */
40 | protected $targetUrlPattern = null;
41 |
42 | /**
43 | * @param string $apikey personal API key
44 | * @param string $repo repository name, e.g. fabpot/Sismo
45 | * @param string $targetUrlPattern status target URL pattern, e.g. http://sismo/%slug%/%sha%
46 | */
47 | public function __construct($apikey, $repo, $targetUrlPattern = null)
48 | {
49 | $this->apikey = $apikey;
50 | $this->repo = trim($repo, '/');
51 |
52 | $this->setTargetUrlPattern($targetUrlPattern);
53 | }
54 |
55 | /**
56 | * @param string $context
57 | *
58 | * @return $this
59 | */
60 | public function setContext($context)
61 | {
62 | $this->context = $context;
63 |
64 | return $this;
65 | }
66 |
67 | /**
68 | * @param string $description
69 | *
70 | * @return $this
71 | */
72 | public function setDescription($description)
73 | {
74 | $this->description = $description;
75 |
76 | return $this;
77 | }
78 |
79 | /**
80 | * @param string $host
81 | *
82 | * @return $this
83 | */
84 | public function setHost($host)
85 | {
86 | $this->host = rtrim($host, '/');
87 |
88 | return $this;
89 | }
90 |
91 | /**
92 | * @param string $targetUrlPattern
93 | *
94 | * @return $this
95 | */
96 | public function setTargetUrlPattern($targetUrlPattern)
97 | {
98 | $this->targetUrlPattern = $targetUrlPattern;
99 |
100 | return $this;
101 | }
102 |
103 | /**
104 | * Notifies a commit.
105 | *
106 | * @param Commit $commit A Commit instance
107 | */
108 | public function notify(Commit $commit)
109 | {
110 | $this->request(
111 | $this->getStatusEndpointUrl($commit),
112 | array(
113 | 'state' => $this->getGitHubState($commit),
114 | 'target_url' => $this->format($this->targetUrlPattern, $commit),
115 | 'description' => $this->format($this->description, $commit),
116 | 'context' => $this->context,
117 | ),
118 | $this->getGitHubHeaders()
119 | );
120 | }
121 |
122 | /**
123 | * @param Commit $commit
124 | *
125 | * @return string
126 | */
127 | protected function getStatusEndpointUrl(Commit $commit)
128 | {
129 | return $this->host."/repos/".$this->repo."/statuses/".$commit->getSha();
130 | }
131 |
132 | /**
133 | * @param Commit $commit
134 | *
135 | * @return string
136 | */
137 | protected function getGitHubState(Commit $commit)
138 | {
139 | switch ($commit->getStatusCode()) {
140 | case 'building':
141 | return 'pending';
142 | case 'success':
143 | return 'success';
144 | case 'failed':
145 | return 'failure';
146 | default:
147 | return 'error';
148 | }
149 | }
150 |
151 | /**
152 | * @return array
153 | */
154 | protected function getGitHubHeaders()
155 | {
156 | return array(
157 | "User-Agent: Sismo GitHub notifier",
158 | "Authorization: Basic ".base64_encode($this->apikey.":x-oauth-basic"),
159 | );
160 | }
161 |
162 | /**
163 | * @param $url
164 | * @param $data
165 | * @param array $headers
166 | *
167 | * @return bool
168 | */
169 | protected function request($url, $data, array $headers = array())
170 | {
171 | $ch = curl_init();
172 |
173 | curl_setopt($ch, CURLOPT_URL, $url);
174 | curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
175 | curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
176 | curl_setopt($ch, CURLOPT_POST, true);
177 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
178 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
179 |
180 | curl_exec($ch);
181 | $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
182 | curl_close($ch);
183 |
184 | return floor($statusCode / 100) == 2;
185 | }
186 | }
187 | // @codeCoverageIgnoreEnd
188 |
--------------------------------------------------------------------------------
/src/Sismo/Builder.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | use Symfony\Component\Process\Process;
15 |
16 | // @codeCoverageIgnoreStart
17 | /**
18 | * Builds commits.
19 | *
20 | * @author Fabien Potencier
21 | */
22 | class Builder
23 | {
24 | private $project;
25 | private $baseBuildDir;
26 | private $buildDir;
27 | private $callback;
28 | private $gitPath;
29 | private $gitCmds;
30 |
31 | public function __construct($buildDir, $gitPath, array $gitCmds)
32 | {
33 | $this->baseBuildDir = $buildDir;
34 | $this->gitPath = $gitPath;
35 | $this->gitCmds = array_replace(array(
36 | 'clone' => 'clone --progress --recursive %repo% %dir% --branch %localbranch%',
37 | 'fetch' => 'fetch origin',
38 | 'prepare' => 'submodule update --init --recursive',
39 | 'checkout' => 'checkout -q -f %branch%',
40 | 'reset' => 'reset --hard %revision%',
41 | 'show' => 'show -s --pretty=format:%format% %revision%',
42 | ), $gitCmds);
43 | }
44 |
45 | public function init(Project $project, $callback = null)
46 | {
47 | $process = new Process(sprintf('%s --version', $this->gitPath));
48 | if ($process->run() > 0) {
49 | throw new \RuntimeException(sprintf('The git binary cannot be found (%s).', $this->gitPath));
50 | }
51 |
52 | $this->project = $project;
53 | $this->callback = $callback;
54 | $this->buildDir = $this->baseBuildDir.'/'.$this->getBuildDir($project);
55 | }
56 |
57 | public function build()
58 | {
59 | file_put_contents($this->buildDir.'/sismo-run-tests.sh', str_replace(array("\r\n", "\r"), "\n", $this->project->getCommand()));
60 |
61 | $process = new Process('sh sismo-run-tests.sh', $this->buildDir);
62 | $process->setTimeout(3600);
63 | $process->run($this->callback);
64 |
65 | return $process;
66 | }
67 |
68 | public function getBuildDir(Project $project)
69 | {
70 | return substr(md5($project->getRepository().$project->getBranch()), 0, 6);
71 | }
72 |
73 | public function prepare($revision, $sync)
74 | {
75 | if (!file_exists($this->buildDir)) {
76 | mkdir($this->buildDir, 0777, true);
77 | }
78 |
79 | if (!file_exists($this->buildDir.'/.git')) {
80 | $this->execute($this->getGitCommand('clone'), sprintf('Unable to clone repository for project "%s".', $this->project));
81 | }
82 |
83 | if ($sync) {
84 | $this->execute($this->gitPath.' '.$this->gitCmds['fetch'], sprintf('Unable to fetch repository for project "%s".', $this->project));
85 | }
86 |
87 | $this->execute($this->getGitCommand('checkout'), sprintf('Unable to checkout branch "%s" for project "%s".', $this->project->getBranch(), $this->project));
88 |
89 | if ($sync) {
90 | $this->execute($this->gitPath.' '.$this->gitCmds['prepare'], sprintf('Unable to update submodules for project "%s".', $this->project));
91 | }
92 |
93 | if (null === $revision || 'HEAD' === $revision) {
94 | $revision = null;
95 | if (file_exists($file = $this->buildDir.'/.git/HEAD')) {
96 | $revision = trim(file_get_contents($file));
97 | if (0 === strpos($revision, 'ref: ')) {
98 | if (file_exists($file = $this->buildDir.'/.git/'.substr($revision, 5))) {
99 | $revision = trim(file_get_contents($file));
100 | } else {
101 | $revision = null;
102 | }
103 | }
104 | }
105 |
106 | if (null === $revision) {
107 | throw new BuildException(sprintf('Unable to get HEAD for branch "%s" for project "%s".', $this->project->getBranch(), $this->project));
108 | }
109 | }
110 |
111 | $this->execute($this->getGitCommand('reset', array('%revision%' => escapeshellarg($revision))), sprintf('Revision "%s" for project "%s" does not exist.', $revision, $this->project));
112 |
113 | $process = $this->execute($this->getGitCommand('show', array('%revision%' => escapeshellarg($revision))), sprintf('Unable to get logs for project "%s".', $this->project));
114 |
115 | return explode("\n", trim($process->getOutput()), 4);
116 | }
117 |
118 | protected function getGitCommand($command, array $replace = array())
119 | {
120 | $replace = array_merge(array(
121 | '%repo%' => escapeshellarg($this->project->getRepository()),
122 | '%dir%' => escapeshellarg($this->buildDir),
123 | '%branch%' => escapeshellarg('origin/'.$this->project->getBranch()),
124 | '%localbranch%' => escapeshellarg($this->project->getBranch()),
125 | '%format%' => '"%H%n%an%n%ci%n%s%n"',
126 | ), $replace);
127 |
128 | return strtr($this->gitPath.' '.$this->gitCmds[$command], $replace);
129 | }
130 |
131 | private function execute($command, $message)
132 | {
133 | if (null !== $this->callback) {
134 | call_user_func($this->callback, 'out', sprintf("Running \"%s\"\n", $command));
135 | }
136 | $process = new Process($command, $this->buildDir);
137 | $process->setTimeout(3600);
138 | $process->run($this->callback);
139 | if (!$process->isSuccessful()) {
140 | throw new BuildException($message);
141 | }
142 |
143 | return $process;
144 | }
145 | }
146 | // @codeCoverageIgnoreEnd
147 |
148 |
--------------------------------------------------------------------------------
/web/css/sismo.css:
--------------------------------------------------------------------------------
1 | /*
2 | Copyright (c) 2009, Yahoo! Inc. All rights reserved.
3 | Code licensed under the BSD License:
4 | http://developer.yahoo.net/yui/license.txt
5 | version: 2.7.0
6 | */
7 | html{color:#000;background:#FFF;}body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,button,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,em,strong,th,var,optgroup{font-style:inherit;font-weight:inherit;}del,ins{text-decoration:none;}li{list-style:none;}caption,th{text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym{border:0;font-variant:normal;}sup{vertical-align:baseline;}sub{vertical-align:baseline;}legend{color:#000;}input,button,textarea,select,optgroup,option{font-family:inherit;font-size:inherit;font-style:inherit;font-weight:inherit;}input,button,textarea,select{*font-size:100%;}
8 |
9 | /* Sismo CSS */
10 | html {
11 | background-color: #182d33;
12 | }
13 |
14 | body {
15 | font-family: Georgia, serif;
16 | color: #fff;
17 | font-size: 16px;
18 | }
19 |
20 | em {
21 | font-style: italic;
22 | }
23 |
24 | strong {
25 | font-weight: bold;
26 | }
27 |
28 | h1 {
29 | font-size: 30px;
30 | margin-top: 30px;
31 | margin-bottom: 10px;
32 | }
33 |
34 | h1 a {
35 | color: #fff;
36 | }
37 |
38 | h2 {
39 | font-size: 26px;
40 | }
41 |
42 | #builds h2 {
43 | font-size: 21px;
44 | }
45 |
46 | #content, footer {
47 | width: 780px;
48 | margin: 0 auto;
49 | background: #274751;
50 | padding: 15px 24px;
51 | }
52 |
53 | #content {
54 | padding-top: 160px;
55 | background: #274751 url(../images/header.png) no-repeat right top;
56 | }
57 |
58 | a {
59 | text-decoration: none;
60 | }
61 |
62 | a:hover {
63 | text-decoration: underline;
64 | }
65 |
66 | .success {
67 | background: url(../images/success.png) repeat-y !important;
68 | }
69 |
70 | .failed {
71 | background: url(../images/failed.png) repeat-y !important;
72 | }
73 |
74 | .success a, .failed a {
75 | color: #fff !important;
76 | }
77 |
78 | #projects {
79 | margin: 15px 0;
80 | margin-top: 30px;
81 | }
82 |
83 | #projects li {
84 | position: relative;
85 | padding: 12px;
86 | margin-bottom: 5px;
87 | font-size: 30px;
88 | vertical-align: bottom;
89 | border: 3px solid #ccc;
90 | }
91 |
92 | #projects li a {
93 | color: #555;
94 | }
95 |
96 | #build {
97 | padding: 10px 15px;
98 | margin-bottom: 15px;
99 | border: 3px solid #ccc;
100 | background: url(../images/nobuild.png) repeat-y;
101 | }
102 |
103 | #build a {
104 | color: #555;
105 | }
106 |
107 | #output {
108 | margin-bottom: 15px;
109 | }
110 |
111 | #output pre {
112 | background: #000;
113 | color: #fff;
114 | padding: 8px;
115 | overflow: auto;
116 | max-height: 300px;
117 | width: 764px;
118 | font-size: 14px;
119 | }
120 |
121 | .meta {
122 | font-size: 13px;
123 | font-family: Georgia, serif;
124 | }
125 |
126 | #builds li {
127 | padding: 8px 12px;
128 | margin-bottom: 5px;
129 | border: 3px solid #ccc;
130 | background: url(../images/nobuild.png) repeat-y;
131 | }
132 |
133 | #builds li a {
134 | color: #fff;
135 | }
136 |
137 | #builds .meta {
138 | font-size: 14px;
139 | }
140 |
141 | .commit {
142 | float: right;
143 | padding-left: 15px;
144 | padding-bottom: 15px;
145 | color: #333 !important;
146 | }
147 |
148 | .commit a {
149 | color: #333 !important;
150 | }
151 |
152 | .status {
153 | float: right;
154 | padding-left: 15px;
155 | padding-bottom: 15px;
156 | color: #333 !important;
157 | font-size: 15px;
158 | }
159 |
160 | footer {
161 | text-align: center;
162 | font-size: 12px;
163 | font-family: Arial;
164 | background: #274751 url(../images/hr.png) no-repeat;
165 | display: block;
166 | }
167 |
168 | footer span {
169 | margin-right: 40px;
170 | }
171 |
172 | footer img {
173 | margin: 0 4px;
174 | vertical-align: middle;
175 | }
176 |
177 | footer a {
178 | color: #fff;
179 | }
180 |
181 | .clearfix:after { content: "\0020"; display: block; height: 0; clear: both; visibility: hidden; overflow: hidden; }
182 | .clearfix { display: inline-block; }
183 | * html .clearfix { height: 1%; }
184 | .clearfix { display: block; }
185 |
186 | #project_command_test {
187 | width: 100%;
188 | height: 150px;
189 | }
190 |
191 | .permalink {
192 | font-size: 60%;
193 | }
194 |
195 | .permalink a {
196 | color: #eee !important;
197 | }
198 |
199 | #back {
200 | float: right;
201 | }
202 |
203 | #back a {
204 | color: #ddd;
205 | }
206 |
207 | #error {
208 | margin-top: 30px;
209 | margin-bottom: 30px;
210 | position: relative;
211 | padding: 12px;
212 | font-size: 22px;
213 | vertical-align: bottom;
214 | border: 3px solid #ccc;
215 | }
216 |
217 | .ansi_color_fg_black { color: black; }
218 | .ansi_color_fg_red { color: red; }
219 | .ansi_color_fg_green { color: green; }
220 | .ansi_color_fg_yellow { color: yellow; }
221 | .ansi_color_fg_blue { color: blue; }
222 | .ansi_color_fg_magenta { color: magenta; }
223 | .ansi_color_fg_cyan { color: cyan; }
224 | .ansi_color_fg_white { color: white; }
225 | .ansi_color_bg_black { background-color: black; padding: 2px 0; }
226 | .ansi_color_bg_red { background-color: red; padding: 2px 0; }
227 | .ansi_color_bg_green { background-color: green; padding: 2px 0; }
228 | .ansi_color_bg_yellow { background-color: yellow; padding: 2px 0; }
229 | .ansi_color_bg_blue { background-color: blue; padding: 2px 0; }
230 | .ansi_color_bg_magenta { background-color: magenta; padding: 2px 0; }
231 | .ansi_color_bg_cyan { background-color: cyan; padding: 2px 0; }
232 | .ansi_color_bg_white { background-color: white; padding: 2px 0; }
233 |
--------------------------------------------------------------------------------
/src/Sismo/Commit.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | /**
15 | * Represents a project commit.
16 | *
17 | * @author Fabien Potencier
18 | */
19 | class Commit
20 | {
21 | private $project;
22 | private $sha;
23 | private $message;
24 | private $author;
25 | private $date;
26 | private $build;
27 | private $output;
28 | private $buildDate;
29 | private $status = 'building';
30 | private $statuses = array('building' => 'building', 'success' => 'succeeded', 'failed' => 'failed');
31 |
32 | /**
33 | * Constructor.
34 | *
35 | * @param Project $project A Project instance
36 | * @param string $sha The sha of the commit
37 | */
38 | public function __construct(Project $project, $sha)
39 | {
40 | $this->project = $project;
41 | $this->sha = $sha;
42 | }
43 |
44 | /**
45 | * Returns a string representation of the Commit.
46 | *
47 | * @return string The string representation of the Commit
48 | */
49 | public function __toString()
50 | {
51 | return sprintf('%s@%s', $this->project, $this->getShortSha());
52 | }
53 |
54 | /**
55 | * Returns true if the commit is being built.
56 | *
57 | * @return Boolean true of the commit is being built, false otherwise
58 | */
59 | public function isBuilding()
60 | {
61 | return 'building' === $this->status;
62 | }
63 |
64 | /**
65 | * Returns true if the commit has already been built.
66 | *
67 | * @return Boolean true of the commit has already been built, false otherwise
68 | */
69 | public function isBuilt()
70 | {
71 | return in_array($this->status, array('success', 'failed'));
72 | }
73 |
74 | /**
75 | * Returns true if the commit was built successfully.
76 | *
77 | * @return Boolean true of the commit was built successfully, false otherwise
78 | */
79 | public function isSuccessful()
80 | {
81 | return 'success' === $this->status;
82 | }
83 |
84 | /**
85 | * Sets the build status code of the commit.
86 | *
87 | * Can be one of "building", "success", or "failed".
88 | *
89 | * @param string $status The commit build status code
90 | */
91 | public function setStatusCode($status)
92 | {
93 | if (!in_array($status, array('building', 'success', 'failed'))) {
94 | throw new \InvalidArgumentException(sprintf('Invalid status code "%s".', $status));
95 | }
96 |
97 | $this->status = $status;
98 | }
99 |
100 | /**
101 | * Gets the build status code of the commit.
102 | *
103 | * Can be one of "building", "success", or "failed".
104 | *
105 | * @return string The commit build status code
106 | */
107 | public function getStatusCode()
108 | {
109 | return $this->status;
110 | }
111 |
112 | /**
113 | * Gets the build status of the commit.
114 | *
115 | * Can be one of "building", "succeeded", or "failed".
116 | *
117 | * @return string The commit build status
118 | */
119 | public function getStatus()
120 | {
121 | return $this->statuses[$this->status];
122 | }
123 |
124 | /**
125 | * Sets the build output.
126 | *
127 | * @param string $output The build output
128 | */
129 | public function setOutput($output)
130 | {
131 | $this->output = $output;
132 | }
133 |
134 | /**
135 | * Gets the raw build output.
136 | *
137 | * The output can contain ANSI code characters.
138 | *
139 | * @return string The raw build output
140 | *
141 | * @see getDecoratedOutput()
142 | */
143 | public function getOutput()
144 | {
145 | return $this->output;
146 | }
147 |
148 | /**
149 | * Gets the commit message.
150 | *
151 | * @return string The commit message
152 | */
153 | public function getMessage()
154 | {
155 | return $this->message;
156 | }
157 |
158 | /**
159 | * Sets the commit message.
160 | *
161 | * @param string $message The commit message
162 | */
163 | public function setMessage($message)
164 | {
165 | $this->message = $message;
166 | }
167 |
168 | /**
169 | * Gets the commit SHA1.
170 | *
171 | * @return string The commit SHA1
172 | */
173 | public function getSha()
174 | {
175 | return $this->sha;
176 | }
177 |
178 | /**
179 | * Gets the short commit SHA1 (6 first characters).
180 | *
181 | * @return string The short commit SHA1
182 | */
183 | public function getShortSha()
184 | {
185 | return substr($this->sha, 0, 6);
186 | }
187 |
188 | /**
189 | * Gets the Project associated with this Commit.
190 | *
191 | * @return Project A Project instance
192 | */
193 | public function getProject()
194 | {
195 | return $this->project;
196 | }
197 |
198 | /**
199 | * Gets the author associated with this commit.
200 | *
201 | * @return string The commit author
202 | */
203 | public function getAuthor()
204 | {
205 | return $this->author;
206 | }
207 |
208 | /**
209 | * Sets the author associated with this commit.
210 | *
211 | * @return string The commit author
212 | */
213 | public function setAuthor($author)
214 | {
215 | $this->author = $author;
216 | }
217 |
218 | /**
219 | * Gets the creation date of this commit.
220 | *
221 | * @return \DateTime A \DateTime instance
222 | */
223 | public function getDate()
224 | {
225 | return $this->date;
226 | }
227 |
228 | /**
229 | * Sets the creation date of this commit.
230 | *
231 | * @param \DateTime $date A \DateTime instance
232 | */
233 | public function setDate(\DateTime $date)
234 | {
235 | $this->date = $date;
236 | }
237 |
238 | /**
239 | * Gets the build date of this commit.
240 | *
241 | * @return \DateTime A \DateTime instance
242 | */
243 | public function getBuildDate()
244 | {
245 | return $this->buildDate;
246 | }
247 |
248 | /**
249 | * Sets the build date of this commit.
250 | *
251 | * @param \DateTime $date A \DateTime instance
252 | */
253 | public function setBuildDate(\DateTime $date)
254 | {
255 | $this->buildDate = $date;
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/Sismo/Storage/Storage.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Storage;
13 |
14 | use Sismo\Project;
15 | use Sismo\Commit;
16 |
17 | /**
18 | * Stores projects and builds information.
19 | *
20 | * @author Fabien Potencier
21 | */
22 | class Storage implements StorageInterface
23 | {
24 | private $db;
25 |
26 | public function __construct(\SQLite3 $db)
27 | {
28 | $this->db = $db;
29 | }
30 |
31 | public function getCommit(Project $project, $sha)
32 | {
33 | $stmt = $this->db->prepare('SELECT slug, sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug AND sha = :sha');
34 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT);
35 | $stmt->bindValue(':sha', $sha, SQLITE3_TEXT);
36 |
37 | if (false !== $result = $stmt->execute()) {
38 | if (false !== $result = $result->fetchArray(\SQLITE3_ASSOC)) {
39 | return $this->createCommit($project, $result);
40 | }
41 | }
42 |
43 | return false;
44 | }
45 |
46 | public function initCommit(Project $project, $sha, $author, \DateTime $date, $message)
47 | {
48 | $stmt = $this->db->prepare('INSERT OR REPLACE INTO `commit` (slug, sha, author, date, message, status, output, build_date) VALUES (:slug, :sha, :author, :date, :message, :status, :output, :build_date)');
49 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT);
50 | $stmt->bindValue(':sha', $sha, SQLITE3_TEXT);
51 | $stmt->bindValue(':author', $author, SQLITE3_TEXT);
52 | $stmt->bindValue(':date', $date->format('Y-m-d H:i:s'), SQLITE3_TEXT);
53 | $stmt->bindValue(':message', $message, SQLITE3_TEXT);
54 | $stmt->bindValue(':status', 'building', SQLITE3_TEXT);
55 | $stmt->bindValue(':output', '', SQLITE3_TEXT);
56 | $stmt->bindValue(':build_date', '', SQLITE3_TEXT);
57 |
58 | if (false === $result = $stmt->execute()) {
59 | // @codeCoverageIgnoreStart
60 | throw new \RuntimeException(sprintf('Unable to save commit "%s" from project "%s".', $sha, $project->getName()));
61 | // @codeCoverageIgnoreEnd
62 | }
63 |
64 | $commit = new Commit($project, $sha);
65 | $commit->setAuthor($author);
66 | $commit->setMessage($message);
67 | $commit->setDate($date);
68 |
69 | return $commit;
70 | }
71 |
72 | public function updateProject(Project $project)
73 | {
74 | $stmt = $this->db->prepare('INSERT OR REPLACE INTO project (slug, name, repository, branch, command, url_pattern) VALUES (:slug, :name, :repository, :branch, :command, :url_pattern)');
75 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT);
76 | $stmt->bindValue(':name', $project->getName(), SQLITE3_TEXT);
77 | $stmt->bindValue(':repository', $project->getRepository(), SQLITE3_TEXT);
78 | $stmt->bindValue(':branch', $project->getBranch(), SQLITE3_TEXT);
79 | $stmt->bindValue(':command', $project->getCommand(), SQLITE3_TEXT);
80 | $stmt->bindValue(':url_pattern', $project->getUrlPattern(), SQLITE3_TEXT);
81 |
82 | if (false === $stmt->execute()) {
83 | // @codeCoverageIgnoreStart
84 | throw new \RuntimeException(sprintf('Unable to save project "%s".', $project->getName()));
85 | // @codeCoverageIgnoreEnd
86 | }
87 |
88 | // related commits
89 | $stmt = $this->db->prepare('SELECT sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug ORDER BY `status` = "building" DESC, build_date DESC LIMIT 100');
90 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT);
91 |
92 | if (false === $results = $stmt->execute()) {
93 | // @codeCoverageIgnoreStart
94 | throw new \RuntimeException(sprintf('Unable to get latest commit for project "%s".', $project->getName()));
95 | // @codeCoverageIgnoreEnd
96 | }
97 |
98 | $commits = array();
99 | while ($result = $results->fetchArray(\SQLITE3_ASSOC)) {
100 | $commits[] = $this->createCommit($project, $result);
101 | }
102 |
103 | $project->setCommits($commits);
104 |
105 | // project building?
106 | $stmt = $this->db->prepare('SELECT COUNT(*) AS count FROM `commit` WHERE slug = :slug AND status = "building"');
107 | $stmt->bindValue(':slug', $project->getSlug(), SQLITE3_TEXT);
108 |
109 | $building = false;
110 | if (false !== $result = $stmt->execute()) {
111 | if (false !== $result = $result->fetchArray(\SQLITE3_ASSOC)) {
112 | if ($result['count'] > 0) {
113 | $building = true;
114 | }
115 | }
116 | }
117 |
118 | $project->setBuilding($building);
119 | }
120 |
121 | public function updateCommit(Commit $commit)
122 | {
123 | $stmt = $this->db->prepare('UPDATE `commit` SET status = :status, output = :output, build_date = CURRENT_TIMESTAMP WHERE slug = :slug AND sha = :sha');
124 | $stmt->bindValue(':slug', $commit->getProject()->getSlug(), SQLITE3_TEXT);
125 | $stmt->bindValue(':sha', $commit->getSha(), SQLITE3_TEXT);
126 | $stmt->bindValue(':status', $commit->getStatusCode(), SQLITE3_TEXT);
127 | $stmt->bindValue(':output', $commit->getOutput(), SQLITE3_TEXT);
128 |
129 | if (false === $stmt->execute()) {
130 | // @codeCoverageIgnoreStart
131 | throw new \RuntimeException(sprintf('Unable to save build "%s@%s".', $commit->getProject()->getName(), $commit->getSha()));
132 | // @codeCoverageIgnoreEnd
133 | }
134 | }
135 |
136 | private function createCommit($project, $result)
137 | {
138 | $commit = new Commit($project, $result['sha']);
139 | $commit->setAuthor($result['author']);
140 | $commit->setMessage($result['message']);
141 | $commit->setDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['date']));
142 | if ($result['build_date']) {
143 | $commit->setBuildDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['build_date']));
144 | }
145 | $commit->setStatusCode($result['status']);
146 | $commit->setOutput($result['output']);
147 |
148 | return $commit;
149 | }
150 |
151 | public function close()
152 | {
153 | $this->db->close();
154 | }
155 |
156 | public function __destruct()
157 | {
158 | $this->close();
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/tests/controllersTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | use Silex\WebTestCase;
13 | use Sismo\Project;
14 | use Symfony\Component\Filesystem\Filesystem;
15 |
16 | class controllersTest extends WebTestCase
17 | {
18 | protected $baseDir;
19 |
20 | public function createApplication()
21 | {
22 | $app = require __DIR__.'/../src/app.php';
23 |
24 | $this->baseDir = realpath(sys_get_temp_dir()).'/sismo';
25 | $fs = new Filesystem();
26 | $fs->mkdir($this->baseDir);
27 | $fs->mkdir($this->baseDir.'/config');
28 | $app['data.path'] = $this->baseDir.'/db';
29 | $app['config.file'] = $this->baseDir.'/config.php';
30 |
31 | // This file does not exist, so app will use default sqlite storage.
32 | $app['config.storage.file'] = $this->baseDir.'/storage.php';
33 |
34 | @unlink($this->app['db.path']);
35 | file_put_contents($app['config.file'], 'app['storage']->close();
47 |
48 | $fs = new Filesystem();
49 | $fs->remove($this->baseDir);
50 | }
51 |
52 | public function testGetProjectsEmpty()
53 | {
54 | $crawler = $this->createClient()->request('GET', '/');
55 |
56 | $this->assertEquals(1, count($crawler->filter('p:contains("No project yet.")')));
57 | }
58 |
59 | public function testPages()
60 | {
61 | $sismo = $this->app['sismo'];
62 | $storage = $this->app['storage'];
63 |
64 | $sismo->addProject(new Project('Twig'));
65 |
66 | $sismo->addProject($project1 = new Project('Silex1'));
67 | $commit = $storage->initCommit($project1, '7d78d5', 'fabien', new \DateTime(), 'foo');
68 | $storage->updateProject($project1);
69 |
70 | $sismo->addProject($project2 = new Project('Silex2'));
71 | $commit = $storage->initCommit($project2, '7d78d5', 'fabien', new \DateTime(), 'foo');
72 | $commit->setStatusCode('success');
73 | $storage->updateCommit($commit);
74 | $storage->updateProject($project2);
75 |
76 | $sismo->addProject($project3 = new Project('Silex3'));
77 | $commit = $storage->initCommit($project3, '7d78d5', 'fabien', new \DateTime(), 'foo');
78 | $commit->setStatusCode('failed');
79 | $storage->updateCommit($commit);
80 | $storage->updateProject($project3);
81 |
82 | // projects page
83 | $client = $this->createClient();
84 | $crawler = $client->request('GET', '/');
85 |
86 | $this->assertEquals(array('Twig', 'Silex1', 'Silex2', 'Silex3'), $crawler->filter('ul#projects li a')->each(function ($node) { return trim($node->nodeValue); }));
87 | $this->assertEquals(array('not built yet', 'building', 'succeeded', 'failed'), $crawler->filter('ul#projects li div')->each(function ($node) { return trim($node->nodeValue); }));
88 | $this->assertEquals(array('no_build', 'building', 'success', 'failed'), $crawler->filter('ul#projects li')->extract('class'));
89 |
90 | $links = $crawler->filter('ul#projects li a')->links();
91 |
92 | // project page
93 | $crawler = $client->click($links[0]);
94 | $this->assertEquals(1, count($crawler->filter('p:contains("Never built yet.")')));
95 |
96 | $crawler = $client->click($links[1]);
97 | $this->assertEquals('#7d78d5 building', trim($crawler->filter('div.commit')->text()));
98 |
99 | $crawler = $client->click($links[2]);
100 | $this->assertEquals('#7d78d5 succeeded', trim($crawler->filter('div.commit')->text()));
101 |
102 | $crawler = $client->click($links[3]);
103 | $this->assertEquals('#7d78d5 failed', trim($crawler->filter('div.commit')->text()));
104 |
105 | // sha page
106 | $crawler = $client->request('GET', '/silex2/7d78d5');
107 | $this->assertEquals('#7d78d5 succeeded', trim($crawler->filter('div.commit')->text()));
108 |
109 | // cc tray
110 | $crawler = $client->request('GET', '/dashboard/cctray.xml');
111 | $this->assertEquals(4, count($crawler->filter('Project')));
112 |
113 | $this->assertEquals(array('Unknown', 'Unknown', 'Success', 'Failure'), $crawler->filter('Project')->extract('lastBuildStatus'));
114 | $this->assertEquals(array('Sleeping', 'Building', 'Sleeping', 'Sleeping'), $crawler->filter('Project')->extract('activity'));
115 | }
116 |
117 | public function testGetNonExistentProject()
118 | {
119 | $crawler = $this->createClient()->request('GET', '/foobar');
120 |
121 | $this->assertEquals('Project "foobar" not found.', $crawler->filter('p')->text());
122 | }
123 |
124 | public function testGetNonExistentProjectOnShaPage()
125 | {
126 | $crawler = $this->createClient()->request('GET', '/foo/bar');
127 |
128 | $this->assertEquals('Project "foo" not found.', $crawler->filter('p')->text());
129 | }
130 |
131 | public function testGetNonExistentSha()
132 | {
133 | $sismo = $this->app['sismo'];
134 | $sismo->addProject(new Project('Twig'));
135 |
136 | $crawler = $this->createClient()->request('GET', '/twig/bar');
137 |
138 | $this->assertEquals('Commit "bar" for project "twig" not found.', $crawler->filter('p')->text());
139 | }
140 |
141 | public function testBuildPage()
142 | {
143 | $token = md5(mt_rand());
144 | $this->app['build.token'] = $token;
145 |
146 | $project = new Project('Twig');
147 |
148 | $this->app['sismo'] = $this->getMockBuilder('Sismo\Sismo')->disableOriginalConstructor()->getMock();
149 | $this->app['sismo']->expects($this->once())->method('hasProject')->will($this->returnValue(true));
150 | $this->app['sismo']->expects($this->once())->method('getProject')->will($this->returnValue($project));
151 | $this->app['sismo']->expects($this->once())->method('build');
152 |
153 | $storage = $this->app['storage'];
154 |
155 | $this->app['sismo']->addProject($project);
156 | $commit = $storage->initCommit($project, '7d78d5', 'fabien', new \DateTime(), 'foo');
157 | $commit->setStatusCode('success');
158 | $storage->updateCommit($commit);
159 | $storage->updateProject($project);
160 |
161 | $client = $this->createClient();
162 | $crawler = $client->request('POST', sprintf('/twig/build/%s', urlencode($token)));
163 |
164 | $this->assertEquals('Triggered build for project "twig".', $crawler->filter('p')->text());
165 | }
166 |
167 | public function testBuildPageTokenNotSet()
168 | {
169 | $this->app['build.token'] = null;
170 |
171 | $client = $this->createClient();
172 | $crawler = $client->request('POST', '/twig/build/foo');
173 |
174 | $this->assertEquals('Not found.', $crawler->filter('p')->text());
175 | }
176 |
177 | public function testBuildPageInvalidToken()
178 | {
179 | $this->app['build.token'] = md5(mt_rand());
180 |
181 | $client = $this->createClient();
182 | $crawler = $client->request('POST', '/twig/build/foo');
183 |
184 | $this->assertEquals('An error occurred', $crawler->filter('p')->text());
185 | }
186 |
187 | public function testBuildPageNonExistentProject()
188 | {
189 | $token = md5(mt_rand());
190 | $this->app['build.token'] = $token;
191 |
192 | $client = $this->createClient();
193 | $crawler = $client->request('POST', sprintf('/foobar/build/%s', urlencode($token)));
194 |
195 | $this->assertEquals('Project "foobar" not found.', $crawler->filter('p')->text());
196 | }
197 | }
198 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/ProjectTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\Project;
15 |
16 | class ProjectTest extends \PHPUnit_Framework_TestCase
17 | {
18 | public function testConstructor()
19 | {
20 | $project = new Project('Twig Local');
21 | $this->assertEquals('Twig Local', $project->getName());
22 | $this->assertEquals('twig-local', $project->getSlug());
23 | $this->assertEquals('master', $project->getBranch());
24 | $this->assertEquals(array(), $project->getNotifiers());
25 |
26 | $project = new Project('Twig Local', 'repo', array(), 'twig');
27 | $this->assertEquals('twig', $project->getSlug());
28 | $this->assertEquals(array(), $project->getNotifiers());
29 |
30 | $project = new Project('Twig Local', 'repo');
31 | $this->assertEquals('repo', $project->getRepository());
32 | $this->assertEquals('master', $project->getBranch());
33 |
34 | $project = new Project('Twig Local', 'repo@feat');
35 | $this->assertEquals('repo', $project->getRepository());
36 | $this->assertEquals('feat', $project->getBranch());
37 |
38 | $project = new Project('Twig Local', 'repo', array(
39 | $notifier1 = $this->getMock('Sismo\Notifier\Notifier'),
40 | $notifier2 = $this->getMock('Sismo\Notifier\Notifier'),
41 | ));
42 | $this->assertSame(array($notifier1, $notifier2), $project->getNotifiers());
43 |
44 | $project = new Project('Twig Local', 'repo', $notifier3 = $this->getMock('Sismo\Notifier\Notifier'));
45 | $this->assertSame(array($notifier3), $project->getNotifiers());
46 | }
47 |
48 | public function testSlug()
49 | {
50 | $project = new Project('Twig Local');
51 | $this->assertEquals('twig-local', $project->getSlug());
52 |
53 | $project->setSlug('twig-local-my-slug');
54 | $this->assertEquals('twig-local-my-slug', $project->getSlug());
55 | }
56 |
57 | public function testToString()
58 | {
59 | $project = new Project('Twig Local');
60 | $this->assertEquals('Twig Local', (string) $project);
61 | }
62 |
63 | public function testBuildingFlag()
64 | {
65 | $project = new Project('Twig Local');
66 | $this->assertFalse($project->isBuilding());
67 |
68 | $project->setBuilding(true);
69 | $this->assertTrue($project->isBuilding());
70 |
71 | $project->setBuilding('foo');
72 | $this->assertTrue($project->isBuilding());
73 |
74 | $project->setBuilding(false);
75 | $this->assertFalse($project->isBuilding());
76 | }
77 |
78 | public function testNotifiers()
79 | {
80 | $project = new Project('Twig Local');
81 | $this->assertEquals(array(), $project->getNotifiers());
82 |
83 | $project->addNotifier($notifier1 = $this->getMock('Sismo\Notifier\Notifier'));
84 | $this->assertSame(array($notifier1), $project->getNotifiers());
85 |
86 | $project->addNotifier($notifier2 = $this->getMock('Sismo\Notifier\Notifier'));
87 | $this->assertSame(array($notifier1, $notifier2), $project->getNotifiers());
88 | }
89 |
90 | public function testBranch()
91 | {
92 | $project = new Project('Twig Local');
93 | $this->assertEquals('master', $project->getBranch());
94 |
95 | $project->setBranch('new-feature');
96 | $this->assertEquals('new-feature', $project->getBranch());
97 | }
98 |
99 | public function testCommits()
100 | {
101 | $project = new Project('Twig Local');
102 | $this->assertEquals(array(), $project->getCommits());
103 |
104 | $project->setCommits(array(
105 | $commit1 = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(),
106 | $commit2 = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(),
107 | $commit3 = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock(),
108 | ));
109 | $this->assertEquals(array($commit1, $commit2, $commit3), $project->getCommits());
110 | $this->assertEquals($commit1, $project->getLatestCommit());
111 | }
112 |
113 | public function testName()
114 | {
115 | $project = new Project('Twig (Local)');
116 | $this->assertEquals('Twig (Local)', $project->getName());
117 | $this->assertEquals('Twig', $project->getShortName());
118 | $this->assertEquals('Local', $project->getSubName());
119 |
120 | $project = new Project('Twig');
121 | $this->assertEquals('Twig', $project->getName());
122 | $this->assertEquals('Twig', $project->getShortName());
123 | $this->assertEquals('', $project->getSubName());
124 | }
125 |
126 | public function testStatus()
127 | {
128 | $project = new Project('Twig Local');
129 | $this->assertEquals('no_build', $project->getStatusCode());
130 | $this->assertEquals('not built yet', $project->getStatus());
131 |
132 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
133 | $commit->expects($this->once())->method('getStatusCode')->will($this->returnValue('success'));
134 | $project->setCommits(array($commit));
135 | $this->assertEquals('success', $project->getStatusCode());
136 |
137 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
138 | $commit->expects($this->once())->method('getStatus')->will($this->returnValue('success'));
139 | $project->setCommits(array($commit));
140 | $this->assertEquals('success', $project->getStatus());
141 | }
142 |
143 | public function testCCStatus()
144 | {
145 | $project = new Project('Twig Local');
146 | $this->assertEquals('Unknown', $project->getCCStatus());
147 |
148 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
149 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true));
150 | $commit->expects($this->once())->method('isSuccessful')->will($this->returnValue(true));
151 | $project->setCommits(array($commit));
152 | $this->assertEquals('Success', $project->getCCStatus());
153 |
154 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
155 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true));
156 | $commit->expects($this->once())->method('isSuccessful')->will($this->returnValue(false));
157 | $project->setCommits(array($commit));
158 | $this->assertEquals('Failure', $project->getCCStatus());
159 | }
160 |
161 | public function testCCActivity()
162 | {
163 | $project = new Project('Twig Local');
164 | $this->assertEquals('Sleeping', $project->getCCActivity());
165 |
166 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
167 | $commit->expects($this->once())->method('isBuilding')->will($this->returnValue(true));
168 | $project->setCommits(array($commit));
169 | $this->assertEquals('Building', $project->getCCActivity());
170 |
171 | $commit = $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
172 | $commit->expects($this->once())->method('isBuilding')->will($this->returnValue(false));
173 | $project->setCommits(array($commit));
174 | $this->assertEquals('Sleeping', $project->getCCActivity());
175 | }
176 |
177 | public function testRepository()
178 | {
179 | $project = new Project('Twig Local');
180 |
181 | $project->setRepository('https://github.com/twigphp/Twig.git');
182 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository());
183 |
184 | $project->setRepository('https://github.com/twigphp/Twig.git@feat');
185 | $this->assertEquals('https://github.com/twigphp/Twig.git', $project->getRepository());
186 | $this->assertEquals('feat', $project->getBranch());
187 | }
188 |
189 | public function testCommand()
190 | {
191 | $project = new Project('Twig Local');
192 | $this->assertEquals('phpunit', $project->getCommand());
193 |
194 | $project->setCommand('/path/to/phpunit');
195 | $this->assertEquals('/path/to/phpunit', $project->getCommand());
196 | }
197 |
198 | public function testDefaultCommand()
199 | {
200 | $project = new Project('Twig Local');
201 | $this->assertEquals('phpunit', $project->getCommand());
202 |
203 | Project::setDefaultCommand('phpunit --colors --strict');
204 | $project2 = new Project('Twig Local');
205 | $this->assertEquals('phpunit', $project->getCommand());
206 | $this->assertEquals('phpunit --colors --strict', $project2->getCommand());
207 |
208 | $project2->setCommand('phpunit');
209 | $this->assertEquals('phpunit', $project2->getCommand());
210 | }
211 |
212 | public function testUrlPattern()
213 | {
214 | $project = new Project('Twig Local');
215 |
216 | $project->setUrlPattern('https://github.com/twigphp/Twig/commit/%commit%');
217 | $this->assertEquals('https://github.com/twigphp/Twig/commit/%commit%', $project->getUrlPattern());
218 | }
219 |
220 | /**
221 | * @expectedException \RuntimeException
222 | */
223 | public function testSlugify()
224 | {
225 | $project = new Project('');
226 | }
227 | }
228 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | Sismo: Your Continuous Testing Server
2 | =====================================
3 |
4 | `Sismo`_ is a *Continuous Testing Server* written in PHP.
5 |
6 | Unlike more "advanced" Continuous Integration Servers (like Jenkins), Sismo
7 | does not try to do more than getting your code, running your tests, and send
8 | you notifications.
9 |
10 | What makes Sismo special?
11 | -------------------------
12 |
13 | Sismo has been optimized to run *locally* on your computer for your *Git*
14 | projects. Even if it can test *remote* repositories, Sismo is better used as a
15 | local ``post-commit`` hook. Whenever you commit changes locally, Sismo runs
16 | the tests and give you *immediate* feedback *before* you actually push your
17 | modifications to the remote repository. So, Sismo is a nice *complement* to
18 | your Continuous Integration Server.
19 |
20 | Sismo is *language and tool agnostic*. Just give it a command that knows how
21 | to run your tests and returns a non-zero exit code when tests do not pass.
22 |
23 | Sounds good? There is more. Sismo is insanely *easy to install* (there is only
24 | one PHP file to download), *easy to configure*, and it comes with a *gorgeous
25 | web interface*.
26 |
27 | .. image:: http://sismo.sensiolabs.org/images/sismo-home.png
28 |
29 | Installation
30 | ------------
31 |
32 | Installing Sismo is as easy as downloading the `sismo.php`_ file and put it
33 | somewhere under your web root directory. That's it, the CLI tool and the web
34 | interface is packed into a single PHP file.
35 |
36 | Note that Sismo needs at least PHP 5.3.3 to run.
37 |
38 | Configuration
39 | -------------
40 |
41 | By default, Sismo reads its configuration from ``~/.sismo/config.php``:
42 |
43 | .. code-block:: php
44 |
45 | setRepository('https://github.com/symfony/symfony.git');
69 | $sf2->setBranch('master');
70 | $sf2->setCommand('./vendors.sh; phpunit');
71 | $sf2->setSlug('symfony-local');
72 | $sf2->setUrlPattern('https://github.com/symfony/symfony/commit/%commit%');
73 | $sf2->addNotifier($notifier);
74 | $projects[] = $sf2;
75 |
76 | return $projects;
77 |
78 | For notifications, you can also use any Cruise Control "tray" software as
79 | Sismo also exposes an XML file in the Cruise Control format::
80 |
81 | http://path/to/sismo.php/dashboard/cctray.xml
82 |
83 | Use `CCMenu`_ on Mac, `CCTray`_ on Windows, `JCCTray`_ on Windows or Linux, or
84 | `CCMonitor`_ for Firefox.
85 |
86 | Using Sismo
87 | -----------
88 |
89 | Build all configured projects by running the ``build`` command:
90 |
91 | .. code-block:: text
92 |
93 | $ php sismo.php build --verbose
94 |
95 | If a build fails, Sismo will send notifications. Use the ``output`` command to
96 | see the latest build output of a project:
97 |
98 | .. code-block:: text
99 |
100 | $ php sismo.php output twig
101 |
102 | If you have configured Sismo to be accessible from the web interface, you can
103 | also check the build outputs there:
104 |
105 | .. image:: http://sismo.sensiolabs.org/images/sismo-project.png
106 |
107 | If your web server runs under a different user than the one you use on the
108 | CLI, you will need to set some environment variables in your virtual host
109 | configuration:
110 |
111 | .. code-block:: apache
112 |
113 | SetEnv SISMO_DATA_PATH "/path/to/sismo/data"
114 | SetEnv SISMO_CONFIG_PATH "/path/to/sismo/config.php"
115 |
116 | The ``build`` command is quite powerful and has many options. Learn more by
117 | appending ``--help``:
118 |
119 | .. code-block:: bash
120 |
121 | $ php sismo.php build --help
122 |
123 | To make Sismo run whenever you commit some changes, save this script in your
124 | project as ``.git/hooks/post-commit`` and make sure it's executable:
125 |
126 | .. code-block:: bash
127 |
128 | #!/bin/sh
129 |
130 | nohup php /path/to/sismo.php --quiet --force build symfony-local `git log -1 HEAD --pretty="%H"` &>/dev/null &
131 |
132 | ``symfony-local`` is the slug of the project. You can also create a
133 | ``post-merge`` script if you want to run Sismo when you merge branches.
134 |
135 | If you are running Sismo (with the single PHP file) with PHP 5.4.0, you can
136 | use the Sismo build-in web server:
137 |
138 | .. code-block:: bash
139 |
140 | $ php sismo.php run localhost:9000
141 |
142 | And then open the browser and point it to http://localhost:9000/sismo.php
143 |
144 | Limitations
145 | -----------
146 |
147 | Sismo is small and simple and it will stay that way. Sismo will never have the
148 | following:
149 |
150 | * a queue (if a project is already being built, newer commits are ignored);
151 | * a web interface for configuration;
152 | * metrics support;
153 | * plugin support;
154 | * other SCM support;
155 | * slaves support;
156 | * built-in authentication.
157 |
158 | ... and probably the feature you have in mind right now and all the ones you
159 | will think of later on ;)
160 |
161 | Tips and Recipes
162 | ----------------
163 |
164 | Change the default Location
165 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~
166 |
167 | Set the following environment variables to customize the default locations
168 | used by Sismo:
169 |
170 | .. code-block:: apache
171 |
172 | # in a .htaccess or httpd.conf Apache configuration file
173 |
174 | SetEnv SISMO_DATA_PATH "/path/to/sismo/data"
175 | SetEnv SISMO_CONFIG_PATH "/path/to/sismo/config.php"
176 |
177 | # for the CLI tool
178 |
179 | export SISMO_DATA_PATH=/path/to/sismo/data/
180 | export SISMO_CONFIG_PATH=/path/to/sismo/config.php
181 |
182 | Tracking multiple Branches
183 | ~~~~~~~~~~~~~~~~~~~~~~~~~~
184 |
185 | To track multiple branches of a project, just make their names unique and set
186 | the branch name::
187 |
188 | $projects[] = new Sismo\GithubProject('Twig (master branch)', '/Users/fabien/Twig');
189 |
190 | $projects[] = new Sismo\GithubProject('Twig (feat-awesome branch)', '/Users/fabien/Twig@feat-awesome');
191 |
192 | Note that Sismo uses the same clone for projects sharing the same repositories
193 | URL.
194 |
195 | Running Sismo for Remote Repositories
196 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
197 |
198 | Using Sismo for remote repositories is as simple as adding the Sismo building
199 | tool in a crontab entry:
200 |
201 | .. code-block:: text
202 |
203 | 0 12 * * * php /path/to/sismo.php --quiet
204 |
205 | For GitHub projects, and other systems that support post-receive URL hooks,
206 | you can set up Sismo to build automatically when a new revision is pushed.
207 | You need to set an environment variable in your Apache configuration:
208 |
209 | .. code-block:: apache
210 |
211 | # in a .htaccess or httpd.conf Apache configuration file
212 |
213 | SetEnv SISMO_BUILD_TOKEN "YOUR_TOKEN"
214 |
215 | You can also set an environment variable in your config file
216 | (``~/.sismo/config.php``):
217 |
218 | .. code-block:: php
219 |
220 | putenv('SISMO_BUILD_TOKEN=YOUR_TOKEN');
221 |
222 | Replace YOUR_TOKEN with something more secure, as anyone with this token
223 | could use it to trigger builds. Then set your post-receive URL appropriately.
224 | For example::
225 |
226 | http://path/to/sismo.php/your_project/build/YOUR_TOKEN
227 |
228 | History in the Web Interface
229 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
230 |
231 | The build history for a project in the web interface is different from the
232 | project history. It is sorted in the order of the builds so that the latest
233 | build output is always at your fingertips.
234 |
235 | Adding a Notifier
236 | ~~~~~~~~~~~~~~~~~
237 |
238 | Sismo comes with the most common notifiers but you can create new ones very
239 | easily: extend the `Sismo\Notifier\Notifier` abstract class and implement the
240 | `notify()` method:
241 |
242 | .. code-block:: php
243 |
244 | public function notify(Commit $commit)
245 | {
246 | // do something with the commit
247 | }
248 |
249 | The `Commit`_ object has many methods that gives you a lot of information
250 | about the commit and its build. You can also get general information about the
251 | project by calling `getProject()`_.
252 |
253 | Use Sismo with composer
254 | ~~~~~~~~~~~~~~~~~~~~~~~
255 |
256 | If a majority of your projects use `composer`_, you can configure Sismo
257 | to install dependencies before running `phpunit`. Add the following code
258 | to your config file:
259 |
260 | .. code-block:: php
261 |
262 | Sismo\Project::setDefaultCommand('if [ -f composer.json ]; then composer install; fi && phpunit');
263 |
264 | .. _Sismo: http://sismo.sensiolabs.org/
265 | .. _sismo.php: http://sismo.sensiolabs.org/get/sismo.php
266 | .. _CCMenu: http://ccmenu.sourceforge.net/
267 | .. _CCTray: http://confluence.public.thoughtworks.org/display/CCNET/CCTray
268 | .. _CCMonitor: http://code.google.com/p/cc-monitor/
269 | .. _JCCTray: http://sourceforge.net/projects/jcctray/
270 | .. _Commit: http://sismo.sensiolabs.org/api/index.html?q=Sismo\Commit
271 | .. _getProject(): http://sismo.sensiolabs.org/api/index.html?q=Sismo\Project
272 | .. _composer: https://getcomposer.org/doc/00-intro.md#globally
273 |
--------------------------------------------------------------------------------
/src/Sismo/Project.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo;
13 |
14 | use Sismo\Notifier\Notifier;
15 |
16 | /**
17 | * Represents a project.
18 | *
19 | * @author Fabien Potencier
20 | */
21 | class Project
22 | {
23 | protected static $defaultCommand = 'phpunit';
24 |
25 | protected $name;
26 | protected $slug;
27 | protected $repository;
28 | protected $branch = 'master';
29 | protected $command;
30 | protected $urlPattern;
31 | protected $commits = array();
32 | protected $building = false;
33 | protected $notifiers = array();
34 |
35 | /**
36 | * Constructor.
37 | *
38 | * @param string $name The project name
39 | * @param string $repository The repository URL
40 | * @param array $notifiers An array of Notifier instances
41 | * @param string $slug The project slug
42 | */
43 | public function __construct($name, $repository = null, $notifiers = array(), $slug = null)
44 | {
45 | $this->name = $name;
46 | $this->slug = $slug ?: $this->slugify($name);
47 | $this->command = static::$defaultCommand;
48 |
49 | if (null !== $repository) {
50 | $this->setRepository($repository);
51 | }
52 |
53 | if (!is_array($notifiers)) {
54 | $notifiers = array($notifiers);
55 | }
56 |
57 | foreach ($notifiers as $notifier) {
58 | $this->addNotifier($notifier);
59 | }
60 | }
61 |
62 | /**
63 | * Returns a string representation of the Project.
64 | *
65 | * @return string The string representation of the Project
66 | */
67 | public function __toString()
68 | {
69 | return $this->name;
70 | }
71 |
72 | /**
73 | * Toggles the building status flag.
74 | *
75 | * @param bool $bool The build status flag
76 | */
77 | public function setBuilding($bool)
78 | {
79 | $this->building = (bool) $bool;
80 | }
81 |
82 | /**
83 | * Returns true if the project is currently being built.
84 | *
85 | * @return bool true if the project is currently being built, false otherwise
86 | */
87 | public function isBuilding()
88 | {
89 | return $this->building;
90 | }
91 |
92 | /**
93 | * Adds a notifier.
94 | *
95 | * @param Notifier $notifier A Notifier instance
96 | */
97 | public function addNotifier(Notifier $notifier)
98 | {
99 | $this->notifiers[] = $notifier;
100 |
101 | return $this;
102 | }
103 |
104 | /**
105 | * Gets the notifiers associated with this project.
106 | *
107 | * @return array An array of Notifier instances
108 | */
109 | public function getNotifiers()
110 | {
111 | return $this->notifiers;
112 | }
113 |
114 | /**
115 | * Sets the branch of the project we are interested in.
116 | *
117 | * @param string $branch The branch name
118 | */
119 | public function setBranch($branch)
120 | {
121 | $this->branch = $branch;
122 |
123 | return $this;
124 | }
125 |
126 | /**
127 | * Gets the project branch name.
128 | *
129 | * @return string The branch name
130 | */
131 | public function getBranch()
132 | {
133 | return $this->branch;
134 | }
135 |
136 | /**
137 | * Sets the commits associated with the project.
138 | *
139 | * @param array $commits An array of Commit instances
140 | */
141 | public function setCommits(array $commits = array())
142 | {
143 | $this->commits = $commits;
144 | }
145 |
146 | /**
147 | * Gets the commits associated with the project.
148 | *
149 | * @return array An array of Commit instances
150 | */
151 | public function getCommits()
152 | {
153 | return $this->commits;
154 | }
155 |
156 | /**
157 | * Gets the latest commit of the project.
158 | *
159 | * @return Commit A Commit instance
160 | */
161 | public function getLatestCommit()
162 | {
163 | return $this->commits ? $this->commits[0] : null;
164 | }
165 |
166 | /**
167 | * Gets the build status code of the latest commit.
168 | *
169 | * If the commit has been built, it returns the Commit::getStatusCode()
170 | * value; if not, it returns "no_build".
171 | *
172 | * @return string The build status code for the latest commit
173 | *
174 | * @see Commit::getStatusCode()
175 | */
176 | public function getStatusCode()
177 | {
178 | return !$this->commits ? 'no_build' : $this->commits[0]->getStatusCode();
179 | }
180 |
181 | /**
182 | * Gets the build status of the latest commit.
183 | *
184 | * If the commit has been built, it returns the Commit::getStatus()
185 | * value; if not, it returns "not built yet".
186 | *
187 | * @return string The build status for the latest commit
188 | *
189 | * @see Commit::getStatus()
190 | */
191 | public function getStatus()
192 | {
193 | return !$this->commits ? 'not built yet' : $this->commits[0]->getStatus();
194 | }
195 |
196 | /**
197 | * Gets the build status of the latest commit as a Cruise Control string.
198 | *
199 | * The value is one of "Unknown", "Success", or "Failure".
200 | *
201 | * @return string The build status for the latest commit
202 | */
203 | public function getCCStatus()
204 | {
205 | if (!$this->commits || !$this->commits[0]->isBuilt()) {
206 | return 'Unknown';
207 | }
208 |
209 | return $this->commits[0]->isSuccessful() ? 'Success' : 'Failure';
210 | }
211 |
212 | /**
213 | * Gets the build status activity of the latest commit as a Cruise Control string.
214 | *
215 | * The value is one of "Building" or "Sleeping".
216 | *
217 | * @return string The build status activity for the latest commit
218 | */
219 | public function getCCActivity()
220 | {
221 | return $this->commits && $this->commits[0]->isBuilding() ? 'Building' : 'Sleeping';
222 | }
223 |
224 | /**
225 | * Gets the project name.
226 | *
227 | * @return string The project name
228 | */
229 | public function getName()
230 | {
231 | return $this->name;
232 | }
233 |
234 | /**
235 | * Gets the project short name.
236 | *
237 | * @return string The project short name
238 | */
239 | public function getShortName()
240 | {
241 | list($name) = explode('(', $this->name);
242 |
243 | return trim($name);
244 | }
245 |
246 | /**
247 | * Gets the project sub name.
248 | *
249 | * @return string The project sub name
250 | */
251 | public function getSubName()
252 | {
253 | if (false !== $pos = strpos($this->name, '(')) {
254 | return trim(substr($this->name, $pos + 1, -1));
255 | }
256 |
257 | return '';
258 | }
259 |
260 | /**
261 | * Gets the project slug.
262 | *
263 | * @return string The project slug
264 | */
265 | public function getSlug()
266 | {
267 | return $this->slug;
268 | }
269 |
270 | /**
271 | * Sets the project slug.
272 | *
273 | * @param string $slug The project slug
274 | */
275 | public function setSlug($slug)
276 | {
277 | $this->slug = $slug;
278 |
279 | return $this;
280 | }
281 |
282 | /**
283 | * Gets the project repository URL.
284 | *
285 | * @return string The project repository URL
286 | */
287 | public function getRepository()
288 | {
289 | return $this->repository;
290 | }
291 |
292 | /**
293 | * Sets the project repository URL.
294 | *
295 | * @param string $url The project repository URL
296 | */
297 | public function setRepository($url)
298 | {
299 | if (false !== strpos($url, '@')) {
300 | list($url, $branch) = explode('@', $url);
301 | $this->branch = $branch;
302 | }
303 |
304 | $this->repository = $url;
305 |
306 | return $this;
307 | }
308 |
309 | /**
310 | * Gets the command to use to build the project.
311 | *
312 | * @return string The build command
313 | */
314 | public function getCommand()
315 | {
316 | return $this->command;
317 | }
318 |
319 | /**
320 | * Sets the command to use to build the project.
321 | *
322 | * @param string $command The build command
323 | */
324 | public function setCommand($command)
325 | {
326 | $this->command = $command;
327 |
328 | return $this;
329 | }
330 |
331 | public static function setDefaultCommand($command)
332 | {
333 | self::$defaultCommand = $command;
334 | }
335 |
336 | /**
337 | * Gets the URL pattern to use to link to commits.
338 | *
339 | * @return string The URL pattern
340 | */
341 | public function getUrlPattern()
342 | {
343 | return $this->urlPattern;
344 | }
345 |
346 | /**
347 | * Sets the URL pattern to use to link to commits.
348 | *
349 | * In a pattern, you can use the "%commit%" placeholder to reference
350 | * the commit SHA1.
351 | *
352 | * @return string The URL pattern
353 | */
354 | public function setUrlPattern($pattern)
355 | {
356 | $this->urlPattern = $pattern;
357 |
358 | return $this;
359 | }
360 |
361 | // code derived from http://php.vrana.cz/vytvoreni-pratelskeho-url.php
362 | private function slugify($text)
363 | {
364 | // replace non letter or digits by -
365 | $text = preg_replace('#[^\\pL\d]+#u', '-', $text);
366 |
367 | // trim
368 | $text = trim($text, '-');
369 |
370 | // transliterate
371 | if (function_exists('iconv')) {
372 | $text = iconv('utf-8', 'us-ascii//TRANSLIT', $text);
373 | }
374 |
375 | // lowercase
376 | $text = strtolower($text);
377 |
378 | // remove unwanted characters
379 | $text = preg_replace('#[^-\w]+#', '', $text);
380 |
381 | if (empty($text)) {
382 | throw new \RuntimeException(sprintf('Unable to compute a slug for "%s". Define it explicitly.', $text));
383 | }
384 |
385 | return $text;
386 | }
387 | }
388 |
--------------------------------------------------------------------------------
/compile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | load($classes, __DIR__.'/build', 'sismo', false);
155 |
156 | $classes = str_replace(' '$base/ccmonitor.twig.xml',
171 | 'error.twig' => '$base/error.twig',
172 | 'layout.twig' => '$base/layout.twig',
173 | 'project.twig' => '$base/project.twig',
174 | 'projects.twig' => '$base/projects.twig',
175 | );
176 |
177 | return \$templates[\$name];
178 | }
179 | public function isFresh(\$name, \$time) { return true; }
180 | }
181 |
182 | \$app['twig.loader'] = \$app->share(function () { return new FakeTwigLoader(); });
183 | }
184 | EOF;
185 |
186 | $templates = '';
187 | $twig = new Twig_Environment(new Twig_Loader_Filesystem(__DIR__.'/src/templates'), array('cache' => sys_get_temp_dir()));
188 | $twig->addExtension(new Symfony\Bridge\Twig\Extension\RoutingExtension(new Symfony\Component\Routing\Generator\UrlGenerator(new Symfony\Component\Routing\RouteCollection(), new Symfony\Component\Routing\RequestContext())));
189 | foreach (array('ccmonitor.twig.xml', 'error.twig', 'layout.twig', 'project.twig', 'projects.twig') as $name) {
190 | $templates .= str_replace('compileSource($twig->getLoader()->getSource($name), $name));
191 | }
192 | $templates = 'namespace {'.str_replace('basepath', 'baseurl', $templates).'}';
193 |
194 | $cli = 'namespace {'.str_replace('get(\'/css/sismo.css\', function() { return new Response(\'%s\', 200, array(\'Content-Type\' => \'text/css\')); });', str_replace("'", "\\'", file_get_contents(__DIR__.'/web/css/sismo.css')));
201 | foreach (array('failed.png', 'header.png', 'hr.png', 'nobuild.png', 'success.png', 'sensio-labs-product.png') as $image) {
202 | $assets .= sprintf('$app->get(\'/images/%s\', function() { return new Response(base64_decode(\'%s\'), 200, array(\'Content-Type\' => \'image/png\')); });', $image, base64_encode(file_get_contents(__DIR__.'/web/images/'.$image)));
203 | }
204 | $assets = 'namespace { use Symfony\Component\HttpFoundation\Response; '.$assets.'}';
205 |
206 | $content = "run();
217 | } else {
218 | \$app->run();
219 | }
220 | }
221 | ";
222 |
223 | // remove require_once calls
224 | $content = preg_replace('#require_once[^;]+?;#', '', $content);
225 |
226 | file_put_contents(__DIR__.'/build/sismo.php', $content);
227 | file_put_contents(__DIR__.'/build/sismo.php',
228 | "
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Storage;
13 |
14 | use Sismo\Project;
15 | use Sismo\Commit;
16 |
17 | /**
18 | * Stores projects and builds information.
19 | *
20 | * @author Toni Uebernickel
21 | */
22 | class PdoStorage implements StorageInterface
23 | {
24 | /**
25 | * An established database connection.
26 | *
27 | * @var \PDO
28 | */
29 | private $db;
30 |
31 | /**
32 | * Constructor.
33 | *
34 | * @param \PDO $con An established PDO connection.
35 | */
36 | public function __construct(\PDO $con)
37 | {
38 | $this->db = $con;
39 | $this->db->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
40 | }
41 |
42 | /**
43 | * Create a PdoStorage by establishing a PDO connection.
44 | *
45 | * @throws \PDOException If the attempt to connect to the requested database fails.
46 | *
47 | * @param string $dsn The data source name.
48 | * @param string $username The username to login with.
49 | * @param string $passwd The password of the given user.
50 | * @param array $options Additional options to pass to the PDO driver.
51 | *
52 | * @return PdoStorage The created storage on the defined connection.
53 | */
54 | public static function create($dsn, $username = null, $passwd = null, array $options = array())
55 | {
56 | return new self(new \PDO($dsn, $username, $passwd, $options));
57 | }
58 |
59 | /**
60 | * Retrieves a commit out of a project.
61 | *
62 | * @param Project $project The project this commit is part of.
63 | * @param string $sha The hash of the commit to retrieve.
64 | *
65 | * @return Commit
66 | */
67 | public function getCommit(Project $project, $sha)
68 | {
69 | $stmt = $this->db->prepare('SELECT slug, sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug AND sha = :sha');
70 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
71 | $stmt->bindValue(':sha', $sha, \PDO::PARAM_STR);
72 |
73 | if ($stmt->execute()) {
74 | if (false !== $result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
75 | return $this->createCommit($project, $result);
76 | }
77 | } else {
78 | // @codeCoverageIgnoreStart
79 | throw new \RuntimeException(sprintf('Unable to retrieve commit "%s" from project "%s".', $sha, $project), 1);
80 | // @codeCoverageIgnoreEnd
81 | }
82 |
83 | return false;
84 | }
85 |
86 | /**
87 | * Initiate, create and save a new commit.
88 | *
89 | * @param Project $project The project of the new commit.
90 | * @param string $sha The hash of the commit.
91 | * @param string $author The name of the author of the new commit.
92 | * @param \DateTime $date The date the new commit was created originally (e.g. by external resources).
93 | * @param string $message The commit message.
94 | *
95 | * @return Commit The newly created commit.
96 | */
97 | public function initCommit(Project $project, $sha, $author, \DateTime $date, $message)
98 | {
99 | $stmt = $this->db->prepare('SELECT COUNT(*) FROM `commit` WHERE slug = :slug');
100 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
101 |
102 | if (false === $stmt->execute()) {
103 | // @codeCoverageIgnoreStart
104 | throw new \RuntimeException(sprintf('Unable to verify existence of commit "%s" from project "%s".', $sha, $project->getName()));
105 | // @codeCoverageIgnoreEnd
106 | }
107 |
108 | if ($stmt->fetchColumn(0)) {
109 | $stmt = $this->db->prepare('UPDATE `commit` SET slug = :slug, sha = :sha, author = :author, date = :date, message = :message, status = :status, output = :output, build_date = :build_date WHERE slug = :slug');
110 | } else {
111 | $stmt = $this->db->prepare('INSERT INTO `commit` (slug, sha, author, date, message, status, output, build_date) VALUES (:slug, :sha, :author, :date, :message, :status, :output, :build_date)');
112 | }
113 |
114 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
115 | $stmt->bindValue(':sha', $sha, \PDO::PARAM_STR);
116 | $stmt->bindValue(':author', $author, \PDO::PARAM_STR);
117 | $stmt->bindValue(':date', $date->format('Y-m-d H:i:s'), \PDO::PARAM_STR);
118 | $stmt->bindValue(':message', $message, \PDO::PARAM_STR);
119 | $stmt->bindValue(':status', 'building', \PDO::PARAM_STR);
120 | $stmt->bindValue(':output', '', \PDO::PARAM_STR);
121 | $stmt->bindValue(':build_date', '', \PDO::PARAM_STR);
122 |
123 | if (false === $stmt->execute()) {
124 | // @codeCoverageIgnoreStart
125 | throw new \RuntimeException(sprintf('Unable to save commit "%s" from project "%s".', $sha, $project->getName()));
126 | // @codeCoverageIgnoreEnd
127 | }
128 |
129 | $commit = new Commit($project, $sha);
130 | $commit->setAuthor($author);
131 | $commit->setMessage($message);
132 | $commit->setDate($date);
133 |
134 | return $commit;
135 | }
136 |
137 | /**
138 | * Create or update the information of a project.
139 | *
140 | * If the project is already available, the information of the existing project will be updated.
141 | *
142 | * @param Project $project The project to create or update.
143 | *
144 | * @return StorageInterface $this
145 | */
146 | public function updateProject(Project $project)
147 | {
148 | $stmt = $this->db->prepare('SELECT COUNT(*) FROM project WHERE slug = :slug');
149 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
150 |
151 | if (false === $stmt->execute()) {
152 | // @codeCoverageIgnoreStart
153 | throw new \RuntimeException(sprintf('Unable to verify existence of project "%s".', $project->getName()));
154 | // @codeCoverageIgnoreEnd
155 | }
156 |
157 | if ($stmt->fetchColumn(0)) {
158 | $stmt = $this->db->prepare('UPDATE project SET slug = :slug, name = :name, repository = :repository, branch = :branch, command = :command, url_pattern = :url_pattern WHERE slug = :slug');
159 | } else {
160 | $stmt = $this->db->prepare('INSERT INTO project (slug, name, repository, branch, command, url_pattern) VALUES (:slug, :name, :repository, :branch, :command, :url_pattern)');
161 | }
162 |
163 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
164 | $stmt->bindValue(':name', $project->getName(), \PDO::PARAM_STR);
165 | $stmt->bindValue(':repository', $project->getRepository(), \PDO::PARAM_STR);
166 | $stmt->bindValue(':branch', $project->getBranch(), \PDO::PARAM_STR);
167 | $stmt->bindValue(':command', $project->getCommand(), \PDO::PARAM_STR);
168 | $stmt->bindValue(':url_pattern', $project->getUrlPattern(), \PDO::PARAM_STR);
169 |
170 | if (false === $stmt->execute()) {
171 | // @codeCoverageIgnoreStart
172 | throw new \RuntimeException(sprintf('Unable to save project "%s".', $project->getName()));
173 | // @codeCoverageIgnoreEnd
174 | }
175 |
176 | // related commits
177 | $stmt = $this->db->prepare('SELECT sha, author, date, build_date, message, status, output FROM `commit` WHERE slug = :slug ORDER BY `status` = "building" DESC, build_date DESC LIMIT 100');
178 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
179 |
180 | if (false === $stmt->execute()) {
181 | // @codeCoverageIgnoreStart
182 | throw new \RuntimeException(sprintf('Unable to get latest commit for project "%s".', $project->getName()));
183 | // @codeCoverageIgnoreEnd
184 | }
185 |
186 | $commits = array();
187 | while ($result = $stmt->fetch(\PDO::FETCH_ASSOC)) {
188 | $commits[] = $this->createCommit($project, $result);
189 | }
190 |
191 | $project->setCommits($commits);
192 |
193 | // project building?
194 | $stmt = $this->db->prepare('SELECT COUNT(*) AS count FROM `commit` WHERE slug = :slug AND status = "building"');
195 | $stmt->bindValue(':slug', $project->getSlug(), \PDO::PARAM_STR);
196 |
197 | $building = false;
198 | if ($stmt->execute() and intval($stmt->fetchColumn(0))) {
199 | $building = true;
200 | }
201 |
202 | $project->setBuilding($building);
203 | }
204 |
205 | /**
206 | * Update the commits information.
207 | *
208 | * The commit is identified by its sha hash.
209 | *
210 | * @param Commit $commit
211 | *
212 | * @return StorageInterface $this
213 | */
214 | public function updateCommit(Commit $commit)
215 | {
216 | $stmt = $this->db->prepare('UPDATE `commit` SET status = :status, output = :output, build_date = :current_date WHERE slug = :slug AND sha = :sha');
217 | $stmt->bindValue(':slug', $commit->getProject()->getSlug(), \PDO::PARAM_STR);
218 | $stmt->bindValue(':sha', $commit->getSha(), \PDO::PARAM_STR);
219 | $stmt->bindValue(':status', $commit->getStatusCode(), \PDO::PARAM_STR);
220 | $stmt->bindValue(':output', $commit->getOutput(), \PDO::PARAM_STR);
221 | $stmt->bindValue(':current_date', date('Y-m-d H:i:s'), \PDO::PARAM_STR);
222 |
223 | if (false === $stmt->execute()) {
224 | // @codeCoverageIgnoreStart
225 | throw new \RuntimeException(sprintf('Unable to save build "%s@%s".', $commit->getProject()->getName(), $commit->getSha()));
226 | // @codeCoverageIgnoreEnd
227 | }
228 | }
229 |
230 | private function createCommit($project, $result)
231 | {
232 | $commit = new Commit($project, $result['sha']);
233 | $commit->setAuthor($result['author']);
234 | $commit->setMessage($result['message']);
235 | $commit->setDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['date']));
236 | if ($result['build_date']) {
237 | $commit->setBuildDate(\DateTime::createFromFormat('Y-m-d H:i:s', $result['build_date']));
238 | }
239 | $commit->setStatusCode($result['status']);
240 | $commit->setOutput($result['output']);
241 |
242 | return $commit;
243 | }
244 |
245 | /**
246 | * Shutdown the storage and all of its external resources.
247 | *
248 | * @return StorageInterface $this
249 | */
250 | public function close()
251 | {
252 | unset($this->db);
253 | }
254 |
255 | public function __destruct()
256 | {
257 | $this->close();
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/src/console.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | use Sismo\Sismo;
13 | use Sismo\BuildException;
14 | use Symfony\Component\Console\Application;
15 | use Symfony\Component\Console\Input\InputInterface;
16 | use Symfony\Component\Console\Input\InputArgument;
17 | use Symfony\Component\Console\Input\InputOption;
18 | use Symfony\Component\Console\Output\OutputInterface;
19 | use Symfony\Component\Process\ProcessBuilder;
20 |
21 | $console = new Application('Sismo', Sismo::VERSION);
22 | $console
23 | ->register('output')
24 | ->setDefinition(array(
25 | new InputArgument('slug', InputArgument::REQUIRED, 'Project slug'),
26 | ))
27 | ->setDescription('Displays the latest output for a project')
28 | ->setHelp(<<%command.name% command displays the latest output for a project:
30 |
31 | php %command.full_name% twig
32 | EOF
33 | )
34 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($app) {
35 | $sismo = $app['sismo'];
36 | $slug = $input->getArgument('slug');
37 | if (!$sismo->hasProject($slug)) {
38 | $output->writeln(sprintf('Project "%s" does not exist.', $slug));
39 |
40 | return 1;
41 | }
42 |
43 | $project = $sismo->getProject($slug);
44 |
45 | if (!$project->getLatestCommit()) {
46 | $output->writeln(sprintf('Project "%s" has never been built yet.', $slug));
47 |
48 | return 2;
49 | }
50 |
51 | $output->write($project->getLatestCommit()->getOutput());
52 |
53 | $now = new \DateTime();
54 | $diff = $now->diff($project->getLatestCommit()->getBuildDate());
55 | if ($m = $diff->format('%i')) {
56 | $time = $m.' minutes';
57 | } else {
58 | $time = $diff->format('%s').' seconds';
59 | }
60 | $output->writeln('');
61 | $output->writeln(sprintf('This output was generated by Sismo %s ago', $time));
62 | })
63 | ;
64 |
65 | $console
66 | ->register('projects')
67 | ->setDescription('List available projects')
68 | ->setHelp(<<%command.name% command displays the available projects Sismo can build.
70 | EOF
71 | )
72 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($app) {
73 | $sismo = $app['sismo'];
74 |
75 | $projects = array();
76 | $width = 0;
77 | foreach ($sismo->getProjects() as $slug => $project) {
78 | $projects[$slug] = $project->getName();
79 | $width = strlen($project->getName()) > $width ? strlen($project->getName()) : $width;
80 | }
81 | $width += 2;
82 |
83 | $output->writeln('');
84 | $output->writeln('Available projects:');
85 | foreach ($projects as $slug => $project) {
86 | $output->writeln(sprintf(" %-${width}s %s", $slug, $project));
87 | }
88 |
89 | $output->writeln('');
90 | })
91 | ;
92 |
93 | $console
94 | ->register('build')
95 | ->setDefinition(array(
96 | new InputArgument('slug', InputArgument::OPTIONAL, 'Project slug'),
97 | new InputArgument('sha', InputArgument::OPTIONAL, 'Commit sha'),
98 | new InputOption('force', '', InputOption::VALUE_NONE, 'Force the build'),
99 | new InputOption('local', '', InputOption::VALUE_NONE, 'Disable remote sync'),
100 | new InputOption('silent', '', InputOption::VALUE_NONE, 'Disable notifications'),
101 | new InputOption('timeout', '', InputOption::VALUE_REQUIRED, 'Time limit'),
102 | new InputOption('data-path', '', InputOption::VALUE_REQUIRED, 'The data path'),
103 | new InputOption('config-file', '', InputOption::VALUE_REQUIRED, 'The config file'),
104 | ))
105 | ->setDescription('Build projects')
106 | ->setHelp(<<%command.name% command builds the latest commit
108 | of all configured projects one after the other:
109 |
110 | php %command.full_name%
111 |
112 | The command loads project configurations from
113 | ~/.sismo/config.php. Change it with the
114 | --config-file option:
115 |
116 | php %command.full_name% --config-file=/path/to/config.php
117 |
118 | Data (repository, DB, ...) are stored in ~/.sismo/data/.
119 | The --data-path option allows you to change the default:
120 |
121 | php %command.full_name% --data-path=/path/to/data
122 |
123 | Pass the project slug to build a specific project:
124 |
125 | php %command.full_name% twig
126 |
127 | Force a specific commit to be built by passing the SHA:
128 |
129 | php %command.full_name% twig a1ef34
130 |
131 | Use --force to force the built even if it has already been
132 | built previously:
133 |
134 | php %command.full_name% twig a1ef34 --force
135 |
136 | Disable notifications with --silent:
137 |
138 | php %command.full_name% twig a1ef34 --silent
139 |
140 | Disable repository synchonization with --local:
141 |
142 | php %command.full_name% twig a1ef34 --local
143 |
144 | Limit the time (in seconds) spent by the command building projects by using
145 | the --timeout option:
146 |
147 | php %command.full_name% twig --timeout 3600
148 |
149 | When you use this command as a cron job, --timeout can avoid
150 | the command to be run concurrently. Be warned that this is a rough estimate as
151 | the time is only checked between two builds. When a build is started, it won't
152 | be stopped if the time limit is over.
153 |
154 | Use the --verbose option to debug builds in case of a
155 | problem.
156 | EOF
157 | )
158 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($app) {
159 | if ($input->getOption('data-path')) {
160 | $app['data.path'] = $input->getOption('data-path');
161 | }
162 | if ($input->getOption('config-file')) {
163 | $app['config.file'] = $input->getOption('config-file');
164 | }
165 | $sismo = $app['sismo'];
166 |
167 | if ($slug = $input->getArgument('slug')) {
168 | if (!$sismo->hasProject($slug)) {
169 | $output->writeln(sprintf('Project "%s" does not exist.', $slug));
170 |
171 | return 1;
172 | }
173 |
174 | $projects = array($sismo->getProject($slug));
175 | } else {
176 | $projects = $sismo->getProjects();
177 | }
178 |
179 | $start = time();
180 | $startedOut = false;
181 | $startedErr = false;
182 | $callback = null;
183 | if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) {
184 | $callback = function ($type, $buffer) use ($output, &$startedOut, &$startedErr) {
185 | if ('err' === $type) {
186 | if (!$startedErr) {
187 | $output->write("\n ERR > ");
188 | $startedErr = true;
189 | $startedOut = false;
190 | }
191 |
192 | $output->write(str_replace("\n", "\n ERR > ", $buffer));
193 | } else {
194 | if (!$startedOut) {
195 | $output->write("\n OUT > ");
196 | $startedOut = true;
197 | $startedErr = false;
198 | }
199 |
200 | $output->write(str_replace("\n", "\n OUT > ", $buffer));
201 | }
202 | };
203 | }
204 |
205 | $flags = 0;
206 | if ($input->getOption('force')) {
207 | $flags = $flags | Sismo::FORCE_BUILD;
208 | }
209 | if ($input->getOption('local')) {
210 | $flags = $flags | Sismo::LOCAL_BUILD;
211 | }
212 | if ($input->getOption('silent')) {
213 | $flags = $flags | Sismo::SILENT_BUILD;
214 | }
215 |
216 | $returnValue = 0;
217 | foreach ($projects as $project) {
218 | // out of time?
219 | if ($input->getOption('timeout') && time() - $start > $input->getOption('timeout')) {
220 | break;
221 | }
222 |
223 | try {
224 | $output->writeln(sprintf('Building Project "%s" (into "%s")', $project, $app['builder']->getBuildDir($project)));
225 | $sismo->build($project, $input->getArgument('sha'), $flags, $callback);
226 |
227 | $output->writeln('');
228 | } catch (BuildException $e) {
229 | $output->writeln("\n".sprintf('%s', $e->getMessage()));
230 |
231 | $returnValue = 1;
232 | }
233 | }
234 |
235 | return $returnValue;
236 | })
237 | ;
238 |
239 | $console
240 | ->register('run')
241 | ->setDefinition(array(
242 | new InputArgument('address', InputArgument::OPTIONAL, 'Address:port', 'localhost:9000'),
243 | ))
244 | ->setDescription('Runs Sismo with PHP built-in web server')
245 | ->setHelp(<<%command.name% command runs the embedded Sismo web server:
247 |
248 | %command.full_name%
249 |
250 | You can also customize the default address and port the web server listens to:
251 |
252 | %command.full_name% 127.0.0.1:8080
253 | EOF
254 | )
255 | ->setCode(function (InputInterface $input, OutputInterface $output) use ($console) {
256 |
257 | if (version_compare(PHP_VERSION, '5.4.0') < 0) {
258 | throw new \Exception('This feature only runs with PHP 5.4.0 or higher.');
259 | }
260 |
261 | $sismo = __DIR__.'/sismo.php';
262 | while (!file_exists($sismo)) {
263 | $dialog = $console->getHelperSet()->get('dialog');
264 | $sismo = $dialog->ask($output, sprintf('I cannot find "%s". What\'s the absoulte path of "sismo.php"? ', $sismo), __DIR__.'/sismo.php');
265 | }
266 |
267 | $output->writeln(sprintf("Sismo running on %s\n", $input->getArgument('address')));
268 |
269 | $builder = new ProcessBuilder(array(PHP_BINARY, '-S', $input->getArgument('address'), $sismo));
270 |
271 | $builder->setWorkingDirectory(getcwd());
272 | $builder->setTimeout(null);
273 | $builder->getProcess()->run(function ($type, $buffer) use ($output) {
274 | if (OutputInterface::VERBOSITY_VERBOSE === $output->getVerbosity()) {
275 | $output->write($buffer);
276 | }
277 | });
278 | })
279 | ;
280 |
281 | return $console;
282 |
--------------------------------------------------------------------------------
/tests/Sismo/Tests/SismoTest.php:
--------------------------------------------------------------------------------
1 |
7 | *
8 | * This source file is subject to the MIT license that is bundled
9 | * with this source code in the file LICENSE.
10 | */
11 |
12 | namespace Sismo\Tests;
13 |
14 | use Sismo\Sismo;
15 | use Sismo\Project;
16 |
17 | class SismoTest extends \PHPUnit_Framework_TestCase
18 | {
19 | public function testProject()
20 | {
21 | $sismo = new Sismo($this->getStorage(), $this->getBuilder());
22 | $this->assertEquals(array(), $sismo->getProjects());
23 | $this->assertFalse($sismo->hasProject('twig'));
24 |
25 | $sismo->addProject($project = new Project('Twig'));
26 | $this->assertTrue($sismo->hasProject('twig'));
27 | $this->assertSame($project, $sismo->getProject('twig'));
28 |
29 | $sismo->addProject($project1 = new Project('Silex'));
30 |
31 | $this->assertSame(array('twig' => $project, 'silex' => $project1), $sismo->getProjects());
32 | }
33 |
34 | public function testAddProjectSaveIt()
35 | {
36 | $storage = $this->getStorage();
37 | $storage->expects($this->once())->method('updateProject');
38 |
39 | $sismo = new Sismo($storage, $this->getBuilder());
40 | $sismo->addProject($project = new Project('Twig'));
41 | $sismo->getProject('twig');
42 | }
43 |
44 | /**
45 | * @expectedException \InvalidArgumentException
46 | */
47 | public function testGetProjectWhenProjectDoesNotExist()
48 | {
49 | $sismo = new Sismo($this->getStorage(), $this->getBuilder());
50 | $sismo->getProject('twig');
51 | }
52 |
53 | public function testBuildIsNotCalledWhenCommitIsAlreadyBuilt()
54 | {
55 | // an already built commit
56 | $commit = $this->getCommit();
57 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true));
58 |
59 | // build won't be triggered
60 | $builder = $this->getBuilder();
61 | $builder->expects($this->never())->method('build');
62 |
63 | $sismo = new Sismo($this->getStorage($commit), $builder);
64 | $sismo->build($this->getProject());
65 | }
66 |
67 | public function testBuildIsForcedWhenCommitIsAlreadyBuiltAndForceIsTrue()
68 | {
69 | // an already built commit
70 | $commit = $this->getCommit();
71 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(true));
72 |
73 | // build is triggered because of FORCE_BUILD flags
74 | $builder = $this->getBuilder();
75 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
76 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess()));
77 |
78 | $sismo = new Sismo($this->getStorage($commit), $builder);
79 | $sismo->build($this->getProject(), null, Sismo::FORCE_BUILD);
80 | }
81 |
82 | public function testBuildIsNotCalledWhenProjectIsBuilding()
83 | {
84 | // a project with a running build
85 | $project = $this->getProject();
86 | $project->expects($this->once())->method('isBuilding')->will($this->returnValue(true));
87 |
88 | // build won't be triggered
89 | $builder = $this->getBuilder();
90 | $builder->expects($this->never())->method('build');
91 |
92 | $sismo = new Sismo($this->getStorage(), $builder);
93 | $sismo->build($project);
94 | }
95 |
96 | public function testBuildIsForcedWhenProjectIsBuildingAndForceIsTrue()
97 | {
98 | // a project with a running build
99 | $project = $this->getProject();
100 | $project->expects($this->once())->method('isBuilding')->will($this->returnValue(true));
101 |
102 | // build is triggered because of FORCE_BUILD flags
103 | $builder = $this->getBuilder();
104 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
105 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess()));
106 |
107 | $commit = $this->getCommit();
108 | $commit->expects($this->once())->method('isBuilt')->will($this->returnValue(false));
109 |
110 | $sismo = new Sismo($this->getStorage($commit), $builder);
111 | $sismo->build($project, null, Sismo::FORCE_BUILD);
112 | }
113 |
114 | public function testBuildIsForcedWhenCommitDoesNotExist()
115 | {
116 | // build is triggered as commit does not exist
117 | $builder = $this->getBuilder();
118 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
119 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess()));
120 |
121 | $storage = $this->getStorage();
122 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($this->getCommit()));
123 |
124 | $sismo = new Sismo($storage, $builder);
125 | $sismo->build($this->getProject());
126 | }
127 |
128 | public function testBuildWithNotifiers()
129 | {
130 | // build is triggered as commit does not exist
131 | $builder = $this->getBuilder();
132 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
133 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess()));
134 |
135 | $storage = $this->getStorage();
136 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($this->getCommit()));
137 |
138 | // notifier will be called
139 | $notifier = $this->getNotifier();
140 | $notifier->expects($this->once())->method('notify');
141 |
142 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock();
143 | $project->expects($this->once())->method('getNotifiers')->will($this->returnValue(array($notifier)));
144 |
145 | $sismo = new Sismo($storage, $builder);
146 | $sismo->build($project);
147 | }
148 |
149 | public function testBuildWithNotifiersWhenSilentIsTrue()
150 | {
151 | // build is triggered as commit does not exist
152 | $builder = $this->getBuilder();
153 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
154 | $builder->expects($this->once())->method('build')->will($this->returnValue($this->getProcess()));
155 |
156 | $storage = $this->getStorage();
157 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($this->getCommit()));
158 |
159 | // notifier won't be called
160 | $notifier = $this->getNotifier();
161 | $notifier->expects($this->never())->method('notify');
162 |
163 | // notifiers won't be get from project
164 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock();
165 | $project->expects($this->never())->method('getNotifiers')->will($this->returnValue(array($notifier)));
166 |
167 | $sismo = new Sismo($storage, $builder);
168 | $sismo->build($project, null, Sismo::SILENT_BUILD);
169 | }
170 |
171 | public function testCommitResultForSuccessBuild()
172 | {
173 | // build is a success
174 | $process = $this->getProcess();
175 | $process->expects($this->once())->method('isSuccessful')->will($this->returnValue(true));
176 | $process->expects($this->once())->method('getOutput')->will($this->returnValue('foo'));
177 |
178 | // build is triggered as commit does not exist
179 | $builder = $this->getBuilder();
180 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
181 | $builder->expects($this->once())->method('build')->will($this->returnValue($process));
182 |
183 | // check commit status
184 | $commit = $this->getCommit();
185 | $commit->expects($this->once())->method('setStatusCode')->with($this->equalTo('success'));
186 | $commit->expects($this->once())->method('setOutput')->with($this->equalTo('foo'));
187 |
188 | // check that storage is updated
189 | $storage = $this->getStorage();
190 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($commit));
191 | $storage->expects($this->once())->method('updateCommit')->with($this->equalTo($commit));
192 |
193 | $sismo = new Sismo($storage, $builder);
194 | $sismo->build($this->getProject());
195 | }
196 |
197 | public function testCommitResultForSuccessFail()
198 | {
199 | // build is a fail
200 | $process = $this->getProcess();
201 | $process->expects($this->once())->method('isSuccessful')->will($this->returnValue(false));
202 | $process->expects($this->once())->method('getOutput')->will($this->returnValue('foo'));
203 | $process->expects($this->once())->method('getErrorOutput')->will($this->returnValue('bar'));
204 |
205 | // build is triggered as commit does not exist
206 | $builder = $this->getBuilder();
207 | $builder->expects($this->once())->method('prepare')->will($this->returnValue(array('sha1', 'fabien', '2011-01-01 01:01:01 +0200', 'initial commit')));
208 | $builder->expects($this->once())->method('build')->will($this->returnValue($process));
209 |
210 | // check commit status
211 | $commit = $this->getCommit();
212 | $commit->expects($this->once())->method('setStatusCode')->with($this->equalTo('failed'));
213 | $commit->expects($this->once())->method('setOutput')->with($this->matchesRegularExpression('/foo.*bar/s'));
214 |
215 | // check that storage is updated
216 | $storage = $this->getStorage();
217 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($commit));
218 | $storage->expects($this->once())->method('updateCommit')->with($this->equalTo($commit));
219 |
220 | $sismo = new Sismo($storage, $builder);
221 | $sismo->build($this->getProject());
222 | }
223 |
224 | private function getBuilder()
225 | {
226 | return $this->getMockBuilder('Sismo\Builder')->disableOriginalConstructor()->getMock();
227 | }
228 |
229 | private function getNotifier()
230 | {
231 | return $this->getMockBuilder('Sismo\Notifier\Notifier')->disableOriginalConstructor()->getMock();
232 | }
233 |
234 | private function getProject()
235 | {
236 | $project = $this->getMockBuilder('Sismo\Project')->disableOriginalConstructor()->getMock();
237 | $project->expects($this->any())->method('getNotifiers')->will($this->returnValue(array()));
238 |
239 | return $project;
240 | }
241 |
242 | private function getCommit()
243 | {
244 | return $this->getMockBuilder('Sismo\Commit')->disableOriginalConstructor()->getMock();
245 | }
246 |
247 | private function getProcess()
248 | {
249 | return $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock();
250 | }
251 |
252 | private function getStorage($commit = null)
253 | {
254 | $storage = $this->getMockBuilder('Sismo\Storage\Storage')->disableOriginalConstructor()->getMock();
255 | if (null !== $commit) {
256 | $storage->expects($this->once())->method('getCommit')->will($this->returnValue($commit));
257 | $storage->expects($this->any())->method('initCommit')->will($this->returnValue($commit));
258 | }
259 |
260 | return $storage;
261 | }
262 | }
263 |
--------------------------------------------------------------------------------