private function getBuildEnv(Build $build) { $env = ['BUILD_ID=' . $build->getId(), 'PROJECT_ID=' . $build->getProject()->getId(), 'CHANNEL=' . $build->getChannel(), 'SSH_URL=' . $build->getProject()->getGitUrl(), 'REF=' . $build->getRef(), 'HASH=' . $build->getHash(), 'IS_PULL_REQUEST=' . ($build->isPullRequest() ? 1 : 0), 'SYMFONY_ENV=prod']; $user = $build->getProject()->getUsers()->first(); if ($user->hasProviderAccessToken('github')) { /** * @todo there must be a way to avoid requiring a valid access token * I think the token is only used to avoid hitting github's * API limit through composer, so maybe there's a way to use a * stage1 specific token instead */ $env[] = 'GITHUB_ACCESS_TOKEN=' . $user->getProviderAccessToken('github'); } return $env; }
/** * @param Build $build * * @return Docker\Container */ public function run(Build $build, $timeout = null) { $producer = $this->websocketProducer; $logger = $this->logger; $docker = $this->docker; $em = $this->doctrine->getManager(); $publish = function ($content) use($build, $producer) { $message = new BuildMessage($build, $content); $producer->publish((string) $message); }; // $publish(' build started ('.date('r').')'); if ($this->getOption('dummy')) { $logger->info('dummy build, sleeping', ['duration' => $this->getOption('dummy_duration')]); sleep($this->getOption('dummy_duration')); $build->setPort(42); return true; } $project = $build->getProject(); $logger->info('starting build', ['build' => $build->getId(), 'project' => $project->getFullName(), 'project_id' => $project->getId(), 'ref' => $build->getRef(), 'hash' => $build->getHash(), 'timeout' => $timeout, 'force_local_build_yml' => $build->getForceLocalBuildYml()]); /** Generate build script using Yuhao */ $logger->info('generating build script'); // $publish(' generating build script'.PHP_EOL); $builder = $project->getDockerContextBuilder(); $builder->from('stage1/yuhao'); $docker->build($builder->getContext(), $build->getImageName('yuhao'), false, true, true); $prepareContainer = new PrepareContainer($build); // @todo move inside PrepareContainer if ($build->getForceLocalBuildYml()) { $prepareContainer->addEnv(['FORCE_LOCAL_STAGE1_YML=1']); } $manager = $docker->getContainerManager(); $manager->create($prepareContainer)->start($prepareContainer)->wait($prepareContainer, $timeout); if ($prepareContainer->getExitCode() != 0) { $exitCode = $prepareContainer->getExitCode(); if (isset(Process::$exitCodes[$exitCode])) { $exitCodeLabel = Process::$exitCodes[$exitCode]; } else { $exitCodeLabel = ''; } $message = sprintf('failed to generate build scripts (exit code %d (%s))', $exitCode, $exitCodeLabel); $publish($message . PHP_EOL); $logger->error($message, ['build' => $build->getId(), 'container' => $prepareContainer->getId(), 'container_name' => $prepareContainer->getName(), 'exit_code' => $exitCode, 'exit_code_label' => $exitCodeLabel]); $docker->commit($prepareContainer, ['repo' => $build->getImageName('yuhao'), 'tag' => 'failed']); throw new Exception($message, $prepareContainer->getExitCode()); } // @todo remove yuhao container // $manager->remove($prepareContainer); => 406 ?! $logger->info('yuhao finished executing, retrieving build script', ['build' => $build->getId()]); $output = ''; $manager->attach($prepareContainer, true, false, false, true, true)->readAttach(function ($type, $chunk) use(&$output) { $output .= $chunk; }); $logger->info('got response from yuhao', ['build' => $build->getId(), 'response' => $output, 'parsed_response' => json_decode($output, true)]); $script = BuildScript::fromJson($output); $script->setBuild($build); $options = $project->getDefaultBuildOptions(); $options = $options->resolve($script->getConfig()); $build->setOptions($options); $logger->info('resolved options', ['options' => $options]); $strategy = array_key_exists('path', $options['dockerfile']) ? new DockerfileStrategy($logger, $docker, $em, $this->websocketProducer, $this->redis, $this->options) : new DefaultStrategy($logger, $docker, $em, $this->websocketProducer, $this->options); $logger->info('elected strategy', ['strategy' => get_class($strategy)]); $em->persist($build); $em->persist($script); $em->flush(); $logger->info('starting build'); $strategy->build($build, $script, $timeout); $logger->info('finished build'); $em->persist($build); $em->persist($script); $em->flush(); /** * Launch App container */ $logger->info('starting app container', ['project' => $build->getProject()->getFullName(), 'image' => $build->getImageName()]); $ports = new PortCollection(80); # @todo DefaultStrategy containers should have an entrypoint # so we don't need to provide an actual command $appContainer = new AppContainer($build, $strategy->getCmd()); $appContainer->addEnv($build->getProject()->getContainerEnv()); $appContainer->setExposedPorts($ports); if ($build->getForceLocalBuildYml()) { $appContainer->addEnv(['FORCE_LOCAL_STAGE1_YML=1']); } try { $manager->create($appContainer)->start($appContainer, ['PortBindings' => $ports->toSpec()]); $logger->info('running app container', ['build' => $build->getId(), 'container' => $appContainer->getId()]); } catch (\Docker\Exception\UnexpectedStatusCodeException $e) { if ($e->getCode() === 404) { throw new \RuntimeException('Could not start app container (' . $e->getMessage() . ')', 404, $e); } throw $e; } // $publish(' build finished ('.date('r').')'.PHP_EOL); return $appContainer; }
public function findNewerScheduledBuilds(Build $build) { $query = $this->createQueryBuilder('b')->select()->leftJoin('b.project', 'p')->where('p.id = ?1')->andWhere('b.ref = ?2')->andWhere('b.createdAt > ?3')->setParameters([1 => $build->getProject()->getId(), 2 => $build->getRef(), 3 => $build->getCreatedAt()->format('Y-m-d H:i:s')])->getQuery(); return $query->execute(); }
/** * @param Build $build * @param string $separator * * @return string */ public function getPullRequestHead(Build $build, $separator = ':') { $project = $build->getProject(); list($name, ) = explode('/', $project->getFullName()); return $name . $separator . $build->getRef(); }
public function __construct(Build $build) { parent::__construct(['Image' => $build->getImageName('yuhao'), 'Cmd' => ['yuhao.sh'], 'Env' => ['SSH_URL=' . $build->getProject()->getGitUrl(), 'REF=' . $build->getRef(), 'IS_PULL_REQUEST=' . ($build->isPullRequest() ? 1 : 0)]]); }
public function build(Build $build, BuildScript $script, $timeout) { $logger = $this->logger; $docker = $this->docker; $websocketProducer = $this->websocketProducer; $redis = $this->redis; $publish = function ($content) use($build, $websocketProducer, $redis) { static $fragment = 0; $message = new BuildMessage($build, $content); $websocketProducer->publish((string) $message); $redis->rpush($build->getLogsList(), json_encode(['type' => Build::LOG_OUTPUT, 'message' => $content, 'stream' => 'stdout', 'microtime' => microtime(true), 'fragment_id' => $fragment++, 'build_id' => $build->getId()])); }; $project = $build->getProject(); $options = $build->getOptions(); $workdir = sys_get_temp_dir() . '/stage1/workdir/' . $build->getId(); if (is_dir($workdir)) { $fs = new Filesystem(); $fs->remove($workdir); } mkdir($workdir, 0777, true); $logger->info('using workdir', ['workdir' => $workdir]); mkdir($workdir . '/ssh', 0755, true); $project->dumpSshKeys($workdir . '/ssh', 'root'); file_put_contents($workdir . '/ssh/config', $project->getSshConfig($workdir . '/ssh')); file_put_contents($workdir . '/git_ssh', "#!/bin/bash\nexec /usr/bin/ssh -F {$workdir}/ssh/config \"\$@\""); chmod($workdir . '/git_ssh', 0777); $GIT_SSH = $workdir . '/git_ssh'; if ($build->isPullRequest()) { $clone = ProcessBuilder::create(['git', 'clone', '--quiet', '--depth', '1', $project->getGitUrl(), $workdir . '/source'])->setEnv('GIT_SSH', $GIT_SSH)->getProcess(); $commandLine = $clone->getCommandLine(); $logger->info('cloning repository', ['command_line' => $commandLine]); $publish('$ ' . substr($commandLine, 0, strrpos($commandLine, ' ')) . PHP_EOL); $clone->run(); $fetch = ProcessBuilder::create(['git', 'fetch', '--quiet', 'origin', 'refs/' . $build->getRef()])->setEnv('GIT_SSH', $GIT_SSH)->setWorkingDirectory($workdir . '/source')->getProcess(); $logger->info('fecthing pull request', ['command_line' => $fetch->getCommandLine()]); $publish('$ ' . $fetch->getCommandLine() . PHP_EOL); $fetch->run(); $checkout = ProcessBuilder::create(['git', 'checkout', '--quiet', '-b', 'pull_request', 'FETCH_HEAD'])->setEnv('GIT_SSH', $GIT_SSH)->setWorkingDirectory($workdir . '/source')->getProcess(); $logger->info('checkouting pull request', ['command_line' => $checkout->getCommandLine()]); $publish('$ ' . $checkout->getCommandLine() . PHP_EOL); $checkout->run(); } else { $clone = ProcessBuilder::create(['git', 'clone', '--quiet', '--depth', '1', '--branch', $build->getRef(), $project->getGitUrl(), $workdir . '/source'])->setEnv('GIT_SSH', $GIT_SSH)->getProcess(); $commandLine = $clone->getCommandLine(); $logger->info('cloning repository', ['command_line' => $commandLine]); $publish('$ ' . substr($commandLine, 0, strrpos($commandLine, ' ')) . PHP_EOL); $clone->run(); } $contextPath = $workdir . '/source/' . $options['dockerfile']['path']; $logger->info('creating docker build context from path', ['context_path' => $contextPath]); $context = new Context($contextPath); $logger->info('starting actual build', ['build' => $build->getId(), 'timeout' => $timeout]); $response = $docker->build($context, $build->getImageName(), false, false, true, false); $error = false; $response->read(function ($output) use($logger, $response, $publish, &$error) { if ($response->headers->get('content-type') === 'application/json') { $output = json_decode($output, true); $logger->info('got data chunk', ['output' => $output]); if (isset($output['stream'])) { $publish($output['stream']); } } else { $message = $output; } }); }