/** * Return data about a particular {@link Member} of the stack for use in API response. * Notes: * 1) This method returns null instead of an array if the member doesn't exists anymore * 2) 'role' can be null in the response. This is the case of an admin, or an operations * user who can create the deployment but is not part of the stack roles. * * @param \DNProject $project * @param int $memberID * @return null|array */ public function getStackMemberData(\DNProject $project, $memberID) { if (empty(self::$_cache_project_members[$project->ID])) { self::$_cache_project_members[$project->ID] = $project->listMembers(); } // we cache all member lookup, even the false results if (!isset(self::$_cache_members[$memberID])) { self::$_cache_members[$memberID] = \Member::get()->byId($memberID); } if (!self::$_cache_members[$memberID]) { return null; } $role = null; foreach (self::$_cache_project_members[$project->ID] as $stackMember) { if ($stackMember['MemberID'] !== $memberID) { continue; } $role = $stackMember['RoleTitle']; } // if an administator is approving, they should be shown as one if ($role === null && \Permission::checkMember(self::$_cache_members[$memberID], 'ADMIN')) { $role = 'Administrator'; } return ['id' => $memberID, 'email' => self::$_cache_members[$memberID]->Email, 'role' => $role, 'name' => self::$_cache_members[$memberID]->getName()]; }
public function testHasDiskQuota() { Config::inst()->update('DNProject', 'defaults', array('DiskQuotaMB' => 0)); $this->assertFalse($this->project->HasDiskQuota()); Config::inst()->update('DNProject', 'defaults', array('DiskQuotaMB' => 2048)); $this->assertTrue($this->project->HasDiskQuota()); $this->project->DiskQuotaMB = 2; $this->assertTrue($this->project->HasDiskQuota()); }
/** * Deploy the given build to the given environment. * * @param DNEnvironment $environment * @param DeploynautLogFile $log * @param DNProject $project * @param array $options */ public function deploy(DNEnvironment $environment, DeploynautLogFile $log, DNProject $project, $options) { $name = $environment->getFullName(); $repository = $project->getLocalCVSPath(); $sha = $options['sha']; $args = array('branch' => $sha, 'repository' => $repository); $this->extend('deployStart', $environment, $sha, $log, $project); $log->write(sprintf('Deploying "%s" to "%s"', $sha, $name)); $this->enableMaintenance($environment, $log, $project); // Use a package generator if specified, otherwise run a direct deploy, which is the default behaviour // if build_filename isn't specified if ($this->packageGenerator) { $log->write(sprintf('Using package generator "%s"', get_class($this->packageGenerator))); try { $args['build_filename'] = $this->packageGenerator->getPackageFilename($project->Name, $sha, $repository, $log); } catch (Exception $e) { $log->write($e->getMessage()); throw $e; } if (empty($args['build_filename'])) { throw new RuntimeException('Failed to generate package.'); } } $command = $this->getCommand('deploy', 'web', $environment, $args, $log); $command->run(function ($type, $buffer) use($log) { $log->write($buffer); }); // Deployment cleanup. We assume it is always safe to run this at the end, regardless of the outcome. $self = $this; $cleanupFn = function () use($self, $environment, $args, $log) { $command = $self->getCommand('deploy:cleanup', 'web', $environment, $args, $log); $command->run(function ($type, $buffer) use($log) { $log->write($buffer); }); if (!$command->isSuccessful()) { $this->extend('cleanupFailure', $environment, $sha, $log, $project); $log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.'); } }; // Once the deployment has run it's necessary to update the maintenance page status if (!empty($options['leaveMaintenancePage'])) { $this->enableMaintenance($environment, $log, $project); } if (!$command->isSuccessful()) { $cleanupFn(); $this->extend('deployFailure', $environment, $sha, $log, $project); throw new RuntimeException($command->getErrorOutput()); } // Check if maintenance page should be removed if (empty($options['leaveMaintenancePage'])) { $this->disableMaintenance($environment, $log, $project); } $cleanupFn(); $log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name)); $this->extend('deployEnd', $environment, $sha, $log, $project); }
/** * Return a numerical counter for the given build. */ protected static function buildname_to_counter($buildname, DNProject $project) { $builds = $project->DNBuildList()->reverse(); $counter = 1; foreach ($builds as $build) { if ($build->FullName() == $buildname) { return $counter; } $counter++; } }
/** * Used by the sync task * * @param string $path * @return \DNProject */ public static function create_from_path($path) { $project = new DNProject(); $project->Name = $path; $project->write(); // add the administrators group as the viewers of the new project $adminGroup = Group::get()->filter('Code', 'administrators')->first(); if ($adminGroup && $adminGroup->exists()) { $project->Viewers()->add($adminGroup); } return $project; }
/** * */ public function setUp() { $this->envPath = '/tmp/deploynaut_test/envs'; Config::inst()->update('Injector', 'DNData', array('constructor' => array(0 => $this->envPath, 1 => '/tmp/deploynaut_test/gitkeys', 2 => Director::baseFolder() . '/assets/transfers'))); parent::setUp(); $this->project = new DNProject(); $this->project->Name = 'testproject'; $this->project->write(); $this->env = new DNEnvironment(); $this->env->Name = 'uat'; $this->env->ProjectID = $this->project->ID; $this->env->write(); }
/** * Construct fields to select any commit * * @param DNProject $project * @return FormField */ protected function buildCommitSelector($project) { // Branches $branches = []; foreach ($project->DNBranchList() as $branch) { $sha = $branch->SHA(); $name = $branch->Name(); $branchValue = sprintf("%s (%s, %s old)", $name, substr($sha, 0, 8), $branch->LastUpdated()->TimeDiff()); $branches[$sha . '-' . $name] = $branchValue; } // Tags $tags = []; foreach ($project->DNTagList()->setLimit(null) as $tag) { $sha = $tag->SHA(); $name = $tag->Name(); $tagValue = sprintf("%s (%s, %s old)", $name, substr($sha, 0, 8), $branch->LastUpdated()->TimeDiff()); $tags[$sha . '-' . $name] = $tagValue; } $tags = array_reverse($tags); // Past deployments $redeploy = []; foreach ($project->DNEnvironmentList() as $dnEnvironment) { $envName = $dnEnvironment->Name; foreach ($dnEnvironment->DeployHistory()->filter('State', \DNDeployment::STATE_COMPLETED) as $deploy) { $sha = $deploy->SHA; if (!isset($redeploy[$envName])) { $redeploy[$envName] = []; } if (!isset($redeploy[$envName][$sha])) { $pastValue = sprintf("%s (deployed %s)", substr($sha, 0, 8), $deploy->obj('LastEdited')->Ago()); $redeploy[$envName][$sha] = $pastValue; } } } // Merge fields $releaseMethods = []; if (!empty($branches)) { $releaseMethods[] = new SelectionGroup_Item('Branch', new DropdownField('Branch', 'Select a branch', $branches), 'Deploy the latest version of a branch'); } if ($tags) { $releaseMethods[] = new SelectionGroup_Item('Tag', new DropdownField('Tag', 'Select a tag', $tags), 'Deploy a tagged release'); } if ($redeploy) { $releaseMethods[] = new SelectionGroup_Item('Redeploy', new GroupedDropdownField('Redeploy', 'Redeploy', $redeploy), 'Redeploy a release that was previously deployed (to any environment)'); } $releaseMethods[] = new SelectionGroup_Item('SHA', new Textfield('SHA', 'Please specify the full SHA'), 'Deploy a specific SHA'); $field = new TabbedSelectionGroup('SelectRelease', $releaseMethods); $field->setValue(reset($releaseMethods)->getValue()); $field->addExtraClass('clearfix'); return $field; }
public function testURLParsing() { $shouldMatchURLs = array('https://github.com/silverstripe/deploynaut.git', 'github.com:silverstripe/deploynaut.git', 'git@github.com:silverstripe/deploynaut.git', 'ssh://git@github.com:22/silverstripe/deploynaut.git'); $project = new DNProject(); foreach ($shouldMatchURLs as $url) { $project->CVSPath = $url; $repositoryInterface = $project->getRepositoryInterface(); $this->assertEquals('https://github.com/silverstripe/deploynaut', $repositoryInterface->URL, "Failed to extract repository from " . $url); } $shouldntMatchURLs = array('https://othersite.com/github.com/ranking'); foreach ($shouldntMatchURLs as $url) { $project->CVSPath = $url; $repositoryInterface = $project->getRepositoryInterface(); $this->assertNull($repositoryInterface); } }
/** * @param SS_HTTPRequest $request */ public function run($request = null) { $projects = DNProject::get(); foreach ($projects as $project) { $environments = $project->DNEnvironmentList(); foreach ($environments as $environment) { $newFilename = basename($environment->Filename); if ($environment->Filename != $newFilename) { echo 'Migrating "' . $environment->Filename . '" to ' . $newFilename . PHP_EOL; $environment->Filename = $newFilename; $environment->write(); } } } $warnings = false; // Check if all project folders exists foreach ($projects as $project) { if (!$project->projectFolderExists()) { $warnings = true; echo 'Project ' . $project->Name . ' don\'t have a cap project folder' . PHP_EOL; } } if ($warnings) { exit(1); } }
public function perform() { set_time_limit(0); $log = new DeploynautLogFile($this->args['logfile']); $projects = DNProject::get()->filter('Name', Convert::raw2sql($this->args['projectName'])); $project = $projects->first(); $path = $project->getLocalCVSPath(); $env = $this->args['env']; $log->write('Starting git fetch for project "' . $project->Name . '"'); // if an alternate user has been configured for clone, run the command as that user // @todo Gitonomy doesn't seem to have any way to prefix the command properly, if you // set 'sudo -u composer git' as the "command" parameter, it tries to run the whole // thing as a single command and fails $user = DNData::inst()->getGitUser(); if (!empty($user)) { $command = sprintf('cd %s && sudo -u %s git fetch -p origin +refs/heads/*:refs/heads/* --tags', $path, $user); $process = new \Symfony\Component\Process\Process($command); $process->setEnv($env); $process->setTimeout(3600); $process->run(); if (!$process->isSuccessful()) { throw new RuntimeException($process->getErrorOutput()); } } else { $repository = new Gitonomy\Git\Repository($path, array('environment_variables' => $env)); $repository->run('fetch', array('-p', 'origin', '+refs/heads/*:refs/heads/*', '--tags')); } $log->write('Git fetch is finished'); }
/** * Sync the in-db project list with a list of file paths * @param array $paths Array of pathnames * @param boolean $dryRun */ public function syncWithPaths($paths, $dryRun = false) { foreach ($paths as $path) { if (!$this->filter('Name', $path)->count()) { $this->message($path); if (!$dryRun) { DNProject::create_from_path($path)->write(); } } } }
/** * @param string $environmentName * @return boolean True if this release has ever been deployed to the given environment */ public function EverDeployedTo($environmentName) { $environments = $this->project->Environments()->filter('Name', $environmentName); if (!$environments->count()) { return false; } $environment = $environments->first(); $deployments = DNDeployment::get()->filter('Status', 'Finished')->filter('EnvironmentID', $environment->ID); if ($deployments->count()) { return true; } return false; }
/** * @return array */ protected function getReferences() { $branches = array(); // Placeholder to put master branch first $firstBranch = null; try { $repository = new Gitonomy\Git\Repository($this->project->getLocalCVSPath()); } catch (Exception $e) { return $branches; } foreach ($repository->getReferences()->getBranches() as $branch) { /** @var DNBranch $obj */ $obj = DNBranch::create($branch, $this->project, $this->data); if ($branch->getName() == 'master') { $firstBranch = array($branch->getName() => $obj); } else { $branches[$branch->getName()] = $obj; } } if ($firstBranch) { $branches = $firstBranch + $branches; } return $branches; }
/** * Run a shell command. * * @param string $command The command to run * @param string|null $workingDir The working dir to run command in * @throws RuntimeException */ protected function runCommand($command, $workingDir = null) { if (!empty($this->user)) { $command = sprintf('sudo -u %s %s', $this->user, $command); } if ($this->log) { $this->log->write(sprintf('Running command: %s', $command)); } $process = new AbortableProcess($command, $workingDir); $process->setEnv($this->project->getProcessEnv()); $process->setTimeout(1800); $process->run(); if (!$process->isSuccessful()) { throw new RuntimeException($process->getErrorOutput()); } }
/** * @var array * @return \DeploymentStrategy */ protected function createStrategy($options) { $strategy = $this->environment->Backend()->planDeploy($this->environment, $options); $data = $strategy->toArray(); $interface = $this->project->getRepositoryInterface(); if ($interface instanceof \ArrayData && $this->canCompareCodeVersions($interface, $data['changes'])) { $compareurl = sprintf('%s/compare/%s...%s', $interface->URL, $data['changes']['Code version']['from'], $data['changes']['Code version']['to']); $data['changes']['Code version']['compareUrl'] = $compareurl; // special case for .platform.yml field so we don't show a huge blob of changes, // but rather a link to where the .platform.yml changes were made in the code if (isset($data['changes']['.platform.yml other'])) { $data['changes']['.platform.yml other']['compareUrl'] = $compareurl; $data['changes']['.platform.yml other']['description'] = ''; } } $this->extend('updateDeploySummary', $data); // Ensure changes that would have been updated are persisted in the object, // such as the comparison URL, so that they will be written to the Strategy // field on the DNDeployment object as part of {@link createDeployment()} $strategy->setChanges($data['changes']); return $strategy; }
/** * Find a build in this set by hash. * * @param string $hash * @return DNCommit */ public function byName($hash) { if ($this->loaded === false) { $this->items = $this->getReferences(); $this->loaded = true; } // The item might not be in the list because of the limit, try to find // in an older version and add it to the list. $found = null; foreach ($this->items as $item) { if ($item->SHA() == $hash) { $found = $item; break; } } if ($found === null) { $repository = new Gitonomy\Git\Repository($this->project->getLocalCVSPath()); $commit = new Gitonomy\Git\Commit($repository, $hash); $found = DNCommit::create($commit, $this->project, $this->data); } return $found; }
public function run($request = null) { // should syncing remove obsolete records? $remove = true; $dryRun = true; if ($request && $request->requestVar('remove') !== NULL) { $remove = (bool) $request->requestVar('remove'); } if ($request && $request->requestVar('dryrun') !== NULL) { $dryRun = (bool) $request->requestVar('dryrun'); } if ($dryRun) { echo "Running in dry run mode, no changes commited, "; echo "the output shows a prediction on what will happen." . PHP_EOL; echo "To skip dryrun, run the task like this:" . PHP_EOL; echo "./framework/sake dev/tasks/SyncProjectsAndEnvironments dryrun=0" . PHP_EOL . PHP_EOL; sleep(3); } $data = Injector::inst()->get('DNData'); $projectPaths = $data->getProjectPaths(); // Sync projects $projects = DNProject::get(); if ($remove) { $this->echoHeader('Removing obsolete projects'); $projects->removeObsolete($projectPaths, $dryRun); } $this->echoHeader('Adding new projects'); $projects->syncWithPaths($projectPaths, $dryRun); $this->echoHeader('Syncing environment files'); foreach ($projects as $project) { $this->echoHeader($project->Name); // Sync environments for each project $environmentPaths = $data->getEnvironmentPaths($project->Name); $project->DNEnvironmentList()->removeObsolete($environmentPaths, $dryRun); $project->DNEnvironmentList()->syncWithPaths($environmentPaths, $dryRun); } }
/** * Provide a list of all projects. * * @return SS_List */ public function DNProjectList() { $memberId = Member::currentUserID(); if (!$memberId) { return new ArrayList(); } if (Permission::check('ADMIN')) { return DNProject::get(); } return Member::get()->filter('ID', $memberId)->relation('Groups')->relation('Projects'); }
/** * Provide a list of all projects. * * @return SS_List */ public function DNProjectList() { $memberId = Member::currentUserID(); if (!$memberId) { return new ArrayList(); } if (Permission::check('ADMIN')) { return DNProject::get(); } $projects = Member::get()->filter('ID', $memberId)->relation('Groups')->relation('Projects'); $this->extend('updateDNProjectList', $projects); return $projects; }
/** * Get environment from URL * * @return DNEnvironment */ protected function getEnvironment() { $projectName = $this->getRequest()->param('Project'); $project = DNProject::get()->filter('Name', $projectName)->first(); $environmentName = $this->getRequest()->param('Environment'); return $project->Environments()->filter('Name', $environmentName)->first(); }
/** * Extracts a *.sspak file referenced through the passed in $dataTransfer * and pushes it to the environment referenced in $dataTransfer. * * @param DNDataTransfer $dataTransfer * @param DeploynautLogFile $log */ protected function dataTransferRestore(DNDataTransfer $dataTransfer, DeploynautLogFile $log) { $environmentObj = $dataTransfer->Environment(); $project = $environmentObj->Project(); $projectName = $project->Name; $environmentName = $environmentObj->Name; $env = $project->getProcessEnv(); $project = DNProject::get()->filter('Name', $projectName)->first(); $name = $projectName . ':' . $environmentName; $tempPath = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'deploynaut-transfer-' . $dataTransfer->ID; mkdir($tempPath, 0700, true); $self = $this; $cleanupFn = function () use($self, $tempPath, $name, $env, $log) { // Rebuild even if failed - maybe we can at least partly recover. $self->rebuild($name, $env, $log); $process = new Process('rm -rf ' . escapeshellarg($tempPath)); $process->run(); }; // Extract *.sspak to a temporary location $log->write('Extracting *.sspak file'); $sspakFilename = $dataTransfer->DataArchive()->ArchiveFile()->FullPath; $sspakCmd = sprintf('sspak extract %s %s', escapeshellarg($sspakFilename), escapeshellarg($tempPath)); $log->write($sspakCmd); $process = new Process($sspakCmd); $process->setTimeout(3600); $process->run(); if (!$process->isSuccessful()) { $log->write('Could not extract the *.sspak file: ' . $process->getErrorOutput()); $cleanupFn(); throw new RuntimeException($process->getErrorOutput()); } // TODO Validate that file actually contains the desired modes // Restore database if (in_array($dataTransfer->Mode, array('all', 'db'))) { // Upload into target environment $log->write('Restore of database to "' . $name . '" started'); $args = array('data_path' => $tempPath . DIRECTORY_SEPARATOR . 'database.sql.gz'); $command = $this->getCommand('data:pushdb', 'db', $name, $args, $env, $log); $command->run(function ($type, $buffer) use($log) { $log->write($buffer); }); if (!$command->isSuccessful()) { $cleanupFn(); $log->write('Restore of database to "' . $name . '" failed: ' . $command->getErrorOutput()); throw new RuntimeException($command->getErrorOutput()); } $log->write('Restore of database to "' . $name . '" done'); } // Restore assets if (in_array($dataTransfer->Mode, array('all', 'assets'))) { // Upload into target environment $log->write('Restore of assets to "' . $name . '" started'); // Extract assets.tar.gz into assets/ $extractCmd = sprintf('cd %s && tar xzf %s', escapeshellarg($tempPath), escapeshellarg($tempPath . DIRECTORY_SEPARATOR . 'assets.tar.gz')); $log->write($extractCmd); $process = new Process($extractCmd); $process->setTimeout(3600); $process->run(); if (!$process->isSuccessful()) { $log->write('Could not extract the assets archive'); $cleanupFn(); throw new RuntimeException($process->getErrorOutput()); } $args = array('data_path' => $tempPath . DIRECTORY_SEPARATOR . 'assets'); $command = $this->getCommand('data:pushassets', 'web', $name, $args, $env, $log); $command->run(function ($type, $buffer) use($log) { $log->write($buffer); }); if (!$command->isSuccessful()) { $cleanupFn(); $log->write('Restore of assets to "' . $name . '" failed: ' . $command->getErrorOutput()); throw new RuntimeException($command->getErrorOutput()); } $log->write('Restore of assets to "' . $name . '" done'); } $log->write('Rebuilding and cleaning up'); $cleanupFn(); }
/** * @return ValidationResult */ protected function validate() { $validation = parent::validate(); if ($validation->valid()) { if (empty($this->Name)) { return $validation->error('The stack must have a name.'); } // The name is used to build filepaths so should be restricted if (!preg_match('/^[a-zA-Z0-9][a-zA-Z0-9\\-\\_]+$/', $this->Name)) { return $validation->error('Project name can only contain alphanumeric, hyphens and underscores.'); } if (empty($this->CVSPath)) { return $validation->error('You must provide a repository URL.'); } $existing = DNProject::get()->filter('Name', $this->Name); if ($this->ID) { $existing = $existing->exclude('ID', $this->ID); } if ($existing->count() > 0) { return $validation->error('A stack already exists with that name.'); } } return $validation; }
/** * Provides a list of environments found in this project. * CAUTION: filterByCallback will change this into an ArrayList! * * @return ArrayList */ public function DNEnvironmentList() { if (!self::$_current_member_cache) { self::$_current_member_cache = Member::currentUser(); } if (self::$_current_member_cache === false) { return new ArrayList(); } $currentMember = self::$_current_member_cache; return $this->Environments()->filterByCallBack(function ($item) use($currentMember) { return $item->canView($currentMember); }); }
/** * Provide a list of all projects. * @return DataList */ public function DNProjectList() { return DNProject::get(); }
public function Link() { // Use a get-var for branch so that it can handle unsafe chars better return Controller::join_links($this->project->Link(), 'branch?name=' . urlencode($this->Name())); }
/** * @return string */ public function Link() { return \Controller::join_links($this->project->Link(), self::ACTION_GIT); }
/** * Return a dependent {@link DNEnvironment} based on this pipeline's dependent environment configuration. * @return DNEnvironment */ public function getDependentEnvironment() { // dependent environment not available $projectName = $this->getConfigSetting('PipelineConfig', 'DependsOnProject'); $environmentName = $this->getConfigSetting('PipelineConfig', 'DependsOnEnvironment'); if (empty($projectName) || empty($environmentName)) { return null; } $project = DNProject::get()->filter('Name', $projectName)->first(); if (!($project && $project->exists())) { throw new Exception(sprintf('Could not find dependent project "%s"', $projectName)); } $environment = DNEnvironment::get()->filter(array('ProjectID' => $project->ID, 'Name' => $environmentName))->first(); if (!($environment && $environment->exists())) { throw new Exception(sprintf('Could not find dependent environment "%s" in project "%s"', $environmentName, $projectName)); } return $environment; }
/** * Validate a specific alert configuration from configuration YAML is correct. * * @param string $name * @param array $config * @param DNProject $project * @param DeploynautLogFile $log * @return boolean */ public function validateAlert($name, $config, $project, $log) { // validate we have an environment set for the alert if (!isset($config['environment'])) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Missing "environment" key in .alerts.yml. Skipped.', $name)); return false; } // validate we have an environmentcheck suite name to check if (!isset($config['check_url'])) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Missing "check_url" key in .alerts.yml. Skipped.', $name)); return false; } // validate we have contacts for the alert if (!isset($config['contacts'])) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Missing "contacts" key in .alerts.yml. Skipped.', $name)); return false; } // validate that each value in the config is valid, build up a list of contacts we'll use later foreach ($config['contacts'] as $contactEmail) { // special case for ops if ($contactEmail == 'ops') { continue; } $contact = $project->AlertContacts()->filter('Email', $contactEmail)->first(); if (!($contact && $contact->exists())) { $log->write(sprintf('WARNING: Failed to configure alert "%s". No such contact "%s". Skipped.', $name, $contactEmail)); return false; } } // validate the environment specified in the alert actually exists if (!DNEnvironment::get()->filter('Name', $config['environment'])->first()) { $log->write(sprintf('WARNING: Failed to configure alert "%s". Invalid environment "%s" in .alerts.yml. Skipped.', $name, $config['environment'])); return false; } return true; }
/** * Does the actual project creation. * * @param $data array * @param $form Form * * @return SS_HTTPResponse */ public function doCreateProject($data, $form) { $form->loadDataFrom($data); $project = DNProject::create(); $form->saveInto($project); $this->extend('onBeforeCreateProject', $project, $data, $form); try { if ($project->write() > 0) { $this->extend('onAfterCreateProject', $project, $data, $form); // If an extension hasn't redirected us, we'll redirect to the project. if (!$this->redirectedTo()) { return $this->redirect($project->Link()); } else { return $this->response; } } else { $form->sessionMessage('Unable to write the stack to the database.', 'bad'); } } catch (ValidationException $e) { $form->sessionMessage($e->getMessage(), 'bad'); } return $this->redirectBack(); }
/** * Deploy the given build to the given environment. * * @param \DNEnvironment $environment * @param \DeploynautLogFile $log * @param \DNProject $project * @param array $options */ public function deploy(\DNEnvironment $environment, \DeploynautLogFile $log, \DNProject $project, $options) { $name = $environment->getFullName(); $repository = $project->getLocalCVSPath(); $sha = $options['sha']; $args = array('branch' => $sha, 'repository' => $repository); $this->extend('deployStart', $environment, $sha, $log, $project); $log->write(sprintf('Deploying "%s" to "%s"', $sha, $name)); $this->enableMaintenance($environment, $log, $project); // Use a package generator if specified, otherwise run a direct deploy, which is the default behaviour // if build_filename isn't specified if ($this->packageGenerator) { $log->write(sprintf('Using package generator "%s"', get_class($this->packageGenerator))); try { $args['build_filename'] = $this->packageGenerator->getPackageFilename($project->Name, $sha, $repository, $log); } catch (Exception $e) { $log->write($e->getMessage()); throw $e; } if (empty($args['build_filename'])) { throw new RuntimeException('Failed to generate package.'); } } $command = $this->getCommand('deploy', 'web', $environment, $args, $log); $command->run(function ($type, $buffer) use($log) { $log->write($buffer); }); $error = null; $deploySuccessful = $command->isSuccessful(); if ($deploySuccessful) { // Deployment automatically removes .htaccess, i.e. disables maintenance. Fine to smoketest. $deploySuccessful = $this->smokeTest($environment, $log); } if (!$deploySuccessful) { $this->enableMaintenance($environment, $log, $project); $rollbackSuccessful = $this->deployRollback($environment, $log, $project, $options, $args); if ($rollbackSuccessful) { // Again, .htaccess removed, maintenance off. $rollbackSuccessful = $this->smokeTest($environment, $log); } if (!$rollbackSuccessful) { $this->enableMaintenance($environment, $log, $project); $currentBuild = $environment->CurrentBuild(); $this->extend('deployRollbackFailure', $environment, $currentBuild, $log, $project); $log->write('Rollback failed'); $error = $command->getErrorOutput(); } else { $error = 'Rollback successful'; } } // Regardless of outcome, try to clean up. $cleanup = $this->getCommand('deploy:cleanup', 'web', $environment, $args, $log); $cleanup->run(function ($type, $buffer) use($log) { $log->write($buffer); }); if (!$cleanup->isSuccessful()) { $this->extend('cleanupFailure', $environment, $sha, $log, $project); $log->write('Warning: Cleanup failed, but fine to continue. Needs manual cleanup sometime.'); } $this->extend('deployEnd', $environment, $sha, $log, $project); if ($error !== null) { throw new RuntimeException($error); } $log->write(sprintf('Deploy of "%s" to "%s" finished', $sha, $name)); }