Last active
March 21, 2018 16:51
-
-
Save pbuyle/79c8fa1215e93926813f9e6a27af7ff2 to your computer and use it in GitHub Desktop.
Deploy to Pantheon with Robo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* This is project's console commands configuration for Robo task runner. | |
* | |
* @see http://robo.li/ | |
*/ | |
class RoboFile extends \Robo\Tasks { | |
use terminusLoadTasks; | |
public function __construct() { | |
// Load .env file from the local directory if it exists. Or use the .env.dist | |
$env_file = file_exists(__DIR__ . '/.env' ) ? '.env' : '.env.dist'; | |
$dotenv = new \Dotenv\Dotenv(__DIR__, $env_file); | |
$dotenv->load(); | |
} | |
/** | |
* Returns the current Git branch. | |
* | |
* Use the `GIT_BRANCH` environment variable if set. Otherwise will use a git | |
* command. | |
* | |
* @param bool $required | |
* Whether or not to throw an exception if the current Git branch cannot be | |
* retrieved, defaults to TRUE. | |
* | |
* @return string | |
*/ | |
protected function getGitBranch($required = TRUE) { | |
$branch = getenv('GIT_BRANCH'); | |
if (!$branch) { | |
$branch = $this->taskExec("git symbolic-ref --short -q HEAD") | |
->printed(false) | |
->run() | |
->getMessage(); | |
} | |
if ($required && !$branch) throw new RuntimeException('Unable to get the current Git branch, fix your working copy or set GIT_BRANCH'); | |
return trim($branch); | |
} | |
/** | |
* Returns the current Git commit hash. | |
* | |
* Use the `GIT_COMMIT` environment variable if set. Otherwise will use a git | |
* command. | |
* | |
* @param bool $required | |
* Whether or not to throw an exception if the current Git commit hash | |
* cannot be retrieved, defaults to TRUE. | |
* | |
* @return string | |
*/ | |
protected function getGitCommitHash($required = TRUE) { | |
$git_ref = getenv('GIT_COMMIT'); | |
if (!$git_ref) { | |
$git_ref = trim($this->taskExec('git rev-parse HEAD') | |
->printed(FALSE) | |
->run() | |
->getMessage()); | |
} | |
if ($required && !$git_ref) throw new RuntimeException('Unable to get the current Git commit hash, fix your working copy or set GIT_COMMIT'); | |
return trim($git_ref); | |
} | |
/** | |
* Returns the Pantheon Git URL. | |
* | |
* Use the `PANTHEON_GIT_URL` environment variable if set. Otherwise will use | |
* a terminus command. | |
* | |
* @param bool $required | |
* Whether or not to throw an exception if the Pantheon Git URL cannot be | |
* retrieved, defaults to TRUE. | |
* | |
* @return string | |
*/ | |
protected function getPantheonGitUrl($required = TRUE) { | |
$git_url = getenv('PANTHEON_GIT_URL'); | |
if (!$git_url) { | |
$git_url = $this->taskTerminus('site connection-info --env=dev --field=git_url') | |
->printed(FALSE) | |
->run() | |
->getMessage(); | |
} | |
if ($required && !$git_url) throw new RuntimeException('Unable to get the Pantheon Git URL, fix terminus or set PANTHEON_GIT_URL'); | |
return trim($git_url); | |
} | |
/** | |
* Push current working folder to Pantheon. | |
* | |
* Code push to Pantheon is done with Git, the theoretical behavior of this | |
* command is to | |
* 1. Checkout the HEAD of the pushed to branch to a temporary folder | |
* 2. Update this working directory to contains exactly what we want to push | |
* to Pantheon (ie. adding/removing/updating all the needed files). | |
* 3. Commit all the changes | |
* 4. Push the changes to Pantheon | |
* | |
* To avoid the resources hungry and slow process of coping files to a | |
* temporary directory, step 2 is is done in an non-obvious way. Instead of | |
* copying the files over the temporary fresh clone, the `.git` of the current | |
* working folder is replaced with the one from the fresh clone. Also, the | |
* `.gitignore` files is temporally overridden as a way to control what is | |
* pushed to Pantheon. This allow Git commands in the working folder to act on | |
* a clone of the the Pantheon repo and then push to it. Once the command | |
* complete (whether on a success or a failure), the original `.git` and | |
* `.gitignore` are restored. | |
* | |
* The command will use the provided comment message if not empty. Otherwise | |
* the command will attempt to generate a meaningful message containing | |
* the BUILD_NUMBER environment variable (if set) and the current Git branch | |
* and commit hash (retrieved either using Git commands or from the | |
* `GIT_BRANCH` and `GIT_COMMIT` environment variables). | |
* | |
* If a `BUILD_TAG` environment variable is set, the commit will also be | |
* tagged. | |
* | |
* @param array $opts | |
* @option $msg|m The commit message used for the push. Either a string or as | |
* a filename prefixed with '@'. If empty, a meaningful message will be | |
* generated. | |
* @option $confirm Pause the command execution right before pushing changes | |
* to the remote Pantheon repository. | |
* @option $no-update-db Disable applying required database updates. | |
* @option $no-features-revert Disable reverting all enabled feature modules. | |
*/ | |
public function pantheonPush($opts = ['msg|m' => '', 'confirm' => false, 'no-update-db' => false, 'no-features-revert' => false]) { | |
// The current Git branch | |
$branch = $this->getGitBranch(); | |
// The current Git ref | |
$git_ref = $this->getGitCommitHash(); | |
// The commit message used when commit/pushing to Pantheon. | |
if (!empty($opts['msg'])) { | |
if ($opts['msg'][0] == '@' && file_exists($filename = substr($opts['msg'], 1))) { | |
$commit_msg = file_get_contents($filename); | |
} | |
else { | |
$commit_msg = $opts['msg']; | |
} | |
} | |
else { | |
if ($build_number = getenv('BUILD_NUMBER')) { | |
$commit_msg = "Push build #$build_number"; | |
} | |
else { | |
$commit_msg = "Push manual build"; | |
} | |
$commit_msg .= "\n\nThis build used branch $branch at commit $git_ref."; | |
} | |
// The URL of the Pantheon git repo for the site. | |
$git_url = $this->getPantheonGitUrl(); | |
// The suffix used for backup files. | |
$backup_suffix = '.' . substr(str_shuffle('23456789abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ'), 0, 12); | |
/** @var Robo\Collection\Collection $collection */ | |
$collection = $this->collection(); | |
// Clone the Pantheon repository and replace original .git with its. | |
$this->_terminus(['site', 'set-connection-mode', '--env=dev', '--mode=git']); | |
$pantheonClone = $this->taskTmpDir() | |
->addToCollection($collection) | |
->getPath(); | |
$this->taskGitStack() | |
->exec(['clone', '--depth 1', $git_url, $pantheonClone]) | |
->addToCollection($collection); | |
// Backup and override files and folders. | |
$backupTask = $this->taskFilesystemStack() | |
// Backup overridden files and folders. | |
->rename('.git', '.git' . $backup_suffix) | |
->rename('.gitignore', '.gitignore' . $backup_suffix) | |
// Override files and folders. | |
->symlink($pantheonClone."/.git", '.git') | |
->copy('pantheon.gitignore', '.gitignore') | |
->addToCollection($collection); | |
// Restore overridden files on completion. | |
// This is registered now so it will always run whether or not the next | |
// tasks succeed. | |
$restoreTask = $this->taskFilesystemStack() | |
// Remove overridden files and folders. | |
->remove('.git') | |
->remove('.gitignore') | |
// Restore backuped files and folders. | |
->rename('.git' . $backup_suffix, '.git') | |
->rename('.gitignore' . $backup_suffix, '.gitignore') | |
->addAsCompletion($collection); | |
// Commit the current content of the working directory. | |
$git_stack = $this->taskGitStack(); | |
// Specifically add any folder with a .git folder using a trailing slash | |
// to avoid addign them as submodule. | |
// See http://debuggable.com/posts/git-fake-submodules:4b563ee4-f3cc-4061-967e-0e48cbdd56cb | |
// and http://stackoverflow.com/questions/2317652/nested-git-repositories-without-submodules#2317870 | |
foreach (\Webmozart\Glob\Glob::glob(__DIR__ . '/**/.git') as $path) { | |
if (dirname($path) != __DIR__) $git_stack->add(substr($path, 0, -4)); | |
} | |
$git_stack | |
->add('.') | |
->commit($commit_msg, "-a") | |
->addToCollection($collection); | |
if ($build_tag = getenv('BUILD_TAG')) { | |
$build_tag = str_replace(['&','#',';','`','|','*','?','~','<','>','^','(',')','[',']','{','}','$'], ' ', $build_tag); | |
$build_tag = preg_replace('/\s+/', '-', $build_tag); | |
$git_stack->tag($build_tag); | |
} | |
if ($opts['confirm']) { | |
$robo = $this; | |
$collection->add(function () use ($robo) { | |
return $robo->confirm("Confirm push?") ? 0 : 1; | |
}); | |
} | |
$this->taskGitStack() | |
->exec(['push', 'origin', 'master', '--tags']) | |
->addToCollection($collection); | |
// Execute Drush commands (unless disabled) | |
if (empty($opts['no-update-db']) || empty($opts['no-features-revert'])) { | |
$terminus = $this->taskTerminusStack() | |
->option('env', 'dev') | |
->addToCollection($collection); | |
if (empty($opts['no-update-db'])) { | |
$terminus->drush('updatedb --yes'); | |
} | |
if (empty($opts['no-features-revert'])) { | |
$terminus->drush('features-revert-all --yes') | |
->drush('cache-clear all'); | |
} | |
} | |
// Execute and clean up the task collection. | |
$collection->run(); | |
} | |
} | |
/** | |
* TODO: Publish on packagist and add as dependencis (will need unit test) | |
*/ | |
trait terminusLoadTasks { | |
/** | |
* Execute a single Terminus command. | |
* | |
* @param string|array $command The Terminus command to execute. | |
* | |
* @return \Robo\Task\Base\Exec | |
*/ | |
protected function taskTerminus($command) { | |
if (is_array($command)) { | |
$command = implode(' ', array_filter($command)); | |
} | |
return $this->taskExec('terminus ' . $command); | |
} | |
/** | |
* Run a Terminus command. | |
* | |
* @param string|array $command The Terminus command to execute. | |
* | |
* @return \Robo\Result | |
*/ | |
protected function _terminus($command) { | |
return $this->taskTerminus($command)->run(); | |
} | |
/** | |
* Execute a stack of Terminus commands. | |
* @return \TerminusStack | |
*/ | |
protected function taskTerminusStack() { | |
return new TerminusStack(); | |
} | |
} | |
/** | |
* Execute a stack of Terminus commands. | |
*/ | |
class TerminusStack extends \Robo\Task\CommandStack { | |
protected $options; | |
use \Robo\Common\CommandArguments; | |
public function __construct($pathToTerminus = 'terminus') { | |
$this->executable = $pathToTerminus; | |
} | |
/** | |
* Execute the given Terminus command. | |
* | |
* Options are prefixed with `--`. Option values must be explicitly escaped | |
* with escapeshellarg if necessary before being passed to this function. For | |
* options without values, simply pass the option names as values in the array | |
* (ie. with a numeric keys) or use NULL as value. | |
* | |
* @paran string $subcommand | |
* The subcommand to execute (`site`, `drush`, `art`, etc.). | |
* @param string|array $command | |
* The command to execute. | |
* @param array $options | |
* The options for the command. | |
* | |
* | |
* @return $this | |
*/ | |
public function exec($subcommand, $command = '', $options = []) { | |
if (is_array($command)) { | |
$command = implode(' ', array_filter($command)); | |
} | |
foreach ($options as $option => $value) { | |
if (!is_numeric($options)) { | |
$option = $value; | |
$value = NULL; | |
} | |
if (strpos($option, '-') !== 0) { | |
$option = "--$option"; | |
} | |
$command .= NULL == $option ? '' : " " . $option; | |
$command .= NULL == $value ? '' : "=" . $value; | |
} | |
return parent::exec("$subcommand $command{$this->arguments}"); | |
} | |
/** | |
* {@inheritdoc} | |
*/ | |
public function option($option, $value = null) | |
{ | |
if ($option !== null and strpos($option, '-') !== 0) { | |
$option = "--$option"; | |
} | |
$this->arguments .= null == $option ? '' : " " . $option; | |
$this->arguments .= null == $value ? '' : "=" . $value; | |
return $this; | |
} | |
/** | |
* Execute a `terminus site` sub-command. | |
* | |
* @param string|array $command | |
* The subcommand to execute, with optiosn an arguments. | |
* @param $options | |
* An array of options for the command. | |
* | |
* @return $this | |
*/ | |
public function site($command, $options = []) { | |
return $this->exec(__FUNCTION__, $command, $options); | |
} | |
/** | |
* Execite a `terminus drush` sub-command. | |
* | |
* @param string|array $command | |
* The subcommand to execute, with optiosn an arguments. | |
* @param $options | |
* An array of options for the command. | |
* | |
* @return $this | |
*/ | |
public function drush($command, $options = []) { | |
return $this->exec(__FUNCTION__, '"' . $command . '"', $options); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment