Skip to content

Instantly share code, notes, and snippets.

@pbuyle
Last active March 21, 2018 16:51
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbuyle/79c8fa1215e93926813f9e6a27af7ff2 to your computer and use it in GitHub Desktop.
Save pbuyle/79c8fa1215e93926813f9e6a27af7ff2 to your computer and use it in GitHub Desktop.
Deploy to Pantheon with Robo
<?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