From 233bd58d48864e37d768207aaa026e0c035a6278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9s=20Monta=C3=B1ez?= Date: Sat, 31 Dec 2016 03:52:25 -0300 Subject: [PATCH] [Nostromo] Initial Commit --- .gitignore | 1 + .mage.yml | 34 ++ LICENSE | 23 + README.md | 4 + bin/mage | 11 + composer.json | 32 ++ composer.lock | 527 ++++++++++++++++++ src/Mage/Command/AbstractCommand.php | 77 +++ src/Mage/Command/BuiltIn/DeployCommand.php | 306 ++++++++++ .../Command/BuiltIn/Releases/ListCommand.php | 141 +++++ .../BuiltIn/Releases/RollbackCommand.php | 148 +++++ src/Mage/Command/BuiltIn/VersionCommand.php | 49 ++ src/Mage/Mage.php | 22 + src/Mage/MageApplication.php | 95 ++++ .../Runtime/Exception/DeploymentException.php | 20 + .../Exception/InvalidEnvironmentException.php | 20 + .../Runtime/Exception/RuntimeException.php | 22 + src/Mage/Runtime/Runtime.php | 398 +++++++++++++ src/Mage/Task/AbstractTask.php | 76 +++ .../BuiltIn/Composer/GenerateAutoloadTask.php | 46 ++ .../Task/BuiltIn/Composer/InstallTask.php | 46 ++ .../BuiltIn/Deploy/Release/CleanupTask.php | 67 +++ .../BuiltIn/Deploy/Release/PrepareTask.php | 43 ++ src/Mage/Task/BuiltIn/Deploy/ReleaseTask.php | 45 ++ src/Mage/Task/BuiltIn/Deploy/RsyncTask.php | 63 +++ .../Task/BuiltIn/Deploy/TarGz/CleanupTask.php | 52 ++ .../Task/BuiltIn/Deploy/TarGz/CopyTask.php | 69 +++ .../Task/BuiltIn/Deploy/TarGz/PrepareTask.php | 62 +++ .../Task/BuiltIn/Git/ChangeBranchTask.php | 88 +++ src/Mage/Task/BuiltIn/Git/UpdateTask.php | 59 ++ .../Task/BuiltIn/Symfony/AsseticDumpTask.php | 54 ++ .../BuiltIn/Symfony/AssetsInstallTask.php | 54 ++ .../Task/BuiltIn/Symfony/CacheClearTask.php | 54 ++ .../Task/BuiltIn/Symfony/CacheWarmupTask.php | 54 ++ src/Mage/Task/ErrorException.php | 32 ++ src/Mage/Task/ExecuteOnRollbackInterface.php | 20 + src/Mage/Task/SkipException.php | 22 + src/Mage/Task/TaskFactory.php | 112 ++++ src/Mage/Utils.php | 121 ++++ 39 files changed, 3169 insertions(+) create mode 100644 .gitignore create mode 100644 .mage.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100755 bin/mage create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Mage/Command/AbstractCommand.php create mode 100644 src/Mage/Command/BuiltIn/DeployCommand.php create mode 100644 src/Mage/Command/BuiltIn/Releases/ListCommand.php create mode 100644 src/Mage/Command/BuiltIn/Releases/RollbackCommand.php create mode 100644 src/Mage/Command/BuiltIn/VersionCommand.php create mode 100644 src/Mage/Mage.php create mode 100644 src/Mage/MageApplication.php create mode 100644 src/Mage/Runtime/Exception/DeploymentException.php create mode 100644 src/Mage/Runtime/Exception/InvalidEnvironmentException.php create mode 100644 src/Mage/Runtime/Exception/RuntimeException.php create mode 100644 src/Mage/Runtime/Runtime.php create mode 100644 src/Mage/Task/AbstractTask.php create mode 100644 src/Mage/Task/BuiltIn/Composer/GenerateAutoloadTask.php create mode 100644 src/Mage/Task/BuiltIn/Composer/InstallTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/Release/CleanupTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/Release/PrepareTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/ReleaseTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/RsyncTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/TarGz/CleanupTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/TarGz/CopyTask.php create mode 100644 src/Mage/Task/BuiltIn/Deploy/TarGz/PrepareTask.php create mode 100644 src/Mage/Task/BuiltIn/Git/ChangeBranchTask.php create mode 100644 src/Mage/Task/BuiltIn/Git/UpdateTask.php create mode 100644 src/Mage/Task/BuiltIn/Symfony/AsseticDumpTask.php create mode 100644 src/Mage/Task/BuiltIn/Symfony/AssetsInstallTask.php create mode 100644 src/Mage/Task/BuiltIn/Symfony/CacheClearTask.php create mode 100644 src/Mage/Task/BuiltIn/Symfony/CacheWarmupTask.php create mode 100644 src/Mage/Task/ErrorException.php create mode 100644 src/Mage/Task/ExecuteOnRollbackInterface.php create mode 100644 src/Mage/Task/SkipException.php create mode 100644 src/Mage/Task/TaskFactory.php create mode 100644 src/Mage/Utils.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..57872d0 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/vendor/ diff --git a/.mage.yml b/.mage.yml new file mode 100644 index 0000000..38184de --- /dev/null +++ b/.mage.yml @@ -0,0 +1,34 @@ +magephp: +# composer: +# path: composer + symfony: +# console: bin/console +# log_dir: app/logs + environments: + production: + user: root +# ssh: -p 22 -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no +# scp: -P 22 -p -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no + branch: test + host_path: /var/www/test + releases: 4 + exclude: + - vendor + - app/cache + - app/log + - web/app_dev.php + hosts: + - webserver + pre-deploy: + - git/update +# - composer/install #: { flags: '--optimize' } +# - composer/generate-autoload + on-deploy: +# - symfony/cache-clear: { env: 'dev' } +# - symfony/cache-warmup: { env: 'dev' } +# - symfony/assets-install: { env: 'dev' } +# - symfony/assetic-dump: { env: 'dev' } + on-release: + post-release: + post-deploy: + diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2130481 --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2011 - 2017 Andrés Montañez + +Permission is hereby granted, free of charge, to any +person obtaining a copy of this software and associated +documentation files (the "Software"), to deal in the +Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the +Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice +shall be included in all copies or substantial portions of +the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR +PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS +OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d850f5d --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Magallanes # +## Insurrection Version ## + +Insurrection: A violent uprising of part or all of a national population against the government or other authority; a mutiny; **a rebellion**. \ No newline at end of file diff --git a/bin/mage b/bin/mage new file mode 100755 index 0000000..16b10c3 --- /dev/null +++ b/bin/mage @@ -0,0 +1,11 @@ +#!/usr/bin/env php +configure(__DIR__ . '/../.mage.yml'); +$mage->run(); \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..093e75f --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "andres-montanez/magallanes", + "description": "A Deployment Tool for PHP Applications", + "homepage": "http://magephp.com", + "license": "MIT", + "type": "library", + "keywords": ["deployment"], + "authors": [ + { + "name": "Andrés Montañez", + "email": "andresmontanez@gmail.com" + } + ], + "require": { + "php": ">=5.5.9", + "monolog/monolog": "^1.0", + "symfony/console": "^3.0", + "symfony/filesystem": "^3.0", + "symfony/finder": "^3.0", + "symfony/yaml": "^3.0", + "symfony/process": "^3.0" + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\": "src/Symfony/Component/" + }, + "psr-0": { "": "src/" }, + "exclude-from-classmap": [ + "**/Tests/" + ] + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..4bd0fec --- /dev/null +++ b/composer.lock @@ -0,0 +1,527 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "hash": "452fb59524b87e73b64df8ae4e513c82", + "content-hash": "9af180a8e49a2a08013cb76345fde19b", + "packages": [ + { + "name": "monolog/monolog", + "version": "1.22.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "bad29cb8d18ab0315e6c477751418a82c850d558" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/bad29cb8d18ab0315e6c477751418a82c850d558", + "reference": "bad29cb8d18ab0315e6c477751418a82c850d558", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "~5.3" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2016-11-26 00:15:39" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10 12:19:37" + }, + { + "name": "symfony/console", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/console.git", + "reference": "d12aa9ca20f4db83ec58410978dab6afcb9d6aaa" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/console/zipball/d12aa9ca20f4db83ec58410978dab6afcb9d6aaa", + "reference": "d12aa9ca20f4db83ec58410978dab6afcb9d6aaa", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "symfony/debug": "~2.8|~3.0", + "symfony/polyfill-mbstring": "~1.0" + }, + "require-dev": { + "psr/log": "~1.0", + "symfony/event-dispatcher": "~2.8|~3.0", + "symfony/filesystem": "~2.8|~3.0", + "symfony/process": "~2.8|~3.0" + }, + "suggest": { + "psr/log": "For using the console logger", + "symfony/event-dispatcher": "", + "symfony/filesystem": "", + "symfony/process": "" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Console\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Console Component", + "homepage": "https://symfony.com", + "time": "2016-12-11 14:34:22" + }, + { + "name": "symfony/debug", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/debug.git", + "reference": "9f923e68d524a3095c5a2ae5fc7220c7cbc12231" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/debug/zipball/9f923e68d524a3095c5a2ae5fc7220c7cbc12231", + "reference": "9f923e68d524a3095c5a2ae5fc7220c7cbc12231", + "shasum": "" + }, + "require": { + "php": ">=5.5.9", + "psr/log": "~1.0" + }, + "conflict": { + "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + }, + "require-dev": { + "symfony/class-loader": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Debug\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Debug Component", + "homepage": "https://symfony.com", + "time": "2016-11-16 22:18:16" + }, + { + "name": "symfony/filesystem", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/filesystem.git", + "reference": "8d4cf7561a5b17e5eb7a02b80d0b8f014a3796d4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/8d4cf7561a5b17e5eb7a02b80d0b8f014a3796d4", + "reference": "8d4cf7561a5b17e5eb7a02b80d0b8f014a3796d4", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Filesystem\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Filesystem Component", + "homepage": "https://symfony.com", + "time": "2016-11-24 00:46:43" + }, + { + "name": "symfony/finder", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "a69cb5d455b4885ca376dc5bb3e1155cc8c08c4b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/a69cb5d455b4885ca376dc5bb3e1155cc8c08c4b", + "reference": "a69cb5d455b4885ca376dc5bb3e1155cc8c08c4b", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Finder Component", + "homepage": "https://symfony.com", + "time": "2016-12-13 09:39:43" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", + "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "suggest": { + "ext-mbstring": "For best performance" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill for the Mbstring extension", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "mbstring", + "polyfill", + "portable", + "shim" + ], + "time": "2016-11-14 01:06:16" + }, + { + "name": "symfony/process", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "02ea84847aad71be7e32056408bb19f3a616cdd3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/02ea84847aad71be7e32056408bb19f3a616cdd3", + "reference": "02ea84847aad71be7e32056408bb19f3a616cdd3", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Process Component", + "homepage": "https://symfony.com", + "time": "2016-11-24 10:40:28" + }, + { + "name": "symfony/yaml", + "version": "v3.2.1", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "a7095af4b97a0955f85c8989106c249fa649011f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/a7095af4b97a0955f85c8989106c249fa649011f", + "reference": "a7095af4b97a0955f85c8989106c249fa649011f", + "shasum": "" + }, + "require": { + "php": ">=5.5.9" + }, + "require-dev": { + "symfony/console": "~2.8|~3.0" + }, + "suggest": { + "symfony/console": "For validating YAML files using the lint command" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2016-12-10 10:07:06" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.5.9" + }, + "platform-dev": [] +} diff --git a/src/Mage/Command/AbstractCommand.php b/src/Mage/Command/AbstractCommand.php new file mode 100644 index 0000000..6a4c9d0 --- /dev/null +++ b/src/Mage/Command/AbstractCommand.php @@ -0,0 +1,77 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Command; + +use Mage\Runtime\Runtime; +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Symfony\Component\Console\Command\Command; + +/** + * Abstract base class for Magallanes Commands + * + * @author Andrés Montañez + */ +abstract class AbstractCommand extends Command +{ + /** + * @var Runtime Current Runtime instance + */ + protected $runtime; + + /** + * @var LoggerInterface|null The instance of the logger, it's optional + */ + private $logger = null; + + /** + * Configure the Command and create the Runtime configuration + * + * @param array $configuration Magallanes configuration + * @return AbstractCommand + */ + public function setConfiguration($configuration) + { + $this->runtime = new Runtime(); + $this->runtime->setConfiguration($configuration); + $this->runtime->setLogger($this->logger); + + return $this; + } + + /** + * Sets the logger + * + * @param LoggerInterface $logger + * @return AbstractCommand + */ + public function setLogger(LoggerInterface $logger = null) + { + $this->logger = $logger; + return $this; + } + + /** + * Logs a message, if logger is valid instance + * + * @param string $message + * @param string $level + * @return AbstractCommand + */ + public function log($message, $level = LogLevel::DEBUG) + { + if ($this->logger instanceof LoggerInterface) { + $this->logger->log($level, $message); + } + + return $this; + } +} diff --git a/src/Mage/Command/BuiltIn/DeployCommand.php b/src/Mage/Command/BuiltIn/DeployCommand.php new file mode 100644 index 0000000..07b2a27 --- /dev/null +++ b/src/Mage/Command/BuiltIn/DeployCommand.php @@ -0,0 +1,306 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Command\BuiltIn; + +use Mage\Runtime\Exception\DeploymentException; +use Mage\Runtime\Exception\InvalidEnvironmentException; +use Mage\Runtime\Exception\RuntimeException; +use Mage\Runtime\Runtime; +use Mage\Task\ErrorException; +use Mage\Task\ExecuteOnRollbackInterface; +use Mage\Task\AbstractTask; +use Mage\Task\SkipException; +use Mage\Task\TaskFactory; +use Mage\Utils; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Mage\Command\AbstractCommand; + +/** + * The Deployment Command + * + * @author Andrés Montañez + */ +class DeployCommand extends AbstractCommand +{ + /** + * @var TaskFactory + */ + protected $taskFactory; + + /** + * Configure the Command + */ + protected function configure() + { + $this + ->setName('deploy') + ->setDescription('Deploy code to hosts') + ->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to') + ; + } + + /** + * Execute the Command + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int|mixed + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Starting Magallanes'); + $output->writeln(''); + + try { + $this->runtime->setEnvironment($input->getArgument('environment')); + } catch (InvalidEnvironmentException $exception) { + $output->writeln(sprintf('%s', $exception->getMessage())); + return $exception->getCode(); + } + + $output->writeln(sprintf(' Environment: %s', $this->runtime->getEnvironment())); + $this->log(sprintf('Environment: %s', $this->runtime->getEnvironment())); + + if ($this->runtime->getEnvironmentConfig('releases', false)) { + $this->runtime->setReleaseId(date('YmdHis')); + $output->writeln(sprintf(' Release ID: %s', $this->runtime->getReleaseId())); + $this->log(sprintf('Release ID: %s', $this->runtime->getReleaseId())); + } + + if ($this->runtime->getConfigOptions('log_file', false)) { + $output->writeln(sprintf(' Logfile: %s', $this->runtime->getConfigOptions('log_file'))); + } + + $output->writeln(''); + + try { + $this->taskFactory = new TaskFactory($this->runtime); + $this->runDeployment($output); + } catch (DeploymentException $exception) { + $output->writeln(sprintf('%s', $exception->getMessage())); + return $exception->getCode(); + } + + $output->writeln('Finished Magallanes'); + + return 0; + } + + /** + * Run the Deployment Process + * + * @param OutputInterface $output + * @throws DeploymentException + */ + protected function runDeployment(OutputInterface $output) + { + // Run Pre Deploy Tasks + $this->runtime->setStage(Runtime::PRE_DEPLOY); + $preDeployTasks = $this->runtime->getTasks(); + + if ($this->runtime->getEnvironmentConfig('branch', false) && !$this->runtime->inRollback()) { + if (!in_array('git/change-branch', $preDeployTasks)) { + array_unshift($preDeployTasks, 'git/change-branch'); + } + } + + if ($this->runtime->getEnvironmentConfig('releases', false) && !$this->runtime->inRollback()) { + if (!in_array('deploy/targz/prepare', $preDeployTasks)) { + array_push($preDeployTasks, 'deploy/targz/prepare'); + } + } + + if (!$this->runTasks($output, $preDeployTasks)) { + throw new DeploymentException(sprintf(' Tasks failed on %s stage, halting deployment', $this->getStageName()), 500); + } + + // Run On Deploy Tasks + $hosts = $this->runtime->getEnvironmentConfig('hosts'); + if (count($hosts) == 0) { + $output->writeln(' No hosts defined, skipping On Deploy tasks'); + $output->writeln(''); + } else { + $this->runtime->setStage(Runtime::ON_DEPLOY); + $onDeployTasks = $this->runtime->getTasks(); + + if ($this->runtime->getEnvironmentConfig('releases', false) && !$this->runtime->inRollback()) { + if (!in_array('deploy/targz/copy', $onDeployTasks)) { + array_unshift($onDeployTasks, 'deploy/targz/copy'); + } + } else { + if (!in_array('deploy/rsync', $onDeployTasks) && !$this->runtime->inRollback()) { + array_unshift($onDeployTasks, 'deploy/rsync'); + } + } + + if ($this->runtime->getEnvironmentConfig('releases', false) && !$this->runtime->inRollback()) { + if (!in_array('deploy/release/prepare', $onDeployTasks)) { + array_unshift($onDeployTasks, 'deploy/release/prepare'); + } + } + + foreach ($hosts as $host) { + $this->runtime->setWorkingHost($host); + if (!$this->runTasks($output, $onDeployTasks)) { + throw new DeploymentException(sprintf(' Tasks failed on %s stage, halting deployment', $this->getStageName()), 500); + } + $this->runtime->setWorkingHost(null); + } + } + + // Run On Release Tasks + $hosts = $this->runtime->getEnvironmentConfig('hosts'); + if (count($hosts) == 0) { + $output->writeln(' No hosts defined, skipping On Release tasks'); + $output->writeln(''); + } else { + $this->runtime->setStage(Runtime::ON_RELEASE); + $onReleaseTasks = $this->runtime->getTasks(); + + if ($this->runtime->getEnvironmentConfig('releases', false)) { + if (!in_array('deploy/release', $onReleaseTasks)) { + array_unshift($onReleaseTasks, 'deploy/release'); + } + } + + foreach ($hosts as $host) { + $this->runtime->setWorkingHost($host); + if (!$this->runTasks($output, $onReleaseTasks)) { + throw new DeploymentException(sprintf(' Tasks failed on %s stage, halting deployment', $this->getStageName()), 500); + } + $this->runtime->setWorkingHost(null); + } + } + + // Run Post Release Tasks + $hosts = $this->runtime->getEnvironmentConfig('hosts'); + if (count($hosts) == 0) { + $output->writeln(' No hosts defined, skipping Post Release tasks'); + $output->writeln(''); + } else { + $this->runtime->setStage(Runtime::POST_RELEASE); + $postReleaseTasks = $this->runtime->getTasks(); + + if ($this->runtime->getEnvironmentConfig('releases', false) && !$this->runtime->inRollback()) { + if (!in_array('deploy/release/cleanup', $postReleaseTasks)) { + array_unshift($postReleaseTasks, 'deploy/release/cleanup'); + } + } + + foreach ($hosts as $host) { + $this->runtime->setWorkingHost($host); + if (!$this->runTasks($output, $postReleaseTasks)) { + throw new DeploymentException(sprintf(' Tasks failed on %s stage, halting deployment', $this->getStageName()), 500); + } + $this->runtime->setWorkingHost(null); + } + } + + // Run Post Deploy Tasks + $this->runtime->setStage(Runtime::POST_DEPLOY); + $postDeployTasks = $this->runtime->getTasks(); + if ($this->runtime->getEnvironmentConfig('releases', false) && !$this->runtime->inRollback()) { + if (!in_array('deploy/targz/cleanup', $postDeployTasks)) { + array_unshift($postDeployTasks, 'deploy/targz/cleanup'); + } + } + + if ($this->runtime->getEnvironmentConfig('branch', false) && !$this->runtime->inRollback()) { + if (!in_array('git/change-branch', $postDeployTasks)) { + array_push($postDeployTasks, 'git/change-branch'); + } + } + + if (!$this->runTasks($output, $postDeployTasks)) { + throw new DeploymentException(sprintf(' Tasks failed on %s stage, halting deployment', $this->getStageName()), 500); + } + } + + /** + * Runs all the tasks + * + * @param OutputInterface $output + * @param $tasks + * @return bool + * @throws RuntimeException + */ + protected function runTasks(OutputInterface $output, $tasks) + { + if (count($tasks) == 0) { + $output->writeln(sprintf(' No tasks defined for %s', $this->getStageName())); + $output->writeln(''); + return true; + } + + if ($this->runtime->getWorkingHost() != null) { + $output->writeln(sprintf(' Starting %s tasks on host %s:', $this->getStageName(), $this->runtime->getWorkingHost())); + } else { + $output->writeln(sprintf(' Starting %s tasks:', $this->getStageName())); + } + + $totalTasks = count($tasks); + $succeededTasks = 0; + + foreach ($tasks as $taskName) { + /** @var AbstractTask $task */ + $task = $this->taskFactory->get($taskName); + $output->write(sprintf(' Running %s ... ', $task->getDescription())); + $this->log(sprintf('Running task %s (%s)', $task->getDescription(), $task->getName())); + + if ($this->runtime->inRollback() && !$task instanceof ExecuteOnRollbackInterface) { + $succeededTasks++; + $output->writeln('SKIPPED'); + $this->log(sprintf('Task %s (%s) finished with SKIPPED, it was in a Rollback', $task->getDescription(), $task->getName())); + } else { + try { + if ($task->execute()) { + $succeededTasks++; + $output->writeln('OK'); + $this->log(sprintf('Task %s (%s) finished with OK', $task->getDescription(), $task->getName())); + } else { + $output->writeln('FAIL'); + $this->log(sprintf('Task %s (%s) finished with FAIL', $task->getDescription(), $task->getName())); + } + } catch (SkipException $exception) { + $succeededTasks++; + $output->writeln('SKIPPED'); + $this->log(sprintf('Task %s (%s) finished with SKIPPED, thrown SkipException', $task->getDescription(), $task->getName())); + } catch (ErrorException $exception) { + $output->writeln(sprintf('FAIL [%s]', $exception->getTrimmedMessage())); + $this->log(sprintf('Task %s (%s) finished with FAIL, with Error "%s"', $task->getDescription(), $task->getName(), $exception->getMessage())); + } + } + } + + if ($succeededTasks != $totalTasks) { + $alertColor = 'red'; + } else { + $alertColor = 'green'; + } + + $output->writeln(sprintf(' Finished %s tasks: %d/%d done.', $this->getStageName(), $alertColor, $succeededTasks, $totalTasks)); + $output->writeln(''); + + return ($succeededTasks == $totalTasks); + } + + /** + * Get the Human friendly Stage name + * + * @return string + */ + protected function getStageName() + { + return Utils::getStageName($this->runtime->getStage()); + } +} diff --git a/src/Mage/Command/BuiltIn/Releases/ListCommand.php b/src/Mage/Command/BuiltIn/Releases/ListCommand.php new file mode 100644 index 0000000..86e0ef9 --- /dev/null +++ b/src/Mage/Command/BuiltIn/Releases/ListCommand.php @@ -0,0 +1,141 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Command\BuiltIn\Releases; + +use Mage\Utils; +use Mage\Runtime\Exception\InvalidEnvironmentException; +use Mage\Runtime\Exception\DeploymentException; +use Mage\Runtime\Exception\RuntimeException; +use Symfony\Component\Process\Process; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Mage\Command\AbstractCommand; + +/** + * Command for Listing all Releases + * + * @author Andrés Montañez + */ +class ListCommand extends AbstractCommand +{ + /** + * Configure the Command + */ + protected function configure() + { + $this + ->setName('releases:list') + ->setDescription('List the releases on an environment') + ->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to') + ; + } + + /** + * Execute the Command + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int|mixed + * @throws DeploymentException + * @throws RuntimeException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Starting Magallanes'); + $output->writeln(''); + + try { + $this->runtime->setEnvironment($input->getArgument('environment')); + } catch (InvalidEnvironmentException $exception) { + $output->writeln(sprintf('%s', $exception->getMessage())); + return $exception->getCode(); + } + + if (!$this->runtime->getEnvironmentConfig('releases', false)) { + throw new DeploymentException('Releases are not enabled', 700); + } + + $output->writeln(sprintf(' Environment: %s', $this->runtime->getEnvironment())); + $this->log(sprintf('Environment: %s', $this->runtime->getEnvironment())); + + if ($this->runtime->getConfigOptions('log_file', false)) { + $output->writeln(sprintf(' Logfile: %s', $this->runtime->getConfigOptions('log_file'))); + } + + $output->writeln(''); + + $hosts = $this->runtime->getEnvironmentConfig('hosts'); + if (count($hosts) == 0) { + $output->writeln('No hosts defined'); + $output->writeln(''); + } else { + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + + foreach ($hosts as $host) { + $this->runtime->setWorkingHost($host); + + // Get List of Releases + $cmdListReleases = sprintf('ls -1 %s/releases', $hostPath); + + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdListReleases, true); + if (!$process->isSuccessful()) { + throw new RuntimeException(sprintf('Unable to retrieve releases from host %s', $host), 800); + } + + $releases = explode(PHP_EOL, trim($process->getOutput())); + rsort($releases); + + if (count($releases) == 0) { + $output->writeln(sprintf(' No releases available on host %s:', $host)); + } else { + // Get Current Release + $cmdCurrentRelease = sprintf('readlink -f %s/current', $hostPath); + + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdCurrentRelease, true); + if (!$process->isSuccessful()) { + throw new RuntimeException(sprintf('Unable to retrieve current release from host %s', $host), 850); + } + + $currentReleaseId = explode('/', trim($process->getOutput())); + $currentReleaseId = $currentReleaseId[count($currentReleaseId) - 1]; + + $output->writeln(sprintf(' Releases on host %s:', $host)); + + foreach ($releases as $releaseId) { + $releaseDate = Utils::getReleaseDate($releaseId); + + $output->write(sprintf(' Release ID: %s - Date: %s [%s]', + $releaseId, + $releaseDate->format('Y-m-d H:i:s'), + Utils::getTimeDiff($releaseDate) + )); + + if ($releaseId == $currentReleaseId) { + $output->writeln(' [current]'); + } else { + $output->writeln(''); + } + } + } + + $this->runtime->setWorkingHost(null); + $output->writeln(''); + } + } + + $output->writeln('Finished Magallanes'); + + return 0; + } +} diff --git a/src/Mage/Command/BuiltIn/Releases/RollbackCommand.php b/src/Mage/Command/BuiltIn/Releases/RollbackCommand.php new file mode 100644 index 0000000..4afaedd --- /dev/null +++ b/src/Mage/Command/BuiltIn/Releases/RollbackCommand.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Command\BuiltIn\Releases; + +use Mage\Task\TaskFactory; +use Mage\Runtime\Exception\InvalidEnvironmentException; +use Mage\Runtime\Exception\DeploymentException; +use Symfony\Component\Process\Process; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; +use Mage\Command\BuiltIn\DeployCommand; + +/** + * Command for Rolling Back a Releases + * + * @author Andrés Montañez + */ +class RollbackCommand extends DeployCommand +{ + /** + * Configure the Command + */ + protected function configure() + { + $this + ->setName('releases:rollback') + ->setDescription('Rollback to a release on an environment') + ->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to') + ->addArgument('release', InputArgument::REQUIRED, 'The ID or the Index of the release to rollback to') + ; + } + + /** + * Execute the Command + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int|mixed + * @throws DeploymentException + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln('Starting Magallanes'); + $output->writeln(''); + + try { + $this->runtime->setEnvironment($input->getArgument('environment')); + } catch (InvalidEnvironmentException $exception) { + $output->writeln(sprintf('%s', $exception->getMessage())); + return $exception->getCode(); + } + + if (!$this->runtime->getEnvironmentConfig('releases', false)) { + throw new DeploymentException('Releases are not enabled', 700); + } + + // Check if the Release exists in all hosts + $releaseToRollback = $input->getArgument('release'); + if ($releaseId = $this->checkReleaseAvailability($releaseToRollback)) { + $this->runtime->setReleaseId($releaseId)->setRollback(true); + + $output->writeln(sprintf(' Environment: %s', $this->runtime->getEnvironment())); + $this->log(sprintf('Environment: %s', $this->runtime->getEnvironment())); + + $output->writeln(sprintf(' Rollback to Release ID: %s', $this->runtime->getReleaseId())); + $this->log(sprintf('Release ID: %s', $this->runtime->getReleaseId())); + + if ($this->runtime->getConfigOptions('log_file', false)) { + $output->writeln(sprintf(' Logfile: %s', $this->runtime->getConfigOptions('log_file'))); + } + + $output->writeln(''); + + // Get the Task Factory + $this->taskFactory = new TaskFactory($this->runtime); + + try { + $this->runDeployment($output); + } catch (DeploymentException $exception) { + $output->writeln(sprintf('%s', $exception->getMessage())); + return $exception->getCode(); + } + } else { + throw new DeploymentException(sprintf('Release %s is not available on all hosts', $releaseToRollback), 720); + } + + $output->writeln('Finished Magallanes'); + + return 0; + } + + /** + * Check if the provided Release ID is available in all hosts + * + * @param string $releaseToRollback Release ID + * @return bool + */ + protected function checkReleaseAvailability($releaseToRollback) + { + $releaseIdCandidate = false; + $hosts = $this->runtime->getEnvironmentConfig('hosts'); + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + + $releaseAvailableInAllHosts = true; + foreach ($hosts as $host) { + $this->runtime->setWorkingHost($host); + + // Get List of Releases + $cmdListReleases = sprintf('ls -1 %s/releases', $hostPath); + + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdListReleases, true); + if (!$process->isSuccessful()) { + $releases = []; + } else { + $releases = explode(PHP_EOL, trim($process->getOutput())); + rsort($releases); + } + + if (in_array($releaseToRollback, $releases)) { + if ($releaseIdCandidate === false) { + $releaseIdCandidate = $releaseToRollback; + } else { + if ($releaseIdCandidate != $releaseToRollback) { + $releaseAvailableInAllHosts = false; + } + } + } + + $this->runtime->setWorkingHost(null); + } + + if ($releaseAvailableInAllHosts) { + return $releaseIdCandidate; + } + + return false; + } +} diff --git a/src/Mage/Command/BuiltIn/VersionCommand.php b/src/Mage/Command/BuiltIn/VersionCommand.php new file mode 100644 index 0000000..f5a1a0f --- /dev/null +++ b/src/Mage/Command/BuiltIn/VersionCommand.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Command\BuiltIn; + +use Mage\Mage; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Mage\Command\AbstractCommand; + +/** + * Version Command, return the current version of Magallanes + * + * @author Andrés Montañez + */ +class VersionCommand extends AbstractCommand +{ + /** + * Configure the Command + */ + protected function configure() + { + $this + ->setName('version') + ->setDescription('Get the version of Magallanes') + ; + } + + /** + * Executes the Command + * + * @param InputInterface $input + * @param OutputInterface $output + * @return int + */ + protected function execute(InputInterface $input, OutputInterface $output) + { + $output->writeln(sprintf('Magallanes v%s [%s]', Mage::VERSION, Mage::CODENAME)); + + return 0; + } +} diff --git a/src/Mage/Mage.php b/src/Mage/Mage.php new file mode 100644 index 0000000..1af7c75 --- /dev/null +++ b/src/Mage/Mage.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage; + +/** + * Mallaganes base class + * + * @author Andrés Montañez + */ +class Mage +{ + const VERSION = '3.0.0-alpha1'; + const CODENAME = 'Nostromo'; +} diff --git a/src/Mage/MageApplication.php b/src/Mage/MageApplication.php new file mode 100644 index 0000000..b0ec8ee --- /dev/null +++ b/src/Mage/MageApplication.php @@ -0,0 +1,95 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage; + +use Mage\Command\AbstractCommand; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; +use Monolog\Logger; +use Monolog\Handler\StreamHandler; +use Symfony\Component\Console\Application; +use Symfony\Component\Yaml\Yaml; +use Mage\Runtime\Exception\RuntimeException; + +/** + * The Console Application for launching the Mage command in a standalone instance + * + * @author Andrés Montañez + */ +class MageApplication extends Application +{ + private $configuration; + protected $logger; + + /** + * Configure the Magallanes Application + * + * @param $file string The YAML file from which to read the configuration + * + * @throws RuntimeException + */ + public function configure($file) + { + $config = Yaml::parse(file_get_contents($file)); + if (array_key_exists('magephp', $config)) { + $this->configuration = $config['magephp']; + + if (array_key_exists('log_dir', $this->configuration)) { + $logfile = sprintf('%s/%s.log', $this->configuration['log_dir'], date('Ymd_His')); + $this->configuration['log_file'] = $logfile; + + $this->logger = new Logger('magephp'); + $this->logger->pushHandler(new StreamHandler($logfile)); + } + } else { + throw new RuntimeException(sprintf('The file "%s" does not have a valid Magallanes configuration.', $file)); + } + } + + /** + * Run the Application + * + * @param InputInterface $input + * @param OutputInterface $output + * @throws Exception + */ + public function run(InputInterface $input = null, OutputInterface $output = null) + { + $this->loadBuiltInCommands(); + + parent::run(); + } + + /** + * Loads the BuiltIn Commands + */ + protected function loadBuiltInCommands() + { + $finder = new Finder(); + $finder->files()->in(__DIR__ . '/Command/BuiltIn')->name('*Command.php'); + + /** @var SplFileInfo $file */ + foreach ($finder as $file) { + $class = substr('\\Mage\\Command\\BuiltIn\\' . str_replace('/', '\\', $file->getRelativePathname()), 0, -4); + if (class_exists($class)) { + $command = new $class(); + + if ($command instanceof AbstractCommand) { + $command->setLogger($this->logger); + $command->setConfiguration($this->configuration); + $this->add($command); + } + } + } + } +} diff --git a/src/Mage/Runtime/Exception/DeploymentException.php b/src/Mage/Runtime/Exception/DeploymentException.php new file mode 100644 index 0000000..5b2d37f --- /dev/null +++ b/src/Mage/Runtime/Exception/DeploymentException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Runtime\Exception; + +/** + * An Error occurred while Deploying + * + * @author Andrés Montañez + */ +class DeploymentException extends RuntimeException +{ +} diff --git a/src/Mage/Runtime/Exception/InvalidEnvironmentException.php b/src/Mage/Runtime/Exception/InvalidEnvironmentException.php new file mode 100644 index 0000000..10f4359 --- /dev/null +++ b/src/Mage/Runtime/Exception/InvalidEnvironmentException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Runtime\Exception; + +/** + * The provided Environment is invalid + * + * @author Andrés Montañez + */ +class InvalidEnvironmentException extends RuntimeException +{ +} diff --git a/src/Mage/Runtime/Exception/RuntimeException.php b/src/Mage/Runtime/Exception/RuntimeException.php new file mode 100644 index 0000000..5b0fc75 --- /dev/null +++ b/src/Mage/Runtime/Exception/RuntimeException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Runtime\Exception; + +use Exception; + +/** + * An Error occurred while running + * + * @author Andrés Montañez + */ +class RuntimeException extends Exception +{ +} diff --git a/src/Mage/Runtime/Runtime.php b/src/Mage/Runtime/Runtime.php new file mode 100644 index 0000000..24b3bbf --- /dev/null +++ b/src/Mage/Runtime/Runtime.php @@ -0,0 +1,398 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Runtime; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use Symfony\Component\Process\Process; +use Mage\Runtime\Exception\InvalidEnvironmentException; + +/** + * Runtime is a container of all run in time configuration, stages of progress, hosts being deployed, etc. + * + * @author Andrés Montañez + */ +class Runtime +{ + const PRE_DEPLOY = 'pre-deploy'; + const ON_DEPLOY = 'on-deploy'; + const POST_DEPLOY = 'post-deploy'; + const ON_RELEASE = 'on-release'; + const POST_RELEASE = 'post-release'; + + /** + * @var array Magallanes configuration + */ + protected $configuration; + + /** + * @var string|null Environment being deployed + */ + protected $environment; + + /** + * @var string Stage of Deployment + */ + protected $stage; + + /** + * @var string|null The host being deployed to + */ + protected $workingHost; + + /** + * @var string|null The Release ID + */ + protected $releaseId = null; + + /** + * @var array Hold a bag of variables for sharing information between tasks, if needed + */ + protected $vars = []; + + /** + * @var LoggerInterface|null The logger instance + */ + protected $logger; + + /** + * @var bool Indicates if a Rollback operation is in progress + */ + protected $rollback = false; + + /** + * Sets the Release ID + * + * @param string $releaseId Release ID + * @return Runtime + */ + public function setReleaseId($releaseId) + { + $this->releaseId = $releaseId; + return $this; + } + + /** + * Retrieve the current Release ID + * + * @return null|string Release ID + */ + public function getReleaseId() + { + return $this->releaseId; + } + + /** + * Sets the Runtime in Rollback mode On or Off + * + * @param bool $inRollback + * @return Runtime + */ + public function setRollback($inRollback) + { + $this->rollback = $inRollback; + return $this; + } + + /** + * Indicates if Runtime is in rollback + * + * @return bool + */ + public function inRollback() + { + return $this->rollback; + } + + /** + * Sets a value in the Vars bag + * + * @param mixed $key Variable name + * @param mixed $value Variable value + * @return Runtime + */ + public function setVar($key, $value) + { + $this->vars[$key] = $value; + return $this; + } + + /** + * Retrieve a value from the Vars bag + * + * @param mixed $key Variable name + * @param mixed $default Variable default value, returned if not found + * @return mixed + */ + public function getVar($key, $default = null) + { + if (array_key_exists($key, $this->vars)) { + return $this->vars[$key]; + } + + return $default; + } + + /** + * Sets the Logger instance + * + * @param LoggerInterface $logger Logger instance + * @return Runtime + */ + public function setLogger(LoggerInterface $logger = null) + { + $this->logger = $logger; + return $this; + } + + /** + * Sets the Magallanes Configuration to the Runtime + * + * @param array $configuration Configuration + * @return Runtime + */ + public function setConfiguration($configuration) + { + $this->configuration = $configuration; + return $this; + } + + /** + * Retrieve the Configuration + * + * @return array + */ + public function getConfiguration() + { + return $this->configuration; + } + + /** + * Retrieves the Configuration options for a specific section in the configuration + * + * @param mixed $key Section name + * @param mixed $default Default value + * @return mixed + */ + public function getConfigOptions($key, $default = null) + { + if (array_key_exists($key, $this->configuration)) { + return $this->configuration[$key]; + } + + return $default; + } + + /** + * Returns the configuration for the current Environment + * If $key is provided, it will be returned only that section, if not found the default value will be returned, + * if $key is not provided, the whole Environment's configuration will be returned + * + * @param mixed $key Section name + * @param mixed $default Default value + * @return mixed + * @throws InvalidEnvironmentException + */ + public function getEnvironmentConfig($key = null, $default = null) + { + if (!array_key_exists($this->environment, $this->configuration['environments'])) { + return []; + } + + $config = $this->configuration['environments'][$this->environment]; + if ($key !== null) { + if (array_key_exists($key, $config)) { + return $config[$key]; + } else { + return $default; + } + } + + return $config; + } + + /** + * Sets the working Environment + * + * @param string $environment Environment name + * @return Runtime + * @throws InvalidEnvironmentException + */ + public function setEnvironment($environment) + { + if (array_key_exists('environments', $this->configuration) && array_key_exists($environment, $this->configuration['environments'])) { + $this->environment = $environment; + return $this; + } + + throw new InvalidEnvironmentException(sprintf('The environment "%s" does not exists.', $environment), 1000); + } + + /** + * Returns the current working Environment + * + * @return null|string + */ + public function getEnvironment() + { + return $this->environment; + } + + /** + * Sets the working stage + * + * @param string $stage Stage code + * @return Runtime + */ + public function setStage($stage) + { + $this->stage = $stage; + return $this; + } + + /** + * Retrieve the current wokring Stage + * + * @return string + */ + public function getStage() + { + return $this->stage; + } + + /** + * Retrieve the defined Tasks for the current Environment and Stage + * + * @return array + * @throws InvalidEnvironmentException + */ + public function getTasks() + { + $config = $this->getEnvironmentConfig(); + if (array_key_exists($this->stage, $config)) { + if (is_array($config[$this->stage])) { + return $config[$this->stage]; + } + } + + return []; + } + + /** + * Sets the working Host + * + * @param string $host Host name + * @return Runtime + */ + public function setWorkingHost($host) + { + $this->workingHost = $host; + return $this; + } + + /** + * Retrieve the working Host + * + * @return null|string + */ + public function getWorkingHost() + { + return $this->workingHost; + } + + /** + * Logs a Message into the Logger + * + * @param string $message Log message + * @param string $level Log Level + */ + public function log($message, $level = LogLevel::DEBUG) + { + if ($this->logger instanceof LoggerInterface) { + $this->logger->log($level, $message); + } + } + + /** + * Executes a command, it will be run Locally or Remotely based on the working Stage + * + * @param string $cmd Command to execute + * @param int $timeout Seconds to wait + * @return Process + */ + public function runCommand($cmd, $timeout = 120) + { + switch ($this->getStage()) { + case self::ON_DEPLOY: + case self::ON_RELEASE: + case self::POST_RELEASE: + return $this->runRemoteCommand($cmd, true, $timeout); + break; + default: + return $this->runLocalCommand($cmd, $timeout); + break; + } + } + + /** + * Execute a command locally + * + * @param string $cmd Command to execute + * @param int $timeout Seconds to wait + * @return Process + */ + public function runLocalCommand($cmd, $timeout = 120) + { + $this->log($cmd, LogLevel::INFO); + + $process = new Process($cmd); + $process->setTimeout($timeout); + $process->run(); + + $this->log($process->getOutput(), LogLevel::DEBUG); + if (!$process->isSuccessful()) { + $this->log($process->getErrorOutput(), LogLevel::ERROR); + } + + return $process; + } + + /** + * Executes a command remotely, if jail is true, it will run inside the Host Path and the Release (if available) + * + * @param string $cmd Command to execute + * @param bool $jail Jail the command + * @param int $timeout Seconds to wait + * @return Process + * @throws InvalidEnvironmentException + */ + public function runRemoteCommand($cmd, $jail = true, $timeout = 120) + { + $user = $this->getEnvironmentConfig('user'); + $host = $this->getWorkingHost(); + $sshFlags = $this->getEnvironmentConfig('ssh', '-p 22 -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'); + + $cmdDelegate = $cmd; + if ($jail) { + $hostPath = rtrim($this->getEnvironmentConfig('host_path'), '/'); + if ($this->getReleaseId()) { + $cmdDelegate = sprintf('cd %s/releases/%s && %s', $hostPath, $this->getReleaseId(), $cmdDelegate); + } else { + $cmdDelegate = sprintf('cd %s && %s', $hostPath, $cmdDelegate); + } + } + + $cmdRemote = str_replace(['"', '&', ';'], ['\"', '\&', '\;'], $cmdDelegate); + $cmdLocal = sprintf('ssh %s %s@%s sh -c \"%s\"', $sshFlags, $user, $host, $cmdRemote); + + return $this->runLocalCommand($cmdLocal, $timeout); + } +} diff --git a/src/Mage/Task/AbstractTask.php b/src/Mage/Task/AbstractTask.php new file mode 100644 index 0000000..645da30 --- /dev/null +++ b/src/Mage/Task/AbstractTask.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task; + +use Mage\Runtime\Runtime; + +/** + * Abstract base class for Magallanes Tasks + * + * @author Andrés Montañez + */ +abstract class AbstractTask +{ + /** + * @var array Task custom options + */ + protected $options = []; + + /** + * @var Runtime + */ + protected $runtime; + + /** + * Get the Name/Code of the Task + * + * @return string + */ + abstract public function getName(); + + /** + * Get a short Description of the Task + * + * @return string + */ + abstract public function getDescription(); + + /** + * Executes the Command + * + * @return bool + */ + abstract public function execute(); + + /** + * Set additional Options for the Task + * + * @param array $options Options + * @return AbstractTask + */ + public function setOptions($options = []) + { + $this->options = $options; + return $this; + } + + /** + * Set the Runtime instance + * + * @param Runtime $runtime + * @return AbstractTask + */ + public function setRuntime(Runtime $runtime) + { + $this->runtime = $runtime; + return $this; + } +} diff --git a/src/Mage/Task/BuiltIn/Composer/GenerateAutoloadTask.php b/src/Mage/Task/BuiltIn/Composer/GenerateAutoloadTask.php new file mode 100644 index 0000000..d0a9255 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Composer/GenerateAutoloadTask.php @@ -0,0 +1,46 @@ + + */ +class GenerateAutoloadTask extends AbstractTask +{ + public function getName() + { + return 'composer/generate-autoload'; + } + + public function getDescription() + { + return '[Composer] Generate Autoload'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['path'] . ' dumpautoload ' . $options['flags']; + + /** @var Process $process */ + $process = $this->runtime->runCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $options = array_merge( + ['path' => 'composer', 'flags' => '--optimize'], + $this->runtime->getConfigOptions('composer', []), + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Composer/InstallTask.php b/src/Mage/Task/BuiltIn/Composer/InstallTask.php new file mode 100644 index 0000000..74f1cab --- /dev/null +++ b/src/Mage/Task/BuiltIn/Composer/InstallTask.php @@ -0,0 +1,46 @@ + + */ +class InstallTask extends AbstractTask +{ + public function getName() + { + return 'composer/install'; + } + + public function getDescription() + { + return '[Composer] Install'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['path'] . ' install ' . $options['flags']; + + /** @var Process $process */ + $process = $this->runtime->runCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $options = array_merge( + ['path' => 'composer', 'flags' => '--dev'], + $this->runtime->getConfigOptions('composer', []), + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/Release/CleanupTask.php b/src/Mage/Task/BuiltIn/Deploy/Release/CleanupTask.php new file mode 100644 index 0000000..395b16b --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/Release/CleanupTask.php @@ -0,0 +1,67 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy\Release; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Release Task - Cleanup old releases + * + * @author Andrés Montañez + */ +class CleanupTask extends AbstractTask +{ + public function getName() + { + return 'deploy/release/cleanup'; + } + + public function getDescription() + { + return '[Release] Cleaning up old Releases'; + } + + public function execute() + { + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + $currentReleaseId = $this->runtime->getReleaseId(); + $maxReleases = $this->runtime->getEnvironmentConfig('releases'); + + $cmdListReleases = sprintf('ls -1 %s/releases', $hostPath); + + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdListReleases, false); + if ($process->isSuccessful()) { + $releases = $process->getOutput(); + $releases = explode(PHP_EOL, trim($releases)); + + if (count($releases) > $maxReleases) { + sort($releases); + $releasesToDelete = array_slice($releases, 0, count($releases) - $maxReleases); + foreach ($releasesToDelete as $releaseId) { + if ($releaseId != $currentReleaseId) { + $cmdDeleteRelease = sprintf('rm -rf %s/releases/%s', $hostPath, $releaseId); + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdDeleteRelease, false); + if (!$process->isSuccessful()) { + return false; + } + } + } + } + + return true; + } + + return false; + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/Release/PrepareTask.php b/src/Mage/Task/BuiltIn/Deploy/Release/PrepareTask.php new file mode 100644 index 0000000..2c82833 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/Release/PrepareTask.php @@ -0,0 +1,43 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy\Release; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Release Task - Create the Release Directory + * + * @author Andrés Montañez + */ +class PrepareTask extends AbstractTask +{ + public function getName() + { + return 'deploy/release/prepare'; + } + + public function getDescription() + { + return '[Release] Preparing Release'; + } + + public function execute() + { + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + + $cmdMakeDir = sprintf('mkdir -p %s/releases/%s', $hostPath, $this->runtime->getReleaseId()); + + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdMakeDir, false); + return $process->isSuccessful(); + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/ReleaseTask.php b/src/Mage/Task/BuiltIn/Deploy/ReleaseTask.php new file mode 100644 index 0000000..25779e2 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/ReleaseTask.php @@ -0,0 +1,45 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy; + +use Mage\Task\ExecuteOnRollbackInterface; +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Release Task - Create the Symlink + * + * @author Andrés Montañez + */ +class ReleaseTask extends AbstractTask implements ExecuteOnRollbackInterface +{ + public function getName() + { + return 'deploy/release'; + } + + public function getDescription() + { + return '[Release] Creating Symlink'; + } + + public function execute() + { + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + $releaseId = $this->runtime->getReleaseId(); + + $cmdLinkRelease = sprintf('cd %s && ln -snf releases/%s current', $hostPath, $releaseId); + + /** @var Process $process */ + $process = $this->runtime->runRemoteCommand($cmdLinkRelease, false); + return $process->isSuccessful(); + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/RsyncTask.php b/src/Mage/Task/BuiltIn/Deploy/RsyncTask.php new file mode 100644 index 0000000..9d6f8a3 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/RsyncTask.php @@ -0,0 +1,63 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Rsync Task - Copy files with Rsync + * + * @author Andrés Montañez + */ +class RsyncTask extends AbstractTask +{ + public function getName() + { + return 'deploy/rsync'; + } + + public function getDescription() + { + return '[Deploy] Copying files with Rsync'; + } + + public function execute() + { + $user = $this->runtime->getEnvironmentConfig('user'); + $host = $this->runtime->getWorkingHost(); + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + $targetDir = rtrim($hostPath, '/'); + + if ($this->runtime->getEnvironmentConfig('releases', false)) { + $targetDir = sprintf('%s/releases/%s', $hostPath, $this->runtime->getReleaseId()); + } + + $excludes = $this->getExcludes(); + $cmdRsync = sprintf('rsync -avz %s ./ %s@%s:%s', $excludes, $user, $host, $targetDir); + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($cmdRsync, 600); + return $process->isSuccessful(); + } + + protected function getExcludes() + { + $excludes = $this->runtime->getEnvironmentConfig('exclude', []); + $excludes = array_merge(['.git'], $excludes); + + foreach ($excludes as &$exclude) { + $exclude = '--exclude=' . $exclude; + } + + return implode(' ', $excludes); + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/TarGz/CleanupTask.php b/src/Mage/Task/BuiltIn/Deploy/TarGz/CleanupTask.php new file mode 100644 index 0000000..516d5c0 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/TarGz/CleanupTask.php @@ -0,0 +1,52 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy\TarGz; + +use Mage\Runtime\Exception\DeploymentException; +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * TarGz Task - Delete temporal Tar + * + * @author Andrés Montañez + */ +class CleanupTask extends AbstractTask +{ + public function getName() + { + return 'deploy/targz/cleanup'; + } + + public function getDescription() + { + return '[Deploy] Cleanup TarGZ file'; + } + + public function execute() + { + if (!$this->runtime->getEnvironmentConfig('releases', false)) { + throw new DeploymentException('This task is only available with releases enabled', 400); + } + + $tarGzLocal = $this->runtime->getVar('targz_local'); + + $cmdDeleteTarGz = sprintf('rm %s', $tarGzLocal); + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($cmdDeleteTarGz); + if ($process->isSuccessful()) { + return true; + } + + return false; + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/TarGz/CopyTask.php b/src/Mage/Task/BuiltIn/Deploy/TarGz/CopyTask.php new file mode 100644 index 0000000..f660689 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/TarGz/CopyTask.php @@ -0,0 +1,69 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy\TarGz; + +use Mage\Runtime\Exception\DeploymentException; +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * TarGz Task - Copy Tar + * + * @author Andrés Montañez + */ +class CopyTask extends AbstractTask +{ + public function getName() + { + return 'deploy/targz/copy'; + } + + public function getDescription() + { + return '[Deploy] Copying files with TarGZ'; + } + + public function execute() + { + if (!$this->runtime->getEnvironmentConfig('releases', false)) { + throw new DeploymentException('This task is only available with releases enabled', 400); + } + + $user = $this->runtime->getEnvironmentConfig('user'); + $host = $this->runtime->getWorkingHost(); + $scpFlags = $this->runtime->getEnvironmentConfig('scp', '-P 22 -q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'); + $hostPath = rtrim($this->runtime->getEnvironmentConfig('host_path'), '/'); + $currentReleaseId = $this->runtime->getReleaseId(); + + $targetDir = sprintf('%s/releases/%s', $hostPath, $currentReleaseId); + + $tarGzLocal = $this->runtime->getVar('targz_local'); + $tarGzRemote = basename($tarGzLocal); + + $cmdCopy = sprintf('scp %s %s %s@%s:%s/%s', $scpFlags, $tarGzLocal, $user, $host, $targetDir, $tarGzRemote); + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($cmdCopy, 300); + if ($process->isSuccessful()) { + $cmdUntar = sprintf('cd %s && tar xfz %s', $targetDir, $tarGzRemote); + $process = $this->runtime->runRemoteCommand($cmdUntar, false, 600); + if ($process->isSuccessful()) { + $cmdDelete = sprintf('rm %s/%s', $targetDir, $tarGzRemote); + $process = $this->runtime->runRemoteCommand($cmdDelete, false); + if ($process->isSuccessful()) { + return true; + } + } + } + + return false; + } +} diff --git a/src/Mage/Task/BuiltIn/Deploy/TarGz/PrepareTask.php b/src/Mage/Task/BuiltIn/Deploy/TarGz/PrepareTask.php new file mode 100644 index 0000000..7644e3e --- /dev/null +++ b/src/Mage/Task/BuiltIn/Deploy/TarGz/PrepareTask.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Deploy\TarGz; + +use Mage\Runtime\Exception\DeploymentException; +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * TarGz Task - Create temporal Tar + * + * @author Andrés Montañez + */ +class PrepareTask extends AbstractTask +{ + public function getName() + { + return 'deploy/targz/prepare'; + } + + public function getDescription() + { + return '[Deploy] Preparing TarGz file'; + } + + public function execute() + { + if (!$this->runtime->getEnvironmentConfig('releases', false)) { + throw new DeploymentException('This task is only available with releases enabled', 400); + } + + $tarGzLocal = tempnam(sys_get_temp_dir(), 'mage'); + $this->runtime->setVar('targz_local', $tarGzLocal); + + $excludes = $this->getExcludes(); + $cmdTarGz = sprintf('tar cfz %s %s ./', $tarGzLocal, $excludes); + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($cmdTarGz, 300); + return $process->isSuccessful(); + } + + protected function getExcludes() + { + $excludes = $this->runtime->getEnvironmentConfig('exclude', []); + $excludes = array_merge(['.git'], $excludes); + + foreach ($excludes as &$exclude) { + $exclude = '--exclude=' . $exclude; + } + + return implode(' ', $excludes); + } +} diff --git a/src/Mage/Task/BuiltIn/Git/ChangeBranchTask.php b/src/Mage/Task/BuiltIn/Git/ChangeBranchTask.php new file mode 100644 index 0000000..7ad9eaa --- /dev/null +++ b/src/Mage/Task/BuiltIn/Git/ChangeBranchTask.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Git; + +use Mage\Task\SkipException; +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Git Task - Checkout Branch + * + * @author Andrés Montañez + */ +class ChangeBranchTask extends AbstractTask +{ + public function getName() + { + return 'git/change-branch'; + } + + public function getDescription() + { + $options = $this->getOptions(); + $branch = $options['branch']; + + if ($this->runtime->getVar('git_revert_branch', false)) { + $branch = $this->runtime->getVar('git_revert_branch'); + } + + return sprintf('[Git] Change Branch (%s)', $branch); + } + + public function execute() + { + $options = $this->getOptions(); + $branch = $options['branch']; + + if (!$this->runtime->getVar('git_revert_branch', false)) { + $cmdGetCurrent = sprintf('%s branch | grep "*"', $options['path']); + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($cmdGetCurrent); + if ($process->isSuccessful()) { + $initialBranch = str_replace('* ', '', trim($process->getOutput())); + + if ($initialBranch == $branch) { + throw new SkipException(); + } else { + $this->runtime->setVar('git_revert_branch', $initialBranch); + } + } else { + return false; + } + } else { + $branch = $this->runtime->getVar('git_revert_branch'); + } + + $cmdChange = sprintf('%s checkout %s', $options['path'], $branch); + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($cmdChange); + return $process->isSuccessful(); + } + + protected function getOptions() + { + $config = $this->runtime->getEnvironmentConfig(); + $branch = 'master'; + if (array_key_exists('branch', $config)) { + $branch = $config['branch']; + } + + $options = array_merge( + ['path' => 'git', 'branch' => $branch], + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Git/UpdateTask.php b/src/Mage/Task/BuiltIn/Git/UpdateTask.php new file mode 100644 index 0000000..80424d4 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Git/UpdateTask.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Git; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Git Task - Pull + * + * @author Andrés Montañez + */ +class UpdateTask extends AbstractTask +{ + public function getName() + { + return 'git/update'; + } + + public function getDescription() + { + return '[Git] Update'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['path'] . ' pull'; + + /** @var Process $process */ + $process = $this->runtime->runLocalCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $config = $this->runtime->getEnvironmentConfig(); + $branch = 'master'; + if (array_key_exists('branch', $config)) { + $branch = $config['branch']; + } + + $options = array_merge( + ['path' => 'git', 'branch' => $branch], + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Symfony/AsseticDumpTask.php b/src/Mage/Task/BuiltIn/Symfony/AsseticDumpTask.php new file mode 100644 index 0000000..6f7eadc --- /dev/null +++ b/src/Mage/Task/BuiltIn/Symfony/AsseticDumpTask.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Symfony; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Symfony Task - Dump Assetics + * + * @author Andrés Montañez + */ +class AsseticDumpTask extends AbstractTask +{ + public function getName() + { + return 'symfony/assetic-dump'; + } + + public function getDescription() + { + return '[Symfony] Assetic Dump'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['console'] . ' assetic:dump --env=' . $options['env'] . ' ' . $options['flags']; + + /** @var Process $process */ + $process = $this->runtime->runCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $options = array_merge( + ['path' => 'bin/console', 'env' => 'dev', 'flags' => ''], + $this->runtime->getConfigOptions('symfony', []), + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Symfony/AssetsInstallTask.php b/src/Mage/Task/BuiltIn/Symfony/AssetsInstallTask.php new file mode 100644 index 0000000..2f0b994 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Symfony/AssetsInstallTask.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Symfony; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Symfony Task - Install Assets + * + * @author Andrés Montañez + */ +class AssetsInstallTask extends AbstractTask +{ + public function getName() + { + return 'symfony/assets-install'; + } + + public function getDescription() + { + return '[Symfony] Assets Install'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['console'] . ' assets:install --env=' . $options['env'] . ' ' . $options['flags'] . ' ' . $options['target']; + + /** @var Process $process */ + $process = $this->runtime->runCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $options = array_merge( + ['path' => 'bin/console', 'env' => 'dev', 'target' => 'web', 'flags' => '--symlink --relative'], + $this->runtime->getConfigOptions('symfony', []), + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Symfony/CacheClearTask.php b/src/Mage/Task/BuiltIn/Symfony/CacheClearTask.php new file mode 100644 index 0000000..a781ab4 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Symfony/CacheClearTask.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Symfony; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Symfony Task - Clear Cache + * + * @author Andrés Montañez + */ +class CacheClearTask extends AbstractTask +{ + public function getName() + { + return 'symfony/cache-clear'; + } + + public function getDescription() + { + return '[Symfony] Cache Clear'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['console'] . ' cache:clear --env=' . $options['env'] . ' ' . $options['flags']; + + /** @var Process $process */ + $process = $this->runtime->runCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $options = array_merge( + ['path' => 'bin/console', 'env' => 'dev', 'flags' => ''], + $this->runtime->getConfigOptions('symfony', []), + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/BuiltIn/Symfony/CacheWarmupTask.php b/src/Mage/Task/BuiltIn/Symfony/CacheWarmupTask.php new file mode 100644 index 0000000..ad0de15 --- /dev/null +++ b/src/Mage/Task/BuiltIn/Symfony/CacheWarmupTask.php @@ -0,0 +1,54 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task\BuiltIn\Symfony; + +use Symfony\Component\Process\Process; +use Mage\Task\AbstractTask; + +/** + * Symfony Task - Cache Warmup + * + * @author Andrés Montañez + */ +class CacheWarmupTask extends AbstractTask +{ + public function getName() + { + return 'symfony/cache-warmup'; + } + + public function getDescription() + { + return '[Symfony] Cache Warmup'; + } + + public function execute() + { + $options = $this->getOptions(); + $command = $options['console'] . ' cache:warmup --env=' . $options['env'] . ' ' . $options['flags']; + + /** @var Process $process */ + $process = $this->runtime->runCommand($command); + + return $process->isSuccessful(); + } + + protected function getOptions() + { + $options = array_merge( + ['path' => 'bin/console', 'env' => 'dev', 'flags' => ''], + $this->runtime->getConfigOptions('symfony', []), + $this->options + ); + + return $options; + } +} diff --git a/src/Mage/Task/ErrorException.php b/src/Mage/Task/ErrorException.php new file mode 100644 index 0000000..c5521f5 --- /dev/null +++ b/src/Mage/Task/ErrorException.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task; + +use Exception; + +/** + * The Task Failed, and it has a Custom Message + * + * @author Andrés Montañez + */ +class ErrorException extends Exception +{ + public function getTrimmedMessage($maxLength = 20) + { + $message = $this->getMessage(); + + if (strlen($message) > $maxLength) { + $message = substr($message, 0, $maxLength) . '...'; + } + + return $message; + } +} diff --git a/src/Mage/Task/ExecuteOnRollbackInterface.php b/src/Mage/Task/ExecuteOnRollbackInterface.php new file mode 100644 index 0000000..2c8fe81 --- /dev/null +++ b/src/Mage/Task/ExecuteOnRollbackInterface.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task; + +/** + * The Task will be Executed in a Rollback + * + * @author Andrés Montañez + */ +interface ExecuteOnRollbackInterface +{ +} diff --git a/src/Mage/Task/SkipException.php b/src/Mage/Task/SkipException.php new file mode 100644 index 0000000..c4f8eb8 --- /dev/null +++ b/src/Mage/Task/SkipException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task; + +use Exception; + +/** + * The Task will be Skipped + * + * @author Andrés Montañez + */ +class SkipException extends Exception +{ +} diff --git a/src/Mage/Task/TaskFactory.php b/src/Mage/Task/TaskFactory.php new file mode 100644 index 0000000..d1d7af4 --- /dev/null +++ b/src/Mage/Task/TaskFactory.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage\Task; + +use Mage\Runtime\Runtime; +use Mage\Runtime\Exception\RuntimeException; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; + +/** + * Task Factory + * + * @author Andrés Montañez + */ +class TaskFactory +{ + /** + * @var Runtime + */ + protected $runtime; + + /** + * @var array Registered Tasks + */ + protected $registeredTasks = []; + + /** + * Constructor + * + * @param Runtime $runtime + */ + public function __construct(Runtime $runtime) + { + $this->runtime = $runtime; + $this->loadBuiltInTasks(); + } + + /** + * Add a Task + * + * @param AbstractTask $task + */ + public function add(AbstractTask $task) + { + $task->setRuntime($this->runtime); + $this->registeredTasks[$task->getName()] = $task; + } + + /** + * Get a Task by it's registered Name/Code, or it can be a Class Name, + * in that case the class will be instantiated + * + * @param string $name Name/Code or Class of the Task + * @return AbstractTask + * @throws RuntimeException + */ + public function get($name) + { + if (is_array($name)) { + $options = $name; + list($name) = array_keys($name); + $options = $options[$name]; + } else { + $options = []; + } + + if (array_key_exists($name, $this->registeredTasks)) { + /** @var AbstractTask $task */ + $task = $this->registeredTasks[$name]; + $task->setOptions($options); + return $task; + } elseif (class_exists($name)) { + $task = new $name(); + if ($task instanceof AbstractTask) { + $task->setOptions($options); + $this->add($task); + return $task; + } + } + + throw new RuntimeException(sprintf('Invalid task name "%s"', $name)); + } + + /** + * Load BuiltIn Tasks + */ + protected function loadBuiltInTasks() + { + $finder = new Finder(); + $finder->files()->in(__DIR__ . '/BuiltIn')->name('*Task.php'); + + /** @var SplFileInfo $file */ + foreach ($finder as $file) { + $class = substr('\\Mage\\Task\\BuiltIn\\' . str_replace('/', '\\', $file->getRelativePathname()), 0, -4); + if (class_exists($class)) { + $task = new $class(); + + if ($task instanceof AbstractTask) { + $this->add($task); + } + } + } + } +} diff --git a/src/Mage/Utils.php b/src/Mage/Utils.php new file mode 100644 index 0000000..e82dc44 --- /dev/null +++ b/src/Mage/Utils.php @@ -0,0 +1,121 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Mage; + +use Mage\Runtime\Runtime; +use DateTime; + +/** + * Utility class for resolving trivial operations + * + * @author Andrés Montañez + */ +class Utils +{ + /** + * Given a stage code it will resolve a human friendly name + * + * @param string $stage + * @return string + */ + public static function getStageName($stage) + { + switch ($stage) { + case Runtime::PRE_DEPLOY: + return 'Pre Deployment'; + break; + + case Runtime::ON_DEPLOY: + return 'On Deployment'; + break; + + case Runtime::POST_DEPLOY: + return 'Post Deployment'; + break; + + case Runtime::ON_RELEASE: + return 'On Release'; + break; + + case Runtime::POST_RELEASE: + return 'Post Release'; + break; + } + + return $stage; + } + + /** + * Given a Release ID, convert it to a DateTime instance + * + * @param string $releaseId The Release ID + * @return DateTime + */ + public static function getReleaseDate($releaseId) + { + $formatted = sprintf('%d%d%d%d-%d%d-%d%d %d%d:%d%d:%d%d', + $releaseId[0], $releaseId[1], $releaseId[2], $releaseId[3], + $releaseId[4], $releaseId[5], + $releaseId[6], $releaseId[7], + $releaseId[8], $releaseId[9], + $releaseId[10], $releaseId[11], + $releaseId[12], $releaseId[13]); + + return new DateTime($formatted); + } + + /** + * Given a Date, calculate friendly how much time has passed + * + * @param DateTime $releaseDate + * @return string + */ + public static function getTimeDiff(DateTime $releaseDate) + { + $textDiff = ''; + $now = new DateTime(); + $diff = $now->diff($releaseDate); + if ($diff->format('%a') <= 7) { + if ($diff->format('%d') == 7) { + $textDiff = 'a week ago'; + } elseif ($diff->format('%d') > 0 && $diff->format('%d') < 7) { + $days = $diff->format('%d'); + if ($days <= 1) { + $textDiff = 'one day ago'; + } else { + $textDiff = $days . ' days ago'; + } + } elseif ($diff->format('%d') == 0 && $diff->format('%h') > 0) { + $hours = $diff->format('%h'); + if ($hours <= 1) { + $textDiff = 'one hour ago'; + } else { + $textDiff = $hours . ' hours ago'; + } + } elseif ($diff->format('%d') == 0 && $diff->format('%h') == 0) { + $minutes = $diff->format('%i'); + if ($minutes <= 1) { + $textDiff = 'one minute ago'; + } else { + $textDiff = $minutes . ' minutes ago'; + } + } elseif ($diff->format('%d') == 0 && $diff->format('%h') == 0 && $diff->format('%i') == 0) { + $seconds = $diff->format('%s'); + if ($seconds < 10) { + $textDiff = 'just now!'; + } else { + $textDiff = $seconds . ' seconds ago'; + } + } + } + return $textDiff; + } +}