php-censor/src/Model/Build.php
Dmitry Khomutov 069026bc2d
Added ability to merge in-database project config over in-repository
config instead of only overwrite. This commit solve issues: #14, #70,
#106, #121.
2018-04-15 15:58:23 +07:00

583 lines
14 KiB
PHP

<?php
namespace PHPCensor\Model;
use PHPCensor\Builder;
use PHPCensor\Store\Factory;
use PHPCensor\Store\ProjectStore;
use PHPCensor\Store\BuildErrorStore;
use Psr\Log\LogLevel;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Yaml\Parser as YamlParser;
use PHPCensor\Model\Base\Build as BaseBuild;
/**
* @author Dan Cryer <dan@block8.co.uk>
*/
class Build extends BaseBuild
{
const STAGE_SETUP = 'setup';
const STAGE_TEST = 'test';
const STAGE_DEPLOY = 'deploy';
const STAGE_COMPLETE = 'complete';
const STAGE_SUCCESS = 'success';
const STAGE_FAILURE = 'failure';
const STAGE_FIXED = 'fixed';
const STAGE_BROKEN = 'broken';
/**
* @var integer
*/
protected $newErrorsCount = null;
/**
* @var string
*/
protected $buildDirectory;
/**
* @var string
*/
protected $buildBranchDirectory;
/**
* @return Project|null
*/
public function getProject()
{
$projectId = $this->getProjectId();
if (!$projectId) {
return null;
}
/** @var ProjectStore $projectStore */
$projectStore = Factory::getStore('Project');
return $projectStore->getById($projectId);
}
/**
* @param string $name
* @param mixed $value
*/
public function addExtraValue($name, $value)
{
$extra = json_decode($this->data['extra'], true);
if ($extra === false) {
$extra = [];
}
$extra[$name] = $value;
$this->setExtra($extra);
}
/**
* Set the value of status only if it synced with db. Must not be null.
*
* @param integer $value
*
* @return boolean
*/
public function setStatusSync($value)
{
$this->validateNotNull('status', $value);
$this->validateInt('status', $value);
if ($this->data['status'] !== $value) {
$store = Factory::getStore('Build');
if ($store->updateStatusSync($this, $value)) {
$this->data['status'] = $value;
return true;
}
}
return false;
}
/**
* Get BuildError models by BuildId for this Build.
*
* @return \PHPCensor\Model\BuildError[]
*/
public function getBuildBuildErrors()
{
return Factory::getStore('BuildError')->getByBuildId($this->getId());
}
/**
* Get BuildMeta models by BuildId for this Build.
*
* @return \PHPCensor\Model\BuildMeta[]
*/
public function getBuildBuildMetas()
{
return Factory::getStore('BuildMeta')->getByBuildId($this->getId());
}
/**
* Get link to commit from another source (i.e. Github)
*/
public function getCommitLink()
{
return '#';
}
/**
* Get link to branch from another source (i.e. Github)
*/
public function getBranchLink()
{
return '#';
}
/**
* Get remote branch (from pull request) from another source (i.e. Github)
*/
public function getRemoteBranch()
{
return $this->getExtra('remote_branch');
}
/**
* Get link to remote branch (from pull request) from another source (i.e. Github)
*/
public function getRemoteBranchLink()
{
return '#';
}
/**
* Get link to tag from another source (i.e. Github)
*/
public function getTagLink()
{
return '#';
}
/**
* Return a template to use to generate a link to a specific file.
*
* @return null
*/
public function getFileLinkTemplate()
{
return null;
}
/**
* Send status updates to any relevant third parties (i.e. Github)
*/
public function sendStatusPostback()
{
return false;
}
/**
* @return string
*/
public function getProjectTitle()
{
$project = $this->getProject();
return $project ? $project->getTitle() : "";
}
/**
* Store build metadata
*
* @param string $key
* @param string $value
*/
public function storeMeta($key, $value)
{
$value = json_encode($value);
Factory::getStore('Build')->setMeta($this->getId(), $key, $value);
}
/**
* Is this build successful?
*/
public function isSuccessful()
{
return ($this->getStatus() === self::STATUS_SUCCESS);
}
/**
* @param Builder $builder
*
* @return bool
*
* @throws \Exception
*/
public function handleConfigBeforeClone(Builder $builder)
{
$buildConfig = $this->getProject()->getBuildConfig();
if ($buildConfig) {
$yamlParser = new YamlParser();
$buildConfig = $yamlParser->parse($buildConfig);
if ($buildConfig && is_array($buildConfig)) {
$builder->setConfig($buildConfig);
}
}
return true;
}
/**
* @param Builder $builder
* @param string $buildPath
*
* @return bool
*
* @throws \Exception
*/
protected function handleConfig(Builder $builder, $buildPath)
{
$yamlParser = new YamlParser();
$overwriteBuildConfig = $this->getProject()->getOverwriteBuildConfig();
$buildConfig = $builder->getConfig();
$repositoryConfig = $this->getZeroConfigPlugins($builder);
if (file_exists($buildPath . '/.php-censor.yml')) {
$repositoryConfig = $yamlParser->parse(
file_get_contents($buildPath . '/.php-censor.yml')
);
} elseif (file_exists($buildPath . '/.phpci.yml')) {
$repositoryConfig = $yamlParser->parse(
file_get_contents($buildPath . '/.phpci.yml')
);
} elseif (file_exists($buildPath . '/phpci.yml')) {
$repositoryConfig = $yamlParser->parse(
file_get_contents($buildPath . '/phpci.yml')
);
}
if (isset($repositoryConfig['build_settings']['clone_depth'])) {
$builder->logWarning(
'Option "build_settings.clone_depth" supported only in additional DB project config.' .
' Please move this option to DB config from your in-repository config file (".php-censor.yml").'
);
}
if (!$buildConfig) {
$buildConfig = $repositoryConfig;
} elseif ($buildConfig && !$overwriteBuildConfig) {
$buildConfig = array_replace_recursive($repositoryConfig, $buildConfig);
}
$builder->setConfig($buildConfig);
return true;
}
/**
* Get an array of plugins to run if there's no .php-censor.yml file.
*
* @param Builder $builder
*
* @return array
*/
protected function getZeroConfigPlugins(Builder $builder)
{
$pluginDir = SRC_DIR . 'Plugin/';
$dir = new \DirectoryIterator($pluginDir);
$config = [
'build_settings' => [
'ignore' => [
'vendor',
]
]
];
foreach ($dir as $item) {
if ($item->isDot()) {
continue;
}
if (!$item->isFile()) {
continue;
}
if ($item->getExtension() != 'php') {
continue;
}
$className = '\PHPCensor\Plugin\\'.$item->getBasename('.php');
$reflectedPlugin = new \ReflectionClass($className);
if (!$reflectedPlugin->implementsInterface('\PHPCensor\ZeroConfigPluginInterface')) {
continue;
}
foreach ([Build::STAGE_SETUP, Build::STAGE_TEST] as $stage) {
if ($className::canExecute($stage, $builder, $this)) {
$config[$stage][$className::pluginName()] = [
'zero_config' => true
];
}
}
}
return $config;
}
/**
* Allows specific build types (e.g. Github) to report violations back to their respective services.
*
* @param Builder $builder
* @param string $plugin
* @param string $message
* @param integer $severity
* @param string $file
* @param integer $lineStart
* @param integer $lineEnd
*/
public function reportError(
Builder $builder,
$plugin,
$message,
$severity = BuildError::SEVERITY_NORMAL,
$file = null,
$lineStart = null,
$lineEnd = null
) {
$writer = $builder->getBuildErrorWriter();
$writer->write(
$plugin,
$message,
$severity,
$file,
$lineStart,
$lineEnd
);
}
/**
* @return string|null
*/
public function getBuildDirectory()
{
if (!$this->getId()) {
return null;
}
$createDate = $this->getCreateDate();
if (empty($this->buildDirectory)) {
$this->buildDirectory = $this->getProjectId() . '/' . $this->getId() . '_' . substr(
md5(($this->getId() . '_' . ($createDate ? $createDate->format('Y-m-d H:i:s') : null))
), 0, 8);
}
return $this->buildDirectory;
}
/**
* @return string|null
*/
public function getBuildBranchDirectory()
{
if (!$this->getId()) {
return null;
}
$createDate = $this->getCreateDate();
if (empty($this->buildBranchDirectory)) {
$this->buildBranchDirectory = $this->getProjectId() . '/' . $this->getBranch() . '_' . substr(
md5(($this->getBranch() . '_' . ($createDate ? $createDate->format('Y-m-d H:i:s') : null))
), 0, 8);
}
return $this->buildBranchDirectory;
}
/**
* @return string|null
*/
public function getBuildPath()
{
if (!$this->getId()) {
return null;
}
return RUNTIME_DIR . 'builds/' . $this->getBuildDirectory() . '/';
}
/**
* Removes the build directory.
*
* @param boolean $withArtifacts
*/
public function removeBuildDirectory($withArtifacts = false)
{
// Get the path and remove the trailing slash as this may prompt PHP
// to see this as a directory even if it's a link.
$buildPath = rtrim($this->getBuildPath(), '/');
if (!$buildPath || !is_dir($buildPath)) {
return;
}
try {
$fileSystem = new Filesystem();
if (is_link($buildPath)) {
// Remove the symlink without using recursive.
exec(sprintf('rm "%s"', $buildPath));
} else {
$fileSystem->remove($buildPath);
}
if ($withArtifacts) {
$buildDirectory = $this->getBuildDirectory();
$fileSystem->remove(PUBLIC_DIR . 'artifacts/pdepend/' . $buildDirectory);
$fileSystem->remove(PUBLIC_DIR . 'artifacts/phpunit/' . $buildDirectory);
}
} catch (\Exception $e) {
}
}
/**
* Get the number of seconds a build has been running for.
*
* @return integer
*/
public function getDuration()
{
$start = $this->getStartDate();
if (empty($start)) {
return 0;
}
$end = $this->getFinishDate();
if (empty($end)) {
$end = new \DateTime();
}
return $end->getTimestamp() - $start->getTimestamp();
}
/**
* get time a build has been running for in hour/minute/seconds format (e.g. 1h 21m 45s)
*
* @return string
*/
public function getPrettyDuration()
{
$start = $this->getStartDate();
if (!$start) {
$start = new \DateTime();
}
$end = $this->getFinishDate();
if (!$end) {
$end = new \DateTime();
}
$diff = date_diff($start, $end);
$parts = [];
foreach (['y', 'm', 'd', 'h', 'i', 's'] as $timePart) {
if ($diff->{$timePart} != 0) {
$parts[] = $diff->{$timePart} . ($timePart == 'i' ? 'm' : $timePart);
}
}
return implode(" ", $parts);
}
/**
* Create a working copy by cloning, copying, or similar.
*
* @param Builder $builder
* @param string $buildPath
*
* @return boolean
*/
public function createWorkingCopy(Builder $builder, $buildPath)
{
return false;
}
/**
* Create an SSH key file on disk for this build.
*
* @param string $cloneTo
*
* @return string
*/
protected function writeSshKey($cloneTo)
{
$tempKeyFile = tempnam(sys_get_temp_dir(), 'key_');
file_put_contents($tempKeyFile, $this->getProject()->getSshPrivateKey());
return $tempKeyFile;
}
/**
* Create an SSH wrapper script for Svn to use, to disable host key checking, etc.
*
* @param string $cloneTo
* @param string $keyFile
*
* @return string
*/
protected function writeSshWrapper($cloneTo, $keyFile)
{
$sshFlags = '-o CheckHostIP=no -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o PasswordAuthentication=no';
// Write out the wrapper script for this build:
$script = <<<OUT
#!/bin/sh
ssh {$sshFlags} -o IdentityFile={$keyFile} $*
OUT;
$tempShFile = tempnam(sys_get_temp_dir(), 'sh_');
file_put_contents($tempShFile, $script);
shell_exec('chmod +x "' . $tempShFile . '"');
return $tempShFile;
}
/**
* @return string
*/
public function getSourceHumanize()
{
switch ($this->getSource()) {
case Build::SOURCE_WEBHOOK:
return 'source_webhook';
case Build::SOURCE_WEBHOOK_PULL_REQUEST:
return 'source_webhook_pull_request';
case Build::SOURCE_MANUAL_WEB:
return 'source_manual_web';
case Build::SOURCE_MANUAL_CONSOLE:
return 'source_manual_console';
case Build::SOURCE_PERIODICAL:
return 'source_periodical';
case Build::SOURCE_UNKNOWN:
default:
return 'source_unknown';
}
}
/**
* @return integer
*/
public function getNewErrorsCount()
{
if (null === $this->newErrorsCount) {
/** @var BuildErrorStore $store */
$store = Factory::getStore('BuildError');
$this->newErrorsCount = $store->getNewErrorsCount($this->getId());
}
return $this->newErrorsCount;
}
}