diff --git a/.gitignore b/.gitignore index a68b5f971..51072d51f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,11 @@ # Cache and compiled -bin/rocketeer.phar -bin/phar -storage .rocketeer - -# Plugins -rocketeer-campfire +bin/phar +bin/rocketeer.phar +composer.lock # Tests -tests/_meta/coverage +tests/_server/foobar # Dependencies -vendor \ No newline at end of file +vendor diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 91e45b800..000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs"] - path = docs - url = https://github.com/Anahkiasen/rocketeer.wiki.git diff --git a/.travis.yml b/.travis.yml index cca97f3fa..dad16d49b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,19 +1,18 @@ -language: php - -php: - - 5.3 - - 5.4 - - 5.5 - - 5.6 - - hhvm - -before_script: - - composer self-update - - composer install --dev --prefer-dist - -matrix: - allow_failures: - - php: hhvm - fast_finish: true - -script: phpunit \ No newline at end of file +language: php + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm-nightly + +before_script: + - travis_retry composer self-update + - travis_retry composer install --no-interaction --prefer-source --dev + +matrix: + allow_failures: + - php: hhvm-nightly + fast_finish: true + +script: phpunit --debug diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b62dbe94..88222a515 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,191 +1,303 @@ -### Changelog - -### 1.2.2 - -- **The Notifier plugin module not has a hook for before and after deployment** +# Changelog + +2.0.0 +----- + +### Added +- Added ability to run tasks in parallel via the `--parallel` flag (or `-P`) +- Added ability to have multiple servers for one connection, just define them in a `servers` array in your connection, each entry being an usual connection credentials array +- Added support for defining contextual configurations in files (`.rocketeer/connections/{connection}/scm.php`, same for stages) +- Core tasks (Deploy, Check, Test, Migrate) now use a module system called Strategies +- Added a `Sync` DeployStrategy in addition to `Clone` and `Copy` that uses rsync to create a new release +- Added static helper `Rocketeer::getDetectedStage` to get the stage Rocketeer think's he's in on the server (for environment mappings) +- Added support for checking of HHVM extensions +- Added `Task::upload(file, destination)` to upload files to remote, destination can be null and the basename of the file would then be used + +### Changed +- Output now lists which tasks were fired by which task/events, how long they should take, in a tree-like format that clarifies tasks and subtasks +- For breaking changes, see the [Upgrade Path](http://rocketeer.autopergamene.eu/#/docs/III-Further/Upgrade-Path) + +### Fixed +- Fixed the `Copy` strategy +- Fixed a bug where registered events in `hooks` would make the notifier plugins fail +- Fixed a bug where `rocketeer current` would fail to find the related task +- Fixed a bug where Artisan wouldn't be found even if at the default location +- Fixed a bug where ignition would fail when the default connection isn't `production` +- Fixed a bug where logs would be misplaced +- Fixed a bug where tasks and events weren't properly loaded in Laravel +- Fixed a bug where releases would be asked to the server at each command, slowing down deployments +- Fixed a bug where events wouldn't be properly rebooted when using connections other than the default ones +- Fixed a bug where Rocketeer would ask for credentials again after switching connection + +1.2.2 - 2014-06-05 +------------------ + +### Added - Add ability to disable composer completely - Add support for ssh-agent for secure connections + +### Changed +- The Notifier plugin module now has a hook for before and after deployment + +### Fixed - Fixed a bug that prevented the `--seed` option from working - Fixed a bug when getting the user's home folder on Windows - Fixed a bug where Composer-related tasks would be run even without a `composer.json` is found - Fixed some compatibility issue with Laravel 4.2 -### 1.2.1 +1.2.1 - 2014-03-31 +------------------ +### Changed +- Split `remote/application_name` in `config/application_name` and `remote/app_directory` to allow contextual application folder name +- The `composer self-update` command is now commented out by default + +### Fixed - Fixed a bug where `composer install` wouldn't return the proper status code and would cancel deployment - Fixed a bug where empty arrays wouldn't override defaults in the configuration - Fixed path to home folder not being properly found in Windows environment -- Split `remote/application_name` in `config/application_name` and `remote/app_directory` to allow contextual application folder name -- The `composer self-update` command is now commented out by default -### 1.2.0 +1.2.0 - 2014-03-08 +------------------ -- **Added various SSH task-running helpers such as `Rocketeer::task(taskname, task)`** -- **Rocketeer now has a `copy` strategy that copies the previous release instead of cloning a new one on deploy** -- **Composer execution is now configurable via a callback** +### Added +- Added various SSH task-running helpers such as `Rocketeer::task(taskname, task)` +- Rocketeer now has a `copy` strategy that copies the previous release instead of cloning a new one on deploy +- Composer execution is now configurable via a callback - Added an option to disable recursive git clone (submodules) - Releases are now sorted by date when printed out in `rollback` and `current` + +### Fixed - Fixed a bug when running Setup would cancel the `--stage` option - Fixed a bug where contextual options weren't properly merged with default ones -### 1.1.2 +1.1.2 - 2014-02-12 +------------------ +### Added - Added a `Rocketeer\Plugins\Notifier` class to easily add third-party deployment notification plugins + +### Fixed - Fixed a bug where the custom tasks/events file/folders might not exist -### 1.1.1 +1.1.1 - 2014-02-08 +------------------ +### Fixed - Fixed a bug where the `before` event if halting wouldn't cancel the Task firing - Fixed a bug where some calls to the facade would crash in `tasks.php` -### 1.1.0 +1.1.0 - 2014-02-08 +------------------ -- **Events can now cancel the queue by returning false or returning `$task->halt(error)`** -- **Rocketeer now logs its output and commands** -- **Releases are now marked as completed or halted to avoid rollback to releases that errored** +### Added +- Events can now cancel the queue by returning false or returning `$task->halt(error)` +- Rocketeer now logs its output and commands +- Releases are now marked as completed or halted to avoid rollback to releases that errored - Rocketeer will now automatically load `.rocketeer/tasks.php`/`.rocketeer/events.php` _or_ the contents of `.rocketeer/tasks`/`.rocketeer/events` if they're folders - Hash is now computed with the actual configuration instead of the modification times to avoid unecessary reflushes - Check task now uses the PHP version required in your `composer.json` file if the latter exists + +### Fixed - Use the server's time to timestamp releases instead of the local time - Fixed a bug where incorrect current release would be returned for multi-servers setups -### 1.0.0 - -**Note : Configuration is now split in multiple files, you'll need to redeploy the configuration files** +1.0.0 - 2014-01-13 +------------------ -- **Rocketeer is now available as a [standalone PHAR](http://rocketeer.autopergamene.eu/versions/rocketeer.phar)** -- **Revamped plugin system** -- **Rocketeer hooks now use `illuminate/event` system, and can fire events during tasks (instead of just before and after)** -- **Permissions setting is now set in a callback to allow custom permissions routines** +### Added +- Rocketeer is now available as a [standalone PHAR](http://rocketeer.autopergamene.eu/versions/rocketeer.phar) +- Revamped plugin system +- Rocketeer hooks now use `illuminate/event` system, and can fire events during tasks (instead of just before and after) +- Permissions setting is now set in a callback to allow custom permissions routines - Rocketeer now looks into `~/.ssh` by default for keys instead of asking - Added the `--clean-all` flag to the `Cleanup` task to prune all but the latest release - Deployments file is now cleared when the config files are changed - Added an option to disable shallow clone as it caused some problems on some servers + +### Deprecated +- Configuration is now split in multiple files, you'll need to redeploy the configuration files + +### Fixed - Fixed a bug where `CurrentRelease` wouldn't show any release with an empty/fresh deployments file - Fix some multiconnections related bugs - Fixed some minor behaviors that were causing `--pretend` and/or `--verbose` to not output SCM commands -### 0.9.0 +0.9.0 - 2013-11-15 +------------------ -- **Rocketeer now supports SVN** -- **Rocketeer now has a [Campfire plugin](https://github.com/Anahkiasen/rocketeer-campfire)** +### Added +- Rocketeer now supports SVN +- Rocketeer now has a [Campfire plugin](https://github.com/Anahkiasen/rocketeer-campfire) - Add option to manually set remote variables when encountering problems - Add keyphrase support -### 0.8.0 +0.8.0 - 2013-10-19 +------------------ -- **Rocketeer can now have specific configurations for stages and connections** -- **Better handling of multiple connections** -- **Added facade shortcuts `Rocketeer::execute(Task)` and `Rocketeer::on(connection[s], Task)` to execute commands on the remote servers** +### Added +- Rocketeer can now have specific configurations for stages and connections +- Better handling of multiple connections +- Added facade shortcuts `Rocketeer::execute(Task)` and `Rocketeer::on(connection[s], Task)` to execute commands on the remote servers - Added the `--list` flag on the `rollback` command to show a list of available releases and pick one to rollback to - Added the `--on` flag to all commands to specify which connections the task should be executed on (ex. `production`, `staging,production`) - Added `deploy:flush` to clear Rocketeer's cache of credentials -### 0.7.0 +0.7.0 - 2013-08-16 +------------------ -- **Rocketeer can now work outside of Laravel** -- **Better handling of SSH keys** +### Added +- Rocketeer can now work outside of Laravel +- Better handling of SSH keys - Permissions are now entirely configurable - Rocketeer now prompts for confirmation before executing the Teardown task - Allow the use of patterns in shared folders -- Share `sessions` folder by default - Rocketeer now prompts for binaries it can't find (composer, phpunit, etc) -### 0.6.5 +### Changed +- Share `sessions` folder by default -- **Make Rocketeer prompt for both server and SCM credentials if they're not stored** -- **`artisan deploy` now deploys the project if the `--version` flat is not passed** +0.6.5 - 2013-07-29 +------------------ + +### Added +- Make Rocketeer prompt for both server and SCM credentials if they're not stored +- `artisan deploy` now deploys the project if the `--version` flat is not passed - Make Rocketeer forget invalid credentials provided by prompt + +### Fixed - Fix a bug where incorrect SCM urls would be generated -### 0.6.4 +0.6.4 - 2013-07-16 +------------------ +### Added - Make the output of commands in realtime when `--verbose` instead of when the command is done + +### Changed +- Reverse sluggification of application name + +### Fixed - Fix a bug where custom Task classes would be analyzed as string commands - Fix Rocketeeer not taking into account custom paths to **app/**, **storage/**, **public/** etc. -- Reverse sluggification of application name -### 0.6.3 +0.6.3 - 2013-07-11 +------------------ +### Changed - Application name is now always sluggified as a security + +### Fixed - Fix a bug where the Check task would fail on pretend mode - Fix a bug where invalid directory separators would get cached and used -### 0.6.2 +0.6.2 - 2013-07-11 +------------------ +### Added - Make the Check task check for the remote presence of the configured SCM + +### Fixed - Fix Rocketeer not being able to use a `composer.phar` on the server -### 0.6.1 +0.6.1 - 2013-07-10 +------------------ -- Fix a bug where the configured user would not have the rights to set permissions +### Fixed +- Fixed a bug where the configured user would not have the rights to set permissions -### 0.6.0 +0.6.0 - 2013-07-06 +------------------ -- **Add multistage strategy** -- **Add compatibility to Laravel 4.0** -- Migrations are now under a `--migrate` flag +### Added +- Add multistage strategy +- Add compatibility to Laravel 4.0 - Split Git from the SCM implementation (**requires a config update**) + +### Changed +- Migrations are now under a `--migrate` flag - Releases are now named as `YmdHis` instead of `time()` - If the `scm.branch` option is empty, Rocketeer will now use the current Git branch -- Fix a delay where the `current` symlink would get updated before the complete end of the deploy -- Fix errors with Git and Composer not canceling deploy -- Fix some compatibility problems with Windows -- Fix a bug where string tasks would not be run in latest release folder -- Fix Apache username and group using `www-data` by default -### 0.5.0 +### Fixed +- Fixed a delay where the `current` symlink would get updated before the complete end of the deploy +- Fixed errors with Git and Composer not canceling deploy +- Fixed some compatibility problems with Windows +- Fixed a bug where string tasks would not be run in latest release folder +- Fixed Apache username and group using `www-data` by default + +0.5.0 - 2013-07-01 +------------------ -- **Add a `deploy:update` task that updates the remote server without doing a new release** -- **Add a `deploy:test` to run the tests on the server** -- **Rocketeer can now prompt for Git credentials if you don't want to store them in the config** +### Added +- Added a `deploy:update` task that updates the remote server without doing a new release +- Added a `deploy:test` to run the tests on the server +- Rocketeer can now prompt for Git credentials if you don't want to store them in the config - The `deploy:check` command now checks PHP extensions for the cache/database/session drivers you set - Rocketeer now share logs by default between releases - Add ability to specify an array of Tasks in Rocketeer::before|after - Added a `$silent` flag to make a `Task::run` call silent no matter what - Rocketeer now displays how long the task took -### 0.4.0 +0.4.0 - 2013-06-26 +------------------ -- **Add ability to share files and folders between releases** -- **Add ability to create custom tasks integrated in the CLI** -- **Add a `deploy:check` Task that checks if the server is ready to receive a Laravel app** -- Add `Task::listContents` and `Task::fileExists` helpers -- Add Task helper to run outstanding migrations -- Add `Rocketeer::add` method on the facade to register custom Tasks -- Fix `Task::runComposer` not taking into account a local `composer.phar` +### Added +- Added ability to share files and folders between releases +- Added ability to create custom tasks integrated in the CLI +- Added a `deploy:check` Task that checks if the server is ready to receive a Laravel app +- Added `Task::listContents` and `Task::fileExists` helpers +- Added Task helper to run outstanding migrations +- Added `Rocketeer::add` method on the facade to register custom Tasks -### 0.3.2 +### Fixed +- Fixed `Task::runComposer` not taking into account a local `composer.phar` +0.3.2 - 2013-06-25 +------------------ + +### Fixed - Fixed wrong tag used in `deploy:cleanup` -### 0.3.1 +0.3.1 - 2013-06-24 +------------------ +### Added - Added `--pretend` flag on all commands to print out a list of the commands that would have been executed instead of running them -### 0.3.0 +0.3.0 - 2013-06-24 +------------------ +### Added - Added `Task::runInFolder` to run tasks in a specific folder - Added `Task::runForCurrentRelease` Task helper -- Fixed a bug where `Task::run` would only return the last line of the command's output - Added `Task::runTests` methods to run the PHPUnit tests of the application - Integrated `Task::runTests` in the `Deploy` task under the `--tests` flag ; failing tests will cancel deploy and rollback -### 0.2.0 +### Fixed +- Fixed a bug where `Task::run` would only return the last line of the command's output + +0.2.0 - 2013-06-24 +------------------ +### Added - The core of Rocketeer's actions is now split into a system of Tasks for flexibility - Added a `Rocketeer` facade to easily add tasks via `before` and `after` (see Tasks docs) -### 0.1.1 +0.1.1 - 2013-06-23 +------------------ +### Fixed - Fixed a bug where the commands would try to connect to the remote hosts on construct - Fixed `ReleasesManager::getPreviousRelease` returning the wrong release -### 0.1.0 +0.1.0 - 2013-06-23 +------------------ -- Add `deploy:teardown` to remove the application from remote servers -- Add support for the connections defined in the remote config file -- Add `deploy:rollback` and `deploy:current` commands -- Add `deploy:cleanup` command -- Add config file -- Add `deploy:setup` and `deploy:deploy` commands +### Added +- Added `deploy:teardown` to remove the application from remote servers +- Added support for the connections defined in the remote config file +- Added `deploy:rollback` and `deploy:current` commands +- Added `deploy:cleanup` command +- Added config file +- Added `deploy:setup` and `deploy:deploy` commands diff --git a/README.md b/README.md index e8584502d..fa3d25553 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,27 @@ [![Dependency Status](https://www.versioneye.com/user/projects/53f1c65f13bb0677b1000744/badge.svg?style=flat)](https://www.versioneye.com/user/projects/53f1c65f13bb0677b1000744) [![Support via Gittip](http://img.shields.io/gittip/Anahkiasen.svg?style=flat)](https://www.gittip.com/Anahkiasen/) -**Rocketeer** is a task runner and deployment package for the PHP world. It is inspired by the [Laravel Framework](http://laravel.com/) philosophy and thus aims to be fast, elegant, and more importantly easy to use. +**Rocketeer** is a modern PHP task runner and deployment package. It is inspired by the [Laravel Framework](http://laravel.com/) philosophy and thus aims to be fast, elegant, and more importantly easy to use. + +Like the latter, emphasis is put on smart defaults and modern development. While it is coded in PHP, it can deploy any project from small HTML/CSS websites to large Rails applications. + +## Main features + +- **Versatile**, support for multiple connections, multiserver connections, multiple stages per server, etc. +- **Fast**, queue tasks and run them in parallel across all your servers and stages +- **Modulable**, not only can you add custom tasks and components, every core part of Rocketeer can be hot swapped, extended, hacked to bits, etc. +- **Preconfigured**, tired of defining the same routines again and again ? Rocketeer is made for modern development and comes with smart defaults and built-in tasks such as installing your application's dependencies +- **Powerful**, releases management, server checks, rollbacks, etc. Every feature you'd expect from a deployment tool is there ## Installation -The easiest way is to get the latest compiled version [from the website](http://rocketeer.autopergamene.eu/versions/rocketeer.phar), put it at the root of the project you want to deploy, and hit `php rocketeer.phar ignite`. You'll get asked a series of questions that should get you up and running in no time. +The fastest way is to grab the binary: + +```bash +curl http://rocketeer.autopergamene.eu/versions/rocketeer.phar > /usr/local/bin/rocketeer && chmod 755 /usr/local/bin/rocketeer +``` -Rocketeer also integrates nicely with the Laravel framework, for that refer to the [Getting Started](https://github.com/Anahkiasen/rocketeer/wiki/Getting-started) pages of the documentation. +More ways to setup Rocketeer can be found in the [official documentation](http://rocketeer.autopergamene.eu/#/docs/I-Introduction/Getting-started). ## Usage @@ -22,21 +36,24 @@ The available commands in Rocketeer are : ``` $ php rocketeer - check Check if the server is ready to receive the application - cleanup Clean up old releases from the server - current Display what the current release is - deploy Deploy the website. - flush Flushes Rocketeer's cache of credentials - help Displays help for a command - ignite Creates Rocketeer's configuration - list Lists commands - rollback Rollback to the previous release, or to a specific one - setup Set up the remote server for deployment - teardown Remove the remote applications and existing caches - test Run the tests on the server and displays the output - update Update the remote server without doing a new release. + check Check if the server is ready to receive the application + cleanup Clean up old releases from the server + current Display what the current release is + deploy Deploys the website + flush Flushes Rocketeer's cache of credentials + help Displays help for a command + ignite Creates Rocketeer's configuration + list Lists commands + rollback Rollback to the previous release, or to a specific one + setup Set up the remote server for deployment + strategies Lists the available options for each strategy + teardown Remove the remote applications and existing caches + test Run the tests on the server and displays the output + update Update the remote server without doing a new release ``` +Documentation can be [found here](https://github.com/rocketeers/docs) + ## Testing ``` bash @@ -45,23 +62,25 @@ $ phpunit ## Contributing -Please see [CONTRIBUTING](https://github.com/anahkiasen/rocketeer/blob/master/CONTRIBUTING.md) for details. +Please see [CONTRIBUTING](https://github.com/rocketeers/rocketeer/blob/master/CONTRIBUTING.md) for details. ## Credits - [Anahkiasen](https://github.com/Anahkiasen) -- [All Contributors](https://github.com/anahkiasen/rocketeer/contributors) +- [All Contributors](https://github.com/rocketeers/rocketeer/contributors) ## License -The MIT License (MIT). Please see [License File](https://github.com/anahkiasen/rocketeer/blob/master/LICENSE) for more information. +The MIT License (MIT). Please see [License File](https://github.com/rocketeers/rocketeer/blob/master/LICENSE) for more information. ----- -## Available plugins +## Available plugins and integrations -- [Campfire](https://github.com/Anahkiasen/rocketeer-campfire) -- [Slack](https://github.com/Anahkiasen/rocketeer-slack) +- [Campfire](https://github.com/rocketeers/rocketeer-campfire) +- [Slack](https://github.com/rocketeers/rocketeer-slack) +- [HipChat](https://github.com/hannesvdvreken/rocketeer-hipchat) +- [Wordpress](https://github.com/mykebates/wp-rocketeer) ## Why not Capistrano ? @@ -71,7 +90,3 @@ But, it remains a Ruby package and one that's tightly coupled to Rails in some w It's also meant to be a lot easier to comprehend, for first-time users or novices, Capistrano is a lot to take at once – Rocketeer aims to be as simple as possible by providing smart defaults and speeding up the time between installing it and first hitting `deploy`. It's also more thought out for the PHP world – although you can configure Capistrano to run Composer and PHPUnit, that's not something it expects from the get go, while those tasks that are a part of every PHP developer are integrated in Rocketeer's core deploy process. - -## Documentation - -Documentation can be [found here](https://github.com/rocketeers/docs) diff --git a/bin/compile b/bin/compile index b67c97911..f88350e72 100755 --- a/bin/compile +++ b/bin/compile @@ -4,7 +4,7 @@ require __DIR__.'/../vendor/autoload.php'; // Create Phar $compiler = new Rocketeer\Console\Compiler; -$phar = $compiler->compile(); +$phar = $compiler->compile(); // Set permissions -chmod($phar, 0755); \ No newline at end of file +chmod($phar, 0755); diff --git a/bin/rocketeer b/bin/rocketeer old mode 100644 new mode 100755 index 832787794..b86ca0ca8 --- a/bin/rocketeer +++ b/bin/rocketeer @@ -2,17 +2,26 @@ boot(); + +$app = Rocketeer::getFacadeApplication(); + +// Compile new phar and extract it +$boris = new Boris('rocketeer> '); +$boris->setLocal(array( + 'app' => $app, +)); + +$boris->start(); diff --git a/composer.json b/composer.json index 880561fb1..1c88b92e1 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,7 @@ "license": "MIT", "keywords": [ "laravel", + "deploy", "deployment", "ssh" ], @@ -14,26 +15,32 @@ } ], "require": { - "php": ">=5.3.0", - "illuminate/support": "~4.1", - "illuminate/config": "~4.1", - "illuminate/console": "~4.1", - "illuminate/container": "~4.1", - "illuminate/filesystem": "~4.1", - "illuminate/events": "~4.1", - "illuminate/remote": "~4.1", - "illuminate/log": "~4.1" + "php": ">=5.4.0", + "illuminate/support": "~4.2", + "illuminate/config": "~4.2", + "illuminate/console": "~4.2", + "illuminate/container": "~4.2", + "illuminate/filesystem": "~4.2", + "illuminate/events": "~4.2", + "illuminate/remote": "~4.2", + "illuminate/log": "~4.2", + "kzykhys/parallel": "~0.1.0" }, "require-dev": { + "phpunit/phpunit": "~4.0", "mockery/mockery": "~0.9", "nesbot/carbon": "~1.4", - "patchwork/utf8": "~1.1.18", + "patchwork/utf8": "~1.1", "herrera-io/box": "~1.5.3", - "phpseclib/phpseclib": "~0.3.5" + "phpseclib/phpseclib": "~0.3.5", + "d11wtq/boris": "~1.0.8", + "raveren/kint": "~0.9.1", + "johnkary/phpunit-speedtrap": "dev-master" }, "suggest": { "anahkiasen/rocketeer-campfire": "Campfire plugin to create deployments notifications", - "anahkiasen/rocketeer-slack": "Slack plugin to create deployments notifications" + "anahkiasen/rocketeer-slack": "Slack plugin to create deployments notifications", + "ext-pcntl": "Allow parallel deployments" }, "bin": [ "bin/rocketeer" @@ -45,7 +52,7 @@ }, "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-develop": "2.0-dev" } } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 034d7f4ca..000000000 --- a/composer.lock +++ /dev/null @@ -1,1019 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "b3b332169ded14fb990f977dd08b7734", - "packages": [ - { - "name": "illuminate/config", - "version": "v4.2.1", - "target-dir": "Illuminate/Config", - "source": { - "type": "git", - "url": "https://github.com/illuminate/config.git", - "reference": "cd99d3dc2ca192b7b4003b5f6b31a4b1dd3c8167" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/config/zipball/cd99d3dc2ca192b7b4003b5f6b31a4b1dd3c8167", - "reference": "cd99d3dc2ca192b7b4003b5f6b31a4b1dd3c8167", - "shasum": "" - }, - "require": { - "illuminate/filesystem": "4.2.*", - "illuminate/support": "4.2.*", - "php": ">=5.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Config": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-23 16:40:19" - }, - { - "name": "illuminate/console", - "version": "v4.2.1", - "target-dir": "Illuminate/Console", - "source": { - "type": "git", - "url": "https://github.com/illuminate/console.git", - "reference": "de40c341eeccb1619106e8e8b954eea239aceb68" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/console/zipball/de40c341eeccb1619106e8e8b954eea239aceb68", - "reference": "de40c341eeccb1619106e8e8b954eea239aceb68", - "shasum": "" - }, - "require": { - "php": ">=5.4.0", - "symfony/console": "2.5.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Console": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-23 16:40:19" - }, - { - "name": "illuminate/container", - "version": "v4.2.1", - "target-dir": "Illuminate/Container", - "source": { - "type": "git", - "url": "https://github.com/illuminate/container.git", - "reference": "15d85ddb8b56fef54db5941c9b112cae069606ae" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/container/zipball/15d85ddb8b56fef54db5941c9b112cae069606ae", - "reference": "15d85ddb8b56fef54db5941c9b112cae069606ae", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Container": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-23 16:40:19" - }, - { - "name": "illuminate/events", - "version": "v4.2.1", - "target-dir": "Illuminate/Events", - "source": { - "type": "git", - "url": "https://github.com/illuminate/events.git", - "reference": "eb23d436806893b6b05d7ad3b6f52212830c7e4e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/events/zipball/eb23d436806893b6b05d7ad3b6f52212830c7e4e", - "reference": "eb23d436806893b6b05d7ad3b6f52212830c7e4e", - "shasum": "" - }, - "require": { - "illuminate/container": "4.2.*", - "illuminate/support": "4.2.*", - "php": ">=5.4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Events": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-23 16:40:19" - }, - { - "name": "illuminate/filesystem", - "version": "v4.2.1", - "target-dir": "Illuminate/Filesystem", - "source": { - "type": "git", - "url": "https://github.com/illuminate/filesystem.git", - "reference": "568f829aebe886bbe84bcd726e1509f830d5a1b0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/filesystem/zipball/568f829aebe886bbe84bcd726e1509f830d5a1b0", - "reference": "568f829aebe886bbe84bcd726e1509f830d5a1b0", - "shasum": "" - }, - "require": { - "illuminate/support": "4.2.*", - "php": ">=5.4.0", - "symfony/finder": "2.5.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Filesystem": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-23 16:40:19" - }, - { - "name": "illuminate/log", - "version": "v4.2.1", - "target-dir": "Illuminate/Log", - "source": { - "type": "git", - "url": "https://github.com/illuminate/log.git", - "reference": "9b3ae8d7159ae45f3db03bf2a83684c7f2b88474" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/log/zipball/9b3ae8d7159ae45f3db03bf2a83684c7f2b88474", - "reference": "9b3ae8d7159ae45f3db03bf2a83684c7f2b88474", - "shasum": "" - }, - "require": { - "illuminate/support": "4.2.*", - "monolog/monolog": "~1.6", - "php": ">=5.4.0" - }, - "require-dev": { - "illuminate/events": "4.2.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Log": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-23 16:40:19" - }, - { - "name": "illuminate/remote", - "version": "v4.2.1", - "target-dir": "Illuminate/Remote", - "source": { - "type": "git", - "url": "https://github.com/illuminate/remote.git", - "reference": "7ba42ea5f526e665062b889632195b947da03d32" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/remote/zipball/7ba42ea5f526e665062b889632195b947da03d32", - "reference": "7ba42ea5f526e665062b889632195b947da03d32", - "shasum": "" - }, - "require": { - "illuminate/filesystem": "4.2.*", - "illuminate/support": "4.2.*", - "php": ">=5.4.0", - "phpseclib/phpseclib": "0.3.*" - }, - "require-dev": { - "illuminate/console": "4.2.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Remote": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-25 01:46:11" - }, - { - "name": "illuminate/support", - "version": "v4.2.1", - "target-dir": "Illuminate/Support", - "source": { - "type": "git", - "url": "https://github.com/illuminate/support.git", - "reference": "f1dd716598f99ff18b455da3e073a8324d65252e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/support/zipball/f1dd716598f99ff18b455da3e073a8324d65252e", - "reference": "f1dd716598f99ff18b455da3e073a8324d65252e", - "shasum": "" - }, - "require": { - "php": ">=5.4.0" - }, - "require-dev": { - "jeremeamia/superclosure": "~1.0", - "patchwork/utf8": "1.1.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.2-dev" - } - }, - "autoload": { - "psr-0": { - "Illuminate\\Support": "" - }, - "files": [ - "Illuminate/Support/helpers.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylorotwell@gmail.com", - "homepage": "https://github.com/taylorotwell", - "role": "Creator of Laravel" - } - ], - "time": "2014-05-30 16:22:28" - }, - { - "name": "monolog/monolog", - "version": "1.10.0", - "source": { - "type": "git", - "url": "https://github.com/Seldaek/monolog.git", - "reference": "25b16e801979098cb2f120e697bfce454b18bf23" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/25b16e801979098cb2f120e697bfce454b18bf23", - "reference": "25b16e801979098cb2f120e697bfce454b18bf23", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "~1.0" - }, - "require-dev": { - "aws/aws-sdk-php": "~2.4, >2.4.8", - "doctrine/couchdb": "~1.0@dev", - "graylog2/gelf-php": "~1.0", - "phpunit/phpunit": "~3.7.0", - "raven/raven": "~0.5", - "ruflin/elastica": "0.90.*" - }, - "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", - "raven/raven": "Allow sending log messages to a Sentry server", - "rollbar/rollbar": "Allow sending log messages to Rollbar", - "ruflin/elastica": "Allow sending log messages to an Elastic Search server" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10.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", - "role": "Developer" - } - ], - "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": "2014-06-04 16:30:04" - }, - { - "name": "phpseclib/phpseclib", - "version": "0.3.6", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "0ea31d9b65d49a8661e93bec19f44e989bd34c69" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/0ea31d9b65d49a8661e93bec19f44e989bd34c69", - "reference": "0ea31d9b65d49a8661e93bec19f44e989bd34c69", - "shasum": "" - }, - "require": { - "php": ">=5.0.0" - }, - "require-dev": { - "squizlabs/php_codesniffer": "1.*" - }, - "suggest": { - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a wide variety of cryptographic operations.", - "pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP < 4.3.3." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.3-dev" - } - }, - "autoload": { - "psr-0": { - "Crypt": "phpseclib/", - "File": "phpseclib/", - "Math": "phpseclib/", - "Net": "phpseclib/", - "System": "phpseclib/" - }, - "files": [ - "phpseclib/Crypt/Random.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "phpseclib/" - ], - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, - { - "name": "Patrick Monnerat", - "email": "pm@datasphere.ch", - "role": "Developer" - }, - { - "name": "Andreas Fischer", - "email": "bantu@phpbb.com", - "role": "Developer" - }, - { - "name": "Hans-Jürgen Petrich", - "email": "petrich@tronic-media.com", - "role": "Developer" - } - ], - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", - "homepage": "http://phpseclib.sourceforge.net", - "keywords": [ - "BigInteger", - "aes", - "asn.1", - "asn1", - "blowfish", - "crypto", - "cryptography", - "encryption", - "rsa", - "security", - "sftp", - "signature", - "signing", - "ssh", - "twofish", - "x.509", - "x509" - ], - "time": "2014-02-28 16:05:05" - }, - { - "name": "psr/log", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/php-fig/log.git", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", - "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", - "shasum": "" - }, - "type": "library", - "autoload": { - "psr-0": { - "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", - "keywords": [ - "log", - "psr", - "psr-3" - ], - "time": "2012-12-21 11:40:51" - }, - { - "name": "symfony/console", - "version": "v2.5.0", - "target-dir": "Symfony/Component/Console", - "source": { - "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "ef4ca73b0b3a10cbac653d3ca482d0cdd4502b2c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/ef4ca73b0b3a10cbac653d3ca482d0cdd4502b2c", - "reference": "ef4ca73b0b3a10cbac653d3ca482d0cdd4502b2c", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1" - }, - "suggest": { - "psr/log": "For using the console logger", - "symfony/event-dispatcher": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Console\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "http://symfony.com", - "time": "2014-05-22 08:54:24" - }, - { - "name": "symfony/finder", - "version": "v2.5.0", - "target-dir": "Symfony/Component/Finder", - "source": { - "type": "git", - "url": "https://github.com/symfony/Finder.git", - "reference": "307aad2c541bbdf43183043645e186ef2cd6b973" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/307aad2c541bbdf43183043645e186ef2cd6b973", - "reference": "307aad2c541bbdf43183043645e186ef2cd6b973", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Finder\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "http://symfony.com", - "time": "2014-05-22 13:47:45" - } - ], - "packages-dev": [ - { - "name": "herrera-io/box", - "version": "1.5.3", - "source": { - "type": "git", - "url": "https://github.com/herrera-io/php-box.git", - "reference": "b5432951f85a56df6012f503881174e7aeec6611" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/herrera-io/php-box/zipball/b5432951f85a56df6012f503881174e7aeec6611", - "reference": "b5432951f85a56df6012f503881174e7aeec6611", - "shasum": "" - }, - "require": { - "ext-phar": "*", - "phine/path": "~1.0", - "php": ">=5.3.3" - }, - "require-dev": { - "herrera-io/annotations": "~1.0", - "herrera-io/phpunit-test-case": "1.*", - "mikey179/vfsstream": "1.1.0", - "phpseclib/phpseclib": "~0.3", - "phpunit/phpunit": "3.7.*" - }, - "suggest": { - "herrera-io/annotations": "For compacting annotated docblocks.", - "phpseclib/phpseclib": "For verifying OpenSSL signed phars without the phar extension." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Herrera\\Box": "src/lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Herrera", - "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io", - "role": "Developer" - } - ], - "description": "A library for simplifying the PHAR build process.", - "homepage": "http://herrera-io.github.com/php-box", - "keywords": [ - "phar" - ], - "time": "2014-02-07 15:48:46" - }, - { - "name": "mockery/mockery", - "version": "0.9.1", - "source": { - "type": "git", - "url": "https://github.com/padraic/mockery.git", - "reference": "17f63ee40ed14a8afb7ba1f0ae15cc4491d719d1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/padraic/mockery/zipball/17f63ee40ed14a8afb7ba1f0ae15cc4491d719d1", - "reference": "17f63ee40ed14a8afb7ba1f0ae15cc4491d719d1", - "shasum": "" - }, - "require": { - "lib-pcre": ">=7.0", - "php": ">=5.3.2" - }, - "require-dev": { - "hamcrest/hamcrest-php": "~1.1", - "phpunit/phpunit": "~4.0", - "satooshi/php-coveralls": "~0.7@dev" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.9.x-dev" - } - }, - "autoload": { - "psr-0": { - "Mockery": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" - }, - { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" - } - ], - "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", - "homepage": "http://github.com/padraic/mockery", - "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" - ], - "time": "2014-05-02 12:16:45" - }, - { - "name": "nesbot/carbon", - "version": "1.9.0", - "source": { - "type": "git", - "url": "https://github.com/briannesbitt/Carbon.git", - "reference": "b94de7192b01d0e80794eae984dcc773220ab0dc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/b94de7192b01d0e80794eae984dcc773220ab0dc", - "reference": "b94de7192b01d0e80794eae984dcc773220ab0dc", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "Carbon": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Brian Nesbitt", - "email": "brian@nesbot.com", - "homepage": "http://nesbot.com" - } - ], - "description": "A simple API extension for DateTime.", - "homepage": "https://github.com/briannesbitt/Carbon", - "keywords": [ - "date", - "datetime", - "time" - ], - "time": "2014-05-13 02:29:30" - }, - { - "name": "patchwork/utf8", - "version": "v1.1.23", - "source": { - "type": "git", - "url": "https://github.com/nicolas-grekas/Patchwork-UTF8.git", - "reference": "c1edbc82aed49ff2bdef0a0f659bf69e3f7b14cc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nicolas-grekas/Patchwork-UTF8/zipball/c1edbc82aed49ff2bdef0a0f659bf69e3f7b14cc", - "reference": "c1edbc82aed49ff2bdef0a0f659bf69e3f7b14cc", - "shasum": "" - }, - "require": { - "lib-pcre": ">=7.3", - "php": ">=5.3.0" - }, - "suggest": { - "ext-iconv": "Use iconv for best performance", - "ext-intl": "Use Intl for best performance", - "ext-mbstring": "Use Mbstring for best performance" - }, - "type": "library", - "autoload": { - "psr-0": { - "Patchwork": "class/", - "Normalizer": "class/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "(Apache-2.0 or GPL-2.0)" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com", - "role": "Developer" - } - ], - "description": "Extensive, portable and performant handling of UTF-8 and grapheme clusters for PHP", - "homepage": "https://github.com/nicolas-grekas/Patchwork-UTF8", - "keywords": [ - "i18n", - "unicode", - "utf-8", - "utf8" - ], - "time": "2014-05-22 13:59:09" - }, - { - "name": "phine/exception", - "version": "1.0.0", - "source": { - "type": "git", - "url": "https://github.com/phine/lib-exception.git", - "reference": "150c6b6090b2ebc53c60e87cb20c7f1287b7b68a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phine/lib-exception/zipball/150c6b6090b2ebc53c60e87cb20c7f1287b7b68a", - "reference": "150c6b6090b2ebc53c60e87cb20c7f1287b7b68a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "league/phpunit-coverage-listener": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Phine\\Exception": "src/lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Herrera", - "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io" - } - ], - "description": "A PHP library for improving the use of exceptions.", - "homepage": "https://github.com/phine/lib-exception", - "keywords": [ - "exception" - ], - "time": "2013-08-27 17:43:25" - }, - { - "name": "phine/path", - "version": "1.1.0", - "source": { - "type": "git", - "url": "https://github.com/phine/lib-path.git", - "reference": "cbe1a5eb6cf22958394db2469af9b773508abddd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phine/lib-path/zipball/cbe1a5eb6cf22958394db2469af9b773508abddd", - "reference": "cbe1a5eb6cf22958394db2469af9b773508abddd", - "shasum": "" - }, - "require": { - "phine/exception": "~1.0", - "php": ">=5.3.3" - }, - "require-dev": { - "league/phpunit-coverage-listener": "~1.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "psr-0": { - "Phine\\Path": "src/lib" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kevin Herrera", - "email": "kevin@herrera.io", - "homepage": "http://kevin.herrera.io" - } - ], - "description": "A PHP library for improving the use of file system paths.", - "homepage": "https://github.com/phine/lib-path", - "keywords": [ - "file", - "path", - "system" - ], - "time": "2013-10-15 22:58:04" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "platform": { - "php": ">=5.3.0" - }, - "platform-dev": [] -} diff --git a/docs b/docs deleted file mode 160000 index cf947b11c..000000000 --- a/docs +++ /dev/null @@ -1 +0,0 @@ -Subproject commit cf947b11c636abca7956b350f3fee9f09837cc34 diff --git a/phpunit.xml b/phpunit.xml index 18ef14b9f..5ff0a69fa 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,39 +1,47 @@ - - - - - - - src/Rocketeer - - src/Rocketeer/RocketeerServiceProvider.php - src/Rocketeer/Console/WhitespaceCompactor.php - src/Rocketeer/Console/Compiler.php - src/Rocketeer/Console - src/Rocketeer/Commands - src/Rocketeer/Facades - - - - - - - tests - - - \ No newline at end of file + + + + + + src/Rocketeer + + src/Rocketeer/Abstracts/AbstractCommand.php + src/Rocketeer/RocketeerServiceProvider.php + src/Rocketeer/Console + src/Rocketeer/Interfaces + src/Rocketeer/Facades + + + + + + + + + + 500 + + + 10 + + + + + + + + + tests + + + diff --git a/src/Rocketeer/Abstracts/AbstractBinary.php b/src/Rocketeer/Abstracts/AbstractBinary.php new file mode 100644 index 000000000..73acb1236 --- /dev/null +++ b/src/Rocketeer/Abstracts/AbstractBinary.php @@ -0,0 +1,221 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Abstracts; + +use Illuminate\Container\Container; +use Illuminate\Support\Arr; +use Illuminate\Support\Str; +use Rocketeer\Traits\HasLocator; + +/** + * A generic class to represent a binary as a class + * + * @author Maxime Fabre + */ +abstract class AbstractBinary +{ + use HasLocator; + + /** + * The core binary + * + * @var string + */ + protected $binary; + + /** + * A parent binary to call this one with + * + * @type AbstractBinary|string + */ + protected $parent; + + /** + * @param Container $app + */ + public function __construct(Container $app) + { + $this->app = $app; + + // Assign default paths + $paths = $this->getKnownPaths(); + if ($this->connections->getConnection() && $paths) { + $binary = Arr::get($paths, 0); + $fallback = Arr::get($paths, 1); + $binary = $this->bash->which($binary, $fallback, false); + + $this->setBinary($binary); + } elseif ($paths) { + $this->setBinary($paths[0]); + } + } + + /** + * Get an array of default paths to look for + * + * @return array + */ + protected function getKnownPaths() + { + return []; + } + + ////////////////////////////////////////////////////////////////////// + ///////////////////////////// PROPERTIES ///////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * @param AbstractBinary|string $parent + */ + public function setParent($parent) + { + $this->parent = $parent; + } + + /** + * @param string $binary + */ + public function setBinary($binary) + { + $this->binary = $binary; + } + + /** + * Get the current binary name + * + * @return string + */ + public function getBinary() + { + return $this->binary; + } + + /** + * Call or execute a command on the Binary + * + * @param string $name + * @param array $arguments + * + * @return string|null + */ + public function __call($name, $arguments) + { + // Execution aliases + if (Str::startsWith($name, 'run')) { + $command = array_shift($arguments); + $command = call_user_func_array([$this, $command], $arguments); + + return $this->bash->$name($command); + } + + // Format name + $name = Str::snake($name, '-'); + + // Prepend command name to arguments and call + array_unshift($arguments, $name); + $command = call_user_func_array([$this, 'getCommand'], $arguments); + + return $command; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// HELPERS /////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Returns a command with the SCM's binary + * + * @param string|null $command + * @param string|string[] $arguments + * @param string|string[] $flags + * + * @return string + */ + public function getCommand($command = null, $arguments = array(), $flags = array()) + { + // Format arguments + $arguments = $this->buildArguments($arguments); + $options = $this->buildOptions($flags); + + // Build command + $binary = $this->binary; + $components = [$command, $arguments, $options]; + foreach ($components as $component) { + if ($component) { + $binary .= ' '.$component; + } + } + + // If the binary has a parent, wrap the call with it + $parent = $this->parent instanceof AbstractBinary ? $this->parent->getBinary() : $this->parent; + $command = $parent.' '.$binary; + + return trim($command); + } + + /** + * @param string|string[] $flags + * + * @return string + */ + protected function buildOptions($flags) + { + // Return if already builts + if (is_string($flags)) { + return $flags; + } + + $options = []; + $flags = (array) $flags; + + // Flip array if necessary + $firstKey = Arr::get(array_keys($flags), 0); + if (!is_null($firstKey) && is_int($firstKey)) { + $flags = array_combine( + array_values($flags), + array_fill(0, count($flags), null) + ); + } + + // Build flags + foreach ($flags as $flag => $value) { + $options[] = $value ? $flag.'="'.$value.'"' : $flag; + } + + return implode(' ', $options); + } + + /** + * @param string|string[] $arguments + * + * @return string + */ + protected function buildArguments($arguments) + { + if (!is_string($arguments)) { + $arguments = (array) $arguments; + $arguments = implode(' ', $arguments); + } + + return $arguments; + } + + /** + * Quote a string + * + * @param string $string + * + * @return string + */ + protected function quote($string) + { + return '"'.$string.'"'; + } +} diff --git a/src/Rocketeer/Abstracts/AbstractCommand.php b/src/Rocketeer/Abstracts/AbstractCommand.php new file mode 100644 index 000000000..aedd97866 --- /dev/null +++ b/src/Rocketeer/Abstracts/AbstractCommand.php @@ -0,0 +1,261 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Abstracts; + +use Closure; +use Illuminate\Console\Command; +use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Input\InputOption; + +/** + * An abstract command with various helpers for all + * subcommands to inherit + * + * @author Maxime Fabre + */ +abstract class AbstractCommand extends Command +{ + /** + * Whether the command's task should be built + * into a pipeline or run straight + * + * @type boolean + */ + protected $straight = false; + + /** + * the task to execute on fire + * + * @var AbstractTask + */ + protected $task; + + /** + * @param AbstractTask|null $task + */ + public function __construct(AbstractTask $task = null) + { + parent::__construct(); + + // If we passed a Task, bind its properties + // to the command + if ($task) { + $this->task = $task; + $this->task->command = $this; + + if (!$this->description && $description = $task->getDescription()) { + $this->setDescription($description); + } + } + } + + /** + * Get the task this command executes + * + * @return AbstractTask + */ + public function getTask() + { + return $this->task; + } + + /** + * Returns the command name. + * + * @return string The command name + */ + public function getName() + { + // Return commands as is in Laravel + if ($this->isInsideLaravel()) { + return $this->name; + } + + $name = str_replace('deploy:', null, $this->name); + $name = str_replace('-', ':', $name); + + return $name; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// EXECUTION ///////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Run the tasks + * + * @return void + */ + abstract public function fire(); + + /** + * Get the console command options. + * + * @return array> + */ + protected function getOptions() + { + return array( + // Options + ['parallel', 'P', InputOption::VALUE_NONE, 'Run the tasks asynchronously instead of sequentially'], + ['pretend', 'p', InputOption::VALUE_NONE, 'Shows which command would execute without actually doing anything'], + ['on', 'C', InputOption::VALUE_REQUIRED, 'The connection(s) to execute the Task in'], + ['stage', 'S', InputOption::VALUE_REQUIRED, 'The stage to execute the Task in'], + // Credentials + ['host', null, InputOption::VALUE_REQUIRED, 'The host to use if asked'], + ['username', null, InputOption::VALUE_REQUIRED, 'The username to use if asked'], + ['password', null, InputOption::VALUE_REQUIRED, 'The password to use if asked'], + ['key', null, InputOption::VALUE_REQUIRED, 'The key to use if asked'], + ['keyphrase', null, InputOption::VALUE_REQUIRED, 'The keyphrase to use if asked'], + ['agent', null, InputOption::VALUE_REQUIRED, 'The agent to use if asked'], + ['repository', null, InputOption::VALUE_REQUIRED, 'The repository to use if asked'], + ); + } + + /** + * Check if the current command is run in the scope of + * Laravel or standalone + * + * @return boolean + */ + public function isInsideLaravel() + { + return $this->laravel->bound('artisan'); + } + + //////////////////////////////////////////////////////////////////// + ///////////////////////////// CORE METHODS ///////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Fire a Tasks Queue + * + * @param string|string[]|\Rocketeer\Abstracts\AbstractTask[] $tasks + * + * @return integer + */ + protected function fireTasksQueue($tasks) + { + // Bind command to container + $this->laravel->instance('rocketeer.command', $this); + + // Check for credentials + $this->laravel['rocketeer.credentials']->getServerCredentials(); + $this->laravel['rocketeer.credentials']->getRepositoryCredentials(); + + if ($this->straight) { + // If we only have a single task, run it + $status = $this->laravel['rocketeer.builder']->buildTask($tasks)->fire(); + } else { + // Run tasks and display timer + $status = $this->time(function () use ($tasks) { + return $this->laravel['rocketeer.queue']->run($tasks); + }); + } + + // Remove command instance + unset($this->laravel['rocketeer.command']); + + // Save history to logs + $logs = $this->laravel['rocketeer.logs']->write(); + foreach ($logs as $log) { + $this->info('Saved logs to '.$log); + } + + return $status ? 0 : 1; + } + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// INPUT //////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Ask a question to the user, with default and/or multiple choices + * + * @param string $question + * @param string|null $default + * @param string[] $choices + * + * @return string + */ + public function askWith($question, $default = null, $choices = array()) + { + $question = $this->formatQuestion($question, $default, $choices); + + // If we provided choices, autocomplete + if ($choices) { + return $this->askWithCompletion($question, $choices, $default); + } + + return $this->ask($question, $default); + } + + /** + * Ask a question to the user, hiding the input + * + * @param string $question + * @param string|null $default + * + * @return string|null + */ + public function askSecretly($question, $default = null) + { + $question = $this->formatQuestion($question, $default); + + return $this->secret($question) ?: $default; + } + + /** + * Adds additional information to a question + * + * @param string $question + * @param string $default + * @param array $choices + * + * @return string + */ + protected function formatQuestion($question, $default, $choices = array()) + { + // If default, show it in the question + if (!is_null($default)) { + $question .= ' ('.$default.')'; + } + + // If multiple choices, show them + if ($choices) { + $question .= ' ['.implode('/', $choices).']'; + } + + return $question; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Time an operation and display it afterwards + * + * @param Closure $callback + * + * @return boolean + */ + public function time(Closure $callback) + { + // Start timer, execute callback, close timer + $timerStart = microtime(true); + $results = $callback(); + $time = round(microtime(true) - $timerStart, 4); + + $this->line('Execution time: '.$time.'s'); + + return $results; + } +} diff --git a/src/Rocketeer/Abstracts/AbstractPackageManager.php b/src/Rocketeer/Abstracts/AbstractPackageManager.php new file mode 100644 index 000000000..ae83e8d2f --- /dev/null +++ b/src/Rocketeer/Abstracts/AbstractPackageManager.php @@ -0,0 +1,62 @@ +getBinary() && $this->hasManifest(); + } + + /** + * Check if the manifest file exists, locally or on server + * + * @return bool + */ + public function hasManifest() + { + $server = $this->paths->getFolder('current/'.$this->manifest); + $server = $this->bash->fileExists($server); + + $local = $this->app['path.base'].DS.$this->manifest; + $local = $this->files->exists($local); + + return $local || $server; + } + + /** + * Get the contents of the manifest file + * + * @return string|null + * @throws \Illuminate\Filesystem\FileNotFoundException + */ + public function getManifestContents() + { + $manifest = $this->app['path.base'].DS.$this->manifest; + if ($this->files->exists($manifest)) { + return $this->files->get($manifest); + } + + return null; + } + + /** + * @return string + */ + public function getManifest() + { + return $this->manifest; + } +} diff --git a/src/Rocketeer/Traits/Plugin.php b/src/Rocketeer/Abstracts/AbstractPlugin.php similarity index 84% rename from src/Rocketeer/Traits/Plugin.php rename to src/Rocketeer/Abstracts/AbstractPlugin.php index dc7e31be0..bbe0b1e2f 100644 --- a/src/Rocketeer/Traits/Plugin.php +++ b/src/Rocketeer/Abstracts/AbstractPlugin.php @@ -7,19 +7,22 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Traits; +namespace Rocketeer\Abstracts; use Illuminate\Container\Container; use Illuminate\Support\Str; -use Rocketeer\TasksHandler; +use Rocketeer\Services\TasksHandler; +use Rocketeer\Traits\HasLocator; /** * A basic abstract class for Rocketeer plugins to extend * * @author Maxime Fabre */ -abstract class Plugin extends AbstractLocatorClass +abstract class AbstractPlugin { + use HasLocator; + /** * The path to the configuration folder * @@ -56,7 +59,7 @@ public function register(Container $app) /** * Register Tasks with Rocketeer * - * @param TasksHandler $queue + * @param \Rocketeer\Services\TasksHandler $queue * * @return void */ diff --git a/src/Rocketeer/Abstracts/AbstractStorage.php b/src/Rocketeer/Abstracts/AbstractStorage.php new file mode 100644 index 000000000..1d510920e --- /dev/null +++ b/src/Rocketeer/Abstracts/AbstractStorage.php @@ -0,0 +1,132 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Abstracts; + +use Closure; +use Illuminate\Container\Container; +use Illuminate\Support\Arr; +use Rocketeer\Traits\HasLocator; + +/** + * Abstract class for storage implementations + * + * @author Maxime Fabre + */ +abstract class AbstractStorage +{ + use HasLocator; + + /** + * The file to act on + * + * @type string + */ + protected $file; + + /** + * Build a new ServerStorage + * + * @param Container $app + * @param string $file + */ + public function __construct(Container $app, $file) + { + $this->app = $app; + $this->file = $file; + } + + /** + * Change the file in use + * + * @param string $file + */ + public function setFile($file) + { + $this->file = $file; + } + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// VALUES /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get a value on the server + * + * @param string|null $key + * @param array|string|Closure|null $fallback + * + * @return string|integer|array + */ + public function get($key = null, $fallback = null) + { + $contents = $this->getContents(); + + return Arr::get($contents, $key, $fallback); + } + + /** + * Set a value on the server + * + * @param string|array $key + * @param mixed|null $value + */ + public function set($key, $value = null) + { + // Set the value on the contents + $contents = (array) $this->getContents(); + if (is_array($key)) { + $contents = $key; + } else { + Arr::set($contents, $key, $value); + } + + $this->saveContents($contents); + } + + /** + * Forget a value from the repository file + * + * @param string $key + */ + public function forget($key) + { + $contents = $this->getContents(); + Arr::forget($contents, $key); + + $this->saveContents($contents); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the full path to the file + * + * @return string + */ + abstract public function getFilepath(); + + /** + * Get the contents of the file + * + * @return array + */ + abstract protected function getContents(); + + /** + * Save the contents of the file + * + * @param array $contents + * + * @return void + */ + abstract protected function saveContents($contents); +} diff --git a/src/Rocketeer/Traits/Task.php b/src/Rocketeer/Abstracts/AbstractTask.php similarity index 55% rename from src/Rocketeer/Traits/Task.php rename to src/Rocketeer/Abstracts/AbstractTask.php index 055a94949..36abe2f55 100644 --- a/src/Rocketeer/Traits/Task.php +++ b/src/Rocketeer/Abstracts/AbstractTask.php @@ -7,18 +7,22 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Traits; +namespace Rocketeer\Abstracts; use DateTime; +use Illuminate\Support\Str; use Rocketeer\Bash; +use Rocketeer\Traits\StepsRunner; /** - * An abstract Task with common helpers, from which all Tasks derive + * An abstract AbstractTask with common helpers, from which all Tasks derive * * @author Maxime Fabre */ -abstract class Task extends Bash +abstract class AbstractTask extends Bash { + use StepsRunner; + /** * The name of the task * @@ -27,32 +31,32 @@ abstract class Task extends Bash protected $name; /** - * A description of what the Task does + * A description of what the task does * * @var string */ protected $description; /** - * Whether the task was halted mid-course + * The event this task is answering to * - * @var boolean + * @type string */ - protected $halted = false; + protected $event; /** - * Whether the Task needs to be run on each stage or globally + * Whether the task was halted mid-course * * @var boolean */ - public $usesStages = true; + protected $halted = false; //////////////////////////////////////////////////////////////////// ////////////////////////////// REFLECTION ////////////////////////// //////////////////////////////////////////////////////////////////// /** - * Get the name of the Task + * Get the name of the task * * @return string */ @@ -62,33 +66,52 @@ public function getName() } /** - * Change the Task's name + * Get the basic name of the task * - * @param string $name + * @return string */ - public function setName($name) + public function getSlug() { - $this->name = $name; + $slug = Str::snake($this->getName(), '-'); + $slug = Str::slug($slug); + + return $slug; } /** - * Get the basic name of the Task + * Get what the task does * * @return string */ - public function getSlug() + public function getDescription() { - return strtolower($this->getName()); + return $this->description; } /** - * Get what the Task does + * Change the task's name * - * @return string + * @param string $name */ - public function getDescription() + public function setName($name) { - return $this->description; + $this->name = ucfirst($name) ?: $this->name; + } + + /** + * @param string $event + */ + public function setEvent($event) + { + $this->event = $event; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description ?: $this->description; } //////////////////////////////////////////////////////////////////// @@ -96,34 +119,38 @@ public function getDescription() //////////////////////////////////////////////////////////////////// /** - * Run the Task + * Run the task * - * @return void + * @return string */ abstract public function execute(); /** * Fire the command * - * @return array + * @return boolean */ public function fire() { - // Fire the Task if the before event passes + // Print status + $results = false; + $this->displayStatus(); + + // Fire the task if the before event passes if ($this->fireEvent('before')) { - $results = $this->execute(); + $this->timer->time($this, function () use (&$results) { + $results = $this->execute(); + }); $this->fireEvent('after'); - - return $results; } - return false; + return $results; } /** * Cancel the task * - * @param string $errors Potential errors to display + * @param string|null $errors Potential errors to display * * @return boolean */ @@ -134,13 +161,14 @@ public function halt($errors = null) $this->command->error($errors); } + $this->fireEvent('halt'); $this->halted = true; return false; } /** - * Whether the Task was halted mid-course + * Whether the task was halted mid-course * * @return boolean */ @@ -158,13 +186,24 @@ public function wasHalted() * * @param string $event * - * @return array|null + * @return boolean */ public function fireEvent($event) { + $event = $this->getQualifiedEvent($event); + $listeners = $this->events->getListeners($event); + // Fire the event - $event = $this->getQualifiedEvent($event); - $result = $this->app['events']->fire($event, array($this), true); + $result = $this->explainer->displayBelow(function () use ($listeners) { + foreach ($listeners as $listener) { + $response = call_user_func_array($listener, [$this]); + if ($response === false) { + return false; + } + } + + return true; + }); // If the event returned a strict false, halt the task if ($result === false) { @@ -193,34 +232,49 @@ public function getQualifiedEvent($event) /** * Display a list of releases and their status * - * @return void + * @codeCoverageIgnore */ protected function displayReleases() { + if (!$this->command) { + return; + } + + $key = 0; + $rows = []; $releases = $this->releasesManager->getValidationFile(); - $this->command->comment('Here are the available releases :'); - $key = 0; + // Append the rows foreach ($releases as $name => $state) { - $name = DateTime::createFromFormat('YmdHis', $name); - $name = $name->format('Y-m-d H:i:s'); - $method = $state ? 'info' : 'error'; - $state = $state ? '✓' : '✘'; + $icon = $state ? '✓' : '✘'; + $color = $state ? 'green' : 'red'; + $date = DateTime::createFromFormat('YmdHis', $name)->format('Y-m-d H:i:s'); + $date = sprintf('%s', $color, $date, $color); + // Add color to row + $rows[] = [$key, $name, $date, $icon]; $key++; - $this->command->$method(sprintf('[%d] %s %s', $key, $name, $state)); } + + // Render table + $this->command->comment('Here are the available releases :'); + $this->command->table( + ['#', 'Path', 'Deployed at', 'Status'], + $rows + ); + + return $rows; } /** - * Execute another Task by name - * - * @param string $task - * - * @return string The Task's output + * Display what the command is and does */ - public function executeTask($task) + protected function displayStatus() { - return $this->app['rocketeer.tasks']->buildTaskFromClass($task)->fire(); + $name = $this->getName(); + $description = $this->getDescription(); + $time = $this->timer->getTaskTime($this); + + $this->explainer->display($name, $description, $this->event, $time); } } diff --git a/src/Rocketeer/Abstracts/Strategies/AbstractCheckStrategy.php b/src/Rocketeer/Abstracts/Strategies/AbstractCheckStrategy.php new file mode 100644 index 000000000..fc5f43823 --- /dev/null +++ b/src/Rocketeer/Abstracts/Strategies/AbstractCheckStrategy.php @@ -0,0 +1,122 @@ +manager; + } + + /** + * @param \Rocketeer\Abstracts\AbstractPackageManager $manager + */ + public function setManager($manager) + { + $this->manager = $manager; + } + + /** + * @return string + */ + public function getLanguage() + { + return $this->language; + } + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// CHECKS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Check that the PM that'll install + * the app's dependencies is present + * + * @return boolean + */ + public function manager() + { + return $this->manager->isExecutable(); + } + + /** + * Check that the language used by the + * application is at the required version + * + * @return boolean + */ + public function language() + { + $required = null; + + // Get the minimum version of the application + if ($manifest = $this->manager->getManifestContents()) { + $required = $this->getLanguageConstraint($manifest); + } + + // Cancel if no version constraint + if (!$required) { + return true; + } + + $version = $this->getCurrentVersion(); + + return version_compare($version, $required, '>='); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// LANGUAGE ////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the version constraint which should be checked against + * + * @param string $manifest + * + * @return string + */ + abstract protected function getLanguageConstraint($manifest); + + /** + * Get the current version in use + * + * @return string + */ + abstract protected function getCurrentVersion(); + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * @param string $manifest + * @param string $handle + * + * @return string + */ + protected function getLanguageConstraintFromJson($manifest, $handle) + { + $manifest = json_decode($manifest, true); + $constraint = (string) Arr::get($manifest, $handle); + $constraint = preg_replace('/[~>= ]+ ?(.+)/', '$1', $constraint); + + return $constraint; + } +} diff --git a/src/Rocketeer/Abstracts/Strategies/AbstractDependenciesStrategy.php b/src/Rocketeer/Abstracts/Strategies/AbstractDependenciesStrategy.php new file mode 100644 index 000000000..ee9927140 --- /dev/null +++ b/src/Rocketeer/Abstracts/Strategies/AbstractDependenciesStrategy.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Abstracts\Strategies; + +use Illuminate\Container\Container; +use Rocketeer\Abstracts\AbstractPackageManager; + +/** + * Abstract class for Dependencies strategies + * + * @author Maxime Fabre + */ +abstract class AbstractDependenciesStrategy extends AbstractStrategy +{ + /** + * The name of the binary + * + * @type string + */ + protected $binary; + + /** + * The package manager instance + * + * @type AbstractPackageManager + */ + protected $manager; + + /** + * @param Container $app + */ + public function __construct(Container $app) + { + $this->app = $app; + $this->manager = $this->binary($this->binary); + } + + /** + * @param AbstractPackageManager $manager + */ + public function setManager($manager) + { + $this->manager = $manager; + } + + /** + * Get an instance of the Binary + * + * @return AbstractPackageManager + */ + protected function getManager() + { + return $this->manager; + } + + /** + * Whether this particular strategy is runnable or not + * + * @return boolean + */ + public function isExecutable() + { + return $this->manager->isExecutable(); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// COMMANDS ////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Install the dependencies + * + * @return bool + */ + public function install() + { + return $this->manager->runForCurrentRelease('install'); + } + + /** + * Update the dependencies + * + * @return boolean + */ + public function update() + { + return $this->manager->runForCurrentRelease('update'); + } +} diff --git a/src/Rocketeer/Abstracts/Strategies/AbstractPolyglotStrategy.php b/src/Rocketeer/Abstracts/Strategies/AbstractPolyglotStrategy.php new file mode 100644 index 000000000..d91b5e84f --- /dev/null +++ b/src/Rocketeer/Abstracts/Strategies/AbstractPolyglotStrategy.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Abstracts\Strategies; + +use Closure; + +abstract class AbstractPolyglotStrategy extends AbstractStrategy +{ + /** + * The various strategies to call + * + * @type array + */ + protected $strategies = []; + + /** + * Execute a method on all sub-strategies + * + * @param string $method + * + * @return boolean[] + */ + protected function executeStrategiesMethod($method) + { + return $this->onStrategies(function (AbstractStrategy $strategy) use ($method) { + return $strategy->$method(); + }); + } + + /** + * Assert that the results of a command are all true + * + * @param boolean[] $results + * + * @return boolean + */ + protected function checkStrategiesResults($results) + { + $results = array_filter($results); + + return count($results) == count($this->strategies); + } + + /** + * @param Closure $callback + * + * @return array + */ + protected function onStrategies(Closure $callback) + { + return $this->explainer->displayBelow(function () use ($callback) { + $results = []; + foreach ($this->strategies as $strategy) { + $instance = $this->getStrategy('Dependencies', $strategy); + if ($instance) { + $results[$strategy] = $callback($instance); + } + } + + return $results; + }); + } +} diff --git a/src/Rocketeer/Abstracts/Strategies/AbstractStrategy.php b/src/Rocketeer/Abstracts/Strategies/AbstractStrategy.php new file mode 100644 index 000000000..996f81a49 --- /dev/null +++ b/src/Rocketeer/Abstracts/Strategies/AbstractStrategy.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Abstracts\Strategies; + +use Illuminate\Support\Arr; +use Rocketeer\Bash; + +/** + * Core class for strategies + * + * @author Maxime Fabre + */ +abstract class AbstractStrategy extends Bash +{ + /** + * @type string + */ + protected $description; + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Whether this particular strategy is runnable or not + * + * @return boolean + */ + public function isExecutable() + { + return true; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Display what the command is and does + * + * @return $this + */ + public function displayStatus() + { + // Recompose strategy and implementation from + // the class name + $components = get_class($this); + $components = explode('\\', $components); + + $name = Arr::get($components, count($components) - 1); + $strategy = Arr::get($components, count($components) - 2); + + $parent = ucfirst($strategy); + $concrete = str_replace('Strategy', null, $name); + $details = $this->getDescription(); + + $this->explainer->display($parent.'/'.$concrete, $details); + + return $this; + } +} diff --git a/src/Rocketeer/Bash.php b/src/Rocketeer/Bash.php index 5cab728ed..5d660bdc4 100644 --- a/src/Rocketeer/Bash.php +++ b/src/Rocketeer/Bash.php @@ -9,13 +9,83 @@ */ namespace Rocketeer; +use Rocketeer\Traits\BashModules\Binaries; +use Rocketeer\Traits\BashModules\Core; +use Rocketeer\Traits\BashModules\Filesystem; +use Rocketeer\Traits\BashModules\Flow; + /** * An helper to execute low-level commands on the remote server * * @author Maxime Fabre */ -class Bash extends Traits\BashModules\Flow +class Bash { - // Composite class - // Don't ask. + use Core; + use Binaries; + use Filesystem; + use Flow; + + /** + * @param string $hook + * @param array $arguments + * + * @return string|array|null + */ + protected function getHookedTasks($hook, array $arguments) + { + $tasks = $this->rocketeer->getOption('strategies.'.$hook); + if (!is_callable($tasks)) { + return; + } + + // Cancel if no tasks to execute + $tasks = (array) call_user_func_array($tasks, $arguments); + if (empty($tasks)) { + return; + } + + // Run commands + return $tasks; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// RUNNERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the implementation behind a strategy + * + * @param string $strategy + * @param string|null $concrete + * + * @return \Rocketeer\Abstracts\Strategies\AbstractStrategy + */ + public function getStrategy($strategy, $concrete = null) + { + $strategy = $this->builder->buildStrategy($strategy, $concrete); + if (!$strategy || !$strategy->isExecutable()) { + return; + } + + return $this->explainer->displayBelow(function () use ($strategy) { + return $strategy->displayStatus(); + }); + } + + /** + * Execute another task by name + * + * @param string $tasks + * + * @return string|false + */ + public function executeTask($tasks) + { + $results = $this->explainer->displayBelow(function () use ($tasks) { + return $this->builder->buildTask($tasks)->fire(); + }); + + return $results; + } } diff --git a/src/Rocketeer/Binaries/AnonymousBinary.php b/src/Rocketeer/Binaries/AnonymousBinary.php new file mode 100644 index 000000000..d601e039d --- /dev/null +++ b/src/Rocketeer/Binaries/AnonymousBinary.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 Rocketeer\Binaries; + +use Rocketeer\Abstracts\AbstractBinary; + +/** + * An wrapper to execute random binaries commands + */ +class AnonymousBinary extends AbstractBinary +{ + // ... +} diff --git a/src/Rocketeer/Binaries/Artisan.php b/src/Rocketeer/Binaries/Artisan.php new file mode 100644 index 000000000..5c2efdfe6 --- /dev/null +++ b/src/Rocketeer/Binaries/Artisan.php @@ -0,0 +1,71 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Binaries; + +use Illuminate\Container\Container; +use Rocketeer\Abstracts\AbstractBinary; + +class Artisan extends AbstractBinary +{ + /** + * @param Container $app + */ + public function __construct(Container $app) + { + parent::__construct($app); + + // Set PHP as parent + $php = new Php($this->app); + $this->setParent($php); + } + + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return array( + 'artisan', + $this->releasesManager->getCurrentReleasePath().'/artisan', + ); + } + + /** + * Run outstranding migrations + * + * @return string + */ + public function migrate() + { + return $this->getCommand('migrate'); + } + + /** + * Seed the database + * + * @return string + */ + public function seed() + { + return $this->getCommand('db:seed'); + } + + /** + * Clear the cache + * + * @return string + */ + public function clearCache() + { + return $this->getCommand('cache:clear'); + } +} diff --git a/src/Rocketeer/Binaries/PackageManagers/Bower.php b/src/Rocketeer/Binaries/PackageManagers/Bower.php new file mode 100644 index 000000000..3037b9f98 --- /dev/null +++ b/src/Rocketeer/Binaries/PackageManagers/Bower.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Binaries\PackageManagers; + +use Rocketeer\Abstracts\AbstractPackageManager; + +class Bower extends AbstractPackageManager +{ + /** + * The name of the manifest file to look for + * + * @type string + */ + protected $manifest = 'bower.json'; + + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return array( + 'bower', + $this->releasesManager->getCurrentReleasePath().'/node_modules/.bin/bower', + ); + } +} diff --git a/src/Rocketeer/Binaries/PackageManagers/Bundler.php b/src/Rocketeer/Binaries/PackageManagers/Bundler.php new file mode 100644 index 000000000..c1875d5c6 --- /dev/null +++ b/src/Rocketeer/Binaries/PackageManagers/Bundler.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Binaries\PackageManagers; + +use Rocketeer\Abstracts\AbstractPackageManager; + +class Bundler extends AbstractPackageManager +{ + /** + * The name of the manifest file to look for + * + * @type string + */ + protected $manifest = 'Gemfile'; + + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return array( + 'bundle', + ); + } +} diff --git a/src/Rocketeer/Binaries/PackageManagers/Composer.php b/src/Rocketeer/Binaries/PackageManagers/Composer.php new file mode 100644 index 000000000..bd8bc9f67 --- /dev/null +++ b/src/Rocketeer/Binaries/PackageManagers/Composer.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 Rocketeer\Binaries\PackageManagers; + +use Rocketeer\Abstracts\AbstractPackageManager; +use Rocketeer\Binaries\Php; + +class Composer extends AbstractPackageManager +{ + /** + * The name of the manifest file to look for + * + * @type string + */ + protected $manifest = 'composer.json'; + + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return array( + 'composer', + $this->releasesManager->getCurrentReleasePath().'/composer.phar', + ); + } + + /** + * Change Composer's binary + * + * @param string $binary + */ + public function setBinary($binary) + { + parent::setBinary($binary); + + // Prepend PHP command if executing from archive + if (strpos($this->getBinary(), 'composer.phar') !== false) { + $php = new Php($this->app); + $this->setParent($php); + } + } +} diff --git a/src/Rocketeer/Binaries/PackageManagers/Npm.php b/src/Rocketeer/Binaries/PackageManagers/Npm.php new file mode 100644 index 000000000..c52f647a4 --- /dev/null +++ b/src/Rocketeer/Binaries/PackageManagers/Npm.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Binaries\PackageManagers; + +use Rocketeer\Abstracts\AbstractPackageManager; + +class Npm extends AbstractPackageManager +{ + /** + * The name of the manifest file to look for + * + * @type string + */ + protected $manifest = 'package.json'; + + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return array( + 'npm', + ); + } +} diff --git a/src/Rocketeer/Binaries/Php.php b/src/Rocketeer/Binaries/Php.php new file mode 100644 index 000000000..fe78eb9a5 --- /dev/null +++ b/src/Rocketeer/Binaries/Php.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 Rocketeer\Binaries; + +use Rocketeer\Abstracts\AbstractBinary; + +class Php extends AbstractBinary +{ + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return ['php']; + } + + /** + * Get the running version of PHP + * + * @return string + */ + public function version() + { + return $this->getCommand(null, null, ['-r' => "print defined('HHVM_VERSION') ? HHVM_VERSION : PHP_VERSION;"]); + } + + /** + * Get the installed extensions + * + * @return string + */ + public function extensions() + { + return $this->getCommand(null, null, ['-m' => null]); + } + + /** + * Whether this PHP installation is an HHVM one or not + * + * @return bool + */ + public function isHhvm() + { + $version = $this->bash->runRaw($this->version(), true); + $version = head($version); + $version = strtolower($version); + + return strpos($version, 'hiphop') !== false; + } +} diff --git a/src/Rocketeer/Binaries/Phpunit.php b/src/Rocketeer/Binaries/Phpunit.php new file mode 100644 index 000000000..baa0adc81 --- /dev/null +++ b/src/Rocketeer/Binaries/Phpunit.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Binaries; + +use Rocketeer\Abstracts\AbstractBinary; + +class Phpunit extends AbstractBinary +{ + /** + * Get an array of default paths to look for + * + * @return string[] + */ + protected function getKnownPaths() + { + return array( + 'phpunit', + $this->releasesManager->getCurrentReleasePath().'/vendor/bin/phpunit', + ); + } +} diff --git a/src/Rocketeer/Commands/AbstractDeployCommand.php b/src/Rocketeer/Commands/AbstractDeployCommand.php deleted file mode 100644 index 7ee250e88..000000000 --- a/src/Rocketeer/Commands/AbstractDeployCommand.php +++ /dev/null @@ -1,228 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Commands; - -use Illuminate\Console\Command; -use Symfony\Component\Console\Input\InputOption; - -/** - * An abstract command with various helpers for all - * subcommands to inherit - * - * @author Maxime Fabre - */ -abstract class AbstractDeployCommand extends Command -{ - /** - * Run the tasks - * - * @return void - */ - abstract public function fire(); - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return array( - array('pretend', 'p', InputOption::VALUE_NONE, 'Returns an array of commands to be executed instead of actually executing them'), - array('on', 'C', InputOption::VALUE_REQUIRED, 'The connection(s) to execute the Task in'), - array('stage', 'S', InputOption::VALUE_REQUIRED, 'The stage to execute the Task in') - ); - } - - /** - * Returns the command name. - * - * @return string The command name - */ - public function getName() - { - // Return commands without namespace if standalone - if (!$this->isInsideLaravel()) { - return str_replace('deploy:', null, $this->name); - } - - return $this->name; - } - - /** - * Check if the current command is run in the scope of - * Laravel or standalone - * - * @return boolean - */ - public function isInsideLaravel() - { - return $this->laravel->bound('artisan'); - } - - //////////////////////////////////////////////////////////////////// - ///////////////////////////// CORE METHODS ///////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Fire a Tasks Queue - * - * @param string|array $tasks - * - * @return mixed - */ - protected function fireTasksQueue($tasks) - { - // Check for credentials - $this->getServerCredentials(); - $this->getRepositoryCredentials(); - - // Start timer - $timerStart = microtime(true); - - // Convert tasks to array if necessary - if (!is_array($tasks)) { - $tasks = array($tasks); - } - - // Bind command to container - $this->laravel->instance('rocketeer.command', $this); - - // Run tasks and display timer - $this->laravel['rocketeer.tasks']->run($tasks, $this); - $this->line('Execution time: '.round(microtime(true) - $timerStart, 4). 's'); - - // Remove commmand instance - unset($this->laravel['rocketeer.command']); - } - - //////////////////////////////////////////////////////////////////// - ///////////////////////////// CREDENTIALS ////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the Repository's credentials - * - * @return void - */ - protected function getRepositoryCredentials() - { - // Check for repository credentials - $repositoryInfos = $this->laravel['rocketeer.rocketeer']->getCredentials(); - $credentials = array('repository'); - if (!array_get($repositoryInfos, 'repository') or $this->laravel['rocketeer.rocketeer']->needsCredentials()) { - $credentials = array('repository', 'username', 'password'); - } - - // Gather credentials - foreach ($credentials as $credential) { - ${$credential} = $this->getCredential($repositoryInfos, $credential); - if (!${$credential}) { - ${$credential} = $this->ask('No '.$credential. ' is set for the repository, please provide one :'); - } - } - - // Save them - $credentials = compact($credentials); - $this->laravel['rocketeer.server']->setValue('credentials', $credentials); - foreach ($credentials as $key => $credential) { - $this->laravel['config']->set('rocketeer::scm.'.$key, $credential); - } - } - - /** - * Get the Server's credentials - * - * @return void - */ - protected function getServerCredentials() - { - if ($connections = $this->option('on')) { - $this->laravel['rocketeer.rocketeer']->setConnections($connections); - } - - // Check for configured connections - $availableConnections = $this->laravel['rocketeer.rocketeer']->getAvailableConnections(); - $activeConnections = $this->laravel['rocketeer.rocketeer']->getConnections(); - - if (count($activeConnections) <= 0) { - $connectionName = $this->ask('No connections have been set, please create one : (production)', 'production'); - $this->storeServerCredentials($availableConnections, $connectionName); - } else { - foreach ($activeConnections as $connectionName) { - $this->storeServerCredentials($availableConnections, $connectionName); - } - } - } - - /** - * Verifies and stores credentials for the given connection name - * - * @param string $connections - * @param string $connectionName - * - * @return void - */ - protected function storeServerCredentials($connections, $connectionName) - { - // Check for server credentials - $connection = array_get($connections, $connectionName, array()); - $credentials = array( - 'host' => true, - 'username' => true, - 'password' => false, - 'keyphrase' => null, - 'key' => false, - 'agent' => false - ); - - // Gather credentials - foreach ($credentials as $credential => $required) { - ${$credential} = $this->getCredential($connection, $credential); - if ($required and !${$credential}) { - ${$credential} = $this->ask('No '.$credential. ' is set for [' .$connectionName. '], please provide one :'); - } - } - - // Get password or key - if (!$password and !$key) { - $type = $this->ask('No password or SSH key is set for [' .$connectionName. '], which would you use ? [key/password]', 'key'); - if ($type == 'key') { - $default = $this->laravel['rocketeer.rocketeer']->getUserHomeFolder().'/.ssh/id_rsa'; - $key = $this->ask('Please enter the full path to your key (' .$default. ')', $default); - $keyphrase = $this->ask('If a keyphrase is required, provide it'); - } else { - $password = $this->ask('Please enter your password'); - } - } - - // Save credentials - $credentials = compact(array_keys($credentials)); - $this->laravel['rocketeer.rocketeer']->syncConnectionCredentials($connectionName, $credentials); - } - - /** - * Check if a credential needs to be filled - * - * @param array $credentials - * @param string $credential - * - * @return string - */ - protected function getCredential($credentials, $credential) - { - $credential = array_get($credentials, $credential); - if (substr($credential, 0, 1) === '{') { - return; - } - - return $credential; - } -} diff --git a/src/Rocketeer/Commands/BaseTaskCommand.php b/src/Rocketeer/Commands/BaseTaskCommand.php deleted file mode 100644 index 573458729..000000000 --- a/src/Rocketeer/Commands/BaseTaskCommand.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Commands; - -use Rocketeer\Traits\Task; - -/** - * A command that wraps around a Task class and runs - * its execute method on fire - * - * @author Maxime Fabre - */ -class BaseTaskCommand extends AbstractDeployCommand -{ - /** - * The default name - * - * @var string - */ - protected $name = 'deploy:custom'; - - /** - * The Task to execute on fire - * - * @var Task - */ - protected $task; - - /** - * Build a new custom command - * - * @param Task $task - * @param string $name A name for the command - */ - public function __construct(Task $task, $name = null) - { - parent::__construct(); - - // Set Task - $this->task = $task; - $this->task->command = $this; - - // Set name - $this->name = $name ?: $task->getSlug(); - $this->name = 'deploy:'.$this->name; - - // Set description - $this->setDescription($task->getDescription()); - } - - /** - * Fire the custom Task - * - * @return string - */ - public function fire() - { - return $this->fireTasksQueue($this->task->getSlug()); - } - - /** - * Get the Task this command executes - * - * @return Task - */ - public function getTask() - { - return $this->task; - } -} diff --git a/src/Rocketeer/Commands/CleanupCommand.php b/src/Rocketeer/Commands/CleanupCommand.php deleted file mode 100644 index c098cc88b..000000000 --- a/src/Rocketeer/Commands/CleanupCommand.php +++ /dev/null @@ -1,56 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Commands; - -use Symfony\Component\Console\Input\InputOption; - -/** - * Runs the Cleanup task to prune deprecated releases - * - * @author Maxime Fabre - */ -class CleanupCommand extends AbstractDeployCommand -{ - /** - * The console command name. - * - * @var string - */ - protected $name = 'deploy:cleanup'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Clean up old releases from the server.'; - - /** - * Execute the tasks - * - * @return array - */ - public function fire() - { - return $this->fireTasksQueue('cleanup'); - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return array_merge(parent::getOptions(), array( - array('clean-all', null, InputOption::VALUE_NONE, 'Cleans up all non-current releases'), - )); - } -} diff --git a/src/Rocketeer/Commands/TestCommand.php b/src/Rocketeer/Commands/TestCommand.php deleted file mode 100644 index 8c1edfeac..000000000 --- a/src/Rocketeer/Commands/TestCommand.php +++ /dev/null @@ -1,44 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Commands; - -/** - * Run the tests on the server and displays the ouput - * - * @author Maxime Fabre - */ -class TestCommand extends AbstractDeployCommand -{ - /** - * The console command name. - * - * @var string - */ - protected $name = 'deploy:test'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Run the tests on the server and displays the output'; - - /** - * The tasks to execute - * - * @return array - */ - public function fire() - { - $this->input->setOption('verbose', true); - - return $this->fireTasksQueue('test'); - } -} diff --git a/src/Rocketeer/Commands/UpdateCommand.php b/src/Rocketeer/Commands/UpdateCommand.php deleted file mode 100644 index 0aeb9ec6e..000000000 --- a/src/Rocketeer/Commands/UpdateCommand.php +++ /dev/null @@ -1,57 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Commands; - -use Symfony\Component\Console\Input\InputOption; - -/** - * Update the remote server without doing a new release - * - * @author Maxime Fabre - */ -class UpdateCommand extends AbstractDeployCommand -{ - /** - * The console command name. - * - * @var string - */ - protected $name = 'deploy:update'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Update the remote server without doing a new release.'; - - /** - * Execute the tasks - * - * @return array - */ - public function fire() - { - return $this->fireTasksQueue('update'); - } - - /** - * Get the console command options. - * - * @return array - */ - protected function getOptions() - { - return array_merge(parent::getOptions(), array( - array('migrate', 'm', InputOption::VALUE_NONE, 'Run the migrations'), - array('seed', 's', InputOption::VALUE_NONE, 'Seed the database after migrating the database'), - )); - } -} diff --git a/src/Rocketeer/Console/Commands/BaseTaskCommand.php b/src/Rocketeer/Console/Commands/BaseTaskCommand.php new file mode 100644 index 000000000..1013711b6 --- /dev/null +++ b/src/Rocketeer/Console/Commands/BaseTaskCommand.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Console\Commands; + +use Rocketeer\Abstracts\AbstractCommand; +use Rocketeer\Abstracts\AbstractTask; + +/** + * A command that wraps around a task class and runs + * its execute method on fire + * + * @author Maxime Fabre + */ +class BaseTaskCommand extends AbstractCommand +{ + /** + * The default name + * + * @var string + */ + protected $name = 'deploy:custom'; + + /** + * Build a new custom command + * + * @param AbstractTask|null $task + * @param string|null $name A name for the command + */ + public function __construct(AbstractTask $task = null, $name = null) + { + parent::__construct($task); + + // Set name + if ($this->name == 'deploy:custom' && $task) { + $this->name = $name ?: $task->getSlug(); + $this->name = 'deploy:'.$this->name; + } + } + + /** + * Fire the custom Task + * + * @return integer + */ + public function fire() + { + return $this->fireTasksQueue($this->task->getSlug()); + } +} diff --git a/src/Rocketeer/Console/Commands/CleanupCommand.php b/src/Rocketeer/Console/Commands/CleanupCommand.php new file mode 100644 index 000000000..03d8a064a --- /dev/null +++ b/src/Rocketeer/Console/Commands/CleanupCommand.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 Rocketeer\Console\Commands; + +use Symfony\Component\Console\Input\InputOption; + +/** + * Runs the Cleanup task to prune deprecated releases + * + * @author Maxime Fabre + */ +class CleanupCommand extends BaseTaskCommand +{ + /** + * Get the console command options. + * + * @return array> + */ + protected function getOptions() + { + return array_merge(parent::getOptions(), array( + ['clean-all', null, InputOption::VALUE_NONE, 'Cleans up all non-current releases'], + )); + } +} diff --git a/src/Rocketeer/Commands/DeployCommand.php b/src/Rocketeer/Console/Commands/DeployCommand.php similarity index 53% rename from src/Rocketeer/Commands/DeployCommand.php rename to src/Rocketeer/Console/Commands/DeployCommand.php index b0157cad4..76575d5fb 100644 --- a/src/Rocketeer/Commands/DeployCommand.php +++ b/src/Rocketeer/Console/Commands/DeployCommand.php @@ -7,7 +7,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Commands; +namespace Rocketeer\Console\Commands; use Symfony\Component\Console\Input\InputOption; @@ -16,26 +16,19 @@ * * @author Maxime Fabre */ -class DeployCommand extends AbstractDeployCommand +class DeployCommand extends BaseTaskCommand { /** - * The console command name. + * The default name * * @var string */ protected $name = 'deploy:deploy'; - /** - * The console command description. - * - * @var string - */ - protected $description = 'Deploy the website.'; - /** * Execute the tasks * - * @return array + * @return integer */ public function fire() { @@ -48,15 +41,15 @@ public function fire() /** * Get the console command options. * - * @return array + * @return array> */ protected function getOptions() { return array_merge(parent::getOptions(), array( - array('tests', 't', InputOption::VALUE_NONE, 'Runs the tests on deploy'), - array('migrate', 'm', InputOption::VALUE_NONE, 'Run the migrations'), - array('seed', 's', InputOption::VALUE_NONE, 'Seed the database (after migrating it if --migrate)'), - array('clean-all', null, InputOption::VALUE_NONE, 'Cleanup all but the current release on deploy'), + ['tests', 't', InputOption::VALUE_NONE, 'Runs the tests on deploy'], + ['migrate', 'm', InputOption::VALUE_NONE, 'Run the migrations'], + ['seed', 's', InputOption::VALUE_NONE, 'Seed the database (after migrating it if --migrate)'], + ['clean-all', null, InputOption::VALUE_NONE, 'Cleanup all but the current release on deploy'], )); } } diff --git a/src/Rocketeer/Commands/FlushCommand.php b/src/Rocketeer/Console/Commands/FlushCommand.php similarity index 73% rename from src/Rocketeer/Commands/FlushCommand.php rename to src/Rocketeer/Console/Commands/FlushCommand.php index 7339c7255..83cc3b902 100644 --- a/src/Rocketeer/Commands/FlushCommand.php +++ b/src/Rocketeer/Console/Commands/FlushCommand.php @@ -7,14 +7,16 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Commands; +namespace Rocketeer\Console\Commands; + +use Rocketeer\Abstracts\AbstractCommand; /** * Flushes any custom storage Rocketeer has created * * @author Maxime Fabre */ -class FlushCommand extends AbstractDeployCommand +class FlushCommand extends AbstractCommand { /** * The console command name. @@ -33,11 +35,15 @@ class FlushCommand extends AbstractDeployCommand /** * Execute the tasks * - * @return array + * @return integer */ public function fire() { - $this->laravel['rocketeer.server']->deleteRepository(); + // Clear the cache of credentials + $this->laravel['rocketeer.storage.local']->destroy(); + $this->info("Rocketeer's cache has been properly flushed"); + + return 0; } } diff --git a/src/Rocketeer/Console/Commands/IgniteCommand.php b/src/Rocketeer/Console/Commands/IgniteCommand.php new file mode 100644 index 000000000..2ec3f99c4 --- /dev/null +++ b/src/Rocketeer/Console/Commands/IgniteCommand.php @@ -0,0 +1,13 @@ +fireTasksQueue('Plugins\Installer'); + } + + /** + * Get the console command arguments. + * + * @return string[][] + */ + protected function getArguments() + { + return array( + ['package', InputArgument::REQUIRED, 'The package to publish the configuration for'], + ); + } +} diff --git a/src/Rocketeer/Console/Commands/Plugins/ListCommand.php b/src/Rocketeer/Console/Commands/Plugins/ListCommand.php new file mode 100644 index 000000000..14c96891f --- /dev/null +++ b/src/Rocketeer/Console/Commands/Plugins/ListCommand.php @@ -0,0 +1,46 @@ +laravel['rocketeer.tasks']->getRegisteredPlugins(); + foreach ($plugins as $plugin => $instance) { + $rows[] = [$plugin]; + } + + $this->table(['Plugin'], $rows); + } +} diff --git a/src/Rocketeer/Console/Commands/Plugins/PublishCommand.php b/src/Rocketeer/Console/Commands/Plugins/PublishCommand.php new file mode 100644 index 000000000..e321ac6ef --- /dev/null +++ b/src/Rocketeer/Console/Commands/Plugins/PublishCommand.php @@ -0,0 +1,54 @@ +laravel); + $publisher->publish($this->argument('package')); + } + + /** + * Get the console command arguments. + * + * @return string[][] + */ + protected function getArguments() + { + return array( + ['package', InputArgument::REQUIRED, 'The package to publish the configuration for'], + ); + } +} diff --git a/src/Rocketeer/Commands/RocketeerCommand.php b/src/Rocketeer/Console/Commands/RocketeerCommand.php similarity index 93% rename from src/Rocketeer/Commands/RocketeerCommand.php rename to src/Rocketeer/Console/Commands/RocketeerCommand.php index 42eade36d..ae7daf1b5 100644 --- a/src/Rocketeer/Commands/RocketeerCommand.php +++ b/src/Rocketeer/Console/Commands/RocketeerCommand.php @@ -7,7 +7,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Commands; +namespace Rocketeer\Console\Commands; use Rocketeer\Rocketeer; @@ -27,8 +27,6 @@ class RocketeerCommand extends DeployCommand /** * Displays the current version - * - * @return string */ public function fire() { diff --git a/src/Rocketeer/Commands/RollbackCommand.php b/src/Rocketeer/Console/Commands/RollbackCommand.php similarity index 51% rename from src/Rocketeer/Commands/RollbackCommand.php rename to src/Rocketeer/Console/Commands/RollbackCommand.php index c3ae0ad40..145e42568 100644 --- a/src/Rocketeer/Commands/RollbackCommand.php +++ b/src/Rocketeer/Console/Commands/RollbackCommand.php @@ -7,63 +7,39 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Commands; +namespace Rocketeer\Console\Commands; -use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputOption; /** * Rollback to the previous release, or to a specific one * * @author Maxime Fabre */ -class RollbackCommand extends AbstractDeployCommand +class RollbackCommand extends BaseTaskCommand { - /** - * The console command name. - * - * @var string - */ - protected $name = 'deploy:rollback'; - - /** - * The console command description. - * - * @var string - */ - protected $description = 'Rollback to the previous release, or to a specific one'; - - /** - * The tasks to execute - * - * @return array - */ - public function fire() - { - return $this->fireTasksQueue('rollback'); - } - /** * Get the console command arguments. * - * @return array + * @return string[][] */ protected function getArguments() { return array( - array('release', InputArgument::OPTIONAL, 'The release to rollback to'), + ['release', InputArgument::OPTIONAL, 'The release to rollback to'], ); } /** * Get the console command options. * - * @return array + * @return array> */ protected function getOptions() { return array_merge(parent::getOptions(), array( - array('list', 'L', InputOption::VALUE_NONE, 'Shows the available releases to rollback to'), + ['list', 'L', InputOption::VALUE_NONE, 'Shows the available releases to rollback to'], )); } } diff --git a/src/Rocketeer/Console/Commands/StrategiesCommand.php b/src/Rocketeer/Console/Commands/StrategiesCommand.php new file mode 100644 index 000000000..f332ce1fb --- /dev/null +++ b/src/Rocketeer/Console/Commands/StrategiesCommand.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Console\Commands; + +use Rocketeer\Abstracts\AbstractCommand; +use Symfony\Component\Console\Helper\Table; + +/** + * Lists the available options for each strategy + * + * @author Maxime Fabre + */ +class StrategiesCommand extends AbstractCommand +{ + /** + * The console command name. + * + * @var string + */ + protected $name = 'deploy:strategies'; + + /** + * The console command description. + * + * @var string + */ + protected $description = 'Lists the available options for each strategy'; + + /** + * Run the tasks + * + * @return void + */ + public function fire() + { + $strategies = array( + 'check' => ['Php', 'Ruby', 'Node'], + 'deploy' => ['Clone', 'Copy', 'Sync'], + 'test' => ['Phpunit'], + 'migrate' => ['Artisan'], + 'dependencies' => ['Composer', 'Bundler', 'Npm', 'Bower', 'Polyglot'], + ); + + $rows = []; + foreach ($strategies as $strategy => $implementations) { + foreach ($implementations as $implementation) { + $instance = $this->laravel['rocketeer.builder']->buildStrategy($strategy, $implementation); + $rows[] = [$strategy, $implementation, $instance->getDescription()]; + } + } + + $this->table(['Strategy', 'Implementation', 'Description'], $rows); + } +} diff --git a/src/Rocketeer/Console/Commands/UpdateCommand.php b/src/Rocketeer/Console/Commands/UpdateCommand.php new file mode 100644 index 000000000..e29bb1993 --- /dev/null +++ b/src/Rocketeer/Console/Commands/UpdateCommand.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Console\Commands; + +use Symfony\Component\Console\Input\InputOption; + +/** + * Update the remote server without doing a new release + * + * @author Maxime Fabre + */ +class UpdateCommand extends BaseTaskCommand +{ + /** + * Get the console command options. + * + * @return array> + */ + protected function getOptions() + { + return array_merge(parent::getOptions(), array( + ['migrate', 'm', InputOption::VALUE_NONE, 'Run the migrations'], + ['seed', 's', InputOption::VALUE_NONE, 'Seed the database after migrating the database'], + )); + } +} diff --git a/src/Rocketeer/Console/Compiler.php b/src/Rocketeer/Console/Compiler.php index 815a54f71..1fd267068 100644 --- a/src/Rocketeer/Console/Compiler.php +++ b/src/Rocketeer/Console/Compiler.php @@ -56,8 +56,6 @@ public function __construct() * Extract an existing Phar * * @param string $destination - * - * @return void */ public function extract($destination) { @@ -99,9 +97,10 @@ public function compile() // Add core files and dependencies $this->addFolder($src); $this->addFolder($vendor, array( - 'mockery', - 'patchwork', + 'd11wtq', 'herrera-io', + 'johnkary', + 'mockery', 'nesbot', 'phine', )); @@ -123,33 +122,31 @@ public function compile() /** * Set the stub to use - * - * @return string */ protected function setStub() { - $this->box->getPhar()->setStub( - StubGenerator::create() - ->index('bin/rocketeer') - ->generate() - ); + $stub = StubGenerator::create() + ->index('bin/rocketeer') + ->generate(); + + $this->box->getPhar()->setStub($stub); } /** * Add a folder to the PHAR * - * @param string $folder - * @param array $ignore + * @param string $folder + * @param string[] $ignore * - * @return array + * @return string[] */ protected function addFolder($folder, array $ignore = array()) { $finder = new Finder(); $finder = $finder->files() - ->ignoreVCS(true) - ->name('*.php') - ->in($folder); + ->ignoreVCS(true) + ->name('*.php') + ->in($folder); // Ignore some files or folders if ($ignore) { diff --git a/src/Rocketeer/Console/Console.php b/src/Rocketeer/Console/Console.php index 2e17e9f6a..830259645 100644 --- a/src/Rocketeer/Console/Console.php +++ b/src/Rocketeer/Console/Console.php @@ -25,26 +25,24 @@ class Console extends Application */ public function getHelp() { - $help = str_replace($this->getLongVersion(), null, parent::getHelp()); + $help = str_replace($this->getLongVersion(), null, parent::getHelp()); + $state = $this->buildBlock('Current state', $this->getCurrentState()); + $help = sprintf('%s'.PHP_EOL.PHP_EOL.'%s%s', $this->getLongVersion(), $state, $help); - return - $this->getLongVersion(). - PHP_EOL.PHP_EOL. - $this->buildBlock('Current state', $this->getCurrentState()). - $help; + return $help; } /** * Build an help block * - * @param string $title - * @param array $informations + * @param string $title + * @param string[] $informations * * @return string */ protected function buildBlock($title, $informations) { - $message = '' .$title. ''; + $message = ''.$title.''; foreach ($informations as $name => $info) { $message .= PHP_EOL.sprintf(' %-15s %s', $name, $info); } @@ -55,7 +53,7 @@ protected function buildBlock($title, $informations) /** * Get current state of the CLI * - * @return array + * @return string[] */ protected function getCurrentState() { diff --git a/src/Rocketeer/Console/WhitespaceCompactor.php b/src/Rocketeer/Console/WhitespaceCompactor.php index 418c054cb..b264c11df 100644 --- a/src/Rocketeer/Console/WhitespaceCompactor.php +++ b/src/Rocketeer/Console/WhitespaceCompactor.php @@ -29,6 +29,6 @@ class WhitespaceCompactor extends Php */ public function supports($file) { - return dirname($file) !== 'src/config' and parent::supports($file); + return dirname($file) !== 'src/config' && parent::supports($file); } } diff --git a/src/Rocketeer/Exceptions/ConnectionException.php b/src/Rocketeer/Exceptions/ConnectionException.php new file mode 100644 index 000000000..7302cb210 --- /dev/null +++ b/src/Rocketeer/Exceptions/ConnectionException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Exceptions; + +use Exception; + +/** + * Exception when Rocketeer can't connect to a server + * + * @author Maxime Fabre + */ +class ConnectionException extends Exception +{ + /** + * Set the credentials that failed to connect + * + * @param array $credentials + */ + public function setCredentials(array $credentials) + { + $this->message .= PHP_EOL.'With credentials:'.PHP_EOL.json_encode($credentials); + } +} diff --git a/src/Rocketeer/Exceptions/MissingCredentialsException.php b/src/Rocketeer/Exceptions/MissingCredentialsException.php new file mode 100644 index 000000000..52b8f5b6b --- /dev/null +++ b/src/Rocketeer/Exceptions/MissingCredentialsException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Exceptions; + +use InvalidArgumentException; + +class MissingCredentialsException extends InvalidArgumentException +{ + // ... +} diff --git a/src/Rocketeer/Exceptions/TaskCompositionException.php b/src/Rocketeer/Exceptions/TaskCompositionException.php new file mode 100644 index 000000000..6729f2d22 --- /dev/null +++ b/src/Rocketeer/Exceptions/TaskCompositionException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Exceptions; + +use Exception; + +class TaskCompositionException extends Exception +{ + // ... +} diff --git a/src/Rocketeer/Facades/Console.php b/src/Rocketeer/Facades/Console.php index 3a5ba9927..38f1979a8 100644 --- a/src/Rocketeer/Facades/Console.php +++ b/src/Rocketeer/Facades/Console.php @@ -13,8 +13,7 @@ * Facade for Rocketeer's CLI * * @author Maxime Fabre - * - * @see Rocketeer\Console\Console + * @see Rocketeer\Console\Console */ class Console extends StandaloneFacade { diff --git a/src/Rocketeer/Facades/Rocketeer.php b/src/Rocketeer/Facades/Rocketeer.php index aaa672ebd..02c19b782 100644 --- a/src/Rocketeer/Facades/Rocketeer.php +++ b/src/Rocketeer/Facades/Rocketeer.php @@ -13,8 +13,7 @@ * Facade for Rocketeer's CLI * * @author Maxime Fabre - * - * @see Rocketeer\TasksQueue + * @see Rocketeer\TasksQueue */ class Rocketeer extends StandaloneFacade { diff --git a/src/Rocketeer/Facades/StandaloneFacade.php b/src/Rocketeer/Facades/StandaloneFacade.php index 84a476ce4..347cdb137 100644 --- a/src/Rocketeer/Facades/StandaloneFacade.php +++ b/src/Rocketeer/Facades/StandaloneFacade.php @@ -9,6 +9,7 @@ */ namespace Rocketeer\Facades; +use Illuminate\Container\Container; use Illuminate\Support\Facades\Facade; use Rocketeer\RocketeerServiceProvider; @@ -16,8 +17,7 @@ * Facade for Rocketeer's CLI * * @author Maxime Fabre - * - * @see Rocketeer\Console\Console + * @see Rocketeer\Console\Console */ abstract class StandaloneFacade extends Facade { @@ -36,7 +36,11 @@ abstract class StandaloneFacade extends Facade protected static function getFacadeAccessor() { if (!static::$app) { - static::$app = RocketeerServiceProvider::make(); + $container = new Container(); + $provider = new RocketeerServiceProvider($container); + $provider->boot(); + + static::$app = $container; } return static::$accessor; diff --git a/src/Rocketeer/Igniter.php b/src/Rocketeer/Igniter.php deleted file mode 100644 index ba245bce2..000000000 --- a/src/Rocketeer/Igniter.php +++ /dev/null @@ -1,213 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer; - -use Illuminate\Container\Container; - -/** - * Finds configurations and paths - * - * @author Maxime Fabre - */ -class Igniter -{ - /** - * The Container - * - * @var Container - */ - protected $app; - - /** - * Build a new Igniter - * - * @param Container $app - */ - public function __construct(Container $app) - { - $this->app = $app; - } - - /** - * Bind paths to the container - * - * @return void - */ - public function bindPaths() - { - $this->bindBase(); - $this->bindConfiguration(); - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// IGNITION /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the path to the configuration folder - * - * @return string - */ - public function getConfigurationPath() - { - // Return path to Laravel configuration - if ($this->app->bound('path')) { - $laravel = $this->app['path'].'/config/packages/anahkiasen/rocketeer'; - if (file_exists($laravel)) { - return $laravel; - } - } - - return $this->app['path.rocketeer.config']; - } - - /** - * Export the configuration files - * - * @return void - */ - public function exportConfiguration() - { - $source = __DIR__.'/../config'; - $destination = $this->getConfigurationPath(); - - // Unzip configuration files - $this->app['files']->copyDirectory($source, $destination); - - return $destination; - } - - /** - * Replace placeholders in configuration - * - * @param string $folder - * @param array $values - * - * @return void - */ - public function updateConfiguration($folder, array $values = array()) - { - // Replace stub values in files - $files = $this->app['files']->files($folder); - foreach ($files as $file) { - foreach ($values as $name => $value) { - $contents = str_replace('{' .$name. '}', $value, file_get_contents($file)); - $this->app['files']->put($file, $contents); - } - } - - // Change repository in use - $application = array_get($values, 'application_name'); - $this->app['rocketeer.server']->setRepository($application); - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// PATHS ///////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Bind the base path to the Container - * - * @return void - */ - protected function bindBase() - { - if ($this->app->bound('path.base')) { - return; - } - - $this->app->instance('path.base', getcwd()); - } - - /** - * Bind paths to the configuration files - * - * @return void - */ - protected function bindConfiguration() - { - $path = $this->getBasePath(); - $logs = $this->getStoragePath(); - - // Prepare the paths to bind - $paths = array( - 'config' => '.rocketeer', - 'events' => '.rocketeer/events', - 'tasks' => '.rocketeer/tasks', - 'logs' => $logs.'/logs', - ); - - foreach ($paths as $key => $file) { - $filename = $path.$file; - - // Check whether we provided a file or folder - if (!is_dir($filename) and file_exists($filename.'.php')) { - $filename .= '.php'; - } - - // Use configuration in current folder if none found - $realpath = realpath('.').'/'.$file; - if (!file_exists($filename) and file_exists($realpath)) { - $filename = $realpath; - } - - $this->app->instance('path.rocketeer.'.$key, $filename); - } - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// HELPERS //////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the base path - * - * @return string - */ - protected function getBasePath() - { - $base = $this->app['path.base'] ? $this->app['path.base'].'/' : ''; - $base = $this->unifySlashes($base); - - return $base; - } - - /** - * Get path to the storage folder - * - * @return string - */ - protected function getStoragePath() - { - // If no path is bound, default to the Rocketeer folder - if (!$this->app->bound('path.storage')) { - return '.rocketeer'; - } - - // Unify slashes - $storage = $this->app['path.storage']; - $storage = $this->unifySlashes($storage); - $storage = str_replace($this->getBasePath(), null, $storage); - - return $storage; - } - - /** - * Unify the slashes to the UNIX mode (forward slashes) - * - * @param string $path - * - * @return string - */ - protected function unifySlashes($path) - { - return str_replace('\\', '/', $path); - } -} diff --git a/src/Rocketeer/Scm/ScmInterface.php b/src/Rocketeer/Interfaces/ScmInterface.php similarity index 86% rename from src/Rocketeer/Scm/ScmInterface.php rename to src/Rocketeer/Interfaces/ScmInterface.php index 72ad1fe15..22a97996b 100644 --- a/src/Rocketeer/Scm/ScmInterface.php +++ b/src/Rocketeer/Interfaces/ScmInterface.php @@ -7,7 +7,7 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer\Scm; +namespace Rocketeer\Interfaces; /** * The interface for all SCM implementations @@ -16,6 +16,13 @@ */ interface ScmInterface { + /** + * Get the current binary name + * + * @return string + */ + public function getBinary(); + /** * Check if the SCM is available * @@ -40,7 +47,7 @@ public function currentBranch(); /** * Clone a repository * - * @param string $destination + * @param string $destination * * @return string */ diff --git a/src/Rocketeer/Interfaces/StorageInterface.php b/src/Rocketeer/Interfaces/StorageInterface.php new file mode 100644 index 000000000..f926a30a7 --- /dev/null +++ b/src/Rocketeer/Interfaces/StorageInterface.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 Rocketeer\Interfaces; + +interface StorageInterface +{ + /** + * Get a value + * + * @param string|null $key + * @param string|null $fallback + * + * @return mixed + */ + public function get($key = null, $fallback = null); + + /** + * Set a value + * + * @param string|array $key + * @param mixed|null $value + * + * @return void + */ + public function set($key, $value = null); + + /** + * Forget a value + * + * @param string $key + * + * @return void + */ + public function forget($key); + + /** + * Destroy the file + * + * @return boolean + */ + public function destroy(); +} diff --git a/src/Rocketeer/Interfaces/Strategies/CheckStrategyInterface.php b/src/Rocketeer/Interfaces/Strategies/CheckStrategyInterface.php new file mode 100644 index 000000000..6e297ac8c --- /dev/null +++ b/src/Rocketeer/Interfaces/Strategies/CheckStrategyInterface.php @@ -0,0 +1,35 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Interfaces\Strategies; + +interface DependenciesStrategyInterface +{ + /** + * Install the dependencies + * + * @return boolean + */ + public function install(); + + /** + * Update the dependencies + * + * @return boolean + */ + public function update(); +} diff --git a/src/Rocketeer/Interfaces/Strategies/DeployStrategyInterface.php b/src/Rocketeer/Interfaces/Strategies/DeployStrategyInterface.php new file mode 100644 index 000000000..573e507b2 --- /dev/null +++ b/src/Rocketeer/Interfaces/Strategies/DeployStrategyInterface.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Interfaces\Strategies; + +/** + * Interface for the various deployment strategies + * + * @author Maxime Fabre + */ +interface DeployStrategyInterface +{ + /** + * Deploy a new clean copy of the application + * + * @param string|null $destination + * + * @return boolean + */ + public function deploy($destination = null); + + /** + * Update the latest version of the application + * + * @param boolean $reset + * + * @return boolean + */ + public function update($reset = true); +} diff --git a/src/Rocketeer/Interfaces/Strategies/MigrateStrategyInterface.php b/src/Rocketeer/Interfaces/Strategies/MigrateStrategyInterface.php new file mode 100644 index 000000000..dfef20b24 --- /dev/null +++ b/src/Rocketeer/Interfaces/Strategies/MigrateStrategyInterface.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 Rocketeer\Interfaces\Strategies; + +/** + * Interface for the various migration strategies + * + * @author Maxime Fabre + */ +interface MigrateStrategyInterface +{ + /** + * Run outstanding migrations + * + * @return boolean + */ + public function migrate(); + + /** + * Seed the database + * + * @return boolean + */ + public function seed(); +} diff --git a/src/Rocketeer/Interfaces/Strategies/TestStrategyInterface.php b/src/Rocketeer/Interfaces/Strategies/TestStrategyInterface.php new file mode 100644 index 000000000..2d46a5b39 --- /dev/null +++ b/src/Rocketeer/Interfaces/Strategies/TestStrategyInterface.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 Rocketeer\Interfaces\Strategies; + +interface TestStrategyInterface +{ + /** + * Run the tests + * + * @return boolean + */ + public function test(); +} diff --git a/src/Rocketeer/LogsHandler.php b/src/Rocketeer/LogsHandler.php deleted file mode 100644 index 65766c2c8..000000000 --- a/src/Rocketeer/LogsHandler.php +++ /dev/null @@ -1,132 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer; - -use Illuminate\Container\Container; - -/** - * Handles rotation of logs - */ -class LogsHandler -{ - /** - * The loggers instances - * - * @var array - */ - protected $loggers = array(); - - /** - * The Container - * - * @var Container - */ - protected $app; - - /** - * Build a new LogsHandler instance - * - * @param Container $app - */ - public function __construct(Container $app) - { - $this->app = $app; - } - - /** - * Log by level - * - * @param string $method - * @param array $parameters - * - * @return void - */ - public function __call($method, $parameters) - { - return $this->log($parameters[0], $method); - } - - /** - * Log a piece of text - * - * @param string $informations - * @param string $level - * - * @return void - */ - public function log($informations, $level = 'info') - { - if ($file = $this->getCurrentLogsFile()) { - return $this->getLogger($file)->$level($informations); - } - } - - /** - * Get the logs file being currently used - * - * @return string - */ - public function getCurrentLogsFile() - { - $logs = $this->app['config']->get('rocketeer::logs'); - if (!$logs) { - return; - } - - $file = $logs($this->app['rocketeer.rocketeer']); - $file = $this->app['path.rocketeer.logs'].'/'.$file; - - return $file; - } - - /** - * Get a logger instance by context - * - * @param string $file - * - * @return Illuminate\Log\Writer - */ - protected function getLogger($file) - { - // Create logger instance if necessary - if (!array_key_exists($file, $this->loggers)) { - $this->createLogsFile($file); - - // Store specific logger instance - $logger = clone $this->app['log']; - $logger->useFiles($file); - $this->loggers[$file] = $logger; - } - - return $this->loggers[$file]; - } - - /** - * Create a logs file if it doesn't exist - * - * @param string $file - * - * @return void - */ - protected function createLogsFile($file) - { - $directory = dirname($file); - - // Create directory - if (!is_dir($directory)) { - $this->app['files']->makeDirectory($directory, 0777, true); - } - - // Create file - if (!file_exists($file)) { - $this->app['files']->put($file, ''); - } - } -} diff --git a/src/Rocketeer/Plugins/AbstractNotifier.php b/src/Rocketeer/Plugins/AbstractNotifier.php new file mode 100644 index 000000000..af12d5241 --- /dev/null +++ b/src/Rocketeer/Plugins/AbstractNotifier.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Plugins; + +use Rocketeer\Abstracts\AbstractPlugin; +use Rocketeer\Services\TasksHandler; +use Rocketeer\Tasks\Subtasks\Notify; + +/** + * A base class for notification services to extends + */ +abstract class AbstractNotifier extends AbstractPlugin +{ + /** + * Register Tasks with Rocketeer + * + * @param \Rocketeer\Services\TasksHandler $queue + * + * @return void + */ + public function onQueue(TasksHandler $queue) + { + // Create the task instance + $notify = new Notify($this->app); + $notify->setNotifier($this); + + $queue->addTaskListeners('deploy', 'before', [clone $notify], -10, true); + $queue->addTaskListeners('deploy', 'after', [clone $notify], -10, true); + } + + /** + * Send a given message + * + * @param string $message + * + * @return void + */ + abstract public function send($message); + + /** + * Get the default message format + * + * @param string $message The message handle + * + * @return string + */ + abstract public function getMessageFormat($message); +} diff --git a/src/Rocketeer/Plugins/Notifier.php b/src/Rocketeer/Plugins/Notifier.php deleted file mode 100644 index 39dcc1777..000000000 --- a/src/Rocketeer/Plugins/Notifier.php +++ /dev/null @@ -1,115 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Plugins; - -use Rocketeer\Traits\Plugin; -use Rocketeer\TasksHandler; - -/** - * A base class for notification services to extends - */ -abstract class Notifier extends Plugin -{ - /** - * Register Tasks with Rocketeer - * - * @param TasksHandler $queue - * - * @return void - */ - public function onQueue(TasksHandler $queue) - { - $me = $this; - - $queue->before('deploy', function ($task) use ($me) { - $me->prepareThenSend($task, 'before_deploy'); - }, -10); - - $queue->after('deploy', function ($task) use ($me) { - $me->prepareThenSend($task, 'after_deploy'); - }, -10); - } - - /** - * Send a given message - * - * @param Task $task - * @param string $message - * - * @return void - */ - abstract public function send($message); - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// MESSAGE //////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the message's components - * - * @return array - */ - protected function getComponents() - { - // Get user name - $user = $this->server->getValue('notifier.name'); - if (!$user) { - $user = $this->command->ask('Who is deploying ?'); - $this->server->setValue('notifier.name', $user); - } - - // Get what was deployed - $branch = $this->rocketeer->getRepositoryBranch(); - $stage = $this->rocketeer->getStage(); - $connection = $this->rocketeer->getConnection(); - - // Get hostname - $credentials = array_get($this->rocketeer->getAvailableConnections(), $connection); - $host = array_get($credentials, 'host'); - if ($stage) { - $connection = $stage.'@'.$connection; - } - - return compact('user', 'branch', 'connection', 'host'); - } - - /** - * Get the default message format - * - * @param string $message The message handle - * - * @return string - */ - abstract protected function getMessageFormat($message); - - /** - * Prepare and send a message - * - * @param Task $task - * @param string $message - * - * @return void - */ - public function prepareThenSend($task, $message) - { - // Don't send a notification if pretending to deploy - if ($task->command->option('pretend')) { - return; - } - - // Build message - $message = $this->getMessageFormat($message); - $message = preg_replace('#\{([0-9])\}#', '%$1\$s', $message); - $message = vsprintf($message, $this->getComponents()); - - // Send it - $this->send($message); - } -} diff --git a/src/Rocketeer/Rocketeer.php b/src/Rocketeer/Rocketeer.php index 48fc5f6f9..5cf042732 100644 --- a/src/Rocketeer/Rocketeer.php +++ b/src/Rocketeer/Rocketeer.php @@ -9,9 +9,7 @@ */ namespace Rocketeer; -use Exception; -use Illuminate\Container\Container; -use Illuminate\Support\Str; +use Rocketeer\Traits\HasLocator; /** * Handles interaction between the User provided informations @@ -21,61 +19,55 @@ */ class Rocketeer { - /** - * The IoC Container - * - * @var Container - */ - protected $app; + use HasLocator; /** - * The current stage + * The Rocketeer version * * @var string */ - protected $stage; + const VERSION = '2.0.0'; /** - * The connections to use + * Returns what stage Rocketeer thinks he's in * - * @var array - */ - protected $connections; - - /** - * The current connection + * @param string $application + * @param string|null $path * - * @var string + * @return string|false */ - protected $connection; + public static function getDetectedStage($application = 'application', $path = null) + { + $current = $path ?: realpath(__DIR__); + preg_match('/'.$application.'\/([a-zA-Z0-9_-]+)\/releases\/([0-9]{14})/', $current, $matches); - /** - * The Rocketeer version - * - * @var string - */ - const VERSION = '1.2.2'; + return isset($matches[1]) ? $matches[1] : false; + } + + ////////////////////////////////////////////////////////////////////// + //////////////////////////// CONFIGURATION /////////////////////////// + ////////////////////////////////////////////////////////////////////// /** - * Build a new ReleasesManager + * Get the name of the application to deploy * - * @param Container $app + * @return string */ - public function __construct(Container $app) + public function getApplicationName() { - $this->app = $app; + return $this->config->get('rocketeer::application_name'); } /** * Get an option from Rocketeer's config file * - * @param string $option + * @param string $option * - * @return mixed + * @return string */ public function getOption($option) { - $original = $this->app['config']->get('rocketeer::'.$option); + $original = $this->config->get('rocketeer::'.$option); if ($contextual = $this->getContextualOption($option, 'stages', $original)) { return $contextual; @@ -91,22 +83,22 @@ public function getOption($option) /** * Get a contextual option * - * @param string $option - * @param string $type [stage,connection] - * @param string|array $original + * @param string $option + * @param string $type [stage,connection] + * @param string|array|null $original * - * @return mixed + * @return string|array|\Closure */ protected function getContextualOption($option, $type, $original = null) { // Switch context switch ($type) { case 'stages': - $contextual = sprintf('rocketeer::on.stages.%s.%s', $this->stage, $option); + $contextual = sprintf('rocketeer::on.stages.%s.%s', $this->connections->getStage(), $option); break; case 'connections': - $contextual = sprintf('rocketeer::on.connections.%s.%s', $this->getConnection(), $option); + $contextual = sprintf('rocketeer::on.connections.%s.%s', $this->connections->getConnection(), $option); break; default: @@ -115,411 +107,11 @@ protected function getContextualOption($option, $type, $original = null) } // Merge with defaults - $value = $this->app['config']->get($contextual); - if (is_array($value) and $original) { + $value = $this->config->get($contextual); + if (is_array($value) && $original) { $value = array_replace($original, $value); } return $value; } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// STAGES //////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Set the stage Tasks will execute on - * - * @param string $stage - * - * @return void - */ - public function setStage($stage) - { - $this->stage = $stage; - - // If we do have a stage, cleanup previous events - if ($stage) { - $this->app['rocketeer.tasks']->registerConfiguredEvents(); - } - } - - /** - * Get the current stage - * - * @return string - */ - public function getStage() - { - return $this->stage; - } - - /** - * Get the various stages provided by the User - * - * @return array - */ - public function getStages() - { - return $this->getOption('stages.stages'); - } - - //////////////////////////////////////////////////////////////////// - ///////////////////////////// APPLICATION ////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Whether the repository used is using SSH or HTTPS - * - * @return boolean - */ - public function needsCredentials() - { - return Str::contains($this->getRepository(), 'https://'); - } - - /** - * Get the available connections - * - * @return array - */ - public function getAvailableConnections() - { - $connections = $this->app['rocketeer.server']->getValue('connections'); - - // Fetch from config file - if (!$connections) { - $connections = $this->app['config']->get('rocketeer::connections'); - } - - // Fetch from remote file - if (!$connections or array_get($connections, 'production.host') == '{host}') { - $connections = $this->app['config']->get('remote.connections'); - } - - return $connections; - } - - /** - * Check if a connection has credentials related to it - * - * @param string $connection - * - * @return boolean - */ - public function isValidConnection($connection) - { - $available = (array) $this->getAvailableConnections(); - - return array_key_exists($connection, $available); - } - - /** - * Get the connection in use - * - * @return string - */ - public function getConnections() - { - // Get cached resolved connections - if ($this->connections) { - return $this->connections; - } - - // Get all and defaults - $connections = (array) $this->app['config']->get('rocketeer::default'); - $default = $this->app['config']->get('remote.default'); - - // Remove invalid connections - $instance = $this; - $connections = array_filter($connections, function ($value) use ($instance) { - return $instance->isValidConnection($value); - }); - - // Return default if no active connection(s) set - if (empty($connections) and $default) { - return array($default); - } - - // Set current connection as default - $this->connections = $connections; - - return $connections; - } - - /** - * Get the active connection - * - * @return string - */ - public function getConnection() - { - // Get cached resolved connection - if ($this->connection) { - return $this->connection; - } - - $connection = array_get($this->getConnections(), 0); - $this->connection = $connection; - - return $this->connection; - } - - /** - * Get the credentials for a particular connection - * - * @param string $connection - * - * @return array - */ - public function getConnectionCredentials($connection = null) - { - $connection = $connection ?: $this->getConnection(); - - return array_get($this->getAvailableConnections(), $connection, array()); - } - - /** - * Sync Rocketeer's credentials with Laravel's - * - * @param string $connection - * @param array $credentials - * - * @return void - */ - public function syncConnectionCredentials($connection = null, array $credentials = array()) - { - // Store credentials if any - if ($credentials) { - $this->app['rocketeer.server']->setValue('connections.'.$connection, $credentials); - } - - // Get connection - $connection = $connection ?: $this->getConnection(); - $credentials = $this->getConnectionCredentials($connection); - - $this->app['config']->set('remote.connections.'.$connection, $credentials); - } - - /** - * Set the active connections - * - * @param string|array $connections - */ - public function setConnections($connections) - { - if (!is_array($connections)) { - $connections = explode(',', $connections); - } - - $this->connections = $connections; - } - - /** - * Set the curent connection - * - * @param string $connection - */ - public function setConnection($connection) - { - if ($this->isValidConnection($connection)) { - $this->connection = $connection; - $this->app['config']->set('remote.default', $connection); - } - } - - /** - * Flush active connection(s) - * - * @return void - */ - public function disconnect() - { - $this->connection = null; - $this->connections = null; - } - - /** - * Get the name of the application to deploy - * - * @return string - */ - public function getApplicationName() - { - return $this->app['config']->get('rocketeer::application_name'); - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////// GIT REPOSITORY ///////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the credentials for the repository - * - * @return array - */ - public function getCredentials() - { - $credentials = $this->app['rocketeer.server']->getValue('credentials'); - if (!$credentials) { - $credentials = $this->getOption('scm'); - } - - // Cast to array - $credentials = (array) $credentials; - - return array_merge(array( - 'repository' => '', - 'username' => '', - 'password' => '', - ), $credentials); - } - - /** - * Get the URL to the Git repository - * - * @param string $username - * @param string $password - * - * @return string - */ - public function getRepository() - { - // Get credentials - $repository = $this->getCredentials(); - $username = array_get($repository, 'username'); - $password = array_get($repository, 'password'); - $repository = array_get($repository, 'repository'); - - // Add credentials if possible - if ($username or $password) { - - // Build credentials chain - $credentials = $password ? $username.':'.$password : $username; - $credentials .= '@'; - - // Add them in chain - $repository = preg_replace('#https://(.+)@#', 'https://', $repository); - $repository = str_replace('https://', 'https://'.$credentials, $repository); - } - - return $repository; - } - - /** - * Get the Git branch - * - * @return string - */ - public function getRepositoryBranch() - { - exec($this->app['rocketeer.scm']->currentBranch(), $fallback); - $fallback = trim($fallback[0]) ?: 'master'; - $branch = $this->getOption('scm.branch') ?: $fallback; - - return $branch; - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// PATHS ///////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get a configured path - * - * @param string $path - * - * @return string - */ - public function getPath($path) - { - return $this->getOption('paths.'.$path); - } - - /** - * Replace patterns in a folder path - * - * @param string $path - * - * @return string - */ - public function replacePatterns($path) - { - $app = $this->app; - - // Replace folder patterns - return preg_replace_callback('/\{[a-z\.]+\}/', function ($match) use ($app) { - $folder = substr($match[0], 1, -1); - - if ($app->bound($folder)) { - return str_replace($app['path.base'].'/', null, $app->make($folder)); - } - - return false; - }, $path); - } - - /** - * Get the path to a folder, taking into account application name and stage - * - * @param string $folder - * - * @return string - */ - public function getFolder($folder = null) - { - $folder = $this->replacePatterns($folder); - - $base = $this->getHomeFolder().'/'; - if ($folder and $this->stage) { - $base .= $this->stage.'/'; - } - $folder = str_replace($base, null, $folder); - - return $base.$folder; - } - - /** - * Get the path to the root folder of the application - * - * @return string - */ - public function getHomeFolder() - { - $rootDirectory = $this->getOption('remote.root_directory'); - $rootDirectory = Str::finish($rootDirectory, '/'); - $appDirectory = $this->getOption('remote.app_directory') ?: $this->getApplicationName(); - - return $rootDirectory.$appDirectory; - } - - /** - * Get the path to the Rocketeer config folder in the users home - * - * @return string - */ - public function getRocketeerConfigFolder() - { - return $this->getUserHomeFolder() . '/.rocketeer'; - } - - /** - * Get the path to the users home folder - * - * @return string - */ - public function getUserHomeFolder() - { - // Get home folder if available (Unix) - if (!empty($_SERVER['HOME'])) { - return $_SERVER['HOME']; - - // Else use the homedrive (Windows) - } elseif (!empty($_SERVER['HOMEDRIVE']) && !empty($_SERVER['HOMEPATH'])) { - return $_SERVER['HOMEDRIVE'] . $_SERVER['HOMEPATH']; - - } else { - throw new Exception('Cannot determine user home directory.'); - } - } } diff --git a/src/Rocketeer/RocketeerServiceProvider.php b/src/Rocketeer/RocketeerServiceProvider.php index 807dfc4ce..2051236a9 100644 --- a/src/Rocketeer/RocketeerServiceProvider.php +++ b/src/Rocketeer/RocketeerServiceProvider.php @@ -11,13 +11,26 @@ use Illuminate\Config\FileLoader; use Illuminate\Config\Repository; -use Illuminate\Container\Container; use Illuminate\Events\Dispatcher; use Illuminate\Http\Request; use Illuminate\Log\Writer; -use Illuminate\Remote\RemoteManager; use Illuminate\Support\ServiceProvider; use Monolog\Logger; +use Rocketeer\Services\Connections\ConnectionsHandler; +use Rocketeer\Services\Connections\LocalConnection; +use Rocketeer\Services\Connections\RemoteHandler; +use Rocketeer\Services\CredentialsGatherer; +use Rocketeer\Services\Display\QueueExplainer; +use Rocketeer\Services\Display\QueueTimer; +use Rocketeer\Services\History\History; +use Rocketeer\Services\History\LogsHandler; +use Rocketeer\Services\Ignition\Configuration; +use Rocketeer\Services\Pathfinder; +use Rocketeer\Services\ReleasesManager; +use Rocketeer\Services\Storages\LocalStorage; +use Rocketeer\Services\Tasks\TasksBuilder; +use Rocketeer\Services\Tasks\TasksQueue; +use Rocketeer\Services\TasksHandler; // Define DS if (!defined('DS')) { @@ -55,208 +68,212 @@ public function register() */ public function boot() { - // Register classes and commands - $this->app = static::make($this->app); + $this->bindPaths(); + $this->bindThirdPartyServices(); + + // Bind Rocketeer's classes + $this->bindCoreClasses(); + $this->bindConsoleClasses(); + $this->bindStrategies(); + + // Load the user's events, tasks, plugins, and configurations + $this->app['rocketeer.igniter']->loadUserConfiguration(); + + // Bind commands + $this->bindCommands(); } /** * Get the services provided by the provider. * - * @return array + * @return string[] */ public function provides() { - return array('rocketeer'); + return ['rocketeer']; } //////////////////////////////////////////////////////////////////// /////////////////////////// CLASS BINDINGS ///////////////////////// //////////////////////////////////////////////////////////////////// - /** - * Make a Rocketeer container - * - * @param Container $app - * - * @return Container - */ - public static function make($app = null) - { - if (!$app) { - $app = new Container; - } - - $serviceProvider = new static($app); - - // Bind core paths and classes - $app = $serviceProvider->bindPaths($app); - $app = $serviceProvider->bindCoreClasses($app); - - // Bind Rocketeer's classes - $app = $serviceProvider->bindClasses($app); - $app = $serviceProvider->bindScm($app); - - // Load the user's events and tasks - $app = $serviceProvider->loadFileOrFolder($app, 'tasks'); - $app = $serviceProvider->loadFileOrFolder($app, 'events'); - - // Bind commands - $app = $serviceProvider->bindCommands($app); - - return $app; - } - /** * Bind the Rocketeer paths - * - * @param Container $app - * - * @return Container */ - public function bindPaths(Container $app) + public function bindPaths() { - $app->bind('rocketeer.igniter', function ($app) { - return new Igniter($app); + $this->app->singleton('rocketeer.paths', function ($app) { + return new Pathfinder($app); }); - // Bind paths - $app['rocketeer.igniter']->bindPaths(); + $this->app->bind('rocketeer.igniter', function ($app) { + return new Configuration($app); + }); - return $app; + // Bind paths + $this->app['rocketeer.igniter']->bindPaths(); } /** * Bind the core classes - * - * @param Container $app - * - * @return Container */ - public function bindCoreClasses(Container $app) + public function bindThirdPartyServices() { - $app->bindIf('files', 'Illuminate\Filesystem\Filesystem'); + $this->app->bindIf('files', 'Illuminate\Filesystem\Filesystem'); - $app->bindIf('request', function () { + $this->app->bindIf('request', function () { return Request::createFromGlobals(); }, true); - $app->bindIf('config', function ($app) { + $this->app->bindIf('config', function ($app) { $fileloader = new FileLoader($app['files'], __DIR__.'/../config'); return new Repository($fileloader, 'config'); }, true); - $app->bindIf('remote', function ($app) { - return new RemoteManager($app); + $this->app->bindIf('rocketeer.remote', function ($app) { + return new RemoteHandler($app); }, true); - $app->bindIf('events', function ($app) { + $this->app->singleton('remote.local', function ($app) { + return new LocalConnection($app); + }); + + $this->app->bindIf('events', function ($app) { return new Dispatcher($app); }, true); - $app->bindIf('log', function () { + $this->app->bindIf('log', function () { return new Writer(new Logger('rocketeer')); }, true); // Register factory and custom configurations - $app = $this->registerConfig($app); - - return $app; + $this->registerConfig(); } /** * Bind the Rocketeer classes to the Container - * - * @param Container $app - * - * @return Container */ - public function bindClasses(Container $app) + public function bindCoreClasses() { - $app->singleton('rocketeer.rocketeer', function ($app) { + $this->app->singleton('rocketeer.rocketeer', function ($app) { return new Rocketeer($app); }); - $app->bind('rocketeer.releases', function ($app) { + $this->app->singleton('rocketeer.connections', function ($app) { + return new ConnectionsHandler($app); + }); + + $this->app->singleton('rocketeer.explainer', function ($app) { + return new QueueExplainer($app); + }); + + $this->app->bind('rocketeer.timer', function ($app) { + return new QueueTimer($app); + }); + + $this->app->singleton('rocketeer.releases', function ($app) { return new ReleasesManager($app); }); - $app->bind('rocketeer.server', function ($app) { + $this->app->singleton('rocketeer.storage.local', function ($app) { $filename = $app['rocketeer.rocketeer']->getApplicationName(); $filename = $filename === '{application_name}' ? 'deployments' : $filename; - return new Server($app, $filename); + return new LocalStorage($app, $filename); }); - $app->bind('rocketeer.bash', function ($app) { + $this->app->bind('rocketeer.bash', function ($app) { return new Bash($app); }); - $app->singleton('rocketeer.queue', function ($app) { + $this->app->singleton('rocketeer.queue', function ($app) { return new TasksQueue($app); }); - $app->singleton('rocketeer.tasks', function ($app) { + $this->app->bind('rocketeer.builder', function ($app) { + return new TasksBuilder($app); + }); + + $this->app->singleton('rocketeer.tasks', function ($app) { return new TasksHandler($app); }); - $app->singleton('rocketeer.logs', function ($app) { + $this->app->singleton('rocketeer.history', function () { + return new History; + }); + + $this->app->singleton('rocketeer.logs', function ($app) { return new LogsHandler($app); }); + } - $app->singleton('rocketeer.console', function () { - return new Console\Console('Rocketeer', Rocketeer::VERSION); + /** + * Bind the CredentialsGatherer and Console application + */ + public function bindConsoleClasses() + { + $this->app->singleton('rocketeer.credentials', function ($app) { + return new CredentialsGatherer($app); }); - $app['rocketeer.console']->setLaravel($app); - $app['rocketeer.rocketeer']->syncConnectionCredentials(); + $this->app->singleton('rocketeer.console', function () { + return new Console\Console('Rocketeer', Rocketeer::VERSION); + }); - return $app; + $this->app['rocketeer.console']->setLaravel($this->app); + $this->app['rocketeer.connections']->syncConnectionCredentials(); } /** * Bind the SCM instance - * - * @param Container $app - * - * @return Container */ - public function bindScm(Container $app) + public function bindStrategies() { - // Currently only one + // Bind SCM class $scm = $this->app['rocketeer.rocketeer']->getOption('scm.scm'); $scm = 'Rocketeer\Scm\\'.ucfirst($scm); - $app->bind('rocketeer.scm', function ($app) use ($scm) { + $this->app->bind('rocketeer.scm', function ($app) use ($scm) { return new $scm($app); }); - return $app; + // Bind strategies + $strategies = $this->app['rocketeer.rocketeer']->getOption('strategies'); + foreach ($strategies as $strategy => $concrete) { + if (!is_string($concrete)) { + continue; + } + + $this->app->bind('rocketeer.strategies.'.$strategy, function ($app) use ($strategy, $concrete) { + return $app['rocketeer.builder']->buildStrategy($strategy, $concrete); + }); + } } /** * Bind the commands to the Container - * - * @param Container $app - * - * @return Container */ - public function bindCommands(Container $app) + public function bindCommands() { // Base commands $tasks = array( - '' => 'Rocketeer', - 'check' => 'Check', - 'cleanup' => 'Cleanup', - 'current' => 'CurrentRelease', - 'deploy' => 'Deploy', - 'flush' => 'Flush', - 'ignite' => 'Ignite', - 'rollback' => 'Rollback', - 'setup' => 'Setup', - 'teardown' => 'Teardown', - 'test' => 'Test', - 'update' => 'Update', + '' => 'Rocketeer', + 'check' => 'Check', + 'cleanup' => 'Cleanup', + 'current' => 'CurrentRelease', + 'deploy' => 'Deploy', + 'flush' => 'Flush', + 'ignite' => 'Ignite', + 'rollback' => 'Rollback', + 'setup' => 'Setup', + 'strategies' => 'Strategies', + 'teardown' => 'Teardown', + 'test' => 'Test', + 'update' => 'Update', + 'plugin-publish' => 'Plugins\Publish', + 'plugin-list' => 'Plugins\List', + 'plugin-install' => 'Plugins\Install', ); // Add User commands @@ -265,51 +282,31 @@ public function bindCommands(Container $app) // Bind the commands foreach ($tasks as $slug => $task) { - - // Check if we have an actual command to use - $commandClass = 'Rocketeer\Commands\\'.$task.'Command'; - $fakeCommand = !class_exists($commandClass); - - // Build command slug - $taskInstance = $this->app['rocketeer.tasks']->buildTaskFromClass($task); - if (is_numeric($slug)) { - $slug = $taskInstance->getSlug(); - } + $command = $this->app['rocketeer.builder']->buildCommand($task, $slug); // Bind Task to container $handle = 'rocketeer.tasks.'.$slug; - $this->app->bind($handle, function () use ($taskInstance) { - return $taskInstance; + $this->app->bind($handle, function () use ($command) { + return $command->getTask(); }); // Add command to array - $command = trim('deploy.'.$slug, '.'); - $this->commands[] = $command; - - // Look for an existing command - if (!$fakeCommand) { - $this->app->singleton($command, function () use ($commandClass) { - return new $commandClass; - }); - - // Else create a fake one - } else { - $this->app->bind($command, function () use ($taskInstance, $slug) { - return new Commands\BaseTaskCommand($taskInstance, $slug); - }); - } + $commandHandle = trim('deploy.'.$slug, '.'); + $this->commands[] = $commandHandle; + // Register command with the container + $this->app->singleton($commandHandle, function () use ($command) { + return $command; + }); } // Add commands to Artisan foreach ($this->commands as $command) { - $app['rocketeer.console']->add($app[$command]); - if (isset($app['events'])) { + $this->app['rocketeer.console']->add($this->app[$command]); + if (isset($this->app['events'])) { $this->commands($command); } } - - return $app; } //////////////////////////////////////////////////////////////////// @@ -318,62 +315,28 @@ public function bindCommands(Container $app) /** * Register factory and custom configurations - * - * @param Container $app - * - * @return Container */ - protected function registerConfig(Container $app) + protected function registerConfig() { // Register config file - $app['config']->package('anahkiasen/rocketeer', __DIR__.'/../config'); - $app['config']->getLoader(); + $this->app['config']->package('anahkiasen/rocketeer', __DIR__.'/../config'); + $this->app['config']->getLoader(); // Register custom config - $custom = $app['path.rocketeer.config']; - if (file_exists($custom)) { - $app['config']->afterLoading('rocketeer', function ($me, $group, $items) use ($custom) { - $customItems = $custom.'/'.$group.'.php'; - if (!file_exists($customItems)) { - return $items; - } - - $customItems = include $customItems; - - return array_replace($items, $customItems); - }); - } - - return $app; - } - - /** - * Load a file or its contents if a folder - * - * @param Container $app - * @param string $handle - * - * @return Container - */ - protected function loadFileOrFolder(Container $app, $handle) - { - // Bind ourselves into the facade to avoid automatic resolution - Facades\Rocketeer::setFacadeApplication($app); - - // If we have one unified tasks file, include it - $file = $app['path.rocketeer.'.$handle]; - if (!is_dir($file) and file_exists($file)) { - include $file; + $set = $this->app['path.rocketeer.config']; + if (!file_exists($set)) { + return; } - // Else include its contents - elseif (is_dir($file)) { - $folder = glob($file.'/*.php'); - foreach ($folder as $file) { - include $file; + $this->app['config']->afterLoading('rocketeer', function ($me, $group, $items) use ($set) { + $customItems = $set.'/'.$group.'.php'; + if (!file_exists($customItems)) { + return $items; } - } - return $app; + $customItems = include $customItems; + + return array_replace($items, $customItems); + }); } } diff --git a/src/Rocketeer/Scm/Git.php b/src/Rocketeer/Scm/Git.php index b90ab7f82..32718b41c 100644 --- a/src/Rocketeer/Scm/Git.php +++ b/src/Rocketeer/Scm/Git.php @@ -9,21 +9,22 @@ */ namespace Rocketeer\Scm; -use Rocketeer\Traits\Scm; +use Rocketeer\Abstracts\AbstractBinary; +use Rocketeer\Interfaces\ScmInterface; /** * The Git implementation of the ScmInterface * * @author Maxime Fabre */ -class Git extends Scm implements ScmInterface +class Git extends AbstractBinary implements ScmInterface { /** * The core binary * * @var string */ - public $binary = 'git'; + protected $binary = 'git'; //////////////////////////////////////////////////////////////////// ///////////////////////////// INFORMATIONS ///////////////////////// @@ -46,7 +47,7 @@ public function check() */ public function currentState() { - return $this->getCommand('rev-parse HEAD'); + return $this->revParse('HEAD'); } /** @@ -56,7 +57,7 @@ public function currentState() */ public function currentBranch() { - return $this->getCommand('rev-parse --abbrev-ref HEAD'); + return $this->revParse('--abbrev-ref HEAD'); } //////////////////////////////////////////////////////////////////// @@ -66,17 +67,24 @@ public function currentBranch() /** * Clone a repository * - * @param string $destination + * @param string $destination * * @return string */ public function checkout($destination) { - $branch = $this->app['rocketeer.rocketeer']->getRepositoryBranch(); - $repository = $this->app['rocketeer.rocketeer']->getRepository(); - $shallow = $this->app['rocketeer.rocketeer']->getOption('scm.shallow') ? ' --depth 1' : ''; + $arguments = array_map([$this, 'quote'], array( + $this->connections->getRepositoryEndpoint(), + $destination, + )); - return $this->getCommand('clone%s -b %s "%s" %s', $shallow, $branch, $repository, $destination); + // Build flags + $flags = ['--branch' => $this->connections->getRepositoryBranch()]; + if ($this->rocketeer->getOption('scm.shallow')) { + $flags['--depth'] = 1; + } + + return $this->clone($arguments, $flags); } /** @@ -86,7 +94,7 @@ public function checkout($destination) */ public function reset() { - return $this->getCommand('reset --hard'); + return $this->getCommand('reset', [], ['--hard']); } /** @@ -96,7 +104,7 @@ public function reset() */ public function update() { - return $this->getCommand('pull'); + return $this->pull(); } /** @@ -106,6 +114,6 @@ public function update() */ public function submodules() { - return $this->getCommand('submodule update --init --recursive'); + return $this->submodule('update', ['--init', '--recursive']); } } diff --git a/src/Rocketeer/Scm/Svn.php b/src/Rocketeer/Scm/Svn.php index a8bf092f3..ed9b621f7 100644 --- a/src/Rocketeer/Scm/Svn.php +++ b/src/Rocketeer/Scm/Svn.php @@ -9,7 +9,9 @@ */ namespace Rocketeer\Scm; -use Rocketeer\Traits\Scm; +use Illuminate\Support\Arr; +use Rocketeer\Abstracts\AbstractBinary; +use Rocketeer\Interfaces\ScmInterface; /** * The Svn implementation of the ScmInterface @@ -17,7 +19,7 @@ * @author Maxime Fabre * @author Gasillo */ -class Svn extends Scm implements ScmInterface +class Svn extends AbstractBinary implements ScmInterface { /** * The core binary @@ -67,17 +69,17 @@ public function currentBranch() /** * Clone a repository * - * @param string $destination + * @param string $destination * * @return string */ public function checkout($destination) { - $branch = $this->app['rocketeer.rocketeer']->getRepositoryBranch(); - $repository = $this->app['rocketeer.rocketeer']->getRepository(); - $repository = rtrim($repository, '/') . '/' . ltrim($branch, '/'); + $branch = $this->connections->getRepositoryBranch(); + $repository = $this->connections->getRepositoryEndpoint(); + $repository = rtrim($repository, '/').'/'.ltrim($branch, '/'); - return $this->getCommand('co %s %s %s', $this->getCredentials(), $repository, $destination); + return $this->co([$repository, $destination], $this->getCredentials()); } /** @@ -87,7 +89,9 @@ public function checkout($destination) */ public function reset() { - return $this->getCommand('status -q | grep -v \'^[~XI ]\' | awk \'{print $2;}\' | xargs %s revert', $this->binary); + $command = sprintf('status -q | grep -v \'^[~XI ]\' | awk \'{print $2;}\' | xargs %s revert', $this->binary); + + return $this->getCommand($command); } /** @@ -97,28 +101,28 @@ public function reset() */ public function update() { - return $this->getCommand('up %s', $this->getCredentials()); + return $this->up([], $this->getCredentials()); } /** * Return credential options * - * @return string + * @return array|array */ protected function getCredentials() { - $options = array('--non-interactive'); - $credentials = $this->app['rocketeer.rocketeer']->getCredentials(); + $options = ['--non-interactive' => null]; + $credentials = $this->connections->getRepositoryCredentials(); // Build command - if ($user = array_get($credentials, 'username')) { - $options[] = '--username=' . $user; + if ($user = Arr::get($credentials, 'username')) { + $options['--username'] = $user; } - if ($pass = array_get($credentials, 'password')) { - $options[] = '--password=' . $pass; + if ($pass = Arr::get($credentials, 'password')) { + $options['--password'] = $pass; } - return implode(' ', $options); + return $options; } /** diff --git a/src/Rocketeer/Server.php b/src/Rocketeer/Server.php deleted file mode 100644 index dd62e82e4..000000000 --- a/src/Rocketeer/Server.php +++ /dev/null @@ -1,315 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer; - -use Exception; -use Illuminate\Container\Container; - -/** - * Provides and persists informations about the remote server - * - * @author Maxime Fabre - */ -class Server -{ - /** - * The IoC Container - * - * @var Container - */ - protected $app; - - /** - * The path to the storage file - * - * @var string - */ - protected $repository; - - /** - * The current hash in use - * - * @var string - */ - protected $hash; - - /** - * Build a new ReleasesManager - * - * @param Container $app - * @param string $filename - * @param string $storage - */ - public function __construct(Container $app, $filename = 'deployments', $storage = null) - { - $this->app = $app; - - // Create repository and update it if necessary - $this->setRepository($filename, $storage); - if ($this->shouldFlush()) { - $this->deleteRepository(); - } - - // Add salt to current repository - $this->setValue('hash', $this->getHash()); - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// SALTS ///////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the current salt in use - * - * @return string - */ - public function getHash() - { - // Return cached hash if any - if ($this->hash) { - return $this->hash; - } - - // Get the contents of the configuration folder - $salt = ''; - $folder = $this->app['rocketeer.igniter']->getConfigurationPath(); - $files = $this->app['files']->glob($folder.'/*.php'); - - // Remove custom files and folders - $handles = array('events', 'tasks'); - foreach ($handles as $handle) { - $path = $this->app['path.rocketeer.'.$handle]; - $index = array_search($path, $files); - if ($index !== false) { - unset($files[$index]); - } - } - - // Compute the salts - foreach ($files as $file) { - $file = $this->app['files']->getRequire($file); - $salt .= json_encode($file); - } - - // Cache it - $this->hash = md5($salt); - - return $this->hash; - } - - //////////////////////////////////////////////////////////////////// - ///////////////////////////// REPOSITORY /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Flushes the repository if required - * - * @return void - */ - public function shouldFlush() - { - $currentHash = $this->getValue('hash'); - - return $currentHash and $currentHash !== $this->getHash(); - } - - /** - * Change the repository in use - * - * @param string $filename - * @param string $storage - */ - public function setRepository($filename, $storage = null) - { - // Create personal storage if necessary - if (!$this->app->bound('path.storage')) { - $storage = $this->app['rocketeer.rocketeer']->getRocketeerConfigFolder(); - $this->app['files']->makeDirectory($storage, 0755, false, true); - } - - // Get path to storage - $storage = $storage ?: $this->app['path.storage'].DS.'meta'; - - $this->repository = $storage.DS.$filename.'.json'; - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////// REMOTE VARIABLES /////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the directory separators on the remove server - * - * @return string - */ - public function getSeparator() - { - // If manually set by the user, return it - $user = $this->app['rocketeer.rocketeer']->getOption('remote.variables.directory_separator'); - if ($user) { - return $user; - } - - $bash = $this->app['rocketeer.bash']; - return $this->getValue('directory_separator', function ($server) use ($bash) { - $separator = $bash->runLast('php -r "echo DIRECTORY_SEPARATOR;"'); - - // Throw an Exception if we receive invalid output - if (strlen($separator) > 1) { - throw new Exception( - 'An error occured while fetching the directory separators used on the server.'.PHP_EOL. - 'Output received was : '.$separator - ); - } - - // Cache separator - $server->setValue('directory_separator', $separator); - - return $separator; - }); - } - - /** - * Get the remote line endings on the remove server - * - * @return string - */ - public function getLineEndings() - { - // If manually set by the user, return it - $user = $this->app['rocketeer.rocketeer']->getOption('remote.variables.line_endings'); - if ($user) { - return $user; - } - - $bash = $this->app['rocketeer.bash']; - return $this->getValue('line_endings', function ($server) use ($bash) { - $endings = $bash->runRaw('php -r "echo PHP_EOL;"'); - $server->setValue('line_endings', $endings); - - return $endings; - }); - } - - /** - * Check if the current project uses Composer - * - * @return boolean - */ - public function usesComposer() - { - $path = $this->app['path.base'].DIRECTORY_SEPARATOR.'composer.json'; - - return $this->app['files']->exists($path); - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// KEYSTORE /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get a value from the repository file - * - * @param string $key - * @param \Closure|string $fallback - * - * @return mixed - */ - public function getValue($key, $fallback = null) - { - $value = array_get($this->getRepository(), $key, null); - - // Get fallback value - if (is_null($value)) { - return is_callable($fallback) ? $fallback($this) : $fallback; - } - - return $value; - } - - /** - * Set a value from the repository file - * - * @param string $key - * @param mixed $value - * - * @return array - */ - public function setValue($key, $value) - { - $repository = $this->getRepository(); - array_set($repository, $key, $value); - - return $this->updateRepository($repository); - } - - /** - * Forget a value from the repository file - * - * @param string $key - * - * @return array - */ - public function forgetValue($key) - { - $repository = $this->getRepository(); - array_forget($repository, $key); - - return $this->updateRepository($repository); - } - - //////////////////////////////////////////////////////////////////// - ////////////////////////// REPOSITORY FILE ///////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Replace the contents of the deployments file - * - * @param array $data - * - * @return array - */ - public function updateRepository($data) - { - // Yup. Don't look at me like that. - @$this->app['files']->put($this->repository, json_encode($data)); - - return $data; - } - - /** - * Get the contents of the deployments file - * - * @return array - */ - public function getRepository() - { - // Cancel if the file doesn't exist - if (!$this->app['files']->exists($this->repository)) { - return array(); - } - - // Get and parse file - $repository = $this->app['files']->get($this->repository); - $repository = json_decode($repository, true); - - return $repository; - } - - /** - * Deletes the deployments file - * - * @return boolean - */ - public function deleteRepository() - { - return $this->app['files']->delete($this->repository); - } -} diff --git a/src/Rocketeer/Services/Connections/Connection.php b/src/Rocketeer/Services/Connections/Connection.php new file mode 100644 index 000000000..4c53d32bf --- /dev/null +++ b/src/Rocketeer/Services/Connections/Connection.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Connections; + +/** + * Base connection class with additional setters + * + * @author Maxime Fabre + */ +class Connection extends \Illuminate\Remote\Connection +{ + /** + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * @return string + */ + public function getUsername() + { + return $this->username; + } +} diff --git a/src/Rocketeer/Services/Connections/ConnectionsHandler.php b/src/Rocketeer/Services/Connections/ConnectionsHandler.php new file mode 100644 index 000000000..5caffcea0 --- /dev/null +++ b/src/Rocketeer/Services/Connections/ConnectionsHandler.php @@ -0,0 +1,428 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Connections; + +use Illuminate\Support\Arr; +use Illuminate\Support\Str; +use Rocketeer\Traits\HasLocator; + +/** + * Handles, get and return, the various connections/stages + * and their credentials + * + * @author Maxime Fabre + */ +class ConnectionsHandler +{ + use HasLocator; + + /** + * The current handle + * + * @type string + */ + protected $handle; + + /** + * The current stage + * + * @var string + */ + protected $stage; + + /** + * The current server + * + * @type integer + */ + protected $currentServer = 0; + + /** + * The connections to use + * + * @var array|null + */ + protected $connections; + + /** + * The current connection + * + * @var string|null + */ + protected $connection; + + /** + * Build the current connection's handle + * + * @param string|null $connection + * @param integer|null $server + * @param string|null $stage + * + * @return string + */ + public function getHandle($connection = null, $server = null, $stage = null) + { + if ($this->handle) { + return $this->handle; + } + + // Get identifiers + $connection = $connection ?: $this->getConnection(); + $server = $server ?: $this->getServer(); + $stage = $stage ?: $this->getStage(); + + // Filter values + $handle = [$connection, $server, $stage]; + if ($this->isMultiserver($connection)) { + $handle = array_filter($handle, function ($value) { + return !is_null($value); + }); + } else { + $handle = array_filter($handle); + } + + // Concatenate + $handle = implode('/', $handle); + $this->handle = $handle; + + return $handle; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// SERVERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * @return int + */ + public function getServer() + { + return $this->currentServer; + } + + /** + * Check if a connection is multiserver or not + * + * @param string $connection + * + * @return boolean + */ + public function isMultiserver($connection) + { + return (bool) count($this->getConnectionCredentials($connection)); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// STAGES //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the current stage + * + * @return string + */ + public function getStage() + { + return $this->stage; + } + + /** + * Set the stage Tasks will execute on + * + * @param string|null $stage + */ + public function setStage($stage) + { + if ($stage == $this->stage) { + return; + } + + $this->stage = $stage; + $this->handle = null; + + // If we do have a stage, cleanup previous events + if ($stage) { + $this->tasks->registerConfiguredEvents(); + } + } + + /** + * Get the various stages provided by the User + * + * @return array + */ + public function getStages() + { + return (array) $this->rocketeer->getOption('stages.stages'); + } + + //////////////////////////////////////////////////////////////////// + ///////////////////////////// APPLICATION ////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Whether the repository used is using SSH or HTTPS + * + * @return boolean + */ + public function needsCredentials() + { + return Str::contains($this->getRepositoryEndpoint(), 'https://'); + } + + /** + * Get the available connections + * + * @return string[][]|string[] + */ + public function getAvailableConnections() + { + // Fetch stored credentials + $storage = (array) $this->localStorage->get('connections'); + + // Merge with defaults from config file + $configuration = (array) $this->config->get('rocketeer::connections'); + + // Fetch from remote file + $remote = (array) $this->config->get('remote.connections'); + + // Merge configurations + $connections = array_replace_recursive($remote, $configuration, $storage); + + // Unify multiservers + foreach ($connections as $key => $servers) { + $servers = Arr::get($servers, 'servers', [$servers]); + $connections[$key] = ['servers' => array_values($servers)]; + } + + return $connections; + } + + /** + * Check if a connection has credentials related to it + * + * @param string $connection + * + * @return boolean + */ + public function isValidConnection($connection) + { + $available = (array) $this->getAvailableConnections(); + + return (bool) Arr::get($available, $connection.'.servers'); + } + + /** + * Get the connection in use + * + * @return string[] + */ + public function getConnections() + { + // Get cached resolved connections + if ($this->connections) { + return $this->connections; + } + + // Get all and defaults + $connections = (array) $this->config->get('rocketeer::default'); + $default = $this->config->get('remote.default'); + + // Remove invalid connections + $instance = $this; + $connections = array_filter($connections, function ($value) use ($instance) { + return $instance->isValidConnection($value); + }); + + // Return default if no active connection(s) set + if (empty($connections) && $default) { + return array($default); + } + + // Set current connection as default + $this->connections = $connections; + + return $connections; + } + + /** + * Set the active connections + * + * @param string|string[] $connections + */ + public function setConnections($connections) + { + if (!is_array($connections)) { + $connections = explode(',', $connections); + } + + $this->connections = $connections; + $this->handle = null; + } + + /** + * Get the active connection + * + * @return string + */ + public function getConnection() + { + // Get cached resolved connection + if ($this->connection) { + return $this->connection; + } + + $connection = Arr::get($this->getConnections(), 0); + $this->connection = $connection; + + return $this->connection; + } + + /** + * Set the current connection + * + * @param string $connection + * @param int $server + */ + public function setConnection($connection, $server = 0) + { + if (!$this->isValidConnection($connection) || $this->connection == $connection) { + return; + } + + // Set the connection + $this->handle = null; + $this->connection = $connection; + $this->localStorage = $server; + + // Update events + $this->tasks->registerConfiguredEvents(); + } + + /** + * Get the credentials for a particular connection + * + * @param string|null $connection + * + * @return string[][] + */ + public function getConnectionCredentials($connection = null) + { + $connection = $connection ?: $this->getConnection(); + $available = $this->getAvailableConnections(); + + return Arr::get($available, $connection.'.servers'); + } + + /** + * Get thecredentials for as server + * + * @param string|null $connection + * @param int $server + * + * @return mixed + */ + public function getServerCredentials($connection = null, $server = 0) + { + $connection = $this->getConnectionCredentials($connection); + + return Arr::get($connection, $server); + } + + /** + * Sync Rocketeer's credentials with Laravel's + * + * @param string|null $connection + * @param string[]|null $credentials + * @param int $server + */ + public function syncConnectionCredentials($connection = null, array $credentials = array(), $server = 0) + { + // Store credentials if any + if ($credentials) { + $this->localStorage->set('connections.'.$connection.'.servers.'.$server, $credentials); + } + + // Get connection + $connection = $connection ?: $this->getConnection(); + $credentials = $this->getConnectionCredentials($connection); + + $this->config->set('remote.connections.'.$connection, $credentials); + } + + /** + * Flush active connection(s) + */ + public function disconnect() + { + $this->connection = null; + $this->connections = null; + } + + //////////////////////////////////////////////////////////////////// + /////////////////////////// GIT REPOSITORY ///////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the credentials for the repository + * + * @return string[] + */ + public function getRepositoryCredentials() + { + $config = (array) $this->rocketeer->getOption('scm'); + $credentials = (array) $this->localStorage->get('credentials'); + + return array_merge($config, $credentials); + } + + /** + * Get the URL to the Git repository + * + * @return string + */ + public function getRepositoryEndpoint() + { + // Get credentials + $repository = $this->getRepositoryCredentials(); + $username = Arr::get($repository, 'username'); + $password = Arr::get($repository, 'password'); + $repository = Arr::get($repository, 'repository'); + + // Add credentials if possible + if ($username || $password) { + + // Build credentials chain + $credentials = $password ? $username.':'.$password : $username; + $credentials .= '@'; + + // Add them in chain + $repository = preg_replace('#https://(.+)@#', 'https://', $repository); + $repository = str_replace('https://', 'https://'.$credentials, $repository); + } + + return $repository; + } + + /** + * Get the Git branch + * + * @return string + */ + public function getRepositoryBranch() + { + exec($this->scm->currentBranch(), $fallback); + $fallback = Arr::get($fallback, 0, 'master'); + $fallback = trim($fallback); + $branch = $this->rocketeer->getOption('scm.branch') ?: $fallback; + + return $branch; + } +} diff --git a/src/Rocketeer/Services/Connections/LocalConnection.php b/src/Rocketeer/Services/Connections/LocalConnection.php new file mode 100644 index 000000000..ec1e5c38c --- /dev/null +++ b/src/Rocketeer/Services/Connections/LocalConnection.php @@ -0,0 +1,152 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Connections; + +use Closure; +use Illuminate\Remote\ConnectionInterface; +use Rocketeer\Traits\HasLocator; + +/** + * Stub of local connections to make Rocketeer work + * locally when necessary + * + * @author Maxime Fabre + */ +class LocalConnection implements ConnectionInterface +{ + use HasLocator; + + /** + * Return status of the last command + * + * @type integer + */ + protected $previousStatus; + + /** + * Define a set of commands as a task. + * + * @param string $task + * @param string|array $commands + * + * @codeCoverageIgnore + * @return void + */ + public function define($task, $commands) + { + // ... + } + + /** + * Run a task against the connection. + * + * @param string $task + * @param Closure|null $callback + * + * @codeCoverageIgnore + * @return void + */ + public function task($task, Closure $callback = null) + { + // ... + } + + /** + * Run a set of commands against the connection. + * + * @param string|array $commands + * @param Closure|null $callback + * + * @return void + */ + public function run($commands, Closure $callback = null) + { + $commands = (array) $commands; + foreach ($commands as $command) { + exec($command, $output, $status); + + $this->previousStatus = $status; + if ($callback) { + $output = (array) $output; + foreach ($output as $line) { + $callback($line.PHP_EOL); + } + } + } + } + + /** + * Get the exit status of the last command. + * + * @return integer|null + */ + public function status() + { + return $this->previousStatus; + } + + /** + * Upload a local file to the server. + * + * @param string $local + * @param string $remote + * + * @codeCoverageIgnore + * @return integer + */ + public function put($local, $remote) + { + $local = $this->files->get($local); + + return $this->putString($local, $remote); + } + + /** + * Get the contents of a remote file. + * + * @param string $remote + * + * @codeCoverageIgnore + * @return string + */ + public function getString($remote) + { + return $this->files->get($remote); + } + + /** + * Display the given line using the default output. + * + * @param string $line + * + * @codeCoverageIgnore + * @return void + */ + public function display($line) + { + $lead = '[local]'; + + $this->command->line($lead.' '.$line); + } + + /** + * Upload a string to to the given file on the server. + * + * @param string $remote + * @param string $contents + * + * @codeCoverageIgnore + * @return integer + */ + public function putString($remote, $contents) + { + return $this->files->put($remote, $contents); + } +} diff --git a/src/Rocketeer/Services/Connections/RemoteHandler.php b/src/Rocketeer/Services/Connections/RemoteHandler.php new file mode 100644 index 000000000..e379fd7d5 --- /dev/null +++ b/src/Rocketeer/Services/Connections/RemoteHandler.php @@ -0,0 +1,145 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Connections; + +use Exception; +use InvalidArgumentException; +use Rocketeer\Exceptions\ConnectionException; +use Rocketeer\Exceptions\MissingCredentialsException; +use Rocketeer\Traits\HasLocator; +use Symfony\Component\Console\Output\NullOutput; + +/** + * Handle creationg and caching of connections + * + * @author Maxime Fabre + * @author Taylor Otwell + */ +class RemoteHandler +{ + use HasLocator; + + /** + * A storage of active connections + * + * @type Connection[] + */ + protected $active = []; + + /** + * Whether the handler is currently connected to any server + * + * @return boolean + */ + public function connected() + { + return (bool) $this->active; + } + + /** + * Create a specific connection or the default one + * + * @param string|null $connection + * @param integer $server + * + * @return Connection + */ + public function connection($connection = null, $server = 0) + { + $name = $connection ?: $this->connections->getConnection(); + $server = $server ?: $this->connections->getServer(); + $handle = $this->connections->getHandle($name, $server); + + // Check the cache + if (isset($this->active[$handle])) { + return $this->active[$handle]; + } + + // Create connection + $credentials = $this->connections->getServerCredentials(); + $connection = $this->makeConnection($name, $credentials); + + // Save to cache + $this->active[$handle] = $connection; + + return $connection; + } + + /** + * @param string $name + * @param array $credentials + * + * @throws MissingCredentialsException + * @return Connection + */ + protected function makeConnection($name, array $credentials) + { + if (!isset($credentials['host']) || !isset($credentials['username'])) { + throw new MissingCredentialsException('Host and/or username is required for '.$name); + } + + $connection = new Connection( + $name, + $credentials['host'], + $credentials['username'], + $this->getAuth($credentials) + ); + + // Set output on connection + $output = $this->hasCommand() ? $this->command->getOutput() : new NullOutput(); + $connection->setOutput($output); + + return $connection; + } + + /** + * Format the appropriate authentication array payload. + * + * @param array $config + * + * @return array + * @throws InvalidArgumentException + */ + protected function getAuth(array $config) + { + if (isset($config['agent']) && $config['agent'] === true) { + return ['agent' => true]; + } elseif (isset($config['key']) && trim($config['key']) != '') { + return ['key' => $config['key'], 'keyphrase' => $config['keyphrase']]; + } elseif (isset($config['keytext']) && trim($config['keytext']) != '') { + return ['keytext' => $config['keytext']]; + } elseif (isset($config['password'])) { + return ['password' => $config['password']]; + } + + throw new MissingCredentialsException('Password / key is required.'); + } + + /** + * Dynamically pass methods to the default connection. + * + * @param string $method + * @param array $parameters + * + * @throws \Rocketeer\Exceptions\ConnectionException + * @return mixed + */ + public function __call($method, $parameters) + { + try { + return call_user_func_array([$this->connection(), $method], $parameters); + } catch (Exception $exception) { + $exception = new ConnectionException($exception->getMessage()); + $exception->setCredentials($this->connections->getServerCredentials()); + + throw $exception; + } + } +} diff --git a/src/Rocketeer/Services/CredentialsGatherer.php b/src/Rocketeer/Services/CredentialsGatherer.php new file mode 100644 index 000000000..429f463f1 --- /dev/null +++ b/src/Rocketeer/Services/CredentialsGatherer.php @@ -0,0 +1,218 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services; + +use Illuminate\Support\Arr; +use Rocketeer\Traits\HasLocator; + +class CredentialsGatherer +{ + use HasLocator; + + /** + * Get the Repository's credentials + */ + public function getRepositoryCredentials() + { + // Check for repository credentials + $repositoryCredentials = $this->connections->getRepositoryCredentials(); + + // Build credentials array + // null values are considered non required + $credentials = array( + 'repository' => true, + 'username' => !is_null(Arr::get($repositoryCredentials, 'username', '')), + 'password' => !is_null(Arr::get($repositoryCredentials, 'password', '')), + ); + + // If we didn't specify a login/password ask for both the first time + if ($this->connections->needsCredentials()) { + // Else assume the repository is passwordless and only ask again for username + $credentials += ['username' => true, 'password' => true]; + } + + // Gather credentials + $credentials = $this->gatherCredentials($credentials, $repositoryCredentials, 'repository'); + + // Save them to local storage and runtime configuration + $this->localStorage->set('credentials', $credentials); + foreach ($credentials as $key => $credential) { + $this->config->set('rocketeer::scm.'.$key, $credential); + } + } + + /** + * Get the LocalStorage's credentials + */ + public function getServerCredentials() + { + if ($connections = $this->command->option('on')) { + $this->connections->setConnections($connections); + } + + // Check for configured connections + $availableConnections = $this->connections->getAvailableConnections(); + $activeConnections = $this->connections->getConnections(); + + // If we didn't set any connection, ask for them + if (!$activeConnections || empty($availableConnections)) { + $connectionName = $this->command->askWith('No connections have been set, please create one:', 'production'); + $this->getConnectionCredentials($connectionName); + + return; + } + + // Else loop through the connections and fill in credentials + foreach ($activeConnections as $connectionName) { + $servers = Arr::get($availableConnections, $connectionName.'.servers'); + $servers = array_keys($servers); + foreach ($servers as $server) { + $this->getConnectionCredentials($connectionName, $server); + } + } + } + + /** + * Verifies and stores credentials for the given connection name + * + * @param string $connectionName + * @param integer|null $server + */ + protected function getConnectionCredentials($connectionName, $server = null) + { + // Get the available connections + $connections = $this->connections->getAvailableConnections(); + + // Get the credentials for the asked connection + $connection = $connectionName.'.servers'; + $connection = !is_null($server) ? $connection.'.'.$server : $connection; + $connection = Arr::get($connections, $connection, []); + + // Update connection name + $handle = $this->connections->getHandle($connectionName, $server); + + // Gather common credentials + $credentials = $this->gatherCredentials(array( + 'host' => true, + 'username' => true, + 'password' => false, + 'keyphrase' => null, + 'key' => false, + 'agent' => false, + ), $connection, $handle); + + // Get password or key + $credentials = $this->getConnectionAuthentication($credentials, $handle); + + // Save credentials + $this->connections->syncConnectionCredentials($connectionName, $credentials, $server); + $this->connections->setConnection($connectionName); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Smart fill-in of the key/password of a connection + * + * @param string[] $credentials + * @param string $handle + * + * @return string[] + */ + protected function getConnectionAuthentication(array $credentials, $handle) + { + // Cancel if already provided + if ($credentials['password'] || $credentials['key']) { + return $credentials; + } + + // Get which type of authentication to use + $types = ['key', 'password']; + $keyPath = $this->paths->getDefaultKeyPath(); + $type = $this->command->askWith('No password or SSH key is set for ['.$handle.'], which would you use?', 'key', $types); + + // Gather the credentials for each + switch ($type) { + case 'key': + $credentials['key'] = $this->command->option('key') ?: $this->command->askWith('Please enter the full path to your key', $keyPath); + $credentials['keyphrase'] = $this->gatherCredential($handle, 'keyphrase', 'If a keyphrase is required, provide it'); + break; + + case 'password': + $credentials['password'] = $this->gatherCredential($handle, 'password'); + break; + } + + return $credentials; + } + + /** + * Loop through credentials and store the missing ones + * + * @param boolean[] $credentials + * @param string[] $current + * @param string $handle + * + * @return string[] + */ + protected function gatherCredentials($credentials, $current, $handle) + { + // Loop through credentials and ask missing ones + foreach ($credentials as $credential => $required) { + $$credential = $this->getCredential($current, $credential); + if ($required && !$$credential) { + $$credential = $this->gatherCredential($handle, $credential); + } + } + + // Reform array + $credentials = compact(array_keys($credentials)); + + return $credentials; + } + + /** + * Look for a credential in the flags or ask for it + * + * @param string $handle + * @param string $credential + * @param string|null $question + * + * @return string + */ + protected function gatherCredential($handle, $credential, $question = null) + { + $question = $question ?: 'No '.$credential.' is set for ['.$handle.'], please provide one:'; + $option = $this->command->option($credential); + $method = $credential == 'password' ? 'askSecretly' : 'askWith'; + + return $option ?: $this->command->$method($question); + } + + /** + * Check if a credential needs to be filled + * + * @param string[] $credentials + * @param string $credential + * + * @return string + */ + protected function getCredential($credentials, $credential) + { + $value = Arr::get($credentials, $credential); + if (substr($value, 0, 1) === '{') { + return; + } + + return $value ?: $this->command->option($credential); + } +} diff --git a/src/Rocketeer/Services/Display/QueueExplainer.php b/src/Rocketeer/Services/Display/QueueExplainer.php new file mode 100644 index 000000000..e548a9567 --- /dev/null +++ b/src/Rocketeer/Services/Display/QueueExplainer.php @@ -0,0 +1,206 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Display; + +use Closure; +use Rocketeer\Traits\HasLocator; + +/** + * Gives some insight into what task is executing, + * what it's doing, what its parent is, etc. + * + * @author Maxime Fabre + */ +class QueueExplainer +{ + use HasLocator; + + /** + * The level at which to display statuses + * + * @type integer + */ + public $level = 0; + + /** + * Length of the longest handle to display + * + * @type integer + */ + protected $longest; + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// STATUS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Execute a task in a level below + * + * @param Closure $callback + * + * @return mixed + */ + public function displayBelow(Closure $callback) + { + if (!$this->hasCommand()) { + return $callback(); + } + + $this->level++; + $results = $callback(); + $this->level--; + + return $results; + } + + /** + * Display a status + * + * @param string|null $info + * @param string|null $details + * @param string|null $origin + * @param float|null $time + */ + public function display($info = null, $details = null, $origin = null, $time = null) + { + if (!$this->hasCommand()) { + return; + } + + // Build handle + $comment = $this->getTree(); + + // Add details + if ($info) { + $comment .= ' '.$info.''; + } + if ($details) { + $comment .= ' ('.$details.')'; + } + if ($origin) { + $comment .= ' fired by '.$origin.''; + } + if ($time) { + $comment .= ' [~'.$time.'s]'; + } + + $this->command->line($comment); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// PROGRESS ////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Format and send a message by the command + * + * @param string $message + * @param string|null $color + * + * @return string|null + */ + public function line($message, $color = null) + { + if (!$this->hasCommand()) { + return; + } + + // Format and pass to Command + $message = $color ? sprintf('%s', $color, $message, $color) : $message; + $message = $this->getTree('==').'=> '.$message; + $this->command->line($message); + + return $message; + } + + /** + * @param string $message + * + * @return string|null + */ + public function success($message) + { + return $this->line($message, 'green'); + } + + /** + * @param string $message + * + * @return string|null + */ + public function error($message) + { + return $this->line($message, 'red'); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the longest size an handle can have + * + * @return integer + */ + protected function getLongestSize() + { + if ($this->longest) { + return $this->longest; + } + + // Build possible handles + $strings = []; + $connections = (array) $this->connections->getAvailableConnections(); + $stages = (array) $this->connections->getStages(); + foreach ($connections as $connection => $servers) { + foreach ($stages as $stage) { + $strings[] = $connection.'/'.count($servers).'/'.$stage; + } + } + + // Get longest string + $strings = array_map('strlen', $strings); + $strings = $strings ? max($strings) : 0; + + // Cache value + $this->longest = $strings + 1; + + return $this->longest; + } + + /** + * @param string $dashes + * + * @return string + */ + protected function getTree($dashes = '--') + { + // Build handle + $numberConnections = count($this->connections->getAvailableConnections()); + $numberStages = count($this->connections->getStages()); + + $tree = null; + if ($numberConnections > 1 || $numberStages > 1) { + $handle = $this->connections->getHandle(); + $spacing = $this->getLongestSize() - strlen($handle); + $spacing = $spacing < 1 ? 1 : $spacing; + $spacing = str_repeat(' ', $spacing); + + // Build tree and command + $tree .= sprintf('%s%s', $handle, $spacing); + } + + // Add tree + $dashes = $this->level ? str_repeat($dashes, $this->level) : null; + $tree .= '|'.$dashes; + + return $tree; + } +} diff --git a/src/Rocketeer/Services/Display/QueueTimer.php b/src/Rocketeer/Services/Display/QueueTimer.php new file mode 100644 index 000000000..f3f2f82b3 --- /dev/null +++ b/src/Rocketeer/Services/Display/QueueTimer.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Display; + +use Closure; +use Rocketeer\Abstracts\AbstractTask; +use Rocketeer\Traits\HasLocator; + +/** + * Saves the execution time of tasks and + * predicts their future ones + * + * @author Maxime Fabre + */ +class QueueTimer +{ + use HasLocator; + + /** + * Time a task operation + * + * @param AbstractTask $task + * @param Closure $callback + * + * @return boolean|null + */ + public function time(AbstractTask $task, Closure $callback) + { + // Start timer, execute callback, close timer + $timerStart = microtime(true); + $callback(); + $time = round(microtime(true) - $timerStart, 4); + + $this->saveTaskTime($task, $time); + } + + /** + * Save the execution time of a task for future reference + * + * @param AbstractTask $task + * @param double $time + */ + public function saveTaskTime(AbstractTask $task, $time) + { + // Don't save times in pretend mode + if ($this->getOption('pretend')) { + return; + } + + // Append the new time to past ones + $past = $this->getTaskTimes($task); + $past[] = $time; + + $this->saveTaskTimes($task, $past); + } + + /** + * Compute the predicted execution time of a task + * + * @param AbstractTask $task + * + * @return double|null + */ + public function getTaskTime(AbstractTask $task) + { + $past = $this->getTaskTimes($task); + if (!$past) { + return; + } + + // Compute average time + $average = array_sum($past) / count($past); + $average = round($average, 2); + + return $average; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////// SETTERS/GETTERS /////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * @param AbstractTask $task + * + * @return array + */ + protected function getTaskTimes(AbstractTask $task) + { + $handle = sprintf('times.%s', $task->getSlug()); + $past = $this->localStorage->get($handle, []); + + return $past; + } + + /** + * @param AbstractTask $task + * @param double[] $past + */ + protected function saveTaskTimes(AbstractTask $task, array $past) + { + $handle = sprintf('times.%s', $task->getSlug()); + $this->localStorage->set($handle, $past); + } +} diff --git a/src/Rocketeer/Services/History/History.php b/src/Rocketeer/Services/History/History.php new file mode 100644 index 000000000..4ec34f490 --- /dev/null +++ b/src/Rocketeer/Services/History/History.php @@ -0,0 +1,58 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\History; + +use Illuminate\Support\Collection; + +class History extends Collection +{ + /** + * Get the history, flattened + * + * @return string[]|string[][] + */ + public function getFlattenedHistory() + { + return $this->getFlattened('history'); + } + + /** + * Get the output, flattened + * + * @return string[]|string[][] + */ + public function getFlattenedOutput() + { + return $this->getFlattened('output'); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get a flattened list of a certain type + * + * @param string $type + * + * @return string[]|string[][] + */ + protected function getFlattened($type) + { + $history = []; + foreach ($this->items as $class => $entries) { + $history = array_merge($history, $entries[$type]); + } + + ksort($history); + + return array_values($history); + } +} diff --git a/src/Rocketeer/Services/History/LogsHandler.php b/src/Rocketeer/Services/History/LogsHandler.php new file mode 100644 index 000000000..e2ffcafd0 --- /dev/null +++ b/src/Rocketeer/Services/History/LogsHandler.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\History; + +use Illuminate\Support\Arr; +use Rocketeer\Traits\HasLocator; + +/** + * Handles rotation of logs + */ +class LogsHandler +{ + use HasLocator; + + /** + * Cache of the logs file to be written + * + * @type array + */ + protected $logs = []; + + /** + * The closure used to name logs + * + * @type \Closure + */ + protected $namer; + + /** + * Save something for the logs + * + * @param string $string + */ + public function log($string) + { + // Create entry in the logs + $file = $this->getCurrentLogsFile(); + if (!isset($this->logs[$file])) { + $this->logs[$file] = []; + } + + $this->logs[$file][] = $string; + } + + /** + * Write the stored logs + * + * @return array + */ + public function write() + { + foreach ($this->logs as $file => $entries) { + $entries = Arr::flatten($entries); + if (!$this->files->exists($file)) { + $this->createLogsFile($file); + } + + $this->files->put($file, implode(PHP_EOL, $entries)); + } + + return array_keys($this->logs); + } + + /** + * Get the logs file being currently used + * + * @return string|false + */ + public function getCurrentLogsFile() + { + if (!$this->namer) { + $this->namer = $this->config->get('rocketeer::logs'); + } + + // Cancel if invalid namer + if (!$this->namer || !is_callable($this->namer)) { + return false; + } + + $namer = $this->namer; + $file = $namer($this->connections); + $file = $this->app['path.rocketeer.logs'].'/'.$file; + + return $file; + } + + /** + * Create a logs file if it doesn't exist + * + * @param string $file + */ + protected function createLogsFile($file) + { + $directory = dirname($file); + + // Create directory + if (!is_dir($directory)) { + $this->files->makeDirectory($directory, 0777, true); + } + + // Create file + if (!file_exists($file)) { + $this->files->put($file, ''); + } + } +} diff --git a/src/Rocketeer/Services/Ignition/Configuration.php b/src/Rocketeer/Services/Ignition/Configuration.php new file mode 100644 index 000000000..be2d8fd28 --- /dev/null +++ b/src/Rocketeer/Services/Ignition/Configuration.php @@ -0,0 +1,285 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ +namespace Rocketeer\Services\Ignition; + +use Closure; +use Illuminate\Support\Arr; +use Rocketeer\Facades; +use Rocketeer\Traits\HasLocator; +use Symfony\Component\Finder\Finder; +use Symfony\Component\Finder\SplFileInfo; + +/** + * Ignites Rocketeer's custom configuration, tasks, events and paths + * depending on what Rocketeer is used on + * + * @author Maxime Fabre + */ +class Configuration +{ + use HasLocator; + + /** + * Bind paths to the container + * + * @return void + */ + public function bindPaths() + { + $this->bindBase(); + $this->bindConfiguration(); + } + + ////////////////////////////////////////////////////////////////////// + ///////////////////////// USER CONFIGURATION ///////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Load the custom files (tasks, events, ...) + */ + public function loadUserConfiguration() + { + $fileLoaders = function () { + $this->loadFileOrFolder('tasks'); + $this->loadFileOrFolder('events'); + }; + + // Defer loading of tasks and events or not + if (is_a($this->app, 'Illuminate\Foundation\Application')) { + $this->app->booted($fileLoaders); + } else { + $fileLoaders(); + } + + // Load plugins + $plugins = (array) $this->config->get('rocketeer::plugins'); + $plugins = array_filter($plugins, 'class_exists'); + foreach ($plugins as $plugin) { + $this->tasks->plugin($plugin); + } + + // Merge contextual configurations + $this->mergeContextualConfigurations(); + $this->mergePluginsConfiguration(); + } + + /** + * Merge the various contextual configurations defined in userland + */ + public function mergeContextualConfigurations() + { + $this->mergeConfigurationFolders(['stages', 'connections'], function (SplFileInfo $file) { + return $this->computeHandleFromPath($file); + }, 'config.php'); + } + + /** + * Merge the plugin configurations defined in userland + */ + public function mergePluginsConfiguration() + { + $this->mergeConfigurationFolders(['plugins'], function (SplFileInfo $file) { + $handle = basename(dirname($file->getPathname())); + $handle .= '::'.$file->getBasename('.php'); + + return $handle; + }); + } + + /** + * Export the configuration files + * + * @return string + */ + public function exportConfiguration() + { + $source = $this->paths->unifyLocalSlashes(__DIR__.'/../../../config'); + $source = realpath($source); + $destination = $this->paths->getConfigurationPath(); + + // Unzip configuration files + $this->files->copyDirectory($source, $destination); + + return $destination; + } + + //////////////////////////////////////////////////////////////////// + ///////////////////////////// CONFIGURATION //////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Replace placeholders in configuration + * + * @param string $folder + * @param string[] $values + */ + public function updateConfiguration($folder, array $values = array()) + { + // Replace stub values in files + $files = $this->files->files($folder); + foreach ($files as $file) { + foreach ($values as $name => $value) { + $contents = str_replace('{'.$name.'}', $value, file_get_contents($file)); + $this->files->put($file, $contents); + } + } + + // Change repository in use + $application = Arr::get($values, 'application_name'); + $this->localStorage->setFile($application); + } + + /** + * Merge configuration files from userland + * + * @param array $folders + * @param callable $computeHandle + * @param string|null $exclude + */ + protected function mergeConfigurationFolders(array $folders, Closure $computeHandle, $exclude = null) + { + // Cancel if not ignited yet + $configuration = $this->app['path.rocketeer.config']; + if (!is_dir($configuration)) { + return; + } + + // Cancel if the subfolders don't exist + $existing = array_filter($folders, function ($path) use ($configuration) { + return is_dir($configuration.DS.$path); + }); + if (!$existing) { + return; + } + + // Get folders to glob + $folders = $this->paths->unifyLocalSlashes($configuration.'/{'.implode(',', $folders).'}/*'); + + // Gather custom files + $finder = new Finder(); + $finder = $finder->in($folders); + if ($exclude) { + $finder = $finder->notName($exclude); + } + + // Bind their contents to the "on" array + $files = $finder->files(); + foreach ($files as $file) { + $contents = include $file->getPathname(); + $handle = $computeHandle($file); + + $this->config->set($handle, $contents); + } + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// PATHS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Bind the base path to the Container + */ + protected function bindBase() + { + if ($this->app->bound('path.base')) { + return; + } + + $this->app->instance('path.base', getcwd()); + } + + /** + * Bind paths to the configuration files + */ + protected function bindConfiguration() + { + // Bind path to the configuration directory + if ($this->isInsideLaravel()) { + $path = $this->paths->getConfigurationPath(); + $storage = $this->paths->getStoragePath(); + } else { + $path = $this->paths->getBasePath().'.rocketeer'; + + $storage = $path; + } + + // Build paths + $paths = array( + 'config' => $path.'', + 'events' => $path.DS.'events', + 'plugins' => $path.DS.'plugins', + 'tasks' => $path.DS.'tasks', + 'logs' => $storage.DS.'logs', + ); + + foreach ($paths as $key => $file) { + + // Check whether we provided a file or folder + if (!is_dir($file) && file_exists($file.'.php')) { + $file .= '.php'; + } + + // Use configuration in current folder if none found + $realpath = realpath('.').DS.basename($file); + if (!file_exists($file) && file_exists($realpath)) { + $file = $realpath; + } + + $this->app->instance('path.rocketeer.'.$key, $file); + } + } + + //////////////////////////////////////////////////////////////////// + /////////////////////////////// HELPERS //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Computes which configuration handle a config file should bind to + * + * @param SplFileInfo $file + * + * @return string + */ + protected function computeHandleFromPath(SplFileInfo $file) + { + // Get realpath + $handle = $file->getRealpath(); + + // Format appropriately + $handle = str_replace($this->app['path.rocketeer.config'].DS, null, $handle); + $handle = str_replace('.php', null, $handle); + $handle = str_replace(DS, '.', $handle); + + return sprintf('rocketeer::on.%s', $handle); + } + + /** + * Load a file or its contents if a folder + * + * @param string $handle + */ + protected function loadFileOrFolder($handle) + { + // Bind ourselves into the facade to avoid automatic resolution + Facades\Rocketeer::setFacadeApplication($this->app); + + // If we have one unified tasks file, include it + $file = $this->app['path.rocketeer.'.$handle]; + if (!is_dir($file) && file_exists($file)) { + include $file; + } // Else include its contents + elseif (is_dir($file)) { + $folder = glob($file.DS.'*.php'); + foreach ($folder as $file) { + include $file; + } + } + } +} diff --git a/src/Rocketeer/Services/Ignition/Plugins.php b/src/Rocketeer/Services/Ignition/Plugins.php new file mode 100644 index 000000000..e81d352b1 --- /dev/null +++ b/src/Rocketeer/Services/Ignition/Plugins.php @@ -0,0 +1,101 @@ + +* +* For the full copyright and license information, please view the LICENSE +* file that was distributed with this source code. +*/ +namespace Rocketeer\Services\Ignition; + +use Illuminate\Support\Arr; +use Rocketeer\Traits\HasLocator; + +/** + * Publishes the plugin's configurations in user-land + * + * @author Maxime Fabre + */ +class Plugins +{ + use HasLocator; + + /** + * Publishes a package's configuration + * + * @param string $package + * + * @return boolean|null + */ + public function publish($package) + { + if ($this->isInsideLaravel()) { + return $this->publishLaravelConfiguration($package); + } + + // Find the plugin's configuration + $paths = array( + $this->app['path.base'].'/vendor/%s/src/config', + $this->app['path.base'].'/vendor/%s/config', + $this->paths->getHomeFolder().'/.composer/vendor/%s/src/config', + $this->paths->getHomeFolder().'/.composer/vendor/%s/config', + ); + + // Check for the first configuration path that exists + $paths = array_filter($paths, function ($path) use ($package) { + return $this->files->isDirectory(sprintf($path, $package)); + }); + $paths = array_values($paths); + + // Cancel if no valid paths + if (empty($paths)) { + return $this->command->error('No configuration found for '.$package); + } + + return $this->publishConfiguration($paths[0]); + } + + /** + * Publishes a configuration within a Laravel application + * + * @param string $package + * + * @return boolean + */ + protected function publishLaravelConfiguration($package) + { + // Publish initial configuration + $this->artisan->call('config:publish', ['package' => $package]); + + // Move under Rocketeer namespace + $path = $this->app['path'].'/config/packages/'.$package; + $destination = preg_replace('/packages\/([^\/]+)/', 'packages/rocketeers', $path); + + return $this->files->move($path, $destination); + } + + /** + * Publishes a configuration within a classic application + * + * @param string $path + * + * @return boolean + */ + protected function publishConfiguration($path) + { + // Get the vendor and package + preg_match('/vendor\/([^\/]+)\/([^\/]+)/', $path, $handle); + $handle = (array) $handle; + $package = Arr::get($handle, 2); + + // Compute and create the destination foldser + $destination = $this->app['path.rocketeer.config']; + $destination = $destination.'/plugins/rocketeers/'.$package; + if (!$this->files->isDirectory($destination)) { + $this->files->makeDirectory($destination, 0755, true); + } + + return $this->files->copyDirectory($path, $destination); + } +} diff --git a/src/Rocketeer/Services/Pathfinder.php b/src/Rocketeer/Services/Pathfinder.php new file mode 100644 index 000000000..03b1b26c7 --- /dev/null +++ b/src/Rocketeer/Services/Pathfinder.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services; + +use Exception; +use Illuminate\Support\Str; +use Rocketeer\Traits\HasLocator; + +/** + * Locates folders and paths on the server and locally + * + * @author Maxime Fabre + */ +class Pathfinder +{ + use HasLocator; + + ////////////////////////////////////////////////////////////////////// + //////////////////////////////// LOCAL /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get a configured path + * + * @param string $path + * + * @return string + */ + public function getPath($path) + { + return $this->rocketeer->getOption('paths.'.$path); + } + + /** + * Get the path to the root folder of the application + * + * @return string + */ + public function getHomeFolder() + { + $rootDirectory = $this->rocketeer->getOption('remote.root_directory'); + $rootDirectory = Str::finish($rootDirectory, '/'); + $appDirectory = $this->rocketeer->getOption('remote.app_directory') ?: $this->rocketeer->getApplicationName(); + + return $rootDirectory.$appDirectory; + } + + /** + * Get the default path for the SSH key + * + * @return string + * @throws Exception + */ + public function getDefaultKeyPath() + { + return $this->getUserHomeFolder().'/.ssh/id_rsa'; + } + + /** + * Get the path to the Rocketeer config folder in the users home + * + * @return string + */ + public function getRocketeerConfigFolder() + { + return $this->getUserHomeFolder().'/.rocketeer'; + } + + /** + * Get the path to the users home folder + * + * @throws Exception + * @return string + */ + public static function getUserHomeFolder() + { + // Get home folder if available (Unix) + if (!empty($_SERVER['HOME'])) { + return $_SERVER['HOME']; + // Else use the home drive (Windows) + } elseif (!empty($_SERVER['HOMEDRIVE']) && !empty($_SERVER['HOMEPATH'])) { + return $_SERVER['HOMEDRIVE'].$_SERVER['HOMEPATH']; + } else { + throw new Exception('Cannot determine user home directory.'); + } + } + + /** + * Get the base path + * + * @return string + */ + public function getBasePath() + { + $base = $this->app['path.base'] ? $this->app['path.base'].'/' : ''; + $base = $this->unifySlashes($base); + + return $base; + } + + /** + * Get the path to the configuration folder + * + * @return string + */ + public function getConfigurationPath() + { + // Return path to Laravel configuration + if ($this->isInsideLaravel()) { + $configuration = $this->app['path'].'/config/packages/anahkiasen/rocketeer'; + } else { + $configuration = $this->app['path.rocketeer.config']; + } + + return $this->unifyLocalSlashes($configuration); + } + + /** + * Get path to the storage folder + * + * @return string + */ + public function getStoragePath() + { + // If no path is bound, default to the Rocketeer folder + if (!$this->app->bound('path.storage')) { + return '.rocketeer'; + } + + // Unify slashes + $storage = $this->app['path.storage']; + $storage = $this->unifySlashes($storage); + $storage = str_replace($this->getBasePath(), null, $storage); + + return $storage; + } + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// SERVER /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the path to a folder, taking into account application name and stage + * + * @param string|null $folder + * + * @return string + */ + public function getFolder($folder = null) + { + $folder = $this->replacePatterns($folder); + + $base = $this->getHomeFolder().'/'; + $stage = $this->connections->getStage(); + if ($folder && $stage) { + $base .= $stage.'/'; + } + $folder = str_replace($base, null, $folder); + + return $base.$folder; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Unify the slashes to the UNIX mode (forward slashes) + * + * @param string $path + * + * @return string + */ + public function unifySlashes($path) + { + return str_replace('\\', '/', $path); + } + + /** + * Unify paths to the local DS + * + * @param string $path + * + * @return string + */ + public function unifyLocalSlashes($path) + { + return preg_replace('#(/|\\\)#', DS, $path); + } + + /** + * Replace patterns in a folder path + * + * @param string $path + * + * @return string + */ + public function replacePatterns($path) + { + $base = $this->getBasePath(); + + // Replace folder patterns + return preg_replace_callback('/\{[a-z\.]+\}/', function ($match) use ($base) { + $folder = substr($match[0], 1, -1); + + // Replace paths from the container + if ($this->app->bound($folder)) { + $path = $this->app->make($folder); + + return str_replace($base, null, $this->unifySlashes($path)); + } + + return false; + }, $path); + } +} diff --git a/src/Rocketeer/ReleasesManager.php b/src/Rocketeer/Services/ReleasesManager.php similarity index 55% rename from src/Rocketeer/ReleasesManager.php rename to src/Rocketeer/Services/ReleasesManager.php index aa34406c0..322237d49 100644 --- a/src/Rocketeer/ReleasesManager.php +++ b/src/Rocketeer/Services/ReleasesManager.php @@ -7,9 +7,12 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer; +namespace Rocketeer\Services; use Illuminate\Container\Container; +use Illuminate\Support\Arr; +use Rocketeer\Services\Storages\ServerStorage; +use Rocketeer\Traits\HasLocator; /** * Provides informations and actions around releases @@ -18,12 +21,7 @@ */ class ReleasesManager { - /** - * The IoC Container - * - * @var Container - */ - protected $app; + use HasLocator; /** * Cache of the validation file @@ -32,6 +30,27 @@ class ReleasesManager */ protected $state = array(); + /** + * Cache of the releases + * + * @type array + */ + public $releases; + + /** + * The next release to come + * + * @type string + */ + protected $nextRelease; + + /** + * The storage + * + * @type ServerStorage + */ + protected $storage; + /** * Build a new ReleasesManager * @@ -39,8 +58,9 @@ class ReleasesManager */ public function __construct(Container $app) { - $this->app = $app; - $this->state = $this->getValidationFile(); + $this->app = $app; + $this->storage = new ServerStorage($app, 'state'); + $this->state = $this->getValidationFile(); } //////////////////////////////////////////////////////////////////// @@ -50,56 +70,65 @@ public function __construct(Container $app) /** * Get all the releases on the server * - * @return array + * @return integer[] */ public function getReleases() { // Get releases on server - $releases = $this->app['rocketeer.bash']->listContents($this->getReleasesPath()); - if (is_array($releases)) { + if (is_null($this->releases)) { + $releases = $this->getReleasesPath(); + $releases = (array) $this->bash->listContents($releases); + + // Filter and sort releases + $releases = array_filter($releases, function ($release) { + return $this->isRelease($release); + }); + rsort($releases); + + $this->releases = (array) $releases; } - return $releases; + return $this->releases; } /** - * Get an array of deprecated releases + * Get an array of non-current releases * - * @return array + * @return integer[] */ - public function getDeprecatedReleases() + public function getNonCurrentReleases() { - $releases = (array) $this->getReleases(); - $maxReleases = $this->app['config']->get('rocketeer::remote.keep_releases'); - - return array_slice($releases, $maxReleases); + return $this->getDeprecatedReleases(1); } /** - * Get an array of invalid releases + * Get an array of deprecated releases * - * @return array + * @param integer|null $treshold + * + * @return integer[] */ - public function getInvalidReleases() + public function getDeprecatedReleases($treshold = null) { - $releases = (array) $this->getReleases(); - $invalid = array_diff($this->state, array_filter($this->state)); - $invalid = array_keys($invalid); + $releases = $this->getReleases(); + $treshold = $treshold ?: $this->config->get('rocketeer::remote.keep_releases'); - return array_intersect($releases, $invalid); + return array_slice($releases, $treshold); } /** - * Get an array of non-current releases + * Get an array of invalid releases * - * @return array + * @return integer[] */ - public function getNonCurrentReleases() + public function getInvalidReleases() { - $releases = (array) $this->getReleases(); + $releases = $this->getReleases(); + $invalid = array_diff($this->state, array_filter($this->state)); + $invalid = array_keys($invalid); - return array_slice($releases, 1); + return array_intersect($releases, $invalid); } //////////////////////////////////////////////////////////////////// @@ -113,25 +142,25 @@ public function getNonCurrentReleases() */ public function getReleasesPath() { - return $this->app['rocketeer.rocketeer']->getFolder('releases'); + return $this->paths->getFolder('releases'); } /** * Get the path to a release * - * @param integer $release + * @param string $release * * @return string */ public function getPathToRelease($release) { - return $this->app['rocketeer.rocketeer']->getFolder('releases/'.$release); + return $this->paths->getFolder('releases/'.$release); } /** * Get the path to the current release * - * @param string $folder A folder in the release + * @param string|null $folder A folder in the release * * @return string */ @@ -155,13 +184,10 @@ public function getCurrentReleasePath($folder = null) */ public function getValidationFile() { - // Get the contents of the validation file - $file = $this->app['rocketeer.rocketeer']->getFolder('state.json'); - $file = $this->app['rocketeer.bash']->getFile($file) ?: '{}'; - $file = (array) json_decode($file, true); + $file = $this->storage->get(); // Fill the missing releases - $releases = (array) $this->getReleases(); + $releases = $this->getReleases(); $releases = array_fill_keys($releases, false); // Sort entries @@ -175,34 +201,20 @@ public function getValidationFile() return $releases; } - /** - * Update the contents of the validation file - * - * @param array $validation - * - * @return void - */ - public function saveValidationFile(array $validation) - { - $file = $this->app['rocketeer.rocketeer']->getFolder('state.json'); - $this->app['rocketeer.bash']->putFile($file, json_encode($validation)); - - $this->state = $validation; - } - /** * Mark a release as valid * - * @param integer $release - * - * @return void + * @param string|null $release */ - public function markReleaseAsValid($release) + public function markReleaseAsValid($release = null) { - $validation = $this->getValidationFile(); - $validation[$release] = true; + $release = $release ?: $this->getCurrentRelease(); - return $this->saveValidationFile($validation); + // If the release is not null, mark it as valid + if ($release) { + $this->state[$release] = true; + $this->storage->set($release, true); + } } /** @@ -214,69 +226,30 @@ public function markReleaseAsValid($release) */ public function checkReleaseState($release) { - return array_get($this->state, $release, true); + return Arr::get($this->state, $release, true); } //////////////////////////////////////////////////////////////////// /////////////////////////// CURRENT RELEASE //////////////////////// //////////////////////////////////////////////////////////////////// - /** - * Sanitize a possible release - * - * @param string $release - * - * @return string - */ - protected function sanitizeRelease($release) - { - return strlen($release) === 14 ? $release : null; - } - - /** - * Get where to store the current release - * - * @return string - */ - protected function getCurrentReleaseKey() - { - $key = 'current_release'; - - // Get the scopes - $connection = $this->app['rocketeer.rocketeer']->getConnection(); - $stage = $this->app['rocketeer.rocketeer']->getStage(); - $scopes = array($connection, $stage); - foreach ($scopes as $scope) { - $key .= $scope ? '.'.$scope : ''; - } - - return $key; - } - /** * Get the current release * - * @return string + * @return string|integer|null */ public function getCurrentRelease() { - // If we have saved the last deployed release, return that - $cached = $this->app['rocketeer.server']->getValue($this->getCurrentReleaseKey()); - if ($cached) { - return $this->sanitizeRelease($cached); - } - - // Else get and save last deployed release - $lastDeployed = array_get($this->getReleases(), 0); - $this->updateCurrentRelease($lastDeployed); + $current = Arr::get($this->getReleases(), 0); + $current = $this->sanitizeRelease($current); - return $this->sanitizeRelease($lastDeployed); + return $this->nextRelease ?: $current; } /** * Get the release before the current one * - * @param string $release A release name + * @param string|null $release A release name * * @return string */ @@ -288,30 +261,67 @@ public function getPreviousRelease($release = null) // Get the one before that, or default to current $key = array_search($current, $releases); + $key = !is_int($key) ? -1 : $key; $next = 1; do { - $release = array_get($releases, $key + $next); + $release = Arr::get($releases, $key + $next); $next++; - } while (!$this->checkReleaseState($release)); + } while (!$this->checkReleaseState($release) && isset($this->state[$release])); return $release ?: $current; } /** - * Update the current release + * Get the next release to come * - * @param string $release A release name - * - * @return void + * @return string */ - public function updateCurrentRelease($release = null) + public function getNextRelease() { - if (!$release) { - $release = $this->app['rocketeer.bash']->getTimestamp(); + if (!$this->nextRelease) { + $this->nextRelease = $this->bash->getTimestamp(); } - $this->app['rocketeer.server']->setValue($this->getCurrentReleaseKey(), $release); + return $this->nextRelease; + } + + /** + * Change the release to come + * + * @param string $release + */ + public function setNextRelease($release) + { + $this->nextRelease = $release; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Sanitize a possible release + * + * @param string|integer $release + * + * @return string|integer|null + */ + protected function sanitizeRelease($release) + { + return $this->isRelease($release) ? $release : null; + } + + /** + * Check if it quacks like a duck + * + * @param string|integer $release + * + * @return bool + */ + protected function isRelease($release) + { + $release = (string) $release; - return $release; + return (bool) preg_match('#[0-9]{14}#', $release); } } diff --git a/src/Rocketeer/Services/StepsBuilder.php b/src/Rocketeer/Services/StepsBuilder.php new file mode 100644 index 000000000..e670505bb --- /dev/null +++ b/src/Rocketeer/Services/StepsBuilder.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services; + +/** + * Saves in an array methods the call signatures + * of the methods called on it + * + * @author Maxime Fabre + */ +class StepsBuilder +{ + /** + * The extisting steps + * + * @type array + */ + protected $steps = []; + + /** + * Add a step + * + * @param string $name + * @param array $arguments + */ + public function __call($name, $arguments) + { + $this->steps[] = [$name, $arguments]; + } + + /** + * Get and clear the steps + * + * @return array + */ + public function pullSteps() + { + $steps = $this->steps; + + $this->steps = []; + + return $steps; + } + + /** + * Get the steps to execute + * + * @return array + */ + public function getSteps() + { + return $this->steps; + } +} diff --git a/src/Rocketeer/Services/Storages/LocalStorage.php b/src/Rocketeer/Services/Storages/LocalStorage.php new file mode 100644 index 000000000..cfeb4ff8b --- /dev/null +++ b/src/Rocketeer/Services/Storages/LocalStorage.php @@ -0,0 +1,248 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Storages; + +use Exception; +use Illuminate\Container\Container; +use Rocketeer\Abstracts\AbstractStorage; +use Rocketeer\Interfaces\StorageInterface; + +/** + * Provides and persists informations in local + * + * @author Maxime Fabre + */ +class LocalStorage extends AbstractStorage implements StorageInterface +{ + /** + * The current hash in use + * + * @var string + */ + protected $hash; + + /** + * The folder where file resides + * + * @type string + */ + protected $folder; + + /** + * Build a new LocalStorage + * + * @param Container $app + * @param string $file + * @param string|null $folder + */ + public function __construct(Container $app, $file = 'deployments', $folder = null) + { + parent::__construct($app, $file); + + // Create personal storage if necessary + if (!$this->app->bound('path.storage')) { + $folder = $this->paths->getRocketeerConfigFolder(); + $this->files->makeDirectory($folder, 0755, false, true); + } + + // Set path to storage folder + $this->folder = $folder ?: $this->app['path.storage'].DS.'meta'; + + // Flush if necessary + if ($this->shouldFlush()) { + $this->destroy(); + } + + $this->set('hash', $this->getHash()); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// SALTS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the current salt in use + * + * @return string + */ + public function getHash() + { + // Return cached hash if any + if ($this->hash) { + return $this->hash; + } + + // Get the contents of the configuration folder + $salt = ''; + $folder = $this->paths->getConfigurationPath(); + $files = $this->files->glob($folder.'/*.php'); + + // Remove custom files and folders + $handles = array('events', 'tasks'); + foreach ($handles as $handle) { + $path = $this->app['path.rocketeer.'.$handle]; + $index = array_search($path, $files); + if ($index !== false) { + unset($files[$index]); + } + } + + // Compute the salts + foreach ($files as $file) { + $file = $this->files->getRequire($file); + $salt .= json_encode($file); + } + + // Cache it + $this->hash = md5($salt); + + return $this->hash; + } + + /** + * Flushes the repository if required + * + * @return boolean + */ + public function shouldFlush() + { + $currentHash = $this->get('hash'); + + return $currentHash && $currentHash !== $this->getHash(); + } + + //////////////////////////////////////////////////////////////////// + /////////////////////////// REMOTE VARIABLES /////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the directory separators on the remove server + * + * @return string + */ + public function getSeparator() + { + // If manually set by the user, return it + $user = $this->rocketeer->getOption('remote.variables.directory_separator'); + if ($user) { + return $user; + } + + return $this->get('directory_separator', function () { + $separator = $this->bash->runLast('php -r "echo DIRECTORY_SEPARATOR;"'); + + // Throw an Exception if we receive invalid output + if (strlen($separator) > 1) { + throw new Exception( + 'An error occured while fetching the directory separators used on the server.'.PHP_EOL. + 'Output received was : '.$separator + ); + } + + // Cache separator + $this->set('directory_separator', $separator); + + return $separator; + }); + } + + /** + * Get the remote line endings on the remove server + * + * @return string + */ + public function getLineEndings() + { + // If manually set by the user, return it + $user = $this->rocketeer->getOption('remote.variables.line_endings'); + if ($user) { + return $user; + } + + return $this->get('line_endings', function () { + $endings = $this->bash->runRaw('php -r "echo PHP_EOL;"'); + $this->set('line_endings', $endings); + + return $endings ?: PHP_EOL; + }); + } + + /** + * Change the folder in use + * + * @param string $folder + */ + public function setFolder($folder) + { + $this->folder = $folder; + } + + /** + * @return string + */ + public function getFolder() + { + return $this->folder; + } + + //////////////////////////////////////////////////////////////////// + ////////////////////////// REPOSITORY FILE ///////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the full path to the file + * + * @return string + */ + public function getFilepath() + { + return $this->folder.'/'.$this->file.'.json'; + } + + /** + * Get the contents of a file + * + * @return array + */ + protected function getContents() + { + // Cancel if the file doesn't exist + if (!$this->files->exists($this->getFilepath())) { + return []; + } + + // Get and parse file + $contents = $this->files->get($this->getFilepath()); + $contents = json_decode($contents, true); + + return $contents; + } + + /** + * Save the contents of a file + * + * @param array $contents + */ + protected function saveContents($contents) + { + // Yup. Don't look at me like that. + @$this->files->put($this->getFilepath(), json_encode($contents)); + } + + /** + * Destroy the file + * + * @return boolean + */ + public function destroy() + { + return $this->files->delete($this->getFilepath()); + } +} diff --git a/src/Rocketeer/Services/Storages/ServerStorage.php b/src/Rocketeer/Services/Storages/ServerStorage.php new file mode 100644 index 000000000..632044fb6 --- /dev/null +++ b/src/Rocketeer/Services/Storages/ServerStorage.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Storages; + +use Rocketeer\Abstracts\AbstractStorage; +use Rocketeer\Interfaces\StorageInterface; + +/** + * Provides and persists informations on the server + * + * @author Maxime Fabre + */ +class ServerStorage extends AbstractStorage implements StorageInterface +{ + /** + * Destroy the file + * + * @return boolean + */ + public function destroy() + { + $this->bash->removeFolder($this->getFilepath()); + + return true; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the full path to the file + * + * @return string + */ + public function getFilepath() + { + return $this->paths->getFolder($this->file.'.json'); + } + + /** + * Get the contents of the file + * + * @return array + */ + protected function getContents() + { + $file = $this->getFilepath(); + $file = $this->bash->getFile($file) ?: '{}'; + $file = (array) json_decode($file, true); + + return $file; + } + + /** + * Save the contents of the file + * + * @param array $contents + */ + protected function saveContents($contents) + { + $file = $this->getFilepath(); + $this->bash->putFile($file, json_encode($contents)); + } +} diff --git a/src/Rocketeer/Services/Tasks/Job.php b/src/Rocketeer/Services/Tasks/Job.php new file mode 100644 index 000000000..488bf9e21 --- /dev/null +++ b/src/Rocketeer/Services/Tasks/Job.php @@ -0,0 +1,26 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Tasks; + +use Illuminate\Support\Fluent; + +/** + * A job storing where a task/multiple tasks need to be executed + * + * @property string connection + * @property integer server + * @property string|null stage + * @property \Rocketeer\Abstracts\AbstractTask[] queue + * @author Maxime Fabre + */ +class Job extends Fluent +{ + // ... +} diff --git a/src/Rocketeer/Services/Tasks/Pipeline.php b/src/Rocketeer/Services/Tasks/Pipeline.php new file mode 100644 index 000000000..0176a9d89 --- /dev/null +++ b/src/Rocketeer/Services/Tasks/Pipeline.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Tasks; + +use Illuminate\Support\Collection; + +/** + * A class representing a pipeline of jobs + * to be executed + * + * @author Maxime Fabre + */ +class Pipeline extends Collection +{ + /** + * The stored results of each task + * + * @type array + */ + protected $results = []; + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// RESULTS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Check if the pipeline failed + * + * @return boolean + */ + public function failed() + { + $succeeded = count(array_filter($this->results)); + + return $succeeded != $this->count(); + } + + /** + * Check if the pipeline ran its course + * + * @return boolean + */ + public function succeeded() + { + return !$this->failed(); + } + + /** + * @return array + */ + public function getResults() + { + return $this->results; + } + + /** + * @param array $results + */ + public function setResults($results) + { + $this->results = $results; + } +} diff --git a/src/Rocketeer/Services/Tasks/TasksBuilder.php b/src/Rocketeer/Services/Tasks/TasksBuilder.php new file mode 100644 index 000000000..66bf9a826 --- /dev/null +++ b/src/Rocketeer/Services/Tasks/TasksBuilder.php @@ -0,0 +1,339 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Tasks; + +use Closure; +use Illuminate\Support\Str; +use Rocketeer\Abstracts\AbstractTask; +use Rocketeer\Binaries\AnonymousBinary; +use Rocketeer\Exceptions\TaskCompositionException; +use Rocketeer\Traits\HasLocator; + +/** + * Handles creating tasks from strings, closures, AbstractTask children, etc. + * + * @author Maxime Fabre + */ +class TasksBuilder +{ + use HasLocator; + + /** + * Build a binary + * + * @param string $binary + * + * @return \Rocketeer\Abstracts\AbstractBinary|\Rocketeer\Abstracts\AbstractPackageManager + */ + public function buildBinary($binary) + { + $class = $this->findQualifiedName($binary, array( + 'Rocketeer\Binaries\PackageManagers\%s', + 'Rocketeer\Binaries\%s', + )); + + // If there is a class by that name + if ($class) { + return new $class($this->app); + } + + // Else wrap the command in an AnonymousBinary + $anonymous = new AnonymousBinary($this->app); + $anonymous->setBinary($binary); + + return $anonymous; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// COMMANDS ////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Build the command bound to a task + * + * @param string|AbstractTask $task + * @param string|null $slug + * + * @return \Rocketeer\Abstracts\AbstractCommand + */ + public function buildCommand($task, $slug = null) + { + // Build the task instance + try { + $instance = $this->buildTask($task); + } catch (TaskCompositionException $exception) { + $instance = null; + } + + // Get the command name + $name = $instance ? $instance->getName() : null; + $name = is_string($task) ? $task : $name; + $command = $this->findQualifiedName($name, array( + 'Rocketeer\Console\Commands\%sCommand', + 'Rocketeer\Console\Commands\BaseTaskCommand', + )); + + $command = new $command($instance, $slug); + $command->setLaravel($this->app); + + return $command; + } + + ////////////////////////////////////////////////////////////////////// + ///////////////////////////// STRATEGIES ///////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Build a strategy + * + * @param string $strategy + * @param string|null $concrete + * + * @return \Rocketeer\Abstracts\Strategies\AbstractStrategy + */ + public function buildStrategy($strategy, $concrete = null) + { + // If we passed a concrete implementation + // build it, otherwise get the bound one + $handle = strtolower($strategy); + if ($concrete) { + $concrete = $this->findQualifiedName($concrete, array( + 'Rocketeer\Strategies\\'.ucfirst($strategy).'\%sStrategy', + )); + if (!$concrete) { + return false; + } + + return new $concrete($this->app); + } + + return $this->app['rocketeer.strategies.'.$handle]; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// TASKS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Build an array of tasks + * + * @param array $tasks + * + * @return array + */ + public function buildTasks(array $tasks) + { + return array_map([$this, 'buildTask'], $tasks); + } + + /** + * Build a task from anything + * + * @param string|Closure|AbstractTask $task + * @param string|null $name + * @param string|null $description + * + * @throws \Rocketeer\Exceptions\TaskCompositionException + * @return AbstractTask + */ + public function buildTask($task, $name = null, $description = null) + { + // Compose the task from their various types + $task = $this->composeTask($task); + + // If the built class is invalid, cancel + if (!$task instanceof AbstractTask) { + throw new TaskCompositionException('Class '.get_class($task).' is not a valid task'); + } + + // Set task properties + $task->setName($name); + $task->setDescription($description); + + return $task; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// COMPOSING ///////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Compose a Task from its various types + * + * @param string|Closure|AbstractTask $task + * + * @return mixed|AbstractTask + * @throws \Rocketeer\Exceptions\TaskCompositionException + */ + protected function composeTask($task) + { + // If already built, return it + if ($task instanceof AbstractTask) { + return $task; + } + + // If we provided a Closure, build a ClosureTask + if ($task instanceof Closure) { + return $this->buildTaskFromClosure($task); + } + + // If we passed a task handle, return it + if ($handle = $this->getTaskHandle($task)) { + return $this->app[$handle]; + } + + // If we passed a command, build a ClosureTask + if (is_array($task) || $this->isStringCommand($task)) { + return $this->buildTaskFromString($task); + } + + // Else it's a class name, get the appropriated task + if (!$task instanceof AbstractTask) { + return $this->buildTaskFromClass($task); + } + } + + /** + * Build a task from a string + * + * @param string|string[] $task + * + * @return AbstractTask + */ + public function buildTaskFromString($task) + { + $stringTask = $task; + $closure = function (AbstractTask $task) use ($stringTask) { + return $task->runForCurrentRelease($stringTask); + }; + + return $this->buildTaskFromClosure($closure, $stringTask); + } + + /** + * Build a task from a Closure or a string command + * + * @param Closure $callback + * @param string|null $stringTask + * + * @return AbstractTask + */ + public function buildTaskFromClosure(Closure $callback, $stringTask = null) + { + /** @type \Rocketeer\Tasks\Closure $task */ + $task = $this->buildTaskFromClass('Rocketeer\Tasks\Closure'); + $task->setClosure($callback); + + // If we had an original string used, store it on + // the task for easier reflection + if ($stringTask) { + $task->setStringTask($stringTask); + } + + return $task; + } + + /** + * Build a task from its name + * + * @param string|AbstractTask $task + * + * @throws TaskCompositionException + * @return AbstractTask + */ + public function buildTaskFromClass($task) + { + if (is_object($task) && $task instanceof AbstractTask) { + return $task; + } + + // Cancel if class doesn't exist + if (!$class = $this->taskClassExists($task)) { + throw new TaskCompositionException('Impossible to build task: '.$task); + } + + return new $class($this->app); + } + + //////////////////////////////////////////////////////////////////// + /////////////////////////////// HELPERS //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the handle of a task from its name + * + * @param string|AbstractTask $task + * + * @return string|null + */ + protected function getTaskHandle($task) + { + // Check the handle if possible + if (!is_string($task)) { + return; + } + + // Compute the handle and check it's bound + $handle = 'rocketeer.tasks.'.Str::snake($task, '-'); + $task = $this->app->bound($handle) ? $handle : null; + + return $task; + } + + /** + * Check if a string is a command or a task + * + * @param string|Closure|AbstractTask $string + * + * @return boolean + */ + protected function isStringCommand($string) + { + return is_string($string) && !$this->taskClassExists($string) && !$this->app->bound('rocketeer.tasks.'.$string); + } + + /** + * Check if a class with the given task name exists + * + * @param string $task + * + * @return string|false + */ + protected function taskClassExists($task) + { + return $this->findQualifiedName($task, array( + 'Rocketeer\Tasks\%s', + 'Rocketeer\Tasks\Subtasks\%s', + )); + } + + /** + * Find a class in various predefined namespaces + * + * @param string $class + * @param string[] $paths + * + * @return string|false + */ + protected function findQualifiedName($class, $paths = array()) + { + $paths[] = '%s'; + + $class = ucfirst($class); + foreach ($paths as $path) { + $path = sprintf($path, $class); + if (class_exists($path)) { + return $path; + } + } + + return false; + } +} diff --git a/src/Rocketeer/Services/Tasks/TasksQueue.php b/src/Rocketeer/Services/Tasks/TasksQueue.php new file mode 100644 index 000000000..75ceb3a40 --- /dev/null +++ b/src/Rocketeer/Services/Tasks/TasksQueue.php @@ -0,0 +1,303 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Services\Tasks; + +use Closure; +use Exception; +use KzykHys\Parallel\Parallel; +use LogicException; +use Rocketeer\Connection; +use Rocketeer\Traits\HasHistory; +use Rocketeer\Traits\HasLocator; + +/** + * Handles running an array of tasks sequentially + * or in parallel + * + * @author Maxime Fabre + */ +class TasksQueue +{ + use HasLocator; + use HasHistory; + + /** + * @type Parallel + */ + protected $parallel; + + /** + * A list of Tasks to execute + * + * @var array + */ + protected $tasks; + + /** + * The Remote connection + * + * @var Connection + */ + protected $remote; + + /** + * @param Parallel $parallel + */ + public function setParallel($parallel) + { + $this->parallel = $parallel; + } + + //////////////////////////////////////////////////////////////////// + ////////////////////////////// SHORTCUTS /////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Execute Tasks on the default connection and + * return their output + * + * @param string|array|Closure $queue + * @param string|string[]|null $connections + * + * @return boolean + */ + public function execute($queue, $connections = null) + { + if ($connections) { + $this->connections->setConnections($connections); + } + + // Run tasks + $this->run($queue); + $history = $this->history->getFlattenedOutput(); + + return end($history); + } + + /** + * Execute Tasks on various connections + * + * @param string|string[] $connections + * @param string|array|Closure $queue + * + * @return boolean + */ + public function on($connections, $queue) + { + return $this->execute($queue, $connections); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// QUEUE ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Run the queue + * Run an array of Tasks instances on the various + * connections and stages provided + * + * @param string|array $tasks An array of tasks + * + * @throws Exception + * @return Pipeline + */ + public function run($tasks) + { + $tasks = (array) $tasks; + $queue = $this->builder->buildTasks($tasks); + $pipeline = $this->buildPipeline($queue); + + // Wrap job in closure pipeline + foreach ($pipeline as $key => $job) { + $pipeline[$key] = function () use ($job) { + return $this->executeJob($job); + }; + } + + // Run the tasks and store the results + if ($this->getOption('parallel')) { + $pipeline = $this->runAsynchronously($pipeline); + } else { + $pipeline = $this->runSynchronously($pipeline); + } + + return $pipeline; + } + + /** + * Build a pipeline of jobs for Parallel to execute + * + * @param array $queue + * + * @return Pipeline + */ + public function buildPipeline(array $queue) + { + // First we'll build the queue + $pipeline = new Pipeline(); + + // Get the connections to execute the tasks on + $connections = (array) $this->connections->getConnections(); + foreach ($connections as $connection) { + $servers = $this->connections->getConnectionCredentials($connection); + $stages = $this->getStages($connection); + + // Add job to pipeline + foreach ($servers as $server => $credentials) { + foreach ($stages as $stage) { + $pipeline[] = new Job(array( + 'connection' => $connection, + 'server' => $server, + 'stage' => $stage, + 'queue' => $queue, + )); + } + } + } + + return $pipeline; + } + + /** + * Run the queue, taking into account the stage + * + * @param Job $job + * + * @return boolean + */ + protected function executeJob(Job $job) + { + // Set proper server + $this->connections->setConnection($job->connection, $job->server); + + foreach ($job->queue as $key => $task) { + if ($task->usesStages()) { + $stage = $task->usesStages() ? $job->stage : null; + $this->connections->setStage($stage); + } + + // Here we fire the task, save its + // output and return its status + $state = $task->fire(); + $this->toOutput($state); + + // If the task didn't finish, display what the error was + if ($task->wasHalted() || $state === false) { + $this->command->error('The tasks queue was canceled by task "'.$task->getName().'"'); + + return false; + } + } + + return true; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// RUNNERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Run the pipeline in order. + * As long as the previous entry didn't fail, continue + * + * @param Pipeline $pipeline + * + * @return Pipeline + */ + protected function runSynchronously(Pipeline $pipeline) + { + $results = []; + + /** @type Closure $task */ + foreach ($pipeline as $key => $task) { + $results[$key] = $task(); + if (!$results[$key]) { + break; + } + } + + // Update Pipeline results + $pipeline->setResults($results); + + return $pipeline; + } + + /** + * Run the pipeline in parallel order + * + * @param Pipeline $pipeline + * + * @return Pipeline + * @throws \Exception + */ + protected function runAsynchronously(Pipeline $pipeline) + { + $this->parallel = $this->parallel ?: new Parallel(); + + // Check if supported + if (!$this->parallel->isSupported()) { + throw new Exception('Parallel jobs require the PCNTL extension'); + } + + try { + $this->parallel = $this->parallel ?: new Parallel(); + $results = $this->parallel->values($pipeline->all()); + $pipeline->setResults($results); + } catch (LogicException $exception) { + return $this->runSynchronously($pipeline); + } + + return $pipeline; + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// STAGES //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the stages of a connection + * + * @param string $connection + * + * @return array + */ + public function getStages($connection) + { + $this->connections->setConnection($connection); + + $stage = $this->rocketeer->getOption('stages.default'); + if ($this->hasCommand()) { + $stage = $this->getOption('stage') ?: $stage; + } + + // Return all stages if "all" + if ($stage == 'all' || !$stage) { + $stage = $this->connections->getStages(); + } + + // Sanitize and filter + $stages = (array) $stage; + $stages = array_filter($stages, [$this, 'isValidStage']); + + return $stages ?: [null]; + } + + /** + * Check if a stage is valid + * + * @param string $stage + * + * @return boolean + */ + public function isValidStage($stage) + { + return in_array($stage, $this->connections->getStages()); + } +} diff --git a/src/Rocketeer/TasksHandler.php b/src/Rocketeer/Services/TasksHandler.php similarity index 54% rename from src/Rocketeer/TasksHandler.php rename to src/Rocketeer/Services/TasksHandler.php index 4637e4d38..8d1a55e16 100644 --- a/src/Rocketeer/TasksHandler.php +++ b/src/Rocketeer/Services/TasksHandler.php @@ -7,19 +7,24 @@ * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ -namespace Rocketeer; +namespace Rocketeer\Services; +use Closure; use Illuminate\Container\Container; -use Rocketeer\Commands\BaseTaskCommand; -use Rocketeer\Traits\AbstractLocatorClass; +use Rocketeer\Abstracts\AbstractTask; +use Rocketeer\Console\Commands\BaseTaskCommand; +use Rocketeer\Tasks; +use Rocketeer\Traits\HasLocator; /** - * Handles the registering and relating of tasks + * Handles the registering and firing of tasks and their events * * @author Maxime Fabre */ -class TasksHandler extends AbstractLocatorClass +class TasksHandler { + use HasLocator; + /** * The registered events * @@ -27,10 +32,17 @@ class TasksHandler extends AbstractLocatorClass */ protected $registeredEvents = array(); + /** + * The registered plugins + * + * @type array + */ + protected $registeredPlugins = array(); + /** * Build a new TasksQueue Instance * - * @param Container $app + * @param Container $app */ public function __construct(Container $app) { @@ -57,17 +69,18 @@ public function __call($method, $parameters) //////////////////////////////////////////////////////////////////// /** - * Register a custom Task with Rocketeer + * Register a custom task with Rocketeer * - * @param Task|string $task - * @param string $name + * @param string|Closure|AbstractTask $task + * @param string|null $name + * @param string|null $description * - * @return Container + * @return BaseTaskCommand */ - public function add($task, $name = null) + public function add($task, $name = null, $description = null) { - // Build Task if necessary - $task = $this->buildTask($task, $name); + // Build task if necessary + $task = $this->builder->buildTask($task, $name, $description); $slug = 'rocketeer.tasks.'.$task->getSlug(); // Add the task to Rocketeer @@ -75,8 +88,9 @@ public function add($task, $name = null) $bound = $this->console->add(new BaseTaskCommand($this->app[$slug])); // Bind to Artisan too - if ($this->app->bound('artisan')) { - $this->app['artisan']->add(new BaseTaskCommand($task)); + if ($this->app->bound('artisan') && $this->app->resolved('artisan')) { + $command = $this->builder->buildCommand($task); + $this->app['artisan']->add($command); } return $bound; @@ -85,14 +99,15 @@ public function add($task, $name = null) /** * Register a task with Rocketeer * - * @param string $name - * @param mixed $task + * @param string $name + * @param string|Closure|AbstractTask $task + * @param string|null $description * - * @return void + * @return BaseTaskCommand */ - public function task($name, $task) + public function task($name, $task, $description = null) { - return $this->add($task, $name); + return $this->add($task, $name, $description); } //////////////////////////////////////////////////////////////////// @@ -100,11 +115,11 @@ public function task($name, $task) //////////////////////////////////////////////////////////////////// /** - * Execute a Task before another one + * Execute a task before another one * - * @param string $task - * @param string|Closure|Task $listeners - * @param integer $priority + * @param string $task + * @param Closure $listeners + * @param integer $priority * * @return void */ @@ -114,11 +129,11 @@ public function before($task, $listeners, $priority = 0) } /** - * Execute a Task after another one + * Execute a task after another one * - * @param string $task - * @param string|Closure|Task $listeners - * @param integer $priority + * @param string $task + * @param Closure $listeners + * @param integer $priority * * @return void */ @@ -139,6 +154,15 @@ public function registerConfiguredEvents() $this->events->forget('rocketeer.'.$event); } + // Clean previously registered plugins + $plugins = $this->registeredPlugins; + $this->registeredPlugins = []; + + // Register plugins again + foreach ($plugins as $plugin) { + $this->plugin($plugin['plugin'], $plugin['configuration']); + } + // Get the registered events $hooks = (array) $this->rocketeer->getOption('hooks'); unset($hooks['custom']); @@ -146,7 +170,7 @@ public function registerConfiguredEvents() // Bind events foreach ($hooks as $event => $tasks) { foreach ($tasks as $task => $listeners) { - $this->registeredEvents[] = $this->addTaskListeners($task, $event, $listeners); + $this->addTaskListeners($task, $event, $listeners, 0, true); } } } @@ -154,20 +178,21 @@ public function registerConfiguredEvents() /** * Register listeners for a particular event * - * @param string $event - * @param array $listeners - * @param integer $priority + * @param string $event + * @param array|callable $listeners + * @param integer $priority * * @return string */ public function listenTo($event, $listeners, $priority = 0) { - // Create array if it doesn't exist - $listeners = $this->buildQueue((array) $listeners); + /** @type AbstractTask[] $listeners */ + $listeners = $this->builder->buildTasks((array) $listeners); // Register events foreach ($listeners as $listener) { - $this->events->listen('rocketeer.'.$event, array($listener, 'execute'), $priority); + $listener->setEvent($event); + $this->events->listen('rocketeer.'.$event, [$listener, 'fire'], $priority); } return $event; @@ -176,49 +201,66 @@ public function listenTo($event, $listeners, $priority = 0) /** * Bind a listener to a task * - * @param string $task - * @param string $event - * @param mixed $listeners - * @param integer $priority + * @param string|array $task + * @param string $event + * @param array|callable $listeners + * @param integer $priority + * @param boolean $register + * + * @throws \Rocketeer\Exceptions\TaskCompositionException + * @return string|null */ - public function addTaskListeners($task, $event, $listeners, $priority = 0) + public function addTaskListeners($task, $event, $listeners, $priority = 0, $register = false) { // Recursive call if (is_array($task)) { foreach ($task as $t) { - $this->addTaskListeners($t, $event, $listeners, $priority); + $this->addTaskListeners($t, $event, $listeners, $priority, $register); } return; } + // Prevent events on anonymous tasks + $slug = $this->builder->buildTask($task)->getSlug(); + if ($slug == 'closure') { + return; + } + // Get event name and register listeners - $event = $this->buildTaskFromClass($task)->getSlug().'.'.$event; + $event = $slug.'.'.$event; $event = $this->listenTo($event, $listeners, $priority); + // Store registered event + if ($register) { + $this->registeredEvents[] = $event; + } + return $event; } /** * Get all of a task's listeners * - * @param Task $task - * @param string $event - * @param boolean $flatten + * @param string|AbstractTask $task + * @param string $event + * @param boolean $flatten * * @return array */ public function getTasksListeners($task, $event, $flatten = false) { // Get events - $task = $this->buildTaskFromClass($task)->getSlug(); + $task = $this->builder->buildTaskFromClass($task)->getSlug(); $events = $this->events->getListeners('rocketeer.'.$task.'.'.$event); // Flatten the queue if requested foreach ($events as $key => $event) { $task = $event[0]; - if ($flatten and $task instanceof Tasks\Closure and $stringTask = $task->getStringTask()) { + if ($flatten && $task instanceof Tasks\Closure && $stringTask = $task->getStringTask()) { $events[$key] = $stringTask; + } elseif ($flatten && $task instanceof AbstractTask) { + $events[$key] = $task->getSlug(); } } @@ -229,6 +271,14 @@ public function getTasksListeners($task, $event, $flatten = false) /////////////////////////////// PLUGINS //////////////////////////// //////////////////////////////////////////////////////////////////// + /** + * @return array + */ + public function getRegisteredPlugins() + { + return $this->registeredPlugins; + } + /** * Register a Rocketeer plugin with Rocketeer * @@ -241,12 +291,23 @@ public function plugin($plugin, array $configuration = array()) { // Build plugin if (is_string($plugin)) { - $plugin = $this->app->make($plugin, array($this->app)); + $plugin = $this->app->make($plugin, [$this->app]); } + // Store registration of plugin + $identifier = get_class($plugin); + if (isset($this->registeredPlugins[$identifier])) { + return; + } + + $this->registeredPlugins[$identifier] = array( + 'plugin' => $plugin, + 'configuration' => $configuration, + ); + // Register configuration $vendor = $plugin->getNamespace(); - $this->config->package('rocketeer/'.$vendor, $plugin->configurationFolder); + $this->config->package('rocketeers/'.$vendor, $plugin->configurationFolder); if ($configuration) { $this->config->set($vendor.'::config', $configuration); } diff --git a/src/Rocketeer/Strategies/Check/NodeStrategy.php b/src/Rocketeer/Strategies/Check/NodeStrategy.php new file mode 100644 index 000000000..6730e8079 --- /dev/null +++ b/src/Rocketeer/Strategies/Check/NodeStrategy.php @@ -0,0 +1,70 @@ + + */ +class NodeStrategy extends AbstractCheckStrategy implements CheckStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Checks if the server is ready to receive a Node application'; + + /** + * The language of the strategy + * + * @type string + */ + protected $language = 'Node'; + + /** + * Get the version constraint which should be checked against + * + * @param string $manifest + * + * @return string + */ + protected function getLanguageConstraint($manifest) + { + return $this->getLanguageConstraintFromJson($manifest, 'engines.node'); + } + + /** + * Get the current version in use + * + * @return string + */ + protected function getCurrentVersion() + { + $version = $this->binary('node')->run('--version'); + $version = str_replace('v', null, $version); + + return $version; + } + + /** + * Check for the required extensions + * + * @return array + */ + public function extensions() + { + return []; + } + + /** + * Check for the required drivers + * + * @return array + */ + public function drivers() + { + return []; + } +} diff --git a/src/Rocketeer/Strategies/Check/PhpStrategy.php b/src/Rocketeer/Strategies/Check/PhpStrategy.php new file mode 100644 index 000000000..27fa37813 --- /dev/null +++ b/src/Rocketeer/Strategies/Check/PhpStrategy.php @@ -0,0 +1,208 @@ +app = $app; + $this->manager = $this->binary('composer'); + } + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// CHECKS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the version constraint which should be checked against + * + * @param string $manifest + * + * @return string + */ + protected function getLanguageConstraint($manifest) + { + return $this->getLanguageConstraintFromJson($manifest, 'require.php'); + } + + /** + * Get the current version in use + * + * @return string + */ + protected function getCurrentVersion() + { + return $this->php()->runLast('version'); + } + + /** + * Check for the required extensions + * + * @return array + */ + public function extensions() + { + $extensions = array( + 'mcrypt' => ['checkPhpExtension', 'mcrypt'], + 'database' => ['checkDatabaseDriver', $this->app['config']->get('database.default')], + 'cache' => ['checkCacheDriver', $this->app['config']->get('cache.driver')], + 'session' => ['checkCacheDriver', $this->app['config']->get('session.driver')], + ); + + // Check PHP extensions + $errors = []; + foreach ($extensions as $check) { + list ($method, $extension) = $check; + + if (!$this->$method($extension)) { + $errors[] = $extension; + } + } + + return $errors; + } + + /** + * Check for the required drivers + * + * @return array + */ + public function drivers() + { + return []; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Check the presence of the correct database PHP extension + * + * @param string $database + * + * @return boolean + */ + public function checkDatabaseDriver($database) + { + switch ($database) { + case 'sqlite': + return $this->checkPhpExtension('pdo_sqlite'); + + case 'mysql': + return $this->checkPhpExtension('mysql') && $this->checkPhpExtension('pdo_mysql'); + + default: + return true; + } + } + + /** + * Check the presence of the correct cache PHP extension + * + * @param string $cache + * + * @return boolean|string + */ + public function checkCacheDriver($cache) + { + switch ($cache) { + case 'memcached': + case 'apc': + return $this->checkPhpExtension($cache); + + case 'redis': + return $this->which('redis-server'); + + default: + return true; + } + } + + /** + * Check the presence of a PHP extension + * + * @param string $extension The extension + * + * @return boolean + */ + public function checkPhpExtension($extension) + { + // Check for HHVM and built-in extensions + if ($this->php()->isHhvm()) { + $this->extensions = array( + '_hhvm', + 'apache', + 'asio', + 'bcmath', + 'bz2', + 'ctype', + 'curl', + 'debugger', + 'fileinfo', + 'filter', + 'gd', + 'hash', + 'hh', + 'iconv', + 'icu', + 'imagick', + 'imap', + 'json', + 'mailparse', + 'mcrypt', + 'memcache', + 'memcached', + 'mysql', + 'odbc', + 'openssl', + 'pcre', + 'phar', + 'reflection', + 'session', + 'soap', + 'std', + 'stream', + 'thrift', + 'url', + 'wddx', + 'xdebug', + 'zip', + 'zlib', + ); + } + + // Get the PHP extensions available + if (!$this->extensions) { + $this->extensions = (array) $this->bash->run($this->php()->extensions(), false, true); + } + + return in_array($extension, $this->extensions); + } +} diff --git a/src/Rocketeer/Strategies/Check/PolyglotStrategy.php b/src/Rocketeer/Strategies/Check/PolyglotStrategy.php new file mode 100644 index 000000000..97bf30331 --- /dev/null +++ b/src/Rocketeer/Strategies/Check/PolyglotStrategy.php @@ -0,0 +1,66 @@ +executeStrategiesMethod('manager'); + + return $this->checkStrategiesResults($results); + } + + /** + * Check that the language used by the + * application is at the required version + * + * @return boolean + */ + public function language() + { + $results = $this->executeStrategiesMethod('language'); + + return $this->checkStrategiesResults($results); + } + + /** + * Check for the required extensions + * + * @return array + */ + public function extensions() + { + $missing = []; + $extensions = $this->executeStrategiesMethod('extensions'); + foreach ($extensions as $extension) { + $missing = array_merge($missing, $extension); + } + + return $missing; + } + + /** + * Check for the required drivers + * + * @return array + */ + public function drivers() + { + $missing = []; + $drivers = $this->executeStrategiesMethod('drivers'); + foreach ($drivers as $driver) { + $missing = array_merge($missing, $driver); + } + + return $missing; + } +} diff --git a/src/Rocketeer/Strategies/Check/RubyStrategy.php b/src/Rocketeer/Strategies/Check/RubyStrategy.php new file mode 100644 index 000000000..733eddb36 --- /dev/null +++ b/src/Rocketeer/Strategies/Check/RubyStrategy.php @@ -0,0 +1,83 @@ +app = $app; + $this->manager = $this->binary('bundler'); + } + + ////////////////////////////////////////////////////////////////////// + /////////////////////////////// CHECKS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the version constraint which should be checked against + * + * @param string $manifest + * + * @return string + */ + protected function getLanguageConstraint($manifest) + { + preg_match('/ruby \'(.+)\'/', $manifest, $matches); + $required = Arr::get((array) $matches, 1); + + return $required; + } + + /** + * Get the current version in use + * + * @return string + */ + protected function getCurrentVersion() + { + $version = $this->binary('ruby')->run('--version'); + $version = preg_replace('/ruby ([0-9\.]+)p?.+/', '$1', $version); + + return $version; + } + + /** + * Check for the required extensions + * + * @return array + */ + public function extensions() + { + return []; + } + + /** + * Check for the required drivers + * + * @return array + */ + public function drivers() + { + return []; + } +} diff --git a/src/Rocketeer/Strategies/Dependencies/BowerStrategy.php b/src/Rocketeer/Strategies/Dependencies/BowerStrategy.php new file mode 100644 index 000000000..ff623a100 --- /dev/null +++ b/src/Rocketeer/Strategies/Dependencies/BowerStrategy.php @@ -0,0 +1,72 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Dependencies; + +use Illuminate\Support\Arr; +use Rocketeer\Abstracts\Strategies\AbstractDependenciesStrategy; +use Rocketeer\Interfaces\Strategies\DependenciesStrategyInterface; + +class BowerStrategy extends AbstractDependenciesStrategy implements DependenciesStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Installs dependencies with Bower'; + + /** + * The name of the binary + * + * @type string + */ + protected $binary = 'bower'; + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// COMMANDS ////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Install the dependencies + * + * @return bool + */ + public function install() + { + return $this->manager->runForCurrentRelease('install', [], $this->getInstallationOptions()); + } + + /** + * Update the dependencies + * + * @return boolean + */ + public function update() + { + return $this->manager->runForCurrentRelease('update', [], $this->getInstallationOptions()); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Get the options to run Bower with + * + * @return array + */ + protected function getInstallationOptions() + { + $credentials = $this->connections->getServerCredentials(); + if (Arr::get($credentials, 'username') == 'root') { + return ['--allow-root' => null]; + } + + return []; + } +} diff --git a/src/Rocketeer/Strategies/Dependencies/BundlerStrategy.php b/src/Rocketeer/Strategies/Dependencies/BundlerStrategy.php new file mode 100644 index 000000000..76b2b7c1e --- /dev/null +++ b/src/Rocketeer/Strategies/Dependencies/BundlerStrategy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Dependencies; + +use Rocketeer\Abstracts\Strategies\AbstractDependenciesStrategy; +use Rocketeer\Interfaces\Strategies\DependenciesStrategyInterface; + +class BundlerStrategy extends AbstractDependenciesStrategy implements DependenciesStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Installs dependencies with Bundler'; + + /** + * The name of the binary + * + * @type string + */ + protected $binary = 'bundler'; +} diff --git a/src/Rocketeer/Strategies/Dependencies/ComposerStrategy.php b/src/Rocketeer/Strategies/Dependencies/ComposerStrategy.php new file mode 100644 index 000000000..e67c82383 --- /dev/null +++ b/src/Rocketeer/Strategies/Dependencies/ComposerStrategy.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Dependencies; + +use Rocketeer\Abstracts\Strategies\AbstractDependenciesStrategy; +use Rocketeer\Interfaces\Strategies\DependenciesStrategyInterface; + +class ComposerStrategy extends AbstractDependenciesStrategy implements DependenciesStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Installs dependencies with Composer'; + + /** + * The name of the binary + * + * @type string + */ + protected $binary = 'composer'; + + /** + * Install the dependencies + * + * @return bool + */ + public function install() + { + return $this->executeHook('install'); + } + + /** + * Update the dependencies + * + * @return boolean + */ + public function update() + { + return $this->executeHook('update'); + } + + /** + * @param string $hook + * + * @return bool + */ + protected function executeHook($hook) + { + $tasks = $this->getHookedTasks('composer.'.$hook, [$this->manager, $this]); + if (!$tasks) { + return true; + } + + $this->runForCurrentRelease($tasks); + + return $this->checkStatus('Composer could not install dependencies'); + } +} diff --git a/src/Rocketeer/Strategies/Dependencies/NpmStrategy.php b/src/Rocketeer/Strategies/Dependencies/NpmStrategy.php new file mode 100644 index 000000000..b3dd47865 --- /dev/null +++ b/src/Rocketeer/Strategies/Dependencies/NpmStrategy.php @@ -0,0 +1,28 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Dependencies; + +use Rocketeer\Abstracts\Strategies\AbstractDependenciesStrategy; +use Rocketeer\Interfaces\Strategies\DependenciesStrategyInterface; + +class NpmStrategy extends AbstractDependenciesStrategy implements DependenciesStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Installs dependencies with NPM'; + + /** + * The name of the binary + * + * @type string + */ + protected $binary = 'npm'; +} diff --git a/src/Rocketeer/Strategies/Dependencies/PolyglotStrategy.php b/src/Rocketeer/Strategies/Dependencies/PolyglotStrategy.php new file mode 100644 index 000000000..a3650a0ad --- /dev/null +++ b/src/Rocketeer/Strategies/Dependencies/PolyglotStrategy.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Dependencies; + +use Rocketeer\Abstracts\Strategies\AbstractPolyglotStrategy; +use Rocketeer\Interfaces\Strategies\DependenciesStrategyInterface; + +class PolyglotStrategy extends AbstractPolyglotStrategy implements DependenciesStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Runs all of the above package managers if necessary'; + + /** + * The various strategies to call + * + * @type array + */ + protected $strategies = ['Bundler', 'Composer', 'Npm', 'Bower']; + + /** + * Install the dependencies + * + * @return boolean[] + */ + public function install() + { + return $this->executeStrategiesMethod('install'); + } + + /** + * Update the dependencies + * + * @return boolean[] + */ + public function update() + { + return $this->executeStrategiesMethod('update'); + } +} diff --git a/src/Rocketeer/Strategies/Deploy/CloneStrategy.php b/src/Rocketeer/Strategies/Deploy/CloneStrategy.php new file mode 100644 index 000000000..650d2e183 --- /dev/null +++ b/src/Rocketeer/Strategies/Deploy/CloneStrategy.php @@ -0,0 +1,75 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Deploy; + +use Rocketeer\Abstracts\Strategies\AbstractStrategy; +use Rocketeer\Interfaces\Strategies\DeployStrategyInterface; + +class CloneStrategy extends AbstractStrategy implements DeployStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Clones a fresh instance of the repository by SCM'; + + /** + * Deploy a new clean copy of the application + * + * @param string|null $destination + * + * @return boolean + */ + public function deploy($destination = null) + { + if (!$destination) { + $destination = $this->releasesManager->getCurrentReleasePath(); + } + + // Executing checkout + $this->explainer->line('Cloning repository in "'.$destination.'"'); + $output = $this->scm->run('checkout', $destination); + + // Cancel if failed and forget credentials + $success = $this->bash->checkStatus('Unable to clone the repository', $output) !== false; + if (!$success) { + $this->localStorage->forget('credentials'); + + return false; + } + + // Deploy submodules + if ($this->rocketeer->getOption('scm.submodules')) { + $this->explainer->line('Initializing submodules if any'); + $this->scm->runForCurrentRelease('submodules'); + } + + return $success; + } + + /** + * Update the latest version of the application + * + * @param boolean $reset + * + * @return string + */ + public function update($reset = true) + { + $this->command->info('Pulling changes'); + $tasks = [$this->scm->update()]; + + // Reset if requested + if ($reset) { + array_unshift($tasks, $this->scm->reset()); + } + + return $this->bash->runForCurrentRelease($tasks); + } +} diff --git a/src/Rocketeer/Strategies/Deploy/CopyStrategy.php b/src/Rocketeer/Strategies/Deploy/CopyStrategy.php new file mode 100644 index 000000000..1e8fe2587 --- /dev/null +++ b/src/Rocketeer/Strategies/Deploy/CopyStrategy.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Deploy; + +use Rocketeer\Interfaces\Strategies\DeployStrategyInterface; + +class CopyStrategy extends CloneStrategy implements DeployStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Copies the previously cloned instance of the repository and update it'; + + /** + * Deploy a new clean copy of the application + * + * @param string|null $destination + * + * @return boolean|string + */ + public function deploy($destination = null) + { + // Get the previous release, if none clone from scratch + $previous = $this->releasesManager->getReleases(); + if (!$previous) { + return parent::deploy($destination); + } + + // If we have a previous release, check its validity + $previous = $this->releasesManager->getPreviousRelease(); + $previous = $this->releasesManager->getPathToRelease($previous); + if (!$previous) { + return parent::deploy($destination); + } + + // Recompute destination + if (!$destination) { + $destination = $this->releasesManager->getCurrentReleasePath(); + } + + // Copy old release into new one + $this->explainer->success('Copying previous release "'.$previous.'" in "'.$destination.'"'); + $this->bash->copy($previous, $destination); + + // Update repository + return $this->update(); + } +} diff --git a/src/Rocketeer/Strategies/Deploy/SyncStrategy.php b/src/Rocketeer/Strategies/Deploy/SyncStrategy.php new file mode 100644 index 000000000..7fa03d763 --- /dev/null +++ b/src/Rocketeer/Strategies/Deploy/SyncStrategy.php @@ -0,0 +1,87 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Deploy; + +use Rocketeer\Abstracts\Strategies\AbstractStrategy; +use Rocketeer\Bash; +use Rocketeer\Interfaces\Strategies\DeployStrategyInterface; + +class SyncStrategy extends AbstractStrategy implements DeployStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Uses rsync to create or update a release from the local files'; + + /** + * Deploy a new clean copy of the application + * + * @param string|null $destination + * + * @return boolean + */ + public function deploy($destination = null) + { + if (!$destination) { + $destination = $this->releasesManager->getCurrentReleasePath(); + } + + // Create receiveing folder + $this->createFolder($destination); + + return $this->rsyncTo($destination); + } + + /** + * Update the latest version of the application + * + * @param boolean $reset + * + * @return boolean + */ + public function update($reset = true) + { + $release = $this->releasesManager->getCurrentReleasePath(); + + return $this->rsyncTo($release); + } + + /** + * Rsyncs the local folder to a remote one + * + * @param string $destination + * + * @return boolean + */ + protected function rsyncTo($destination) + { + // Build host handle + $credentials = $this->connections->getServerCredentials(); + $handle = array_get($credentials, 'host'); + if ($user = array_get($credentials, 'username')) { + $handle = $user.'@'.$handle; + } + + // Create options + $options = '--verbose --recursive --rsh="ssh"'; + $excludes = ['.git', 'vendor']; + foreach ($excludes as $exclude) { + $options .= ' --exclude="'.$exclude.'"'; + } + + // Create binary and command + $rsync = $this->binary('rsync'); + $rsync = $rsync->getCommand(null, ['./', $handle.':'.$destination], $options); + + return $this->bash->onLocal(function (Bash $bash) use ($rsync) { + return $bash->run($rsync); + }); + } +} diff --git a/src/Rocketeer/Strategies/Migrate/ArtisanStrategy.php b/src/Rocketeer/Strategies/Migrate/ArtisanStrategy.php new file mode 100644 index 000000000..e3ae7d883 --- /dev/null +++ b/src/Rocketeer/Strategies/Migrate/ArtisanStrategy.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Strategies\Migrate; + +use Rocketeer\Abstracts\Strategies\AbstractStrategy; +use Rocketeer\Interfaces\Strategies\MigrateStrategyInterface; + +class ArtisanStrategy extends AbstractStrategy implements MigrateStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Migrates your database with Laravel\'s Artisan CLI'; + + /** + * Whether this particular strategy is runnable or not + * + * @return boolean + */ + public function isExecutable() + { + return (bool) $this->artisan()->getBinary(); + } + + /** + * Run outstanding migrations + * + * @return boolean|null + */ + public function migrate() + { + return $this->artisan()->runForCurrentRelease('migrate'); + } + + /** + * Seed the database + * + * @return boolean|null + */ + public function seed() + { + return $this->artisan()->runForCurrentRelease('seed'); + } +} diff --git a/src/Rocketeer/Strategies/Test/PhpunitStrategy.php b/src/Rocketeer/Strategies/Test/PhpunitStrategy.php new file mode 100644 index 000000000..e7218d2b9 --- /dev/null +++ b/src/Rocketeer/Strategies/Test/PhpunitStrategy.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 Rocketeer\Strategies\Test; + +use Rocketeer\Abstracts\Strategies\AbstractStrategy; +use Rocketeer\Interfaces\Strategies\TestStrategyInterface; + +class PhpunitStrategy extends AbstractStrategy implements TestStrategyInterface +{ + /** + * @type string + */ + protected $description = 'Run the tests with PHPUnit'; + + /** + * Whether this particular strategy is runnable or not + * + * @return boolean + */ + public function isExecutable() + { + return (bool) $this->phpunit()->getBinary(); + } + + /** + * Run the task + * + * @return boolean + */ + public function test() + { + // Run PHPUnit + $arguments = ['--stop-on-failure' => null]; + $output = $this->runForCurrentRelease(array( + $this->phpunit()->getCommand(null, [], $arguments), + )); + + $status = $this->checkStatus('Tests failed', $output, 'Tests passed successfully'); + if (!$status) { + $this->explainer->error('Tests failed'); + } + + return $status; + } +} diff --git a/src/Rocketeer/Tasks/Check.php b/src/Rocketeer/Tasks/Check.php index 7d1baf657..bc2260ca7 100644 --- a/src/Rocketeer/Tasks/Check.php +++ b/src/Rocketeer/Tasks/Check.php @@ -9,59 +9,72 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Check if the server is ready to receive the application * * @author Maxime Fabre */ -class Check extends Task +class Check extends AbstractTask { - /** - * The PHP extensions loaded on server - * - * @var array - */ - protected $extensions = array(); - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ protected $description = 'Check if the server is ready to receive the application'; /** - * Whether the Task needs to be run on each stage or globally + * Whether the task needs to be run on each stage or globally * * @var boolean */ public $usesStages = false; /** - * Run the Task + * Run the task * - * @return void + * @return boolean|null */ public function execute() { - $errors = array(); - $checks = $this->getChecks(); + $check = $this->getStrategy('Check'); + $errors = []; + + // Check the depoy strategy + if ($this->rocketeer->getOption('strategies.deploy') !== 'sync' && !$this->checkScm()) { + $errors[] = $this->scm->getBinary().' could not be found'; + } - foreach ($checks as $check) { - list($check, $error) = $check; + // Check package manager + $manager = class_basename($check->getManager()); + $manager = str_replace('Strategy', null, $manager); + $this->explainer->line('Checking presence of '.$manager); + if (!$check->manager()) { + $errors[] = sprintf('The %s package manager could not be found', $manager); + } - $argument = null; - if (is_array($error)) { - $argument = $error[0]; - $error = $error[1]; - } + // Check language + $language = $check->getLanguage(); + $this->explainer->line('Checking '.$language.' version'); + if (!$check->language()) { + $errors[] = $language.' is not at the required version'; + } + + // Check extensions + $this->explainer->line('Checking presence of required extensions'); + $extensions = $check->extensions(); + if (!empty($extensions)) { + $errors[] = 'The following extensions could not be found: '.implode(', ', $extensions); + } - // If the check fail, print an error message - if (!$this->$check($argument)) { - $errors[] = $error; - } + // Check drivers + $this->explainer->line('Checking presence of required drivers'); + $drivers = $check->drivers(); + if (!empty($drivers)) { + $errors[] = 'The following drivers could not be found: '.implode(', ', $drivers); } // Return false if any error @@ -70,30 +83,7 @@ public function execute() } // Display confirmation message - $this->command->info('Your server is ready to deploy'); - } - - /** - * Get the checks to execute - * - * @return array - */ - protected function getChecks() - { - $extension = 'The %s extension does not seem to be loaded on the server'; - $database = $this->app['config']->get('database.default'); - $cache = $this->app['config']->get('cache.driver'); - $session = $this->app['config']->get('session.driver'); - - return array( - array('checkScm', $this->scm->binary. ' could not be found'), - array('checkPhpVersion', 'The version of PHP on the server does not match Laravel\'s requirements'), - array('checkComposer', 'Composer does not seem to be present on the server'), - array('checkPhpExtension', array('mcrypt', sprintf($extension, 'mcrypt'))), - array('checkDatabaseDriver', array($database, sprintf($extension, $database))), - array('checkCacheDriver', array($cache, sprintf($extension, $cache))), - array('checkCacheDriver', array($session, sprintf($extension, $session))), - ); + $this->explainer->line('Your server is ready to deploy'); } //////////////////////////////////////////////////////////////////// @@ -107,122 +97,10 @@ protected function getChecks() */ public function checkScm() { - $this->command->comment('Checking presence of '.$this->scm->binary); - $this->history[] = $this->scm->execute('check'); - - return $this->remote->status() == 0; - } - - /** - * Check if Composer is on the server - * - * @return boolean - */ - public function checkComposer() - { - if (!$this->server->usesComposer()) { - return true; - } - - $this->command->comment('Checking presence of Composer'); - - return $this->composer(); - } - - /** - * Check if the server is ready to support PHP - * - * @return boolean - */ - public function checkPhpVersion() - { - $required = null; - - // Get the minimum PHP version of the application - $composer = $this->app['path.base'].'/composer.json'; - if ($this->app['files']->exists($composer)) { - $composer = $this->app['files']->get($composer); - $composer = json_decode($composer, true); - - // Strip versions of constraints - $required = array_get($composer, 'require.php'); - $required = preg_replace('/>=/', '', $required); - } - - // Cancel if no PHP version found - if (!$required) { - return true; - } - - $this->command->comment('Checking PHP version'); - $version = $this->runLast($this->php('-r "print PHP_VERSION;"')); - - return version_compare($version, $required, '>='); - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// HELPERS //////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Check the presence of the correct database PHP extension - * - * @param string $database - * - * @return boolean - */ - public function checkDatabaseDriver($database) - { - switch ($database) { - case 'sqlite': - return $this->checkPhpExtension('pdo_sqlite'); - - case 'mysql': - return $this->checkPhpExtension('mysql') and $this->checkPhpExtension('pdo_mysql'); - - default: - return true; - } - } - - /** - * Check the presence of the correct cache PHP extension - * - * @param string $cache - * - * @return boolean - */ - public function checkCacheDriver($cache) - { - switch ($cache) { - case 'memcached': - case 'apc': - return $this->checkPhpExtension($cache); - - case 'redis': - return $this->which('redis-server'); - - default: - return true; - } - } - - /** - * Check the presence of a PHP extension - * - * @param string $extension The extension - * - * @return boolean - */ - public function checkPhpExtension($extension) - { - $this->command->comment('Checking presence of '.$extension. ' extension'); - - // Get the PHP extensions available - if (!$this->extensions) { - $this->extensions = (array) $this->run($this->php('-m'), false, true); - } + $this->explainer->line('Checking presence of '.$this->scm->getBinary()); + $results = $this->scm->run('check'); + $this->toOutput($results); - return in_array($extension, $this->extensions); + return $this->getConnection()->status() == 0; } } diff --git a/src/Rocketeer/Tasks/Cleanup.php b/src/Rocketeer/Tasks/Cleanup.php index a7e42896a..026638660 100644 --- a/src/Rocketeer/Tasks/Cleanup.php +++ b/src/Rocketeer/Tasks/Cleanup.php @@ -10,50 +10,55 @@ namespace Rocketeer\Tasks; use Illuminate\Support\Str; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; +use Rocketeer\Services\Storages\ServerStorage; /** * Clean up old releases from the server * * @author Maxime Fabre */ -class Cleanup extends Task +class Cleanup extends AbstractTask { - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ protected $description = 'Clean up old releases from the server'; /** - * Run the Task - * - * @return void + * Run the task */ public function execute() { // If no releases to prune if (!$trash = $this->getReleasesToCleanup()) { - return $this->command->comment('No releases to prune from the server'); + return $this->explainer->line('No releases to prune from the server'); } // Prune releases - foreach ($trash as $release) { - $this->removeFolder($this->releasesManager->getPathToRelease($release)); - } + $trash = array_map([$this->releasesManager, 'getPathToRelease'], $trash); + $this->removeFolder($trash); // Create final message - $trash = sizeof($trash); + $trash = count($trash); $message = sprintf('Removing %d %s from the server', $trash, Str::plural('release', $trash)); - return $this->command->line($message); + // Delete state file + if ($this->getOption('clean-all')) { + $state = new ServerStorage($this->app, 'state'); + $state->destroy(); + $this->releasesManager->markReleaseAsValid(); + } + + return $this->explainer->line($message); } /** * Get an array of releases to prune * - * @return array + * @return integer[] */ protected function getReleasesToCleanup() { diff --git a/src/Rocketeer/Tasks/Closure.php b/src/Rocketeer/Tasks/Closure.php index a181dd784..8c56c69ee 100644 --- a/src/Rocketeer/Tasks/Closure.php +++ b/src/Rocketeer/Tasks/Closure.php @@ -10,19 +10,19 @@ namespace Rocketeer\Tasks; use Closure as AnonymousFunction; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** - * A Task that wraps around a closure and execute it + * a task that wraps around a closure and execute it * * @author Maxime Fabre */ -class Closure extends Task +class Closure extends AbstractTask { /** * A Closure to execute at runtime * - * @var Closure + * @var AnonymousFunction */ protected $closure; @@ -34,9 +34,32 @@ class Closure extends Task protected $stringTask; /** - * Create a Task from a Closure + * Get the name of the task * - * @param AnonymousFunction $closure + * @return string + */ + public function getName() + { + return parent::getName() ?: 'Arbitrary task'; + } + + /** + * Get what the task does + * + * @return string + */ + public function getDescription() + { + $flattened = (array) $this->getStringTask(); + $flattened = implode('/', $flattened); + + return parent::getDescription() ?: $flattened; + } + + /** + * Create a task from a Closure + * + * @param AnonymousFunction $closure */ public function setClosure(AnonymousFunction $closure) { @@ -44,9 +67,9 @@ public function setClosure(AnonymousFunction $closure) } /** - * Get the Task's Closure + * Get the task's Closure * - * @return Closure + * @return AnonymousFunction */ public function getClosure() { @@ -74,7 +97,7 @@ public function setStringTask($task) } /** - * Run the Task + * Run the task * * @return void */ diff --git a/src/Rocketeer/Tasks/CurrentRelease.php b/src/Rocketeer/Tasks/CurrentRelease.php index bd81e3a70..e7fb21055 100644 --- a/src/Rocketeer/Tasks/CurrentRelease.php +++ b/src/Rocketeer/Tasks/CurrentRelease.php @@ -10,46 +10,56 @@ namespace Rocketeer\Tasks; use DateTime; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Display what the current release is * * @author Maxime Fabre */ -class CurrentRelease extends Task +class CurrentRelease extends AbstractTask { - /** - * A description of what the Task does + /** + * The slug of the task + * + * @var string + */ + protected $name = 'Current'; + + /** + * A description of what the task does * * @var string */ protected $description = 'Display what the current release is'; /** - * Run the Task + * Run the task * - * @return void + * @return string|null */ public function execute() { // Get the current stage - $stage = $this->rocketeer->getStage(); + $stage = $this->connections->getStage(); $stage = $stage ? ' for stage '.$stage : ''; // Check if a release has been deployed already $currentRelease = $this->releasesManager->getCurrentRelease(); if (!$currentRelease) { - return $this->command->error('No release has yet been deployed'.$stage); + return $this->explainer->error('No release has yet been deployed'.$stage); } // Create state message $date = DateTime::createFromFormat('YmdHis', $currentRelease)->format('Y-m-d H:i:s'); $state = $this->runForCurrentRelease($this->scm->currentState()); - $message = sprintf('The current release' .$stage. ' is %s (%s deployed at %s)', $currentRelease, $state, $date); + $message = sprintf( + 'The current release'.$stage.' is %s (%s deployed at %s)', + $currentRelease, $state, $date + ); // Display current and past releases - $this->command->line($message); + $this->explainer->line($message); $this->displayReleases(); return $message; diff --git a/src/Rocketeer/Tasks/Dependencies.php b/src/Rocketeer/Tasks/Dependencies.php new file mode 100644 index 000000000..5d3f69189 --- /dev/null +++ b/src/Rocketeer/Tasks/Dependencies.php @@ -0,0 +1,38 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Tasks; + +use Rocketeer\Abstracts\AbstractTask; + +class Dependencies extends AbstractTask +{ + /** + * A description of what the task does + * + * @var string + */ + protected $description = 'Installs or update the dependencies on server'; + + /** + * Run the task + * + * @return boolean + */ + public function execute() + { + $method = $this->getOption('update', true) ? 'update' : 'install'; + $dependencies = $this->getStrategy('Dependencies'); + if (!$dependencies) { + return true; + } + + return $dependencies->$method(); + } +} diff --git a/src/Rocketeer/Tasks/Deploy.php b/src/Rocketeer/Tasks/Deploy.php index b7a4e20e7..f87998b3a 100644 --- a/src/Rocketeer/Tasks/Deploy.php +++ b/src/Rocketeer/Tasks/Deploy.php @@ -9,138 +9,94 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Deploy the website * * @author Maxime Fabre */ -class Deploy extends Task +class Deploy extends AbstractTask { /** - * Methods that can halt deployment + * The console command description. * - * @var array + * @var string */ - protected $halting = array(); + protected $description = 'Deploys the website'; /** - * Run the Task + * Run the task * - * @return void + * @return boolean|null */ public function execute() { - // Create halting events - $this->createEvents(); - - // Setup if necessary + // Check if server is ready for deployment if (!$this->isSetup()) { - $this->command->error('Server is not ready, running Setup task'); + $this->explainer->error('Server is not ready, running Setup task'); $this->executeTask('Setup'); } - // Update current release - $release = $this->releasesManager->updateCurrentRelease(); + // Check if local is ready for deployment + if (!$this->executeTask('Primer')) { + return $this->halt('Project is not ready for deploy. You were almost fired.'); + } + + // Setup the new release + $release = $this->releasesManager->getNextRelease(); - // Run halting methods - foreach ($this->halting as $method) { - if (!$this->fireEvent($method)) { - return false; - } + // Create release and set it up + $this->steps()->executeTask('CreateRelease'); + $this->steps()->executeTask('Dependencies'); - if (!$this->$method()) { - return $this->halt(); - } + if ($this->getOption('tests')) { + $this->steps()->executeTask('Test'); } - // Set permissions - $this->setApplicationPermissions(); + // Create release and set permissions + $this->steps()->setApplicationPermissions(); // Run migrations - $this->runMigrationsAndSeed(); + if ($this->getOption('migrate') || $this->getOption('seed')) { + $this->steps()->executeTask('Migrate'); + } // Synchronize shared folders and files - $this->syncSharedFolders(); + $this->steps()->syncSharedFolders(); // Run before-symlink events - if (!$this->fireEvent('before-symlink')) { - return $this->halt(); - } - - // Update symlink and mark release as valid - $this->updateSymlink(); - $this->releasesManager->markReleaseAsValid($release); - - $this->command->info('Successfully deployed release '.$release); - } + $this->steps()->fireEvent('before-symlink'); - //////////////////////////////////////////////////////////////////// - /////////////////////////////// SUBTASKS /////////////////////////// - //////////////////////////////////////////////////////////////////// + // Update symlink + $this->steps()->updateSymlink(); - /** - * Run PHPUnit tests - * - * @return void - */ - protected function checkTestsResults() - { - if ($this->getOption('tests') and !$this->runTests()) { - $this->command->error('Tests failed'); - - return false; + // Run the steps until one fails + if (!$this->runSteps()) { + return $this->halt(); } - return true; - } - - /** - * Run migrations and seed database - * - * @return void - */ - protected function runMigrationsAndSeed() - { - $seed = $this->getOption('seed'); + $this->releasesManager->markReleaseAsValid($release); - if ($this->getOption('migrate')) { - return $this->runMigrations($seed); - } elseif ($seed) { - return $this->runSeed(); - } + $this->explainer->line('Successfully deployed release '.$release); } //////////////////////////////////////////////////////////////////// /////////////////////////////// HELPERS //////////////////////////// //////////////////////////////////////////////////////////////////// - /** - * Create the events Deploy will run - * - * @return void - */ - protected function createEvents() - { - $strategy = $this->rocketeer->getOption('remote.strategy'); - $this->halting = array( - $strategy.'Repository', - 'runComposer', - 'checkTestsResults', - ); - } - /** * Set permissions for the folders used by the application * - * @return void + * @return boolean */ protected function setApplicationPermissions() { $files = (array) $this->rocketeer->getOption('remote.permissions.files'); - foreach ($files as $file) { + foreach ($files as &$file) { $this->setPermissions($file); } + + return true; } } diff --git a/src/Rocketeer/Tasks/Ignite.php b/src/Rocketeer/Tasks/Ignite.php index 52481ac51..0371473d8 100644 --- a/src/Rocketeer/Tasks/Ignite.php +++ b/src/Rocketeer/Tasks/Ignite.php @@ -9,17 +9,18 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Illuminate\Support\Arr; +use Rocketeer\Abstracts\AbstractTask; /** * A task to ignite Rocketeer * * @author Maxime Fabre */ -class Ignite extends Task +class Ignite extends AbstractTask { - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ @@ -78,16 +79,17 @@ protected function createOutsideConfiguration() protected function getConfigurationInformations() { // Replace credentials - $repositoryCredentials = $this->rocketeer->getCredentials(); - $name = basename($this->app['path.base']); + $repositoryCredentials = $this->connections->getRepositoryCredentials(); + $name = basename($this->app['path.base']); return array_merge( - $this->rocketeer->getConnectionCredentials(), + $this->connections->getServerCredentials(), array( - 'scm_repository' => $repositoryCredentials['repository'], - 'scm_username' => $repositoryCredentials['username'], - 'scm_password' => $repositoryCredentials['password'], - 'application_name' => $this->command->ask("What is your application's name ? (" .$name. ")", $name), + 'connection' => preg_replace('/#[0-9]+/', null, $this->connections->getConnection()), + 'scm_repository' => Arr::get($repositoryCredentials, 'repository'), + 'scm_username' => Arr::get($repositoryCredentials, 'username'), + 'scm_password' => Arr::get($repositoryCredentials, 'password'), + 'application_name' => $this->command->ask('What is your application\'s name ? ('.$name.')', $name), ) ); } diff --git a/src/Rocketeer/Tasks/Migrate.php b/src/Rocketeer/Tasks/Migrate.php new file mode 100644 index 000000000..5db14f074 --- /dev/null +++ b/src/Rocketeer/Tasks/Migrate.php @@ -0,0 +1,56 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Tasks; + +use Rocketeer\Abstracts\AbstractTask; + +class Migrate extends AbstractTask +{ + /** + * The console command description. + * + * @var string + */ + protected $description = 'Migrates and/or seed the database'; + + /** + * Run the task + * + * @return boolean|boolean[] + */ + public function execute() + { + $results = []; + + // Get strategy and options + $migrate = $this->getOption('migrate'); + $seed = $this->getOption('seed'); + $strategy = $this->getStrategy('Migrate'); + + // Cancel if nothing to run + if (!$strategy || (!$migrate && !$seed)) { + return true; + } + + // Migrate the database + if ($migrate) { + $this->explainer->line('Running outstanding migrations'); + $results[] = $strategy->migrate(); + } + + // Seed it + if ($seed) { + $this->explainer->line('Seeding database'); + $results[] = $strategy->seed(); + } + + return $results; + } +} diff --git a/src/Rocketeer/Tasks/Plugins/Installer.php b/src/Rocketeer/Tasks/Plugins/Installer.php new file mode 100644 index 000000000..5c67a0a9a --- /dev/null +++ b/src/Rocketeer/Tasks/Plugins/Installer.php @@ -0,0 +1,50 @@ +command->argument('package'); + $folder = $this->paths->getRocketeerConfigFolder(); + + // Add version if necessary + if (strpos($package, ':') === false) { + $package .= ':dev-master'; + } + + $command = $this->composer()->require($package, array( + '--working-dir' => $folder, + )); + + // Install plugin + $this->explainer->line('Installing '.$package); + $this->run($this->shellCommand($command)); + + // Prune duplicate Rocketeer + $this->files->deleteDirectory($folder.'/vendor/anahkiasen/rocketeer'); + } +} diff --git a/src/Rocketeer/Tasks/Rollback.php b/src/Rocketeer/Tasks/Rollback.php index 6f870e582..3d9ed0cee 100644 --- a/src/Rocketeer/Tasks/Rollback.php +++ b/src/Rocketeer/Tasks/Rollback.php @@ -9,40 +9,47 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Rollback to the previous release, or to a specific one * * @author Maxime Fabre */ -class Rollback extends Task +class Rollback extends AbstractTask { /** - * Run the Task + * The console command description. * - * @return void + * @var string + */ + protected $description = 'Rollback to the previous release, or to a specific one'; + + /** + * Run the task + * + * @return string|null */ public function execute() { // Get previous release $rollbackRelease = $this->getRollbackRelease(); if (!$rollbackRelease) { - $this->command->error('Rocketeer could not rollback as no releases have yet been deployed'); + return $this->explainer->error('Rocketeer could not rollback as no releases have yet been deployed'); } // If no release specified, display the available ones - if (array_get($this->command->option(), 'list')) { + if ($this->command->option('list')) { $releases = $this->releasesManager->getReleases(); $this->displayReleases(); // Get actual release name from date - $rollbackRelease = $this->command->ask('Which one do you want to go back to ? (0)', 0); + $rollbackRelease = $this->command->askWith('Which one do you want to go back to ?', 0); $rollbackRelease = $releases[$rollbackRelease]; } // Rollback release - $this->command->info('Rolling back to release '.$rollbackRelease); + $this->explainer->success('Rolling back to release '.$rollbackRelease); $this->updateSymlink($rollbackRelease); } @@ -53,11 +60,11 @@ public function execute() /** * Get the release to rollback to * - * @return integer + * @return integer|null */ protected function getRollbackRelease() { - $release = array_get($this->command->argument(), 'release'); + $release = $this->command->argument('release'); if (!$release) { $release = $this->releasesManager->getPreviousRelease(); } diff --git a/src/Rocketeer/Tasks/Setup.php b/src/Rocketeer/Tasks/Setup.php index e2b1ed9ec..b1cafc236 100644 --- a/src/Rocketeer/Tasks/Setup.php +++ b/src/Rocketeer/Tasks/Setup.php @@ -9,38 +9,38 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Set up the remote server for deployment * * @author Maxime Fabre */ -class Setup extends Task +class Setup extends AbstractTask { - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ protected $description = 'Set up the remote server for deployment'; /** - * Whether the Task needs to be run on each stage or globally + * Whether the task needs to be run on each stage or globally * * @var boolean */ public $usesStages = false; /** - * Run the Task + * Run the task * - * @return void + * @return string|false|null */ public function execute() { - // Check if requirments are met - if ($this->executeTask('Check') === false and !$this->getOption('pretend')) { + // Check if requirements are met + if ($this->executeTask('Check') === false && !$this->getOption('pretend')) { return false; } @@ -49,19 +49,19 @@ public function execute() $this->createStages(); // Set setup to true - $this->server->setValue('is_setup', true); + $this->localStorage->set('is_setup', true); // Get server informations - $this->command->comment('Getting some informations about the server'); - $this->server->getSeparator(); - $this->server->getLineEndings(); + $this->explainer->line('Getting some informations about the server'); + $this->localStorage->getSeparator(); + $this->localStorage->getLineEndings(); // Create confirmation message $application = $this->rocketeer->getApplicationName(); - $homeFolder = $this->rocketeer->getHomeFolder(); + $homeFolder = $this->paths->getHomeFolder(); $message = sprintf('Successfully setup "%s" at "%s"', $application, $homeFolder); - return $this->command->info($message); + return $this->explainer->success($message); } //////////////////////////////////////////////////////////////////// @@ -76,22 +76,22 @@ public function execute() protected function createStages() { // Get stages - $availableStages = $this->rocketeer->getStages(); - $originalStage = $this->rocketeer->getStage(); + $availableStages = $this->connections->getStages(); + $originalStage = $this->connections->getStage(); if (empty($availableStages)) { - $availableStages = array(null); + $availableStages = [null]; } // Create folders foreach ($availableStages as $stage) { - $this->rocketeer->setStage($stage); + $this->connections->setStage($stage); $this->createFolder('releases', true); $this->createFolder('current', true); $this->createFolder('shared', true); } if ($originalStage) { - $this->rocketeer->setStage($originalStage); + $this->connections->setStage($originalStage); } } } diff --git a/src/Rocketeer/Tasks/Subtasks/CreateRelease.php b/src/Rocketeer/Tasks/Subtasks/CreateRelease.php new file mode 100644 index 000000000..a07d297e8 --- /dev/null +++ b/src/Rocketeer/Tasks/Subtasks/CreateRelease.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Tasks\Subtasks; + +use Rocketeer\Abstracts\AbstractTask; + +/** + * Creates a new release on the server + * + * @author Maxime Fabre + */ +class CreateRelease extends AbstractTask +{ + /** + * A description of what the task does + * + * @var string + */ + protected $description = 'Creates a new release on the server'; + + /** + * Run the task + * + * @return string + */ + public function execute() + { + return $this->getStrategy('Deploy')->deploy(); + } +} diff --git a/src/Rocketeer/Tasks/Subtasks/Notify.php b/src/Rocketeer/Tasks/Subtasks/Notify.php new file mode 100644 index 000000000..2c6bbba98 --- /dev/null +++ b/src/Rocketeer/Tasks/Subtasks/Notify.php @@ -0,0 +1,113 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Tasks\Subtasks; + +use Illuminate\Support\Arr; +use Rocketeer\Abstracts\AbstractTask; +use Rocketeer\Plugins\AbstractNotifier; + +/** + * Notify a third-party service + * + * @author Maxime Fabre + */ +class Notify extends AbstractTask +{ + /** + * The message format + * + * @type AbstractNotifier + */ + protected $notifier; + + /** + * A description of what the task does + * + * @var string + */ + protected $description = 'Notify a third-party service'; + + /** + * Run the task + * + * @return string|null + */ + public function execute() + { + $hook = str_replace('deploy.', null, $this->event).'_deploy'; + + $this->prepareThenSend($hook); + } + + /** + * @param AbstractNotifier $notifier + */ + public function setNotifier($notifier) + { + $this->notifier = $notifier; + } + + //////////////////////////////////////////////////////////////////// + /////////////////////////////// MESSAGE //////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Get the message's components + * + * @return string[] + */ + protected function getComponents() + { + // Get user name + $user = $this->localStorage->get('notifier.name'); + if (!$user) { + $user = $this->command->ask('Who is deploying ?'); + $this->localStorage->set('notifier.name', $user); + } + + // Get what was deployed + $branch = $this->connections->getRepositoryBranch(); + $stage = $this->connections->getStage(); + $connection = $this->connections->getConnection(); + $server = $this->connections->getServer(); + + // Get hostname + $credentials = $this->connections->getServerCredentials($connection, $server); + $host = Arr::get($credentials, 'host'); + if ($stage) { + $connection = $stage.'@'.$connection; + } + + return compact('user', 'branch', 'connection', 'host'); + } + + /** + * Prepare and send a message + * + * @param string $message + * + * @return void + */ + public function prepareThenSend($message) + { + // Don't send a notification if pretending to deploy + if ($this->command->option('pretend')) { + return; + } + + // Build message + $message = $this->notifier->getMessageFormat($message); + $message = preg_replace('#\{([0-9])\}#', '%$1\$s', $message); + $message = vsprintf($message, $this->getComponents()); + + // Send it + $this->notifier->send($message); + } +} diff --git a/src/Rocketeer/Tasks/Subtasks/Primer.php b/src/Rocketeer/Tasks/Subtasks/Primer.php new file mode 100644 index 000000000..691913a12 --- /dev/null +++ b/src/Rocketeer/Tasks/Subtasks/Primer.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 Rocketeer\Tasks\Subtasks; + +use Rocketeer\Abstracts\AbstractTask; + +/** + * Executes some sanity-check commands before deploy + * + * @author Maxime Fabre + */ +class Primer extends AbstractTask +{ + /** + * A description of what the task does + * + * @var string + */ + protected $description = 'Run local checks to ensure deploy can proceed'; + + /** + * Whether to run the commands locally + * or on the server + * + * @type boolean + */ + protected $local = true; + + /** + * Whether the task needs to be run on each stage or globally + * + * @var boolean + */ + public $usesStages = false; + + /** + * Run the task + * + * @return boolean + */ + public function execute() + { + $tasks = $this->getHookedTasks('primer', [$this]); + if (!$tasks) { + return true; + } + + $this->run($tasks); + + return $this->status(); + } +} diff --git a/src/Rocketeer/Tasks/Teardown.php b/src/Rocketeer/Tasks/Teardown.php index 870221123..6146dcf5c 100644 --- a/src/Rocketeer/Tasks/Teardown.php +++ b/src/Rocketeer/Tasks/Teardown.php @@ -9,47 +9,50 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Remove the remote applications and existing caches * * @author Maxime Fabre */ -class Teardown extends Task +class Teardown extends AbstractTask { - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ protected $description = 'Remove the remote applications and existing caches'; /** - * Whether the Task needs to be run on each stage or globally + * Whether the task needs to be run on each stage or globally * * @var boolean */ public $usesStages = false; /** - * Run the Task + * Run the task * * @return void */ public function execute() { // Ask confirmation - $confirm = $this->command->confirm('This will remove all folders on the server, not just releases. Do you want to proceed ?'); + $confirm = $this->command->confirm( + 'This will remove all folders on the server, not just releases. Do you want to proceed ?' + ); + if (!$confirm) { return $this->command->info('Teardown aborted'); } // Remove remote folders - $this->removeFolder(); + $this->removeFolder($this->paths->getFolder()); // Remove deployments file - $this->server->deleteRepository(); + $this->localStorage->destroy(); $this->command->info('The application was successfully removed from the remote servers'); } diff --git a/src/Rocketeer/Tasks/Test.php b/src/Rocketeer/Tasks/Test.php index 7d8f632b7..af0445101 100644 --- a/src/Rocketeer/Tasks/Test.php +++ b/src/Rocketeer/Tasks/Test.php @@ -9,32 +9,34 @@ */ namespace Rocketeer\Tasks; -use Rocketeer\Traits\Task; +use Rocketeer\Abstracts\AbstractTask; /** * Run the tests on the server and displays the output * * @author Maxime Fabre */ -class Test extends Task +class Test extends AbstractTask { - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ protected $description = 'Run the tests on the server and displays the output'; /** - * Run the Task + * Run the task * - * @return void + * @return boolean */ public function execute() { - // Update repository - $this->command->info('Testing the application'); + $tester = $this->getStrategy('Test'); + if (!$tester) { + return true; + } - return $this->runTests(); + return $tester->test(); } } diff --git a/src/Rocketeer/Tasks/Update.php b/src/Rocketeer/Tasks/Update.php index 3242322e2..8b736d833 100644 --- a/src/Rocketeer/Tasks/Update.php +++ b/src/Rocketeer/Tasks/Update.php @@ -16,39 +16,51 @@ */ class Update extends Deploy { - /** - * A description of what the Task does + /** + * A description of what the task does * * @var string */ protected $description = 'Update the remote server without doing a new release'; /** - * Run the Task + * Run the task * - * @return void + * @return boolean|null */ public function execute() { + // Check if local is ready for deployment + if (!$this->executeTask('Primer')) { + return $this->halt('Project is not ready for deploy. You were almost fired.'); + } + // Update repository - $this->updateRepository(); + if (!$this->getStrategy('Deploy')->update()) { + return $this->halt(); + } // Recreate symlinks if necessary - $this->syncSharedFolders(); + $this->steps()->syncSharedFolders(); // Recompile dependencies and stuff - $this->runComposer(); + $this->steps()->executeTask('Dependencies'); // Set permissions - $this->setApplicationPermissions(); + $this->steps()->setApplicationPermissions(); // Run migrations - if ($this->getOption('migrate')) { - $this->runMigrations($this->getOption('seed')); + if ($this->getOption('migrate') || $this->getOption('seed')) { + $this->steps()->executeTask('Migrate'); + } + + // Run the steps + if (!$this->runSteps()) { + return $this->halt(); } // Clear cache - $this->runForCurrentRelease($this->artisan('cache:clear')); + $this->artisan()->runForCurrentRelease('clearCache'); $this->command->info('Successfully updated application'); } diff --git a/src/Rocketeer/TasksQueue.php b/src/Rocketeer/TasksQueue.php deleted file mode 100644 index 0217a1184..000000000 --- a/src/Rocketeer/TasksQueue.php +++ /dev/null @@ -1,315 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer; - -use Closure; -use Rocketeer\Traits\AbstractLocatorClass; -use Rocketeer\Traits\Task; - -/** - * Handles the building and execution of tasks - * - * @author Maxime Fabre - */ -class TasksQueue extends AbstractLocatorClass -{ - /** - * A list of Tasks to execute - * - * @var array - */ - protected $tasks; - - /** - * The Remote connection - * - * @var Connection - */ - protected $remote; - - /** - * The output of the queue - * - * @var array - */ - protected $output = array(); - - //////////////////////////////////////////////////////////////////// - ////////////////////////////// SHORTCUTS /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Execute Tasks on the default connection - * - * @param string|array|Closure $queue - * @param string|array $connections - * - * @return array - */ - public function execute($queue, $connections = null) - { - if ($connections) { - $this->rocketeer->setConnections($connections); - } - - $queue = (array) $queue; - $queue = $this->buildQueue($queue); - - return $this->run($queue); - } - - /** - * Execute Tasks on various connections - * - * @param string|array $connections - * @param string|array|Closure $queue - * - * @return array - */ - public function on($connections, $queue) - { - return $this->execute($queue, $connections); - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// QUEUE ///////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Run the queue - * - * Run an array of Tasks instances on the various - * connections and stages provided - * - * @param array $tasks An array of tasks - * - * @return array An array of output - */ - public function run(array $tasks) - { - // First we'll build the queue - $queue = $this->buildQueue($tasks); - - // Get the connections to execute the tasks on - $connections = (array) $this->rocketeer->getConnections(); - foreach ($connections as $connection) { - $this->rocketeer->setConnection($connection); - - // Check if we provided a stage - $stage = $this->getStage(); - $stages = $this->rocketeer->getStages(); - if ($stage and in_array($stage, $stages)) { - $stages = array($stage); - } - - // Run the Tasks on each stage - if (!empty($stages)) { - foreach ($stages as $stage) { - $this->runQueue($queue, $stage); - } - } else { - $this->runQueue($queue); - } - } - - return $this->output; - } - - /** - * Run the queue, taking into account the stage - * - * @param array $tasks - * @param string $stage - * - * @return boolean - */ - protected function runQueue($tasks, $stage = null) - { - foreach ($tasks as $task) { - $currentStage = $task->usesStages() ? $stage : null; - $this->rocketeer->setStage($currentStage); - - // Here we fire the task and if it was halted - // at any point, we cancel the whole queue - $state = $task->fire(); - $this->output[] = $state; - if ($task->wasHalted() or $state === false) { - $this->command->error('Deployment was canceled by task "'.$task->getName(). '"'); - return false; - } - } - - return true; - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// BUILDING /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Build a queue from a list of tasks - * - * Here we will take the various Tasks names, closures and string tasks - * and unify all of those to actual Task instances - * - * @param array $tasks - * - * @return array - */ - public function buildQueue(array $tasks) - { - foreach ($tasks as &$task) { - $task = $this->buildTask($task); - } - - return $tasks; - } - - /** - * Build a task from anything - * - * @param mixed $task - * @param string $name - * - * @return Task - */ - public function buildTask($task, $name = null) - { - // Check the handle if possible - if (is_string($task)) { - $handle = 'rocketeer.tasks.'.$task; - } - - // If we provided a Closure or a string command, build it - if ($task instanceof Closure or $this->isStringCommand($task)) { - $task = $this->buildTaskFromClosure($task); - } - - // Check for an existing container binding - elseif (isset($handle) and $this->app->bound($handle)) { - return $this->app[$handle]; - } - - // Build remaining tasks - if (!$task instanceof Task) { - $task = $this->buildTaskFromClass($task); - } - - // Set the task's name - if ($name) { - $task->setName($name); - } - - return $task; - } - - /** - * Build a Task from a Closure or a string command - * - * @param Closure|string $task - * - * @return Task - */ - public function buildTaskFromClosure($task) - { - // If the User provided a string to execute - // We'll build a closure from it - if ($this->isStringCommand($task)) { - $stringTask = $task; - $closure = function ($task) use ($stringTask) { - return $task->runForCurrentRelease($stringTask); - }; - - // If the User provided a Closure - } elseif ($task instanceof Closure) { - $closure = $task; - } - - // Now that we unified it all to a Closure, we build - // a Closure Task from there - $task = $this->buildTaskFromClass('Rocketeer\Tasks\Closure'); - $task->setClosure($closure); - - // If we had an original string used, store it on - // the task for easier reflection - if (isset($stringTask)) { - $task->setStringTask($stringTask); - } - - return $task; - } - - /** - * Build a Task from its name - * - * @param string $task - * - * @return Task - */ - public function buildTaskFromClass($task) - { - if (is_object($task) and $task instanceof Task) { - return $task; - } - - // Shortcut for calling Rocketeer Tasks - if (class_exists('Rocketeer\Tasks\\'.ucfirst($task))) { - $task = 'Rocketeer\Tasks\\'.ucfirst($task); - } - - // Cancel if class doesn't exist - if (!class_exists($task)) { - return $task; - } - - return new $task($this->app); - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// STAGES //////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Get the stage to execute Tasks in - * If null, execute on all stages - * - * @return string - */ - protected function getStage() - { - $stage = $this->rocketeer->getOption('stages.default'); - if ($this->hasCommand()) { - $stage = $this->command->option('stage') ?: $stage; - } - - // Return all stages if "all" - if ($stage == 'all') { - $stage = null; - } - - return $stage; - } - - //////////////////////////////////////////////////////////////////// - /////////////////////////////// HELPERS //////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Check if a string is a command or a task - * - * @param string $string - * - * @return boolean - */ - protected function isStringCommand($string) - { - return is_string($string) and !class_exists($string) and !$this->app->bound('rocketeer.tasks.'.$string); - } -} diff --git a/src/Rocketeer/Traits/AbstractLocatorClass.php b/src/Rocketeer/Traits/AbstractLocatorClass.php deleted file mode 100644 index 7e85d5a7f..000000000 --- a/src/Rocketeer/Traits/AbstractLocatorClass.php +++ /dev/null @@ -1,95 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Traits; - -use Illuminate\Container\Container; - -/** - * An abstract for Service Locator-based classes with adds - * a few shortcuts to Rocketeer classes - * - * @property ReleasesManager $releasesManager - * @property Rocketeer $rocketeer - * @property Server $server - * @property Illuminate\Remote\Connection $remote - * @property Traits\Scm $scm - * - * @author Maxime Fabre - */ -abstract class AbstractLocatorClass -{ - /** - * The IoC Container - * - * @var Container - */ - protected $app; - - /** - * Build a new Task - * - * @param Container $app - * @param Command|null $command - */ - public function __construct(Container $app) - { - $this->app = $app; - } - - /** - * Get an instance from the Container - * - * @param string $key - * - * @return object - */ - public function __get($key) - { - $shortcuts = array( - 'command' => 'rocketeer.command', - 'console' => 'rocketeer.console', - 'logs' => 'rocketeer.logs', - 'queue' => 'rocketeer.queue', - 'releasesManager' => 'rocketeer.releases', - 'rocketeer' => 'rocketeer.rocketeer', - 'scm' => 'rocketeer.scm', - 'server' => 'rocketeer.server', - 'tasks' => 'rocketeer.tasks', - ); - - // Replace shortcuts - if (array_key_exists($key, $shortcuts)) { - $key = $shortcuts[$key]; - } - - return $this->app[$key]; - } - - /** - * Set an instance on the Container - * - * @param string $key - * @param object $value - */ - public function __set($key, $value) - { - $this->app[$key] = $value; - } - - /** - * Check if the current instance has a Command bound - * - * @return boolean - */ - protected function hasCommand() - { - return $this->app->bound('rocketeer.command'); - } -} diff --git a/src/Rocketeer/Traits/BashModules/Binaries.php b/src/Rocketeer/Traits/BashModules/Binaries.php index ebaa31f6c..9924727da 100644 --- a/src/Rocketeer/Traits/BashModules/Binaries.php +++ b/src/Rocketeer/Traits/BashModules/Binaries.php @@ -10,243 +10,144 @@ namespace Rocketeer\Traits\BashModules; /** - * Handles findingand calling binaries + * Handles finding and calling binaries * * @author Maxime Fabre */ -class Binaries extends Filesystem +trait Binaries { //////////////////////////////////////////////////////////////////// /////////////////////////////// BINARIES /////////////////////////// //////////////////////////////////////////////////////////////////// /** - * Prefix a command with the right path to PHP + * Get an AnonymousBinary instance * - * @param string $command + * @param string $binary * - * @return string + * @return \Rocketeer\Abstracts\AbstractBinary|\Rocketeer\Abstracts\AbstractPackageManager */ - public function php($command = null) + public function binary($binary) { - $php = $this->which('php'); - - return trim($php. ' ' .$command); + return $this->builder->buildBinary($binary); } - // Artisan - //////////////////////////////////////////////////////////////////// - /** - * Prefix a command with the right path to Artisan - * - * @param string $command - * @param array $flags + * Prefix a command with the right path to PHP * - * @return string + * @return \Rocketeer\Binaries\Php */ - public function artisan($command = null, $flags = array()) + public function php() { - $artisan = $this->which('artisan') ?: 'artisan'; - foreach ($flags as $name => $value) { - $command .= ' --'.$name; - $command .= $value ? '="' .$value. '"' : ''; - } - - return $this->php($artisan. ' ' .$command); + return $this->binary('php'); } /** - * Run an artisan command - * - * @param string $command - * @param array $flags + * Prefix a command with the right path to Composer * - * @return string + * @return \Rocketeer\Binaries\Composer */ - public function runArtisan($command = null, $flags = array()) + public function composer() { - // Check if the seeds/migration need to be forced - $forced = array('migrate', 'db:seed'); - if (in_array($command, $forced) && $this->versionCheck('4.2.0')) { - $flags['force'] = ''; - } - - // Create full command - $command = $this->artisan($command, $flags); - - return $this->runForCurrentRelease($command); + return $this->binary('composer'); } /** - * Run any outstanding migrations - * - * @param boolean $seed Whether the database should also be seeded - * - * @return string + * @return \Rocketeer\Binaries\Phpunit */ - public function runMigrations($seed = false) + public function phpunit() { - $this->command->comment('Running outstanding migrations'); - $flags = $seed ? array('seed' => '') : array(); - - return $this->runArtisan('migrate', $flags); + return $this->binary('phpunit'); } /** - * Seed the database - * - * @param string $class A class to seed - * - * @return string + * @return \Rocketeer\Binaries\Artisan */ - public function runSeed($class = null) + public function artisan() { - $this->command->comment('Seeding database'); - $flags = $class ? array('class' => $class) : array(); - - return $this->runArtisan('db:seed', $flags); + return $this->binary('artisan'); } - // PHPUnit //////////////////////////////////////////////////////////////////// - - /** - * Run the application's tests - * - * @param string $arguments Additional arguments to pass to PHPUnit - * - * @return boolean - */ - public function runTests($arguments = null) - { - // Look for PHPUnit - $phpunit = $this->which('phpunit', $this->releasesManager->getCurrentReleasePath().'/vendor/bin/phpunit'); - if (!$phpunit) { - return true; - } - - // Run PHPUnit - $this->command->info('Running tests...'); - $output = $this->runForCurrentRelease(array( - $phpunit. ' --stop-on-failure '.$arguments, - )); - - return $this->checkStatus('Tests failed', $output, 'Tests passed successfully'); - } - - // Composer + /////////////////////////////// HELPERS //////////////////////////// //////////////////////////////////////////////////////////////////// /** - * Prefix a command with the right path to Composer + * Get the path to a binary * - * @param string $command + * @param string $binary The name of the binary + * @param string|null $fallback A fallback location + * @param boolean $prompt * * @return string */ - public function composer($command = null) + public function which($binary, $fallback = null, $prompt = true) { - $composer = $this->which('composer', $this->releasesManager->getCurrentReleasePath().'/composer.phar'); - - // Prepend PHP command - if (strpos($composer, 'composer.phar') !== false) { - $composer = $this->php($composer); - } - - return trim($composer. ' ' .$command); - } - - /** - * Run Composer on the folder - * - * @param boolean $force - * - * @return string - */ - public function runComposer($force = false) - { - if (!$this->server->usesComposer() and !$force) { - return true; - } - - // Find Composer - $composer = $this->composer(); - if (!$composer) { - return true; - } + $locations = array( + [$this->localStorage, 'get', 'paths.'.$binary], + [$this->paths, 'getPath', $binary], + [$this, 'runSilently', 'which '.$binary], + ); - // Get the Composer commands to run - $tasks = $this->rocketeer->getOption('remote.composer'); - if (!is_callable($tasks)) { - return true; + // Add fallback if provided + if ($fallback) { + $locations[] = [$this, 'runSilently', 'which '.$fallback]; } - // Cancel if no tasks to execute - $tasks = (array) $tasks($this); - if (empty($tasks)) { - return true; + // Add command prompt if possible + if ($this->hasCommand() && $prompt) { + $prompt = $binary.' could not be found, please enter the path to it'; + $locations[] = [$this->command, 'ask', $prompt]; } - // Run commands - $this->command->comment('Installing Composer dependencies'); - $this->runForCurrentRelease($tasks); - - return $this->checkStatus('Composer could not install dependencies'); + return $this->whichFrom($binary, $locations); } - //////////////////////////////////////////////////////////////////// - /////////////////////////////// HELPERS //////////////////////////// - //////////////////////////////////////////////////////////////////// - /** - * Get a binary + * Scan an array of locations for a binary * - * @param string $binary The name of the binary - * @param string $fallback A fallback location + * @param string $binary + * @param array $locations * * @return string */ - public function which($binary, $fallback = null) + protected function whichFrom($binary, array $locations) { - $location = false; - $locations = array( - array($this->server, 'getValue', 'paths.'.$binary), - array($this->rocketeer, 'getPath', $binary), - array($this, 'runSilently', 'which '.$binary), - ); - - // Add fallback if provided - if ($fallback) { - $locations[] = array($this, 'runSilently', 'which '.$fallback); - } - - // Add command prompt if possible - if ($this->hasCommand()) { - $prompt = $binary. ' could not be found, please enter the path to it'; - $locations[] = array($this->command, 'ask', $prompt); - } + $location = false; // Look in all the locations $tryout = 0; - while (!$location and array_key_exists($tryout, $locations)) { + while (!$location && array_key_exists($tryout, $locations)) { list($object, $method, $argument) = $locations[$tryout]; + // Execute method $location = $object->$method($argument); + + // Verify existence of returned path + if (strpos($location, 'not found') !== false || !$this->fileExists($location)) { + $location = null; + } + $tryout++; } - // Store found location - $this->server->setValue('paths.'.$binary, $location); + // Store found location or remove it if invalid + if (!$this->local) { + if ($location) { + $this->localStorage->set('paths.'.$binary, $location); + } else { + $this->localStorage->forget('paths.'.$binary); + } + } - return $location ?: false; + return $location ?: $binary; } /** * Check the Laravel version * - * @param string $version The version to check against - * @param string $operator The operator (default: '>=') + * @param string $version The version to check against + * @param string $operator The operator (default: '>=') * * @return bool */ diff --git a/src/Rocketeer/Traits/BashModules/Core.php b/src/Rocketeer/Traits/BashModules/Core.php index 639952f51..707bb7d62 100644 --- a/src/Rocketeer/Traits/BashModules/Core.php +++ b/src/Rocketeer/Traits/BashModules/Core.php @@ -9,35 +9,61 @@ */ namespace Rocketeer\Traits\BashModules; +use Closure; use Illuminate\Support\Str; -use Rocketeer\Traits\AbstractLocatorClass; +use Rocketeer\Traits\HasHistory; +use Rocketeer\Traits\HasLocator; /** * Core handling of running commands and returning output * * @author Maxime Fabre */ -class Core extends AbstractLocatorClass +trait Core { + use HasLocator; + use HasHistory; + /** - * An history of executed commands + * Whether to run the commands locally + * or on the server * - * @var array + * @type boolean */ - protected $history = array(); + protected $local = false; - //////////////////////////////////////////////////////////////////// - /////////////////////////////// HISTORY //////////////////////////// - //////////////////////////////////////////////////////////////////// + /** + * @param boolean $local + */ + public function setLocal($local) + { + $this->local = $local; + } /** - * Get the Task's history + * Get which Connection to call commands with * - * @return array + * @return \Illuminate\Remote\ConnectionInterface */ - public function getHistory() + public function getConnection() { - return $this->history; + return $this->local ? $this->app['remote.local'] : $this->remote; + } + + /** + * Run a series of commands in local + * + * @param Closure $callback + * + * @return boolean + */ + public function onLocal(Closure $callback) + { + $this->local = true; + $results = $callback($this); + $this->local = false; + + return $results; } //////////////////////////////////////////////////////////////////// @@ -47,42 +73,48 @@ public function getHistory() /** * Run actions on the remote server and gather the ouput * - * @param string|array $commands One or more commands - * @param boolean $silent Whether the command should stay silent no matter what - * @param boolean $array Whether the output should be returned as an array + * @param string|array $commands One or more commands + * @param boolean $silent Whether the command should stay silent no matter what + * @param boolean $array Whether the output should be returned as an array * - * @return string|array + * @return string|null */ public function run($commands, $silent = false, $array = false) { $commands = $this->processCommands($commands); $verbose = $this->getOption('verbose') && !$silent; + $pretend = $this->getOption('pretend'); - // Log the commands for pretend - if ($this->getOption('pretend') and !$silent) { - return $this->addCommandsToHistory($commands); + // Log the commands + if (!$silent) { + $this->toHistory($commands); + } + + // Display for pretend mode + if ($verbose || ($pretend && !$silent)) { + $this->toOutput($commands); + $flattened = implode(PHP_EOL.'$ ', $commands); + $this->command->line('$ '.$flattened.''); + + if ($pretend) { + return count($commands) == 1 ? $commands[0] : $commands; + } } // Run commands - $me = $this; $output = null; - $this->remote->run($commands, function ($results) use (&$output, $verbose, $me) { + $this->getConnection()->run($commands, function ($results) use (&$output, $verbose) { $output .= $results; if ($verbose) { - $me->remote->display(trim($results)); + $display = $this->cleanOutput($results); + $this->getConnection()->display(trim($display)); } }); // Process and log the output and commands $output = $this->processOutput($output, $array, true); - $this->logs->log($commands); - $this->logs->log($output); - - // Append output - if (!$silent) { - $this->history[] = $output; - } + $this->toOutput($output); return $output; } @@ -91,7 +123,7 @@ public function run($commands, $silent = false, $array = false) * Run a command get the last line output to * prevent noise * - * @param string|array $commands + * @param string $commands * * @return string */ @@ -107,17 +139,17 @@ public function runLast($commands) * Run a raw command, without any processing, and * get its output as a string or array * - * @param string|array $commands - * @param boolean $array Whether the output should be returned as an array - * @param boolean $trim Whether the output should be trimmed + * @param string $commands + * @param boolean $array Whether the output should be returned as an array + * @param boolean $trim Whether the output should be trimmed * - * @return string + * @return string|string[] */ public function runRaw($commands, $array = false, $trim = false) { // Run commands $output = null; - $this->remote->run($commands, function ($results) use (&$output) { + $this->getConnection()->run($commands, function ($results) use (&$output) { $output .= $results; }); @@ -130,10 +162,10 @@ public function runRaw($commands, $array = false, $trim = false) /** * Run commands silently * - * @param string|array $commands - * @param boolean $array + * @param string|array $commands + * @param boolean $array * - * @return string + * @return string|null */ public function runSilently($commands, $array = false) { @@ -143,10 +175,10 @@ public function runSilently($commands, $array = false) /** * Run commands in a folder * - * @param string $folder - * @param string|array $tasks + * @param string|null $folder + * @param string|array $tasks * - * @return string + * @return string|null */ public function runInFolder($folder = null, $tasks = array()) { @@ -156,34 +188,48 @@ public function runInFolder($folder = null, $tasks = array()) } // Prepend folder - array_unshift($tasks, 'cd '.$this->rocketeer->getFolder($folder)); + array_unshift($tasks, 'cd '.$this->paths->getFolder($folder)); return $this->run($tasks); } + /** + * Check the status of the last command + * + * @return bool + */ + public function status() + { + return $this->getOption('pretend') ? true : $this->getConnection()->status() == 0; + } + /** * Check the status of the last run command, return an error if any * - * @param string $error The message to display on error - * @param string $output The command's output - * @param string $success The message to display on success + * @param string $error The message to display on error + * @param string|null $output The command's output + * @param string|null $success The message to display on success * - * @return boolean|string + * @return boolean */ public function checkStatus($error, $output = null, $success = null) { // If all went well - if ($this->remote->status() == 0) { + if ($this->status()) { if ($success) { - $this->command->comment($success); + $this->explainer->success($success); } return $output || true; } - // Else - $this->command->error($error); - print $output.PHP_EOL; + // Else display the error + $error = sprintf('An error occured: "%s"', $error); + if ($output) { + $error .= ', while running:'.PHP_EOL.$output; + } + + $this->explainer->error($error); return false; } @@ -206,32 +252,6 @@ public function getTimestamp() return $timestamp; } - /** - * Get an option from the Command - * - * @param string $option - * - * @return string - */ - protected function getOption($option) - { - return $this->hasCommand() ? $this->command->option($option) : null; - } - - /** - * Add an array/command to the history - * - * @param string|array $commands - */ - protected function addCommandsToHistory($commands) - { - $this->command->line(implode(PHP_EOL, $commands)); - $commands = (sizeof($commands) == 1) ? $commands[0] : $commands; - $this->history[] = $commands; - - return $commands; - } - //////////////////////////////////////////////////////////////////// ///////////////////////////// PROCESSORS /////////////////////////// //////////////////////////////////////////////////////////////////// @@ -239,14 +259,16 @@ protected function addCommandsToHistory($commands) /** * Process an array of commands * - * @param string|array $commands + * @param string|array $commands * * @return array */ - protected function processCommands($commands) + public function processCommands($commands) { - $stage = $this->rocketeer->getStage(); - $separator = $this->server->getSeparator(); + $stage = $this->connections->getStage(); + $separator = $this->localStorage->getSeparator(); + $shell = $this->rocketeer->getOption('remote.shell'); + $shelled = $this->rocketeer->getOption('remote.shelled'); // Cast commands to array if (!is_array($commands)) { @@ -262,29 +284,63 @@ protected function processCommands($commands) } // Add stage flag to Artisan commands - if (Str::contains($command, 'artisan') and $stage) { - $command .= ' --env='.$stage; + if (Str::contains($command, 'artisan') && $stage) { + $command .= ' --env="'.$stage.'"'; } + // Create shell if asked + if ($shell && Str::contains($command, $shelled)) { + $command = $this->shellCommand($command); + } } return $commands; } + /** + * Clean the output of various intruding bits + * + * @param string $output + * + * @return string + */ + protected function cleanOutput($output) + { + return strtr($output, array( + 'stdin: is not a tty' => null, + )); + } + + /** + * Pass a command through shell execution + * + * @param string $command + * + * @return string + */ + protected function shellCommand($command) + { + return "bash --login -c '".$command."'"; + } + /** * Process the output of a command * - * @param string|array $output - * @param boolean $array Whether to return an array or a string - * @param boolean $trim Whether to trim the output or not + * @param string $output + * @param boolean $array Whether to return an array or a string + * @param boolean $trim Whether to trim the output or not * * @return string|array */ protected function processOutput($output, $array = false, $trim = true) { + // Remove polluting strings + $output = $this->cleanOutput($output); + // Explode output if necessary if ($array) { - $output = explode($this->server->getLineEndings(), $output); + $delimiter = $this->localStorage->getLineEndings() ?: PHP_EOL; + $output = explode($delimiter, $output); } // Trim output diff --git a/src/Rocketeer/Traits/BashModules/Filesystem.php b/src/Rocketeer/Traits/BashModules/Filesystem.php index e5f176222..a0b67c912 100644 --- a/src/Rocketeer/Traits/BashModules/Filesystem.php +++ b/src/Rocketeer/Traits/BashModules/Filesystem.php @@ -14,7 +14,7 @@ * * @author Maxime Fabre */ -class Filesystem extends Core +trait Filesystem { //////////////////////////////////////////////////////////////////// /////////////////////////////// COMMON ///////////////////////////// @@ -23,8 +23,8 @@ class Filesystem extends Core /** * Symlinks two folders * - * @param string $folder The folder in shared/ - * @param string $symlink The folder that will symlink to it + * @param string $folder The folder in shared/ + * @param string $symlink The folder that will symlink to it * * @return string */ @@ -47,13 +47,17 @@ public function symlink($folder, $symlink) /** * Move a file * - * @param string $origin - * @param string $destination + * @param string $origin + * @param string $destination * - * @return string + * @return string|null */ public function move($origin, $destination) { + if (!$this->fileExists($origin)) { + return; + } + return $this->fromTo('mv', $origin, $destination); } @@ -67,13 +71,13 @@ public function move($origin, $destination) */ public function copy($origin, $destination) { - return $this->fromTo('cp', $origin, $destination); + return $this->fromTo('cp -r', $origin, $destination); } /** * Get the contents of a directory * - * @param string $directory + * @param string $directory * * @return array */ @@ -85,13 +89,13 @@ public function listContents($directory) /** * Check if a file exists * - * @param string $file Path to the file + * @param string $file Path to the file * * @return boolean */ public function fileExists($file) { - $exists = $this->runRaw('[ -e ' .$file. ' ] && echo "true"'); + $exists = $this->runRaw('[ -e '.$file.' ] && echo "true"'); return trim($exists) == 'true'; } @@ -107,7 +111,7 @@ public function setPermissions($folder) { // Get path to folder $folder = $this->releasesManager->getCurrentReleasePath($folder); - $this->command->comment('Setting permissions for '.$folder); + $this->explainer->line('Setting permissions for '.$folder); // Get permissions options $callback = $this->rocketeer->getOption('remote.permissions.callback'); @@ -134,7 +138,7 @@ public function setPermissions($folder) */ public function getFile($file) { - return $this->remote->getString($file); + return $this->getConnection()->getString($file); } /** @@ -147,7 +151,25 @@ public function getFile($file) */ public function putFile($file, $contents) { - $this->remote->putString($file, $contents); + $this->getConnection()->putString($file, $contents); + } + + /** + * Upload a local file to remote + * + * @param string $file + * @param string|null $destination + */ + public function upload($file, $destination = null) + { + if (!file_exists($file)) { + return; + } + + // Get contents and destination + $destination = $destination ?: basename($file); + + $this->getConnection()->put($file, $destination); } //////////////////////////////////////////////////////////////////// @@ -157,8 +179,8 @@ public function putFile($file, $contents) /** * Create a folder in the application's folder * - * @param string $folder The folder to create - * @param boolean $recursive + * @param string|null $folder The folder to create + * @param boolean $recursive * * @return string The task */ @@ -166,19 +188,23 @@ public function createFolder($folder = null, $recursive = false) { $recursive = $recursive ? '-p ' : null; - return $this->run('mkdir '.$recursive.$this->rocketeer->getFolder($folder)); + return $this->run('mkdir '.$recursive.$this->paths->getFolder($folder)); } /** * Remove a folder in the application's folder * - * @param string $folder The folder to remove + * @param array|string|null $folders The folder to remove * * @return string The task */ - public function removeFolder($folder = null) + public function removeFolder($folders = null) { - return $this->run('rm -rf '.$this->rocketeer->getFolder($folder)); + $folders = (array) $folders; + $folders = array_map([$this->paths, 'getFolder'], $folders); + $folders = implode(' ', $folders); + + return $this->run('rm -rf '.$folders); } //////////////////////////////////////////////////////////////////// diff --git a/src/Rocketeer/Traits/BashModules/Flow.php b/src/Rocketeer/Traits/BashModules/Flow.php index d1cee0c67..1b3a0f60c 100644 --- a/src/Rocketeer/Traits/BashModules/Flow.php +++ b/src/Rocketeer/Traits/BashModules/Flow.php @@ -14,8 +14,15 @@ * * @author Maxime Fabre */ -class Flow extends Scm +trait Flow { + /** + * Whether the task needs to be run on each stage or globally + * + * @var boolean + */ + public $usesStages = true; + /** * Check if the remote server is setup * @@ -23,25 +30,25 @@ class Flow extends Scm */ public function isSetup() { - return $this->fileExists($this->rocketeer->getFolder('current')); + return $this->fileExists($this->paths->getFolder('current')); } /** - * Check if the Task uses stages + * Check if the task uses stages * * @return boolean */ public function usesStages() { - $stages = $this->rocketeer->getStages(); + $stages = $this->connections->getStages(); - return $this->usesStages and !empty($stages); + return $this->usesStages && !empty($stages); } /** * Run actions in the current release's folder * - * @param string|array $tasks One or more tasks + * @param string|array $tasks One or more tasks * * @return string */ @@ -57,20 +64,22 @@ public function runForCurrentRelease($tasks) /** * Sync the requested folders and files * - * @return void + * @return boolean */ protected function syncSharedFolders() { $shared = (array) $this->rocketeer->getOption('remote.shared'); - foreach ($shared as $file) { + foreach ($shared as &$file) { $this->share($file); } + + return true; } /** * Update the current symlink * - * @param integer $release A release to mark as current + * @param integer|null $release A release to mark as current * * @return string */ @@ -78,12 +87,12 @@ public function updateSymlink($release = null) { // If the release is specified, update to make it the current one if ($release) { - $this->releasesManager->updateCurrentRelease($release); + $this->releasesManager->setNextRelease($release); } // Get path to current/ folder and latest release $currentReleasePath = $this->releasesManager->getCurrentReleasePath(); - $currentFolder = $this->rocketeer->getFolder('current'); + $currentFolder = $this->paths->getFolder('current'); return $this->symlink($currentReleasePath, $currentFolder); } @@ -91,7 +100,7 @@ public function updateSymlink($release = null) /** * Share a file or folder between releases * - * @param string $file Path to the file in a release folder + * @param string $file Path to the file in a release folder * * @return string */ @@ -106,7 +115,7 @@ public function share($file) $this->move($currentFile, $sharedFile); } - $this->command->comment('Sharing file '.$currentFile); + $this->explainer->line('Sharing file '.$currentFile); return $this->symlink($sharedFile, $currentFile); } diff --git a/src/Rocketeer/Traits/BashModules/Scm.php b/src/Rocketeer/Traits/BashModules/Scm.php deleted file mode 100644 index 04ce66994..000000000 --- a/src/Rocketeer/Traits/BashModules/Scm.php +++ /dev/null @@ -1,102 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Traits\BashModules; - -/** - * Repository handling - * - * @author Maxime Fabre - */ -class Scm extends Binaries -{ - /** - * Copies the repository into a release folder and update it - * - * @param string $destination - * - * @return string - */ - public function copyRepository($destination = null) - { - // Get the previous release, if none clone from scratch - $previous = $this->releasesManager->getPreviousRelease(); - $previous = $this->releasesManager->getPathToRelease($previous); - if (!$previous) { - return $this->cloneRepository($destination); - } - - // Recompute destination - if (!$destination) { - $destination = $this->releasesManager->getCurrentReleasePath(); - } - - // Copy old release into new one - $this->command->info('Copying previous release "' .$previous. '" in "' .$destination. '"'); - $this->copy($previous, $destination); - - // Update repository - return $this->updateRepository(); - } - - /** - * Clone the repo into a release folder - * - * @param string $destination Where to clone to - * - * @return string - */ - public function cloneRepository($destination = null) - { - if (!$destination) { - $destination = $this->releasesManager->getCurrentReleasePath(); - } - - // Executing checkout - $this->command->info('Cloning repository in "' .$destination. '"'); - $output = $this->scm->execute('checkout', $destination); - $this->history[] = $output; - - // Cancel if failed and forget credentials - $success = $this->checkStatus('Unable to clone the repository', $output) !== false; - if (!$success) { - $this->server->forgetValue('credentials'); - - return false; - } - - // Deploy submodules - if ($this->rocketeer->getOption('scm.submodules')) { - $this->command->info('Initializing submodules if any'); - $this->runForCurrentRelease($this->scm->submodules()); - } - - return $success; - } - - /** - * Update the current release - * - * @param boolean $reset Whether the repository should be reset first - * - * @return string - */ - public function updateRepository($reset = true) - { - $this->command->info('Pulling changes'); - $tasks = array($this->scm->update()); - - // Reset if requested - if ($reset) { - array_unshift($tasks, $this->scm->reset()); - } - - return $this->runForCurrentRelease($tasks); - } -} diff --git a/src/Rocketeer/Traits/HasHistory.php b/src/Rocketeer/Traits/HasHistory.php new file mode 100644 index 000000000..cb452da65 --- /dev/null +++ b/src/Rocketeer/Traits/HasHistory.php @@ -0,0 +1,110 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Traits; + +use Illuminate\Support\Arr; + +/** + * A class that maintains an history of results/commands + * + * @property \Rocketeer\Services\History\History history + * @author Maxime Fabre + */ +trait HasHistory +{ + /** + * Get the class's history + * + * @param string|null $type + * + * @return array + */ + public function getHistory($type = null) + { + $handle = $this->getHistoryHandle(); + $history = $this->history[$handle]; + $history = Arr::get($history, $type); + + return $history; + } + + /** + * Append an entry to the history + * + * @param array|string|boolean $command + */ + public function toHistory($command) + { + $this->appendTo('history', $command); + } + + /** + * Append an entry to the output + * + * @param array|string|boolean $output + */ + public function toOutput($output) + { + $this->appendTo('output', $output); + } + + /** + * Get the class's handle in the history + * + * @return string + */ + protected function getHistoryHandle() + { + $handle = get_called_class(); + + // Create entry if it doesn't exist yet + if (!isset($this->history[$handle])) { + $this->history[$handle] = array( + 'history' => [], + 'output' => [], + ); + } + + return $handle; + } + + /** + * Append something to the history + * + * @param string $type + * @param string|array|boolean $command + */ + protected function appendTo($type, $command) + { + // Flatten one-liners + $command = (array) $command; + $command = array_values($command); + $flattened = count($command) == 1 ? $command[0] : $command; + + // Save to logs + if ($type == 'history') { + $command = array_map(function ($command) { + return '$ '.$command; + }, $command); + } + + $this->logs->log($command); + + // Get the various handles + $handle = $this->getHistoryHandle(); + $history = $this->getHistory(); + $timestamp = (string) microtime(true); + + // Set new history on correct handle + $history[$type][$timestamp] = $flattened; + + $this->history[$handle] = $history; + } +} diff --git a/src/Rocketeer/Traits/HasLocator.php b/src/Rocketeer/Traits/HasLocator.php new file mode 100644 index 000000000..5ada3039a --- /dev/null +++ b/src/Rocketeer/Traits/HasLocator.php @@ -0,0 +1,154 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace Rocketeer\Traits; + +use Illuminate\Container\Container; +use Illuminate\Support\Arr; + +/** + * A trait for Service Locator-based classes wich adds + * a few shortcuts to Rocketeer classes + * + * @property \Illuminate\Config\Repository config + * @property \Illuminate\Events\Dispatcher events + * @property \Illuminate\Filesystem\Filesystem files + * @property \Illuminate\Log\Writer log + * @property \Rocketeer\Abstracts\AbstractCommand command + * @property \Rocketeer\Bash bash + * @property \Rocketeer\Console\Console console + * @property \Rocketeer\Interfaces\ScmInterface scm + * @property \Rocketeer\Rocketeer rocketeer + * @property \Rocketeer\Services\Connections\ConnectionsHandler connections + * @property \Rocketeer\Services\Connections\RemoteHandler remote + * @property \Rocketeer\Services\CredentialsGatherer credentials + * @property \Rocketeer\Services\Display\QueueExplainer explainer + * @property \Rocketeer\Services\Display\QueueTimer timer + * @property \Rocketeer\Services\History\History history + * @property \Rocketeer\Services\Pathfinder paths + * @property \Rocketeer\Services\ReleasesManager releasesManager + * @property \Rocketeer\Services\Storages\LocalStorage localStorage + * @property \Rocketeer\Services\Tasks\TasksBuilder builder + * @property \Rocketeer\Services\Tasks\TasksQueue queue + * @property \Rocketeer\Services\TasksHandler tasks + * @author Maxime Fabre + */ +trait HasLocator +{ + /** + * The IoC Container + * + * @var Container + */ + protected $app; + + /** + * Build a new AbstractTask + * + * @param Container $app + */ + public function __construct(Container $app) + { + $this->app = $app; + } + + /** + * Get an instance from the Container + * + * @param string $key + * + * @return object + */ + public function __get($key) + { + $shortcuts = array( + 'bash' => 'rocketeer.bash', + 'builder' => 'rocketeer.builder', + 'command' => 'rocketeer.command', + 'connections' => 'rocketeer.connections', + 'console' => 'rocketeer.console', + 'credentials' => 'rocketeer.credentials', + 'explainer' => 'rocketeer.explainer', + 'history' => 'rocketeer.history', + 'localStorage' => 'rocketeer.storage.local', + 'logs' => 'rocketeer.logs', + 'paths' => 'rocketeer.paths', + 'queue' => 'rocketeer.queue', + 'releasesManager' => 'rocketeer.releases', + 'remote' => 'rocketeer.remote', + 'rocketeer' => 'rocketeer.rocketeer', + 'scm' => 'rocketeer.scm', + 'tasks' => 'rocketeer.tasks', + 'timer' => 'rocketeer.timer', + ); + + // Replace shortcuts + if (isset($shortcuts[$key])) { + $key = $shortcuts[$key]; + } + + return $this->app[$key]; + } + + /** + * Set an instance on the Container + * + * @param string $key + * @param object $value + */ + public function __set($key, $value) + { + $this->app[$key] = $value; + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// COMMAND /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Check if the current instance has a Command bound + * + * @return boolean + */ + protected function hasCommand() + { + return $this->app->bound('rocketeer.command'); + } + + /** + * Get an option from the Command + * + * @param string $option + * @param bool $loose + * + * @return string + */ + public function getOption($option, $loose = false) + { + if (!$this->hasCommand()) { + return null; + } + + return $loose ? Arr::get($this->command->option(), $option) : $this->command->option($option); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// CONTEXT /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Check if the class is executed inside a Laravel application + * + * @return boolean + */ + public function isInsideLaravel() + { + return $this->app->bound('path'); + } +} diff --git a/src/Rocketeer/Traits/Scm.php b/src/Rocketeer/Traits/Scm.php deleted file mode 100644 index 649538ab2..000000000 --- a/src/Rocketeer/Traits/Scm.php +++ /dev/null @@ -1,71 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ -namespace Rocketeer\Traits; - -/** - * An abstract class with helpers for SCM implementations - * - * @author Maxime Fabre - */ -abstract class Scm -{ - /** - * The IoC Container - * - * @var Container - */ - protected $app; - - /** - * Build a new Git instance - * - * @param Container $app - */ - public function __construct($app) - { - $this->app = $app; - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// HELPERS /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Returns a command with the SCM's binary - * - * @param string $commands... - * - * @return string - */ - public function getCommand() - { - $arguments = func_get_args(); - $arguments[0] = $this->binary. ' ' .$arguments[0]; - - return call_user_func_array('sprintf', $arguments); - } - - /** - * Execute one of the commands - * - * @param string $command - * @param string $arguments,... - * - * @return mixed - */ - public function execute() - { - $arguments = func_get_args(); - $command = array_shift($arguments); - $command = call_user_func_array(array($this, $command), $arguments); - - return $this->app['rocketeer.bash']->run($command); - } -} diff --git a/src/Rocketeer/Traits/StepsRunner.php b/src/Rocketeer/Traits/StepsRunner.php new file mode 100644 index 000000000..75c829ff5 --- /dev/null +++ b/src/Rocketeer/Traits/StepsRunner.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 Rocketeer\Traits; + +use Rocketeer\Services\StepsBuilder; + +/** + * Gives a class the ability to prepare steps to run and + * loop over them + * + * @author Maxime Fabre + */ +trait StepsRunner +{ + /** + * @type StepsBuilder + */ + protected $steps; + + /** + * @return StepsBuilder + */ + public function steps() + { + if (!$this->steps) { + $this->steps = new StepsBuilder; + } + + return $this->steps; + } + + /** + * Execute an array of calls until one halts + * + * @return boolean + */ + public function runSteps() + { + foreach ($this->steps()->pullSteps() as $step) { + list($method, $arguments) = $step; + $arguments = (array) $arguments; + + $results = call_user_func_array([$this, $method], $arguments); + $results = $results ?: $this->status(); + if (!$results) { + return false; + } + } + + return true; + } +} diff --git a/src/config/config.php b/src/config/config.php index a9642da73..0765eebdd 100644 --- a/src/config/config.php +++ b/src/config/config.php @@ -1,16 +1,27 @@ - '{application_name}', + // Plugins + //////////////////////////////////////////////////////////////////// + + // The plugins to load + 'plugins' => array( + // 'Rocketeer\Plugins\Slack\RocketeerSlack', + ), + // Logging //////////////////////////////////////////////////////////////////// // The schema to use to name log files - 'logs' => function ($rocketeer) { - return sprintf('%s-%s-%s.log', $rocketeer->getConnection(), $rocketeer->getStage(), date('Ymd')); + 'logs' => function (ConnectionsHandler $connections) { + return sprintf('%s-%s-%s.log', $connections->getConnection(), $connections->getStage(), date('Ymd')); }, // Remote access @@ -19,13 +30,13 @@ //////////////////////////////////////////////////////////////////// // The default remote connection(s) to execute tasks on - 'default' => array('production'), + 'default' => array('{connection}'), // The various connections you defined // You can leave all of this empty or remove it entirely if you don't want // to track files with credentials : Rocketeer will prompt you for your credentials // and store them locally - 'connections' => array( + 'connections' => array( 'production' => array( 'host' => '{host}', 'username' => '{username}', @@ -54,12 +65,10 @@ 'on' => array( // Stages configurations - 'stages' => array( - ), + 'stages' => array(), // Connections configuration - 'connections' => array( - ), + 'connections' => array(), ), diff --git a/src/config/hooks.php b/src/config/hooks.php index a0a07fb2a..05b52ab30 100644 --- a/src/config/hooks.php +++ b/src/config/hooks.php @@ -5,7 +5,7 @@ // Here you can define in the `before` and `after` array, Tasks to execute // before or after the core Rocketeer Tasks. You can either put a simple command, // a closure which receives a $task object, or the name of a class extending - // the Rocketeer\Traits\Task class + // the Rocketeer\Abstracts\AbstractTask class // // In the `custom` array you can list custom Tasks classes to be added // to Rocketeer. Those will then be available in the command line @@ -20,12 +20,12 @@ ), // Tasks to execute after the core Rocketeer Tasks - 'after' => array( + 'after' => array( 'setup' => array(), 'deploy' => array(), 'cleanup' => array(), ), - + // Custom Tasks to register with Rocketeer 'custom' => array(), diff --git a/src/config/paths.php b/src/config/paths.php index f366b416a..a5cbcf753 100644 --- a/src/config/paths.php +++ b/src/config/paths.php @@ -17,6 +17,6 @@ 'composer' => '', // Path to the Artisan CLI - 'artisan' => '', + 'artisan' => 'artisan', ); diff --git a/src/config/remote.php b/src/config/remote.php index 0e4c61756..340558715 100644 --- a/src/config/remote.php +++ b/src/config/remote.php @@ -1,59 +1,58 @@ - 'clone', +return array( // Remote server ////////////////////////////////////////////////////////////////////// // Variables about the servers. Those can be guessed but in // case of problem it's best to input those manually - 'variables' => array( + 'variables' => array( 'directory_separator' => '/', 'line_endings' => "\n", ), - // The process that will be executed by Composer - 'composer' => function ($task) { - return array( - // $task->composer('self-update'), - $task->composer('install --no-interaction --no-dev --prefer-dist'), - ); - }, - // The number of releases to keep at all times - 'keep_releases' => 4, + 'keep_releases' => 4, // Folders //////////////////////////////////////////////////////////////////// // The root directory where your applications will be deployed - 'root_directory' => '/home/www/', + // This path *needs* to start at the root, ie. start with a / + 'root_directory' => '/home/www/', // The folder the application will be cloned in // Leave empty to use `application_name` as your folder name - 'app_directory' => '', + 'app_directory' => '', // A list of folders/file to be shared between releases // Use this to list folders that need to keep their state, like // user uploaded data, file-based databases, etc. - 'shared' => array( + 'shared' => array( '{path.storage}/logs', '{path.storage}/sessions', ), - // Permissions + // Execution + ////////////////////////////////////////////////////////////////////// + + // If enabled will force a shell to be created + // which is required for some tools like RVM or NVM + 'shell' => false, + + // An array of commands to run under shell + 'shelled' => ['which', 'ruby', 'npm', 'bower', 'bundle', 'grunt'], + + // Permissions$ //////////////////////////////////////////////////////////////////// - 'permissions' => array( + 'permissions' => array( // The folders and files to set as web writable // You can pass paths in brackets, so {path.public} will return // the correct path to the public folder - 'files' => array( + 'files' => array( 'app/database/production.sqlite', '{path.storage}', '{path.public}', diff --git a/src/config/scm.php b/src/config/scm.php index 1a87d66b4..0cd2f404d 100644 --- a/src/config/scm.php +++ b/src/config/scm.php @@ -4,7 +4,7 @@ ////////////////////////////////////////////////////////////////////// // The SCM used (supported: "git", "svn") - 'scm' => 'git', + 'scm' => 'git', // The SSH/HTTPS address to your repository // Example: https://github.com/vendor/website.git @@ -24,7 +24,7 @@ // or not – this means a clone with just the latest state of your // application (no history) // If you're having problems cloning, try setting this to false - 'shallow' => true, + 'shallow' => true, // Recursively pull in submodules. Works only with GIT. 'submodules' => true, diff --git a/src/config/stages.php b/src/config/stages.php index 20ffb77d4..b581e142c 100644 --- a/src/config/stages.php +++ b/src/config/stages.php @@ -8,9 +8,10 @@ // Adding entries to this array will split the remote folder in stages // Like /var/www/yourapp/staging and /var/www/yourapp/production - 'stages' => array(), + 'stages' => array(), // The default stage to execute tasks on when --stage is not provided + // Falsey means all of them 'default' => '', ); diff --git a/src/config/strategies.php b/src/config/strategies.php new file mode 100644 index 000000000..82205d883 --- /dev/null +++ b/src/config/strategies.php @@ -0,0 +1,51 @@ + 'Php', + + // Which strategy to use to create a new release + 'deploy' => 'Clone', + + // Which strategy to use to test your application + 'test' => 'Phpunit', + + // Which strategy to use to migrate your database + 'migrate' => 'Artisan', + + // Which strategy to use to install your application's dependencies + 'dependencies' => 'Polyglot', + + // Execution hooks + ////////////////////////////////////////////////////////////////////// + + 'composer' => array( + 'install' => function (Composer $composer, $task) { + return $composer->install([], ['--no-interaction' => null, '--no-dev' => null, '--prefer-dist' => null]); + }, + 'update' => function (Composer $composer) { + return $composer->update(); + }, + ), + + // Here you can configure the Primer tasks + // which will run a set of commands on the local + // machine, determining whether the deploy can proceed + // or not + 'primer' => function (Primer $task) { + return array( + // $task->executeTask('Test'), + // $task->binary('grunt')->execute('lint'), + ); + }, + +); diff --git a/tests/Abstracts/AbstractBinaryTest.php b/tests/Abstracts/AbstractBinaryTest.php new file mode 100644 index 000000000..19d08fca6 --- /dev/null +++ b/tests/Abstracts/AbstractBinaryTest.php @@ -0,0 +1,23 @@ +mock('rocketeer.bash', 'Bash', function ($mock) { + return $mock->shouldReceive('run')->once()->withAnyArgs()->andReturnUsing(function ($arguments) { + return $arguments; + }); + }); + + $scm = new Git($this->app); + $command = $scm->run('checkout', $this->server); + $expected = $this->replaceHistoryPlaceholders(['git clone "{repository}" "{server}" --branch="master" --depth="1"']); + + $this->assertEquals($expected[0], $command); + } +} diff --git a/tests/Abstracts/AbstractStorageTest.php b/tests/Abstracts/AbstractStorageTest.php new file mode 100644 index 000000000..ed3e58c2f --- /dev/null +++ b/tests/Abstracts/AbstractStorageTest.php @@ -0,0 +1,43 @@ + 'caca']; + $this->localStorage->set($matcher); + $contents = $this->localStorage->get(); + unset($contents['hash']); + + $this->assertEquals($matcher, $contents); + } + + public function testCanGetValue() + { + $this->assertEquals('bar', $this->localStorage->get('foo')); + } + + public function testCanSetValue() + { + $this->localStorage->set('foo', 'baz'); + + $this->assertEquals('baz', $this->localStorage->get('foo')); + } + + public function testCanDestroy() + { + $this->localStorage->destroy(); + + $this->assertFalse($this->files->exists(__DIR__.'/_meta/deployments.json')); + } + + public function testCanFallbackIfFileDoesntExist() + { + $this->localStorage->destroy(); + + $this->assertEquals(null, $this->localStorage->get('foo')); + } +} diff --git a/tests/Abstracts/AbstractTaskTest.php b/tests/Abstracts/AbstractTaskTest.php new file mode 100644 index 000000000..2236b65c6 --- /dev/null +++ b/tests/Abstracts/AbstractTaskTest.php @@ -0,0 +1,141 @@ +task('Check', array( + 'verbose' => true, + )); + + ob_start(); + $task->run('ls'); + $output = ob_get_clean(); + + $this->assertContains('tests', $output); + } + + public function testCanPretendToRunTasks() + { + $task = $this->pretendTask(); + $commands = $task->run('ls'); + + $this->assertEquals('ls', $commands); + } + + public function testCanGetDescription() + { + $task = $this->task('Setup'); + + $this->assertNotNull($task->getDescription()); + } + + public function testCanFireEventsDuringTasks() + { + $this->expectOutputString('foobar'); + $this->swapConfig(['rocketeer::hooks' => []]); + + $this->tasks->listenTo('closure.test.foobar', function () { + echo 'foobar'; + }); + + $this->queue->execute(function ($task) { + $task->fireEvent('test.foobar'); + }, 'staging'); + } + + public function testTaskCancelsIfEventHalts() + { + $this->expectOutputString('abc'); + + $this->swapConfig(array( + 'rocketeer::hooks' => [], + )); + + $this->tasks->registerConfiguredEvents(); + $this->tasks->listenTo('deploy.before', array( + function () { + echo 'a'; + + return true; + }, + function () { + echo 'b'; + + return 'lol'; + }, + function () { + echo 'c'; + + return false; + }, + function () { + echo 'd'; + }, + )); + + $task = $this->pretendTask('Deploy'); + $task->fire(); + } + + public function testCanListenToSubtasks() + { + $this->swapConfig(array( + 'rocketeer::hooks' => [], + )); + + $this->tasks->listenTo('dependencies.before', ['ls']); + + $this->pretendTask('Deploy')->fire(); + + $history = $this->history->getFlattenedOutput(); + $this->assertHistory(array( + 'cd {server}/releases/{release}', + 'ls', + ), array_get($history, 3)); + } + + public function testDoesntDuplicateQueuesOnSubtasks() + { + $this->swapConfig(array( + 'rocketeer::default' => ['staging', 'production'], + )); + + $this->pretend(); + $this->queue->run('Deploy'); + + $this->assertCount(20, $this->history->getFlattenedHistory()); + } + + public function testCanHookIntoHaltingEvent() + { + $this->expectOutputString('halted'); + + $this->tasks->before('deploy', 'Rocketeer\Dummies\MyCustomHaltingTask'); + + $this->tasks->listenTo('deploy.halt', function () { + echo 'halted'; + }); + + $this->pretendTask('Deploy')->fire(); + } + + public function testCanDisplayReleasesTable() + { + $headers = ['#', 'Path', 'Deployed at', 'Status']; + $releases = array( + [0, 20000000000000, '1999-11-30 00:00:00', '✓'], + [1, 15000000000000, '1499-11-30 00:00:00', '✘'], + [2, 10000000000000, '0999-11-30 00:00:00', '✓'], + ); + + $this->app['rocketeer.command'] = $this->getCommand() + ->shouldReceive('table')->with($headers, $releases)->andReturn(null)->once() + ->mock(); + + $this->task('CurrentRelease')->execute(); + } +} diff --git a/tests/Abstracts/Strategies/AbstractStrategyTest.php b/tests/Abstracts/Strategies/AbstractStrategyTest.php new file mode 100644 index 000000000..06cbc7131 --- /dev/null +++ b/tests/Abstracts/Strategies/AbstractStrategyTest.php @@ -0,0 +1,16 @@ +app['path.base'] = realpath(__DIR__.'/../../..'); + + $this->usesComposer(false); + $strategy = $this->builder->buildStrategy('Dependencies', 'Composer'); + $this->assertTrue($strategy->isExecutable()); + } +} diff --git a/tests/BashTest.php b/tests/BashTest.php index 9f2bb9082..1a4031ed7 100644 --- a/tests/BashTest.php +++ b/tests/BashTest.php @@ -8,7 +8,10 @@ class BashTest extends RocketeerTestCase public function testBashIsCorrectlyComposed() { $contents = $this->task->runRaw('ls', true, true); + if (count($contents) !== 11) { + var_dump($contents); + } - $this->assertCount(12, $contents); + $this->assertCount(11, $contents); } } diff --git a/tests/Binaries/AnonymousBinaryTest.php b/tests/Binaries/AnonymousBinaryTest.php new file mode 100644 index 000000000..de432180e --- /dev/null +++ b/tests/Binaries/AnonymousBinaryTest.php @@ -0,0 +1,15 @@ +app); + $anonymous->setBinary('foobar'); + + $this->assertEquals('foobar foo bar --lol', $anonymous->foo('bar', '--lol')); + } +} diff --git a/tests/Binaries/ArtisanTest.php b/tests/Binaries/ArtisanTest.php new file mode 100644 index 000000000..b278dc5a8 --- /dev/null +++ b/tests/Binaries/ArtisanTest.php @@ -0,0 +1,16 @@ +binaries['php']; + $artisan = new Artisan($this->app); + + $commands = $artisan->migrate(); + $this->assertEquals($php.' artisan migrate', $commands); + } +} diff --git a/tests/Binaries/ComposerTest.php b/tests/Binaries/ComposerTest.php new file mode 100644 index 000000000..0766f7dd0 --- /dev/null +++ b/tests/Binaries/ComposerTest.php @@ -0,0 +1,16 @@ +app); + $composer->setBinary('composer.phar'); + + $this->assertEquals($this->binaries['php'].' composer.phar install', $composer->install()); + } +} diff --git a/tests/Binaries/PhpTest.php b/tests/Binaries/PhpTest.php new file mode 100644 index 000000000..8b5b46007 --- /dev/null +++ b/tests/Binaries/PhpTest.php @@ -0,0 +1,16 @@ +app); + $hhvm = $php->isHhvm(); + $defined = defined('HHVM_VERSION'); + + $this->assertEquals($defined, $hhvm); + } +} diff --git a/tests/Console/ConsoleTest.php b/tests/Console/ConsoleTest.php index 60f6d2d18..d818f5fab 100644 --- a/tests/Console/ConsoleTest.php +++ b/tests/Console/ConsoleTest.php @@ -7,7 +7,7 @@ class ConsoleTest extends RocketeerTestCase { public function testCanRunStandaloneConsole() { - $console = exec('php bin/rocketeer --version'); + $console = exec('php bin/rocketeer --version --no-ansi'); $this->assertContains('Rocketeer version', $console); } diff --git a/tests/Dummies/DummyBeforeAfterNotifier.php b/tests/Dummies/DummyBeforeAfterNotifier.php new file mode 100644 index 000000000..b607851a0 --- /dev/null +++ b/tests/Dummies/DummyBeforeAfterNotifier.php @@ -0,0 +1,33 @@ +halt(); + } +} diff --git a/tests/Dummies/MyCustomTask.php b/tests/Dummies/MyCustomTask.php index 393cf8d4e..bff08b8f6 100644 --- a/tests/Dummies/MyCustomTask.php +++ b/tests/Dummies/MyCustomTask.php @@ -1,9 +1,9 @@ igniter = new Igniter($this->app); - unset($this->app['path.base']); - } - - //////////////////////////////////////////////////////////////////// - //////////////////////////////// TESTS ///////////////////////////// - //////////////////////////////////////////////////////////////////// - - public function testDoesntRebindBasePath() - { - $base = 'src'; - $this->app->instance('path.base', $base); - $this->igniter->bindPaths(); - - $this->assertEquals($base, $this->app['path.base']); - } - - public function testCanBindBasePath() - { - $this->igniter->bindPaths(); - - $this->assertEquals(realpath(__DIR__.'/..'), $this->app['path.base']); - } - - public function testCanBindConfigurationPaths() - { - $this->igniter->bindPaths(); - - $root = realpath(__DIR__.'/..'); - $this->assertEquals($root.'/.rocketeer', $this->app['path.rocketeer.config']); - } - - public function testCanBindTasksAndEventsPaths() - { - $this->igniter->bindPaths(); - $this->igniter->exportConfiguration(); - - // Create some fake files - $root = realpath(__DIR__.'/../.rocketeer'); - $this->app['files']->put($root.'/events.php', ''); - $this->app['files']->makeDirectory($root.'/tasks'); - - $this->igniter->bindPaths(); - - $this->assertEquals($root.'/tasks', $this->app['path.rocketeer.tasks']); - $this->assertEquals($root.'/events.php', $this->app['path.rocketeer.events']); - } - - public function testCanExportConfiguration() - { - $this->igniter->bindPaths(); - $this->igniter->exportConfiguration(); - - $this->assertFileExists(__DIR__.'/../.rocketeer'); - } - - public function testCanReplaceStubsInConfigurationFile() - { - $this->igniter->bindPaths(); - $path = $this->igniter->exportConfiguration(); - $this->igniter->updateConfiguration($path, array('scm_username' => 'foobar')); - - $this->assertFileExists(__DIR__.'/../.rocketeer'); - $this->assertContains('foobar', file_get_contents(__DIR__.'/../.rocketeer/scm.php')); - } - - public function testCanSetCurrentApplication() - { - $this->mock('rocketeer.server', 'Server', function ($mock) { - return $mock->shouldReceive('setRepository')->once()->with('foobar'); - }); - - $this->igniter->bindPaths(); - $path = $this->igniter->exportConfiguration(); - $this->igniter->updateConfiguration($path, array('application_name' => 'foobar', 'scm_username' => 'foobar')); - - $this->assertFileExists(__DIR__.'/../.rocketeer'); - $this->assertContains('foobar', file_get_contents(__DIR__.'/../.rocketeer/config.php')); - } -} diff --git a/tests/MetaTest.php b/tests/MetaTest.php index 313d1f5aa..95c3336ad 100644 --- a/tests/MetaTest.php +++ b/tests/MetaTest.php @@ -12,7 +12,7 @@ public function testCanOverwriteTasksViaContainer() return new MyCustomTask($app); }); - $queue = $this->app['rocketeer.tasks']->on('production', array('cleanup'), $this->getCommand()); - $this->assertEquals(array('foobar'), $queue); + $this->queue->on('production', ['cleanup'], $this->getCommand()); + $this->assertEquals(['foobar'], $this->history->getFlattenedOutput()); } } diff --git a/tests/Plugins/AbstractNotifierTest.php b/tests/Plugins/AbstractNotifierTest.php new file mode 100644 index 000000000..43367f32d --- /dev/null +++ b/tests/Plugins/AbstractNotifierTest.php @@ -0,0 +1,77 @@ +swapConfig(array( + 'rocketeer::stages.stages' => array('staging', 'production'), + 'rocketeer::hooks' => array(), + 'rocketeer::connections' => array( + 'production' => array( + 'host' => 'foo.bar.com', + ), + ), + )); + $this->tasks->registerConfiguredEvents(); + + $this->notifier = new DummyNotifier($this->app); + $this->tasks->plugin($this->notifier); + } + + public function testCanAskForNameIfNoneProvided() + { + $this->expectOutputString('foobar finished deploying branch "master" on "staging@production" (foo.bar.com)'); + + $this->mockCommand([], ['ask' => 'foobar']); + $this->mock('rocketeer.storage.local', 'LocalStorage', function ($mock) { + return $mock + ->shouldIgnoreMissing() + ->shouldReceive('get')->with('connections') + ->shouldReceive('get')->with('notifier.name')->andReturn(null) + ->shouldReceive('set')->once()->with('notifier.name', 'foobar'); + }); + $this->mock('rocketeer.connections', 'ConnectionsHandler', function ($mock) { + return $mock + ->shouldReceive('getRepositoryBranch')->andReturn('master') + ->shouldReceive('getStage')->andReturn('staging') + ->shouldReceive('getConnection')->andReturn('production') + ->shouldReceive('getServer')->andReturn('0') + ->shouldReceive('getServerCredentials')->andReturn(['host' => 'foo.bar.com']); + }); + + $this->task('deploy')->fireEvent('before'); + } + + public function testCanAppendStageToDetails() + { + $this->expectOutputString('Jean Eude finished deploying branch "master" on "staging@production" (foo.bar.com)'); + $this->localStorage->set('notifier.name', 'Jean Eude'); + $this->tasks->registerConfiguredEvents(); + $this->connections->setStage('staging'); + + $this->task('Deploy')->fireEvent('before'); + } + + public function testCanSendDeploymentsNotifications() + { + $this->expectOutputString('Jean Eude finished deploying branch "master" on "production" (foo.bar.com)'); + $this->localStorage->set('notifier.name', 'Jean Eude'); + + $this->task('Deploy')->fireEvent('after'); + } + + public function testDoesntSendNotificationsInPretendMode() + { + $this->expectOutputString(''); + $this->localStorage->set('notifier.name', 'Jean Eude'); + + $this->pretendTask('Deploy')->fireEvent('after'); + } +} diff --git a/tests/Plugins/NotifierTest.php b/tests/Plugins/NotifierTest.php deleted file mode 100644 index 2c898a6b6..000000000 --- a/tests/Plugins/NotifierTest.php +++ /dev/null @@ -1,54 +0,0 @@ -swapConfig(array( - 'rocketeer::stages.stages' => array('staging', 'production'), - 'rocketeer::hooks' => array(), - 'rocketeer::connections' => array( - 'production' => array( - 'host' => 'foo.bar.com' - ), - ), - )); - $this->app['rocketeer.tasks']->registerConfiguredEvents(); - - $this->notifier = new DummyNotifier($this->app); - $this->app['rocketeer.tasks']->plugin($this->notifier); - } - - public function testCanAppendStageToDetails() - { - $this->expectOutputString('Jean Eude finished deploying branch "master" on "staging@production" (foo.bar.com)'); - $this->app['rocketeer.server']->setValue('notifier.name', 'Jean Eude'); - $this->app['rocketeer.rocketeer']->setStage('staging'); - $this->notifier = new DummyNotifier($this->app); - $this->app['rocketeer.tasks']->plugin($this->notifier); - - $this->task('Deploy')->fireEvent('after'); - } - - public function testCanSendDeploymentsNotifications() - { - $this->expectOutputString('Jean Eude finished deploying branch "master" on "production" (foo.bar.com)'); - $this->app['rocketeer.server']->setValue('notifier.name', 'Jean Eude'); - - $this->task('Deploy')->fireEvent('after'); - } - - public function testDoesntSendNotificationsInPretendMode() - { - $this->expectOutputString(''); - $this->app['rocketeer.server']->setValue('notifier.name', 'Jean Eude'); - - $this->pretendTask('Deploy')->fireEvent('after'); - } -} diff --git a/tests/ReleasesManagerTest.php b/tests/ReleasesManagerTest.php deleted file mode 100644 index 4f1dc89f6..000000000 --- a/tests/ReleasesManagerTest.php +++ /dev/null @@ -1,134 +0,0 @@ -app['rocketeer.releases']->getCurrentRelease(); - - $this->assertEquals(20000000000000, $currentRelease); - } - - public function testCanGetStateOfReleases() - { - $validation = $this->app['rocketeer.releases']->getValidationFile(); - - $this->assertEquals(array( - 10000000000000 => true, - 15000000000000 => false, - 20000000000000 => true, - ), $validation); - } - - public function testCanGetInvalidReleases() - { - $validation = $this->app['rocketeer.releases']->getInvalidReleases(); - - $this->assertEquals(array(1 => 15000000000000), $validation); - } - - public function testCanUpdateStateOfReleases() - { - $this->app['rocketeer.releases']->markReleaseAsValid(15000000000000); - $validation = $this->app['rocketeer.releases']->getValidationFile(); - - $this->assertEquals(array( - 10000000000000 => true, - 15000000000000 => true, - 20000000000000 => true, - ), $validation); - } - - public function testCanMarkReleaseAsValid() - { - $this->app['rocketeer.releases']->markReleaseAsValid(123456789); - $validation = $this->app['rocketeer.releases']->getValidationFile(); - - $this->assertEquals(array( - 10000000000000 => true, - 15000000000000 => false, - 20000000000000 => true, - 123456789 => true, - ), $validation); - } - - public function testCanGetCurrentReleaseFromServerIfUncached() - { - $this->mock('rocketeer.server', 'Server', function ($mock) { - return $mock - ->shouldReceive('getValue')->with('current_release.production')->once()->andReturn(null) - ->shouldReceive('setValue')->with('current_release.production', '20000000000000')->once() - ->shouldReceive('getSeparator')->andReturn('/') - ->shouldReceive('getLineEndings')->andReturn(PHP_EOL); - }); - - $currentRelease = $this->app['rocketeer.releases']->getCurrentRelease(); - - $this->assertEquals(20000000000000, $currentRelease); - } - - public function testCanGetReleasesPath() - { - $releasePath = $this->app['rocketeer.releases']->getReleasesPath(); - - $this->assertEquals($this->server.'/releases', $releasePath); - } - - public function testCanGetCurrentReleaseFolder() - { - $currentReleasePath = $this->app['rocketeer.releases']->getCurrentReleasePath(); - - $this->assertEquals($this->server.'/releases/20000000000000', $currentReleasePath); - } - - public function testCanGetReleases() - { - $releases = $this->app['rocketeer.releases']->getReleases(); - - $this->assertEquals(array(1 => 15000000000000, 0 => 20000000000000, 2 => 10000000000000), $releases); - } - - public function testCanGetDeprecatedReleases() - { - $releases = $this->app['rocketeer.releases']->getDeprecatedReleases(); - - $this->assertEquals(array(15000000000000, 10000000000000), $releases); - } - - public function testCanGetPreviousValidRelease() - { - $currentRelease = $this->app['rocketeer.releases']->getPreviousRelease(); - - $this->assertEquals(10000000000000, $currentRelease); - } - - public function testReturnsCurrentReleaseIfNoPreviousValidRelease() - { - file_put_contents($this->server.'/state.json', json_encode(array( - '10000000000000' => false, - '15000000000000' => false, - '20000000000000' => true, - ))); - - $currentRelease = $this->app['rocketeer.releases']->getPreviousRelease(); - - $this->assertEquals(20000000000000, $currentRelease); - } - - public function testCanUpdateCurrentRelease() - { - $this->app['rocketeer.releases']->updateCurrentRelease(30000000000000); - - $this->assertEquals(30000000000000, $this->app['rocketeer.server']->getValue('current_release.production')); - } - - public function testCanGetFolderInRelease() - { - $folder = $this->app['rocketeer.releases']->getCurrentReleasePath('{path.storage}'); - - $this->assertEquals($this->server.'/releases/20000000000000/app/storage', $folder); - } -} diff --git a/tests/RocketeerTest.php b/tests/RocketeerTest.php index 9ea59f0f9..3922d47b8 100644 --- a/tests/RocketeerTest.php +++ b/tests/RocketeerTest.php @@ -5,131 +5,9 @@ class RocketeerTest extends RocketeerTestCase { - //////////////////////////////////////////////////////////////////// - //////////////////////////////// TESTS ///////////////////////////// - //////////////////////////////////////////////////////////////////// - - public function testCanGetAvailableConnections() - { - $connections = $this->app['rocketeer.rocketeer']->getAvailableConnections(); - $this->assertEquals(array('production', 'staging'), array_keys($connections)); - - $this->app['rocketeer.server']->setValue('connections.custom.username', 'foobar'); - $connections = $this->app['rocketeer.rocketeer']->getAvailableConnections(); - $this->assertEquals(array('custom'), array_keys($connections)); - } - - public function testCanGetCurrentConnection() - { - $this->swapConfig(array('rocketeer::default' => 'foobar')); - $this->assertEquals('production', $this->app['rocketeer.rocketeer']->getConnection()); - - $this->swapConfig(array('rocketeer::default' => 'production')); - $this->assertEquals('production', $this->app['rocketeer.rocketeer']->getConnection()); - - $this->swapConfig(array('rocketeer::default' => 'staging')); - $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getConnection()); - } - - public function testCanChangeConnection() - { - $this->assertEquals('production', $this->app['rocketeer.rocketeer']->getConnection()); - - $this->app['rocketeer.rocketeer']->setConnection('staging'); - $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getConnection()); - - $this->app['rocketeer.rocketeer']->setConnections('staging,production'); - $this->assertEquals(array('staging', 'production'), $this->app['rocketeer.rocketeer']->getConnections()); - } - - public function testCanUseSshRepository() - { - $repository = 'git@github.com:'.$this->repository; - $this->expectRepositoryConfig($repository, '', ''); - - $this->assertEquals($repository, $this->app['rocketeer.rocketeer']->getRepository()); - } - - public function testCanUseHttpsRepository() - { - $this->expectRepositoryConfig('https://github.com/'.$this->repository, 'foobar', 'bar'); - - $this->assertEquals('https://foobar:bar@github.com/'.$this->repository, $this->app['rocketeer.rocketeer']->getRepository()); - } - - public function testCanUseHttpsRepositoryWithUsernameProvided() - { - $this->expectRepositoryConfig('https://foobar@github.com/'.$this->repository, 'foobar', 'bar'); - - $this->assertEquals('https://foobar:bar@github.com/'.$this->repository, $this->app['rocketeer.rocketeer']->getRepository()); - } - - public function testCanUseHttpsRepositoryWithOnlyUsernameProvided() - { - $this->expectRepositoryConfig('https://foobar@github.com/'.$this->repository, 'foobar', ''); - - $this->assertEquals('https://foobar@github.com/'.$this->repository, $this->app['rocketeer.rocketeer']->getRepository()); - } - - public function testCanCleanupProvidedRepositoryFromCredentials() - { - $this->expectRepositoryConfig('https://foobar@github.com/'.$this->repository, 'Anahkiasen', ''); - - $this->assertEquals('https://Anahkiasen@github.com/'.$this->repository, $this->app['rocketeer.rocketeer']->getRepository()); - } - - public function testCanUseHttpsRepositoryWithoutCredentials() - { - $this->expectRepositoryConfig('https://github.com/'.$this->repository, '', ''); - - $this->assertEquals('https://github.com/'.$this->repository, $this->app['rocketeer.rocketeer']->getRepository()); - } - - public function testCanCheckIfRepositoryNeedsCredentials() - { - $this->expectRepositoryConfig('https://github.com/'.$this->repository, '', ''); - $this->assertTrue($this->app['rocketeer.rocketeer']->needsCredentials()); - } - - public function testCangetRepositoryBranch() - { - $this->assertEquals('master', $this->app['rocketeer.rocketeer']->getRepositoryBranch()); - } - public function testCanGetApplicationName() { - $this->assertEquals('foobar', $this->app['rocketeer.rocketeer']->getApplicationName()); - } - - public function testCanGetHomeFolder() - { - $this->assertEquals($this->server.'', $this->app['rocketeer.rocketeer']->getHomeFolder()); - } - - public function testCanGetFolderWithStage() - { - $this->app['rocketeer.rocketeer']->setStage('test'); - - $this->assertEquals($this->server.'/test/current', $this->app['rocketeer.rocketeer']->getFolder('current')); - } - - public function testCanGetAnyFolder() - { - $this->assertEquals($this->server.'/current', $this->app['rocketeer.rocketeer']->getFolder('current')); - } - - public function testCanReplacePatternsInFolders() - { - $folder = $this->app['rocketeer.rocketeer']->getFolder('{path.storage}'); - - $this->assertEquals($this->server.'/app/storage', $folder); - } - - public function testCannotReplaceUnexistingPatternsInFolders() - { - $folder = $this->app['rocketeer.rocketeer']->getFolder('{path.foobar}'); - - $this->assertEquals($this->server.'/', $folder); + $this->assertEquals('foobar', $this->rocketeer->getApplicationName()); } public function testCanUseRecursiveStageConfiguration() @@ -139,49 +17,40 @@ public function testCanUseRecursiveStageConfiguration() 'rocketeer::on.stages.staging.scm.branch' => 'staging', )); - $this->assertEquals('master', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); - $this->app['rocketeer.rocketeer']->setStage('staging'); - $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); + $this->assertOptionValueEquals('master', 'scm.branch'); + $this->connections->setStage('staging'); + $this->assertOptionValueEquals('staging', 'scm.branch'); } public function testCanUseRecursiveConnectionConfiguration() { $this->swapConfig(array( - 'rocketeer::default' => 'production', + 'rocketeer::default' => 'production', 'rocketeer::scm.branch' => 'master', 'rocketeer::on.connections.staging.scm.branch' => 'staging', )); - $this->assertEquals('master', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); + $this->assertOptionValueEquals('master', 'scm.branch'); $this->swapConfig(array( - 'rocketeer::default' => 'staging', + 'rocketeer::default' => 'staging', 'rocketeer::scm.branch' => 'master', 'rocketeer::on.connections.staging.scm.branch' => 'staging', )); - $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getOption('scm.branch')); + $this->assertOptionValueEquals('staging', 'scm.branch'); } - //////////////////////////////////////////////////////////////////// - //////////////////////////////// HELPERS /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Make the config return specific SCM config - * - * @param string $repository - * @param string $username - * @param string $password - * - * @return void - */ - protected function expectRepositoryConfig($repository, $username, $password) + public function testRocketeerCanGuessWhichStageHesIn() { - $this->swapConfig(array( - 'rocketeer::scm' => array( - 'repository' => $repository, - 'username' => $username, - 'password' => $password, - ), - )); + $path = '/home/www/foobar/production/releases/12345678901234/app'; + $stage = Rocketeer::getDetectedStage('foobar', $path); + $this->assertEquals('production', $stage); + + $path = '/home/www/foobar/staging/releases/12345678901234/app'; + $stage = Rocketeer::getDetectedStage('foobar', $path); + $this->assertEquals('staging', $stage); + + $path = '/home/www/foobar/releases/12345678901234/app'; + $stage = Rocketeer::getDetectedStage('foobar', $path); + $this->assertEquals(false, $stage); } } diff --git a/tests/Scm/GitTest.php b/tests/Scm/GitTest.php index c4039ba60..18aefe09c 100644 --- a/tests/Scm/GitTest.php +++ b/tests/Scm/GitTest.php @@ -8,7 +8,7 @@ class GitTest extends RocketeerTestCase /** * The current SCM instance * - * @var Rocketeer\Scm\Git + * @var Git */ protected $scm; @@ -46,30 +46,34 @@ public function testCanGetCurrentBranch() public function testCanGetCheckout() { - $this->mock('rocketeer.rocketeer', 'Rocketeer', function ($mock) { + $this->mock('rocketeer.rocketeer', 'Rocketeer\Rocketeer', function ($mock) { + return $mock->shouldReceive('getOption')->once()->with('scm.shallow')->andReturn(true); + }); + $this->mock('rocketeer.connections', 'ConnectionsHandler', function ($mock) { return $mock - ->shouldReceive('getOption')->once()->with('scm.shallow')->andReturn(true) - ->shouldReceive('getRepository')->once()->andReturn('http://github.com/my/repository') + ->shouldReceive('getRepositoryEndpoint')->once()->andReturn('http://github.com/my/repository') ->shouldReceive('getRepositoryBranch')->once()->andReturn('develop'); }); $command = $this->scm->checkout($this->server); - $this->assertEquals('git clone --depth 1 -b develop "http://github.com/my/repository" ' .$this->server, $command); + $this->assertEquals('git clone "http://github.com/my/repository" "'.$this->server.'" --branch="develop" --depth="1"', $command); } public function testCanGetDeepClone() { - $this->mock('rocketeer.rocketeer', 'Rocketeer', function ($mock) { + $this->mock('rocketeer.rocketeer', 'Rocketeer\Rocketeer', function ($mock) { + return $mock->shouldReceive('getOption')->once()->with('scm.shallow')->andReturn(false); + }); + $this->mock('rocketeer.connections', 'ConnectionsHandler', function ($mock) { return $mock - ->shouldReceive('getOption')->once()->with('scm.shallow')->andReturn(false) - ->shouldReceive('getRepository')->once()->andReturn('http://github.com/my/repository') + ->shouldReceive('getRepositoryEndpoint')->once()->andReturn('http://github.com/my/repository') ->shouldReceive('getRepositoryBranch')->once()->andReturn('develop'); }); $command = $this->scm->checkout($this->server); - $this->assertEquals('git clone -b develop "http://github.com/my/repository" ' .$this->server, $command); + $this->assertEquals('git clone "http://github.com/my/repository" "'.$this->server.'" --branch="develop"', $command); } public function testCanGetReset() diff --git a/tests/Scm/SvnTest.php b/tests/Scm/SvnTest.php new file mode 100644 index 000000000..0490ee661 --- /dev/null +++ b/tests/Scm/SvnTest.php @@ -0,0 +1,95 @@ +scm = new Svn($this->app); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// TESTS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + public function testCanGetCheck() + { + $command = $this->scm->check(); + + $this->assertEquals('svn --version', $command); + } + + public function testCanGetCurrentState() + { + $command = $this->scm->currentState(); + + $this->assertEquals('svn info -r "HEAD" | grep "Revision"', $command); + } + + public function testCanGetCurrentBranch() + { + $command = $this->scm->currentBranch(); + + $this->assertEquals('echo trunk', $command); + } + + public function testCanGetCheckout() + { + $this->mock('rocketeer.connections', 'ConnectionsHandler', function ($mock) { + return $mock + ->shouldReceive('getRepositoryCredentials')->once()->andReturn(['username' => 'foo', 'password' => 'bar']) + ->shouldReceive('getRepositoryEndpoint')->once()->andReturn('http://github.com/my/repository') + ->shouldReceive('getRepositoryBranch')->once()->andReturn('develop'); + }); + + $command = $this->scm->checkout($this->server); + + $this->assertEquals('svn co http://github.com/my/repository/develop '.$this->server.' --non-interactive --username="foo" --password="bar"', $command); + } + + public function testCanGetDeepClone() + { + $this->mock('rocketeer.connections', 'ConnectionsHandler', function ($mock) { + return $mock + ->shouldReceive('getRepositoryCredentials')->once()->andReturn(['username' => 'foo', 'password' => 'bar']) + ->shouldReceive('getRepositoryEndpoint')->once()->andReturn('http://github.com/my/repository') + ->shouldReceive('getRepositoryBranch')->once()->andReturn('develop'); + }); + + $command = $this->scm->checkout($this->server); + + $this->assertEquals('svn co http://github.com/my/repository/develop '.$this->server.' --non-interactive --username="foo" --password="bar"', $command); + } + + public function testCanGetReset() + { + $command = $this->scm->reset(); + + $this->assertEquals("svn status -q | grep -v '^[~XI ]' | awk '{print $2;}' | xargs svn revert", $command); + } + + public function testCanGetUpdate() + { + $command = $this->scm->update(); + + $this->assertEquals('svn up --non-interactive', $command); + } + + public function testCanGetSubmodules() + { + $command = $this->scm->submodules(); + + $this->assertEmpty($command); + } +} diff --git a/tests/ServerTest.php b/tests/ServerTest.php deleted file mode 100644 index 24d269dc3..000000000 --- a/tests/ServerTest.php +++ /dev/null @@ -1,89 +0,0 @@ -app['path.storage'] = null; - $this->app->offsetUnset('path.storage'); - - new Server($this->app); - - $storage = $this->app['rocketeer.rocketeer']->getRocketeerConfigFolder(); - $exists = file_exists($storage); - $this->app['files']->deleteDirectory($storage); - $this->assertTrue($exists); - } - - public function testCanGetValueFromDeploymentsFile() - { - $this->assertEquals('bar', $this->app['rocketeer.server']->getValue('foo')); - } - - public function testCanSetValueInDeploymentsFile() - { - $this->app['rocketeer.server']->setValue('foo', 'baz'); - - $this->assertEquals('baz', $this->app['rocketeer.server']->getValue('foo')); - } - - public function testCandeleteRepository() - { - $this->app['rocketeer.server']->deleteRepository(); - - $this->assertFalse($this->app['files']->exists(__DIR__.'/_meta/deployments.json')); - } - - public function testCanFallbackIfFileDoesntExist() - { - $this->app['rocketeer.server']->deleteRepository(); - - $this->assertEquals(null, $this->app['rocketeer.server']->getValue('foo')); - } - - public function testCanGetLineEndings() - { - $this->app['rocketeer.server']->deleteRepository(); - - $this->assertEquals(PHP_EOL, $this->app['rocketeer.server']->getLineEndings()); - } - - public function testCanGetSeparators() - { - $this->app['rocketeer.server']->deleteRepository(); - - $this->assertEquals(DIRECTORY_SEPARATOR, $this->app['rocketeer.server']->getSeparator()); - } - - public function testCanComputeHashAccordingToContentsOfFiles() - { - $this->mock('files', 'Filesystem', function ($mock) { - return $mock - ->shouldReceive('put')->once() - ->shouldReceive('exists')->twice()->andReturn(false) - ->shouldReceive('glob')->once()->andReturn(array('foo', 'bar')) - ->shouldReceive('getRequire')->once()->with('foo')->andReturn(array('foo')) - ->shouldReceive('getRequire')->once()->with('bar')->andReturn(array('bar')); - }); - - $hash = $this->app['rocketeer.server']->getHash(); - - $this->assertEquals(md5('["foo"]["bar"]'), $hash); - } - - public function testCanCheckIfComposerIsNeeded() - { - $this->usesComposer(true); - $this->assertTrue($this->app['rocketeer.server']->usesComposer()); - - $this->usesComposer(false); - $this->assertFalse($this->app['rocketeer.server']->usesComposer()); - } -} diff --git a/tests/Services/Connections/ConnectionsHandlerTest.php b/tests/Services/Connections/ConnectionsHandlerTest.php new file mode 100644 index 000000000..264ace472 --- /dev/null +++ b/tests/Services/Connections/ConnectionsHandlerTest.php @@ -0,0 +1,179 @@ +connections->getAvailableConnections(); + $this->assertEquals(array('production', 'staging'), array_keys($connections)); + + $this->app['rocketeer.storage.local']->set('connections.custom.username', 'foobar'); + $connections = $this->connections->getAvailableConnections(); + $this->assertEquals(array('production', 'staging', 'custom'), array_keys($connections)); + } + + public function testCanGetCurrentConnection() + { + $this->swapConfig(array('rocketeer::default' => 'foobar')); + $this->assertConnectionEquals('production'); + + $this->swapConfig(array('rocketeer::default' => 'production')); + $this->assertConnectionEquals('production'); + + $this->swapConfig(array('rocketeer::default' => 'staging')); + $this->assertConnectionEquals('staging'); + } + + public function testCanChangeConnection() + { + $this->assertConnectionEquals('production'); + + $this->connections->setConnection('staging'); + $this->assertConnectionEquals('staging'); + + $this->connections->setConnections('staging,production'); + $this->assertEquals(array('staging', 'production'), $this->connections->getConnections()); + } + + public function testCanUseSshRepository() + { + $repository = 'git@github.com:'.$this->repository; + $this->expectRepositoryConfig($repository, '', ''); + + $this->assertRepositoryEquals($repository); + } + + public function testCanUseHttpsRepository() + { + $this->expectRepositoryConfig('https://github.com/'.$this->repository, 'foobar', 'bar'); + + $this->assertRepositoryEquals('https://foobar:bar@github.com/'.$this->repository); + } + + public function testCanUseHttpsRepositoryWithUsernameProvided() + { + $this->expectRepositoryConfig('https://foobar@github.com/'.$this->repository, 'foobar', 'bar'); + + $this->assertRepositoryEquals('https://foobar:bar@github.com/'.$this->repository); + } + + public function testCanUseHttpsRepositoryWithOnlyUsernameProvided() + { + $this->expectRepositoryConfig('https://foobar@github.com/'.$this->repository, 'foobar', ''); + + $this->assertRepositoryEquals('https://foobar@github.com/'.$this->repository); + } + + public function testCanCleanupProvidedRepositoryFromCredentials() + { + $this->expectRepositoryConfig('https://foobar@github.com/'.$this->repository, 'Anahkiasen', ''); + + $this->assertRepositoryEquals('https://Anahkiasen@github.com/'.$this->repository); + } + + public function testCanUseHttpsRepositoryWithoutCredentials() + { + $this->expectRepositoryConfig('https://github.com/'.$this->repository, '', ''); + + $this->assertRepositoryEquals('https://github.com/'.$this->repository); + } + + public function testCanCheckIfRepositoryNeedsCredentials() + { + $this->expectRepositoryConfig('https://github.com/'.$this->repository, '', ''); + $this->assertTrue($this->connections->needsCredentials()); + } + + public function testCangetRepositoryBranch() + { + $this->assertEquals('master', $this->connections->getRepositoryBranch()); + } + + public function testFillsConnectionCredentialsHoles() + { + $connections = $this->connections->getAvailableConnections(); + $this->assertArrayHasKey('production', $connections); + + $this->app['rocketeer.storage.local']->set('connections', array( + 'staging' => array( + 'host' => 'foobar', + 'username' => 'user', + 'password' => '', + 'keyphrase' => '', + 'key' => '/Users/user/.ssh/id_rsa', + 'agent' => '', + ), + )); + $connections = $this->connections->getAvailableConnections(); + $this->assertArrayHasKey('production', $connections); + } + + public function testCanCreateHandleForCurrent() + { + $handle = $this->connections->getHandle('foo', 2, 'staging'); + + $this->assertEquals('foo/2/staging', $handle); + } + + public function testDoesntDisplayServerNumberIfNotMultiserver() + { + $handle = $this->connections->getHandle('foo', 0, 'staging'); + + $this->assertEquals('foo/staging', $handle); + } + + public function testDoesntResetConnectionIfSameAsCurrent() + { + $this->mock('rocketeer.tasks', 'TasksHandler', function ($mock) { + return $mock + ->shouldReceive('registerConfiguredEvents')->once(); + }, false); + + $this->connections->setConnection('production'); + $this->connections->setConnection('production'); + $this->connections->setConnection('production'); + } + + public function testDoesntResetStageIfSameAsCurrent() + { + $this->mock('rocketeer.tasks', 'TasksHandler', function ($mock) { + return $mock + ->shouldReceive('registerConfiguredEvents')->once(); + }, false); + + $this->connections->setStage('foobar'); + $this->connections->setStage('foobar'); + $this->connections->setStage('foobar'); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// HELPERS /////////////////////////// + //////////////////////////////////////////////////////////////////// + + /** + * Make the config return specific SCM config + * + * @param string $repository + * @param string $username + * @param string $password + * + * @return void + */ + protected function expectRepositoryConfig($repository, $username, $password) + { + $this->swapConfig(array( + 'rocketeer::scm' => array( + 'repository' => $repository, + 'username' => $username, + 'password' => $password, + ), + )); + } +} diff --git a/tests/Services/Connections/LocalConnectionTest.php b/tests/Services/Connections/LocalConnectionTest.php new file mode 100644 index 000000000..177f26fc9 --- /dev/null +++ b/tests/Services/Connections/LocalConnectionTest.php @@ -0,0 +1,16 @@ +task; + $task->setLocal(true); + $task->run('ls'); + + $this->assertTrue($task->status()); + } +} diff --git a/tests/Services/Connections/RemoteHandlerTest.php b/tests/Services/Connections/RemoteHandlerTest.php new file mode 100644 index 000000000..46281dd42 --- /dev/null +++ b/tests/Services/Connections/RemoteHandlerTest.php @@ -0,0 +1,115 @@ +handler = new RemoteHandler($this->app); + unset($this->app['rocketeer.command']); + } + + public function testCanCreateConnection() + { + $this->swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array( + 'host' => 'foobar.com', + 'username' => 'foobar', + 'password' => 'foobar', + ), + ), + )); + + $connection = $this->handler->connection(); + + $this->assertInstanceOf('Rocketeer\Services\Connections\Connection', $connection); + $this->assertEquals('production', $connection->getName()); + $this->assertEquals('foobar', $connection->getUsername()); + } + + public function testThrowsExceptionIfMissingCredentials() + { + $this->setExpectedException('Rocketeer\Exceptions\MissingCredentialsException'); + + $this->swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array( + 'host' => 'foobar.com', + 'username' => 'foobar', + ), + ), + )); + + $this->handler->connection(); + } + + public function testThrowsExceptionIfMissingInformations() + { + $this->setExpectedException('Rocketeer\Exceptions\MissingCredentialsException'); + + $this->swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array( + 'username' => 'foobar', + 'password' => 'foobar', + ), + ), + )); + + $this->handler->connection(); + } + + public function testCachesConnections() + { + $this->swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array( + 'host' => 'foobar.com', + 'username' => 'foobar', + 'password' => 'foobar', + ), + ), + )); + + $connection = $this->handler->connection(); + $this->assertInstanceOf('Rocketeer\Services\Connections\Connection', $connection); + $this->assertEquals('production', $connection->getName()); + + $this->swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array(), + ), + )); + + $connection = $this->handler->connection(); + $this->assertInstanceOf('Rocketeer\Services\Connections\Connection', $connection); + $this->assertEquals('production', $connection->getName()); + } + + public function testThrowsExceptionIfUnableToConnect() + { + $this->setExpectedException('Rocketeer\Exceptions\ConnectionException'); + + $this->swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array( + 'host' => 'foobar.com', + 'username' => 'foobar', + 'password' => 'foobar', + ), + ), + )); + + $this->handler->run('ls'); + } +} diff --git a/tests/Services/CredentialsGathererTest.php b/tests/Services/CredentialsGathererTest.php new file mode 100644 index 000000000..43afc54df --- /dev/null +++ b/tests/Services/CredentialsGathererTest.php @@ -0,0 +1,244 @@ +repository = 'git@github.com:Anahkiasen/rocketeer.git'; + $this->username = 'Anahkiasen'; + $this->password = 'foobar'; + $this->host = 'some.host'; + } + + public function testIgnoresPlaceholdersWhenFillingCredentials() + { + $this->mockAnswers(array( + 'No repository is set for [repository]' => $this->repository, + 'No username is set for [repository]' => $this->username, + 'No password is set for [repository]' => $this->password, + )); + $this->command->shouldReceive('option')->andReturn(null); + + $this->givenConfiguredRepositoryCredentials(['repository' => '{foobar}']); + + $this->assertStoredCredentialsEquals(array( + 'repository' => $this->repository, + 'username' => $this->username, + 'password' => $this->password, + )); + + $this->credentials->getRepositoryCredentials(); + } + + public function testCanGetRepositoryCredentials() + { + $this->mockAnswers(array( + 'No repository is set for [repository]' => $this->repository, + 'No username is set for [repository]' => $this->username, + 'No password is set for [repository]' => $this->password, + )); + $this->command->shouldReceive('option')->andReturn(null); + + $this->givenConfiguredRepositoryCredentials([]); + + $this->assertStoredCredentialsEquals(array( + 'repository' => $this->repository, + 'username' => $this->username, + 'password' => $this->password, + )); + + $this->credentials->getRepositoryCredentials(); + } + + public function testDoesntAskForRepositoryCredentialsIfUneeded() + { + $this->mockAnswers(); + $this->command->shouldReceive('option')->andReturn(null); + + $this->givenConfiguredRepositoryCredentials([ + 'repository' => $this->repository, + 'username' => null, + 'password' => null, + ], false); + $this->assertStoredCredentialsEquals(array( + 'repository' => $this->repository, + 'username' => null, + 'password' => null, + )); + + $this->credentials->getRepositoryCredentials(); + } + + public function testCanFillRepositoryCredentialsIfNeeded() + { + $this->mockAnswers(array( + 'No username is set for [repository]' => $this->username, + 'No password is set for [repository]' => null, + )); + $this->command->shouldReceive('option')->andReturn(null); + + $this->givenConfiguredRepositoryCredentials(['repository' => $this->repository], true); + + $this->assertStoredCredentialsEquals(array( + 'repository' => $this->repository, + 'username' => 'Anahkiasen', + 'password' => null, + )); + + $this->credentials->getRepositoryCredentials(); + } + + public function testCanGetServerCredentialsIfNoneDefined() + { + $this->swapConfig(array( + 'remote.connections' => [], + )); + + $this->mockAnswers(array( + 'No host is set for [production]' => $this->host, + 'No username is set for [production]' => $this->username, + 'No password is set for [production]' => $this->password, + )); + + $this->command->shouldReceive('askWith')->with('No connections have been set, please create one:', 'production')->andReturn('production'); + $this->command->shouldReceive('askWith')->with( + 'No password or SSH key is set for [production], which would you use?', + 'key', ['key', 'password'] + )->andReturn('password'); + $this->command->shouldReceive('option')->andReturn(null); + + $this->credentials->getServerCredentials(); + + $credentials = $this->connections->getServerCredentials('production', 0); + $this->assertEquals(array( + 'host' => $this->host, + 'username' => $this->username, + 'password' => $this->password, + 'keyphrase' => null, + 'key' => null, + 'agent' => null, + ), $credentials); + } + + public function testCanPassCredentialsAsFlags() + { + $this->swapConfig(array( + 'remote.connections' => [], + )); + + $this->mockAnswers(array( + 'No username is set for [production]' => $this->username, + )); + + $this->command->shouldReceive('askWith')->with('No connections have been set, please create one:', 'production')->andReturn('production'); + $this->command->shouldReceive('askWith')->with( + 'No password or SSH key is set for [production], which would you use?', + 'key', ['key', 'password'] + )->andReturn('password'); + $this->command->shouldReceive('option')->with('host')->andReturn($this->host); + $this->command->shouldReceive('option')->with('password')->andReturn($this->password); + $this->command->shouldReceive('option')->andReturn(null); + + $this->credentials->getServerCredentials(); + + $credentials = $this->connections->getServerCredentials('production', 0); + $this->assertEquals(array( + 'host' => $this->host, + 'username' => $this->username, + 'password' => $this->password, + 'keyphrase' => null, + 'key' => null, + 'agent' => null, + ), $credentials); + } + + public function testCanGetCredentialsForSpecifiedConnection() + { + $key = $this->paths->getDefaultKeyPath(); + $this->mockAnswers(array( + 'No host is set for [staging/0]' => $this->host, + 'No username is set for [staging/0]' => $this->username, + 'If a keyphrase is required, provide it' => 'KEYPHRASE', + )); + + $this->command->shouldReceive('option')->with('on')->andReturn('staging'); + $this->command->shouldReceive('option')->andReturn(null); + $this->command->shouldReceive('askWith')->with( + 'Please enter the full path to your key', $key + )->andReturn($key); + $this->command->shouldReceive('askWith')->with( + 'No password or SSH key is set for [staging/0], which would you use?', + 'key', ['key', 'password'] + )->andReturn('key'); + + $this->credentials->getServerCredentials(); + + $credentials = $this->connections->getServerCredentials('staging', 0); + $this->assertEquals(array( + 'host' => $this->host, + 'username' => $this->username, + 'password' => null, + 'keyphrase' => 'KEYPHRASE', + 'key' => $key, + 'agent' => null, + ), $credentials); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Mock a set of question/answers + * + * @param array $answers + */ + protected function mockAnswers($answers = array()) + { + $this->mock('rocketeer.command', 'Command', function ($mock) use ($answers) { + if (!$answers) { + return $mock->shouldReceive('ask')->never(); + } + + foreach ($answers as $question => $answer) { + $question = strpos($question, 'is set for') !== false ? $question.', please provide one:' : $question; + $method = strpos($question, 'password') !== false ? 'askSecretly' : 'askWith'; + $mock = $mock->shouldReceive($method)->with($question)->andReturn($answer); + } + + return $mock; + }); + } + + /** + * Assert a certain set of credentials are saved to storage + * + * @param array $credentials + */ + protected function assertStoredCredentialsEquals(array $credentials) + { + $this->mock('rocketeer.storage.local', 'LocalStorage', function ($mock) use ($credentials) { + return $mock->shouldReceive('set')->with('credentials', $credentials); + }); + } + + /** + * @param array $credentials + * @param boolean $need + */ + protected function givenConfiguredRepositoryCredentials(array $credentials, $need = false) + { + $this->mock('rocketeer.connections', 'ConnectionsHandler', function ($mock) use ($need, $credentials) { + return $mock + ->shouldReceive('needsCredentials')->andReturn($need) + ->shouldReceive('getRepositoryCredentials')->andReturn($credentials); + }); + } +} diff --git a/tests/Services/History/HistoryTest.php b/tests/Services/History/HistoryTest.php new file mode 100644 index 000000000..cffc8181f --- /dev/null +++ b/tests/Services/History/HistoryTest.php @@ -0,0 +1,32 @@ +bash->toHistory('foo'); + usleep($this->sleep); + $this->bash->toHistory(['bar', 'baz']); + + $history = $this->history->getFlattenedHistory(); + $this->assertEquals(['foo', ['bar', 'baz']], $history); + } + + public function testCanGetFlattenedOutput() + { + $this->bash->toOutput('foo'); + usleep($this->sleep); + $this->bash->toOutput(['bar', 'baz']); + + $history = $this->history->getFlattenedOutput(); + $this->assertEquals(['foo', ['bar', 'baz']], $history); + } +} diff --git a/tests/LogsHandlerTest.php b/tests/Services/History/LogsHandlerTest.php similarity index 52% rename from tests/LogsHandlerTest.php rename to tests/Services/History/LogsHandlerTest.php index 884ac1d10..d3ebd6212 100644 --- a/tests/LogsHandlerTest.php +++ b/tests/Services/History/LogsHandlerTest.php @@ -1,5 +1,5 @@ app['rocketeer.logs']->getCurrentLogsFile(); + $logs = $this->logs->getCurrentLogsFile(); $this->assertEquals($this->server.'/logs/production-.log', $logs); - $this->app['rocketeer.rocketeer']->setConnection('staging'); - $this->app['rocketeer.rocketeer']->setStage('foobar'); - $logs = $this->app['rocketeer.logs']->getCurrentLogsFile(); + $this->connections->setConnection('staging'); + $this->connections->setStage('foobar'); + $logs = $this->logs->getCurrentLogsFile(); $this->assertEquals($this->server.'/logs/staging-foobar.log', $logs); } public function testCanLogInformations() { - $this->app['rocketeer.logs']->log('foobar', 'error'); - $logs = $this->app['rocketeer.logs']->getCurrentLogsFile(); + $this->logs->log('foobar'); + $this->logs->write(); + $logs = $this->logs->getCurrentLogsFile(); $logs = file_get_contents($logs); - $this->assertContains('rocketeer.ERROR: foobar [] []', $logs); - } - - public function testCanLogViaMagicMethods() - { - $this->app['rocketeer.logs']->error('foobar'); - $logs = $this->app['rocketeer.logs']->getCurrentLogsFile(); - $logs = file_get_contents($logs); - - $this->assertContains('rocketeer.ERROR: foobar [] []', $logs); + $this->assertContains('foobar', $logs); } public function testCanCreateLogsFolderIfItDoesntExistAlready() { $this->app['path.rocketeer.logs'] = $this->server.'/newlogs'; - $this->app['rocketeer.logs']->error('foobar'); - $logs = $this->app['rocketeer.logs']->getCurrentLogsFile(); + $this->logs->log('foobar'); + $this->logs->write(); + $logs = $this->logs->getCurrentLogsFile(); $this->assertFileExists($logs); $this->app['files']->deleteDirectory(dirname($logs)); diff --git a/tests/Services/Ignition/ConfigurationTest.php b/tests/Services/Ignition/ConfigurationTest.php new file mode 100644 index 000000000..d8de045af --- /dev/null +++ b/tests/Services/Ignition/ConfigurationTest.php @@ -0,0 +1,147 @@ +igniter = new Configuration($this->app); + unset($this->app['path.base']); + unset($this->app['path']); + } + + //////////////////////////////////////////////////////////////////// + //////////////////////////////// TESTS ///////////////////////////// + //////////////////////////////////////////////////////////////////// + + public function testDoesntRebindBasePath() + { + $base = 'src'; + $this->app->instance('path.base', $base); + $this->igniter->bindPaths(); + + $this->assertEquals($base, $this->app['path.base']); + } + + public function testCanBindBasePath() + { + $this->igniter->bindPaths(); + + $this->assertEquals(realpath(__DIR__.'/../../..'), $this->app['path.base']); + } + + public function testCanBindConfigurationPaths() + { + $this->igniter->bindPaths(); + + $root = realpath(__DIR__.'/../../..'); + $this->assertEquals($root.'/.rocketeer', $this->app['path.rocketeer.config']); + } + + public function testCanBindTasksAndEventsPaths() + { + $this->igniter->bindPaths(); + $this->igniter->exportConfiguration(); + + // Create some fake files + $root = realpath(__DIR__.'/../../../.rocketeer'); + $this->files->put($root.'/events.php', ''); + $this->files->makeDirectory($root.'/tasks'); + + $this->igniter->bindPaths(); + + $this->assertEquals($root.'/tasks', $this->app['path.rocketeer.tasks']); + $this->assertEquals($root.'/events.php', $this->app['path.rocketeer.events']); + } + + public function testCanExportConfiguration() + { + $this->igniter->bindPaths(); + $this->igniter->exportConfiguration(); + + $this->assertFileExists(__DIR__.'/../../../.rocketeer'); + } + + public function testCanReplaceStubsInConfigurationFile() + { + $this->igniter->bindPaths(); + $path = $this->igniter->exportConfiguration(); + $this->igniter->updateConfiguration($path, array('scm_username' => 'foobar')); + + $this->assertFileExists(__DIR__.'/../../../.rocketeer'); + $this->assertContains('foobar', file_get_contents(__DIR__.'/../../../.rocketeer/scm.php')); + } + + public function testCanSetCurrentApplication() + { + $this->mock('rocketeer.storage.local', 'LocalStorage', function ($mock) { + return $mock->shouldReceive('setFile')->once()->with('foobar'); + }); + + $this->igniter->bindPaths(); + $path = $this->igniter->exportConfiguration(); + $this->igniter->updateConfiguration($path, array('application_name' => 'foobar', 'scm_username' => 'foobar')); + + $this->assertFileExists(__DIR__.'/../../../.rocketeer'); + $this->assertContains('foobar', file_get_contents(__DIR__.'/../../../.rocketeer/config.php')); + } + + public function testCanLoadFilesOrFolder() + { + $config = $this->customConfig; + $this->app['path.base'] = dirname($config); + + $this->files->makeDirectory($config.'/events', 0755, true); + $this->files->put($config.'/tasks.php', 'files->put($config.'/events/some-event.php', 'igniter->bindPaths(); + $this->igniter->loadUserConfiguration(); + $this->tasks->registerConfiguredEvents(); + + $task = $this->builder->buildTask('DisplayFiles'); + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $task); + $this->assertEquals('DisplayFiles', $task->getName()); + + $events = $this->tasks->getTasksListeners($task, 'before'); + $this->assertCount(1, $events); + $this->assertEquals('whoami', $events[0][0]->getStringTask()); + } + + public function testCanUseFilesAndFoldersForContextualConfig() + { + $this->mock('config', 'Config', function ($mock) { + return $mock->shouldReceive('set')->once()->with('rocketeer::on.connections.production.scm', ['scm' => 'svn']); + }); + + $file = $this->customConfig.'/connections/production/scm.php'; + $this->files->makeDirectory(dirname($file), 0755, true); + $this->app['path.rocketeer.config'] = realpath($this->customConfig); + + file_put_contents($file, ' "svn");'); + + $this->igniter->mergeContextualConfigurations(); + } + + public function testDoesntCrashIfNoSubfolder() + { + $this->files->makeDirectory($this->customConfig, 0755, true); + $this->app['path.rocketeer.config'] = realpath($this->customConfig); + + $this->igniter->mergeContextualConfigurations(); + } +} diff --git a/tests/Services/Ignition/PluginsTest.php b/tests/Services/Ignition/PluginsTest.php new file mode 100644 index 000000000..8a0138334 --- /dev/null +++ b/tests/Services/Ignition/PluginsTest.php @@ -0,0 +1,72 @@ +plugins = new Plugins($this->app); + $this->from = $this->app['path.base'].'/vendor/anahkiasen/rocketeer-slack/config'; + } + + public function testCanPublishClassicPluginConfiguration() + { + unset($this->app['path']); + + $this->mockFiles(function ($mock) { + $destination = $this->app['path.rocketeer.config'].'/plugins/rocketeers/rocketeer-slack'; + + return $mock + ->shouldReceive('isDirectory')->with($this->from)->andReturn(true) + ->shouldReceive('isDirectory')->with($destination)->andReturn(false) + ->shouldReceive('makeDirectory')->with($destination)->andReturn(true) + ->shouldReceive('copyDirectory')->with($this->from, $destination); + }); + + $this->plugins->publish('anahkiasen/rocketeer-slack'); + } + + public function testCancelsIfNoValidConfigurationPath() + { + unset($this->app['path']); + + $this->mockFiles(function ($mock) { + return $mock + ->shouldReceive('isDirectory')->with($this->from)->andReturn(false) + ->shouldReceive('copyDirectory')->never(); + }); + + $this->plugins->publish('anahkiasen/rocketeer-slack'); + } + + public function testCanPublishLaravelConfiguration() + { + $this->mock('artisan'); + + $this->mockFiles(function ($mock) { + $destination = $this->app['path'].'/config/packages/rocketeers/rocketeer-slack'; + + return $mock + ->shouldReceive('isDirectory')->with($this->from)->andReturn(true) + ->shouldReceive('isDirectory')->with($destination)->andReturn(false) + ->shouldReceive('makeDirectory')->with($destination)->andReturn(true) + ->shouldReceive('copyDirectory')->with($this->from, $destination); + }); + + $this->plugins->publish('anahkiasen/rocketeer-slack'); + } +} diff --git a/tests/Services/PathfinderTest.php b/tests/Services/PathfinderTest.php new file mode 100644 index 000000000..6d2a8bc5c --- /dev/null +++ b/tests/Services/PathfinderTest.php @@ -0,0 +1,139 @@ +assertEquals($this->server, $this->paths->getHomeFolder()); + } + + public function testCanGetFolderWithStage() + { + $this->connections->setStage('test'); + + $this->assertEquals($this->server.'/test/current', $this->paths->getFolder('current')); + } + + public function testCanGetAnyFolder() + { + $this->assertEquals($this->server.'/current', $this->paths->getFolder('current')); + } + + public function testCanReplacePatternsInFolders() + { + $folder = $this->paths->getFolder('{path.storage}'); + + $this->assertEquals($this->server.'/app/storage', $folder); + } + + public function testCannotReplaceUnexistingPatternsInFolders() + { + $folder = $this->paths->getFolder('{path.foobar}'); + + $this->assertEquals($this->server.'/', $folder); + } + + public function testCanReplacePlaceholdersOnWindows() + { + $this->app['path.base'] = 'c:\xampp\htdocs\project'; + $this->app['path.foobar'] = 'c:\xampp\htdocs\project\lol'; + + $this->assertEquals($this->server.'/lol', $this->paths->getFolder('{path.foobar}')); + } + + public function testCanGetUserHomeFolder() + { + $_SERVER['HOME'] = '/some/folder'; + $home = $this->paths->getUserHomeFolder(); + + $this->assertEquals('/some/folder', $home); + } + + public function testCanGetWindowsHomeFolder() + { + unset($_SERVER['HOME']); + + $_SERVER['HOMEDRIVE'] = 'C:'; + $_SERVER['HOMEPATH'] = '\Users\someuser'; + $home = $this->paths->getUserHomeFolder(); + + $this->assertEquals('C:\Users\someuser', $home); + } + + public function testCanGetWindowsHomeFolderStatically() + { + unset($_SERVER['HOME']); + + $_SERVER['HOMEDRIVE'] = 'C:'; + $_SERVER['HOMEPATH'] = '\Users\someuser'; + $home = Pathfinder::getUserHomeFolder(); + + $this->assertEquals('C:\Users\someuser', $home); + } + + public function testCancelsIfNoHomeFolder() + { + $this->setExpectedException('Exception'); + + $_SERVER['HOME'] = null; + $_SERVER['HOMEDRIVE'] = 'C:'; + $_SERVER['HOMEPATH'] = null; + $this->paths->getUserHomeFolder(); + } + + public function testCanGetRocketeerFolder() + { + $_SERVER['HOME'] = '/some/folder'; + $rocketeer = $this->paths->getRocketeerConfigFolder(); + + $this->assertEquals('/some/folder/.rocketeer', $rocketeer); + } + + public function testCanGetBoundPath() + { + $this->swapConfig(array( + 'rocketeer::paths.php' => '/bin/php', + )); + $path = $this->paths->getPath('php'); + + $this->assertEquals('/bin/php', $path); + } + + public function testCanGetStoragePathWhenNoneBound() + { + unset($this->app['path.storage']); + + $storage = $this->paths->getStoragePath(); + $this->assertEquals('.rocketeer', $storage); + } + + public function testCanGetStoragePathIfUnix() + { + $this->app['path.base'] = '/app'; + $this->app['path.storage'] = '/app/local/folder'; + + $storage = $this->paths->getStoragePath(); + $this->assertEquals('local/folder', $storage); + } + + public function testCanGetStorageIfWindows() + { + $this->app['path.base'] = 'C:\Sites\app'; + $this->app['path.storage'] = 'C:\Sites\app\local\folder'; + + $storage = $this->paths->getStoragePath(); + $this->assertEquals('local/folder', $storage); + } + + public function testCanGetStorageWhenBothForSomeReason() + { + $this->app['path.base'] = 'C:\Sites\app'; + $this->app['path.storage'] = 'C:/Sites/app/local/folder'; + + $storage = $this->paths->getStoragePath(); + $this->assertEquals('local/folder', $storage); + } +} diff --git a/tests/Services/ReleasesManagerTest.php b/tests/Services/ReleasesManagerTest.php new file mode 100644 index 000000000..37f2444fc --- /dev/null +++ b/tests/Services/ReleasesManagerTest.php @@ -0,0 +1,202 @@ +releasesManager->getCurrentRelease(); + + $this->assertEquals(20000000000000, $currentRelease); + } + + public function testCanGetStateOfReleases() + { + $validation = $this->releasesManager->getValidationFile(); + + $this->assertEquals(array( + 10000000000000 => true, + 15000000000000 => false, + 20000000000000 => true, + ), $validation); + } + + public function testCanGetInvalidReleases() + { + $validation = $this->releasesManager->getInvalidReleases(); + + $this->assertEquals([1 => 15000000000000], $validation); + } + + public function testCanUpdateStateOfReleases() + { + $this->releasesManager->markReleaseAsValid(15000000000000); + $validation = $this->releasesManager->getValidationFile(); + + $this->assertEquals(array( + 10000000000000 => true, + 15000000000000 => true, + 20000000000000 => true, + ), $validation); + } + + public function testCanMarkReleaseAsValid() + { + $this->releasesManager->markReleaseAsValid(123456789); + $validation = $this->releasesManager->getValidationFile(); + + $this->assertEquals(array( + 10000000000000 => true, + 15000000000000 => false, + 20000000000000 => true, + 123456789 => true, + ), $validation); + } + + public function testCanGetCurrentReleaseFromServerIfUncached() + { + $this->mock('rocketeer.storage.local', 'LocalStorage', function ($mock) { + return $mock + ->shouldReceive('getSeparator')->andReturn('/') + ->shouldReceive('getLineEndings')->andReturn(PHP_EOL); + }); + + $currentRelease = $this->releasesManager->getCurrentRelease(); + + $this->assertEquals(20000000000000, $currentRelease); + } + + public function testCanGetReleasesPath() + { + $releasePath = $this->releasesManager->getReleasesPath(); + + $this->assertEquals($this->server.'/releases', $releasePath); + } + + public function testCanGetCurrentReleaseFolder() + { + $currentReleasePath = $this->releasesManager->getCurrentReleasePath(); + + $this->assertEquals($this->server.'/releases/20000000000000', $currentReleasePath); + } + + public function testCanGetReleases() + { + $releases = $this->releasesManager->getReleases(); + + $this->assertEquals([1 => 15000000000000, 0 => 20000000000000, 2 => 10000000000000], $releases); + } + + public function testCanGetDeprecatedReleases() + { + $releases = $this->releasesManager->getDeprecatedReleases(); + + $this->assertEquals([15000000000000, 10000000000000], $releases); + } + + public function testCanGetPreviousValidRelease() + { + $currentRelease = $this->releasesManager->getPreviousRelease(); + + $this->assertEquals(10000000000000, $currentRelease); + } + + public function testReturnsCurrentReleaseIfNoPreviousValidRelease() + { + $this->mockState(array( + '10000000000000' => false, + '15000000000000' => false, + '20000000000000' => true, + )); + + $currentRelease = $this->releasesManager->getPreviousRelease(); + + $this->assertEquals(20000000000000, $currentRelease); + } + + public function testReturnsCurrentReleaseIfOnlyRelease() + { + $this->mockState(array( + '20000000000000' => true, + )); + + $currentRelease = $this->releasesManager->getPreviousRelease(); + + $this->assertEquals(20000000000000, $currentRelease); + } + + public function testReturnsCorrectPreviousReleaseIfUpdatedBeforehand() + { + $this->mockState(array( + '20000000000000' => true, + )); + + $previous = $this->releasesManager->getPreviousRelease(); + + $this->assertEquals(20000000000000, $previous); + } + + public function testCanReturnPreviousReleaseIfNoReleases() + { + $this->mock('rocketeer.bash', 'Rocketeer\Bash', function ($mock) { + return $mock + ->shouldReceive('getFile')->times(1) + ->shouldReceive('listContents')->once()->with($this->server.'/releases')->andReturn([]); + }); + + $this->mockState(array()); + + $previous = $this->releasesManager->getPreviousRelease(); + $this->assertNull($previous); + } + + public function testCanGetFolderInRelease() + { + $folder = $this->releasesManager->getCurrentReleasePath('{path.storage}'); + + $this->assertEquals($this->server.'/releases/20000000000000/app/storage', $folder); + } + + public function testDoesntPingForReleasesAllTheFuckingTime() + { + $this->mock('rocketeer.bash', 'Rocketeer\Bash', function ($mock) { + return $mock + ->shouldReceive('getFile')->times(1) + ->shouldReceive('listContents')->once()->with($this->server.'/releases')->andReturn([20000000000000]); + }); + + $this->releasesManager->getNonCurrentReleases(); + $this->releasesManager->getNonCurrentReleases(); + $this->releasesManager->getNonCurrentReleases(); + $this->releasesManager->getNonCurrentReleases(); + } + + public function testDoesntPingForReleasesIfNoReleases() + { + $this->mock('rocketeer.bash', 'Rocketeer\Bash', function ($mock) { + return $mock + ->shouldReceive('getFile')->times(1) + ->shouldReceive('listContents')->once()->with($this->server.'/releases')->andReturn([]); + }); + + $this->releasesManager->getNonCurrentReleases(); + $this->releasesManager->getNonCurrentReleases(); + $this->releasesManager->getNonCurrentReleases(); + $this->releasesManager->getNonCurrentReleases(); + } + + public function testIgnoresErrorsAndStuffWhenFetchingReleases() + { + $this->mock('rocketeer.bash', 'Rocketeer\Bash', function ($mock) { + return $mock + ->shouldReceive('getFile')->times(1) + ->shouldReceive('listContents')->times(1)->with($this->server.'/releases')->andReturn(['IMPOSSIBLE BECAUSE NOPE FUCK YOU']); + }); + + $releases = $this->releasesManager->getReleases(); + + $this->assertEmpty($releases); + } +} diff --git a/tests/Services/Storages/LocalStorageTest.php b/tests/Services/Storages/LocalStorageTest.php new file mode 100644 index 000000000..d41bd1f65 --- /dev/null +++ b/tests/Services/Storages/LocalStorageTest.php @@ -0,0 +1,73 @@ +localStorage->getFilepath(); + $this->localStorage->destroy(); + + $this->assertFileNotExists($file); + } + + public function testCanCreateDeploymentsFileAnywhere() + { + $this->app['path.storage'] = null; + $this->app->offsetUnset('path.storage'); + + new LocalStorage($this->app); + + $storage = $this->paths->getRocketeerConfigFolder(); + $exists = file_exists($storage); + $this->files->deleteDirectory($storage); + $this->assertTrue($exists); + } + + public function testCanGetLineEndings() + { + $this->localStorage->destroy(); + + $this->assertEquals(PHP_EOL, $this->localStorage->getLineEndings()); + } + + public function testCanGetSeparators() + { + $this->localStorage->destroy(); + + $this->assertEquals(DIRECTORY_SEPARATOR, $this->localStorage->getSeparator()); + } + + public function testCanComputeHashAccordingToContentsOfFiles() + { + $this->mockFiles(function ($mock) { + return $mock + ->shouldReceive('put')->once() + ->shouldReceive('exists')->twice()->andReturn(false) + ->shouldReceive('glob')->once()->andReturn(['foo', 'bar']) + ->shouldReceive('getRequire')->once()->with('foo')->andReturn(['foo']) + ->shouldReceive('getRequire')->once()->with('bar')->andReturn(['bar']); + }); + + $storage = new LocalStorage($this->app, 'deployments', $this->server); + $hash = $storage->getHash(); + + $this->assertEquals(md5('["foo"]["bar"]'), $hash); + } + + public function testCanSwitchFolder() + { + $storage = new LocalStorage($this->app, 'foo', '/foo'); + $storage->setFolder($this->server); + $file = $storage->getFilepath(); + + $this->assertEquals($this->server, $storage->getFolder()); + $this->assertEquals($this->server.'/foo.json', $file); + } +} diff --git a/tests/Services/Storages/ServerStorageTest.php b/tests/Services/Storages/ServerStorageTest.php new file mode 100644 index 000000000..87f6beda8 --- /dev/null +++ b/tests/Services/Storages/ServerStorageTest.php @@ -0,0 +1,16 @@ +app, 'test'); + $file = $server->getFilepath(); + $server->destroy(); + + $this->assertFileNotExists($file); + } +} diff --git a/tests/Services/Tasks/JobTest.php b/tests/Services/Tasks/JobTest.php new file mode 100644 index 000000000..adb886efb --- /dev/null +++ b/tests/Services/Tasks/JobTest.php @@ -0,0 +1,25 @@ +swapConfig(['rocketeer::default' => ['production', 'staging']]); + + $pipeline = $this->queue->buildPipeline(['ls']); + + $this->assertInstanceOf('Illuminate\Support\Collection', $pipeline); + $this->assertCount(2, $pipeline); + $this->assertInstanceOf('Rocketeer\Services\Tasks\Job', $pipeline[0]); + $this->assertInstanceOf('Rocketeer\Services\Tasks\Job', $pipeline[1]); + + $this->assertEquals(['ls'], $pipeline[0]->queue); + $this->assertEquals(['ls'], $pipeline[1]->queue); + + $this->assertEquals('production', $pipeline[0]->connection); + $this->assertEquals('staging', $pipeline[1]->connection); + } +} diff --git a/tests/Services/Tasks/TasksBuilderTest.php b/tests/Services/Tasks/TasksBuilderTest.php new file mode 100644 index 000000000..3d45f97d3 --- /dev/null +++ b/tests/Services/Tasks/TasksBuilderTest.php @@ -0,0 +1,88 @@ +builder->buildTaskFromClass('Rocketeer\Tasks\Deploy'); + + $this->assertInstanceOf('Rocketeer\Abstracts\AbstractTask', $task); + } + + public function testCanBuildCustomTaskByName() + { + $tasks = $this->builder->buildTasks(['Rocketeer\Tasks\Check']); + + $this->assertInstanceOf('Rocketeer\Tasks\Check', $tasks[0]); + } + + public function testCanBuildTaskFromString() + { + $string = 'echo "I love ducks"'; + + $string = $this->builder->buildTaskFromString($string); + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $string); + + $closure = $string->getClosure(); + $this->assertInstanceOf('Closure', $closure); + + $closureReflection = new ReflectionFunction($closure); + $this->assertEquals(array('stringTask' => 'echo "I love ducks"'), $closureReflection->getStaticVariables()); + + $this->assertEquals('I love ducks', $string->execute()); + } + + public function testCanBuildTaskFromClosure() + { + $originalClosure = function ($task) { + return $task->getCommand()->info('echo "I love ducks"'); + }; + + $closure = $this->builder->buildTaskFromClosure($originalClosure); + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $closure); + $this->assertEquals($originalClosure, $closure->getClosure()); + } + + public function testCanBuildTasks() + { + $queue = array( + 'foobar', + function () { + return 'lol'; + }, + 'Rocketeer\Tasks\Deploy', + ); + + $queue = $this->builder->buildTasks($queue); + + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[0]); + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[1]); + $this->assertInstanceOf('Rocketeer\Tasks\Deploy', $queue[2]); + } + + public function testThrowsExceptionOnUnbuildableTask() + { + $this->setExpectedException('Rocketeer\Exceptions\TaskCompositionException'); + + $this->builder->buildTaskFromClass('Nope'); + } + + public function testCanCreateCommandOfTask() + { + $command = $this->builder->buildCommand('Rocketeer', ''); + $this->assertInstanceOf('Rocketeer\Console\Commands\RocketeerCommand', $command); + $this->assertEquals('deploy', $command->getName()); + + $command = $this->builder->buildCommand('Deploy', 'lol'); + $this->assertInstanceOf('Rocketeer\Console\Commands\DeployCommand', $command); + $this->assertEquals('deploy:deploy', $command->getName()); + + $command = $this->builder->buildCommand('ls', 'ls'); + $this->assertInstanceOf('Rocketeer\Console\Commands\BaseTaskCommand', $command); + $this->assertEquals('deploy:ls', $command->getName()); + } +} diff --git a/tests/Services/Tasks/TasksQueueTest.php b/tests/Services/Tasks/TasksQueueTest.php new file mode 100644 index 000000000..e0d5fd659 --- /dev/null +++ b/tests/Services/Tasks/TasksQueueTest.php @@ -0,0 +1,141 @@ +swapConfig(array( + 'rocketeer::default' => 'production', + )); + + $this->expectOutputString('JOEY DOESNT SHARE FOOD'); + $this->queue->run(array( + function () { + print 'JOEY DOESNT SHARE FOOD'; + }, + ), $this->getCommand()); + } + + public function testCanRunQueueOnDifferentConnectionsAndStages() + { + $this->swapConfig(array( + 'rocketeer::default' => ['staging', 'production'], + 'rocketeer::stages.stages' => ['first', 'second'], + )); + + $output = array(); + $queue = array( + function ($task) use (&$output) { + $output[] = $task->connections->getConnection().' - '.$task->connections->getStage(); + }, + ); + + $pipeline = $this->queue->run($queue); + + $this->assertTrue($pipeline->succeeded()); + $this->assertEquals(array( + 'staging - first', + 'staging - second', + 'production - first', + 'production - second', + ), $output); + } + + public function testCanRunQueueViaExecute() + { + $this->swapConfig(array( + 'rocketeer::default' => 'production', + )); + + $pipeline = $this->queue->run(array( + 'ls -a', + function () { + return 'JOEY DOESNT SHARE FOOD'; + }, + )); + + $output = array_slice($this->history->getFlattenedOutput(), 2, 3); + $this->assertTrue($pipeline->succeeded()); + $this->assertEquals(array( + '.'.PHP_EOL.'..'.PHP_EOL.'.gitkeep', + 'JOEY DOESNT SHARE FOOD', + ), $output); + } + + public function testCanRunOnMultipleConnectionsViaOn() + { + $this->swapConfig(array( + 'rocketeer::stages.stages' => array('first', 'second'), + )); + + $this->queue->on(array('staging', 'production'), function ($task) { + return $task->connections->getConnection().' - '.$task->connections->getStage(); + }); + + $this->assertEquals(array( + 'staging - first', + 'staging - second', + 'production - first', + 'production - second', + ), $this->history->getFlattenedOutput()); + } + + public function testCanRunTasksInParallel() + { + $parallel = Mockery::mock('Parallel') + ->shouldReceive('isSupported')->andReturn(true) + ->shouldReceive('values')->once()->with(Mockery::type('array')) + ->mock(); + + $this->mockCommand(['parallel' => true]); + $this->queue->setParallel($parallel); + + $task = function () { + sleep(1); + + return time(); + }; + + $this->queue->execute(array( + $task, + $task, + )); + } + + public function testCanCancelQueueIfTaskFails() + { + $this->expectOutputString('The tasks queue was canceled by task "MyCustomHaltingTask"'); + + $this->mockCommand([], array( + 'error' => function ($error) { + echo $error; + }, + )); + + $pipeline = $this->queue->run(array( + 'Rocketeer\Dummies\MyCustomHaltingTask', + 'Rocketeer\Dummies\MyCustomTask', + )); + + $this->assertTrue($pipeline->failed()); + $this->assertEquals([false], $this->history->getFlattenedOutput()); + } + + public function testFallbacksToSynchonousIfErrorWhenRunningParallels() + { + $parallel = Mockery::mock('Parallel') + ->shouldReceive('isSupported')->andReturn(true) + ->shouldReceive('values')->once()->andThrow('LogicException') + ->mock(); + + $this->mockCommand(['parallel' => true]); + $this->queue->setParallel($parallel); + + $this->queue->run(['ls']); + } +} diff --git a/tests/Services/TasksHandlerTest.php b/tests/Services/TasksHandlerTest.php new file mode 100644 index 000000000..5e06bd145 --- /dev/null +++ b/tests/Services/TasksHandlerTest.php @@ -0,0 +1,196 @@ +tasks->add('Rocketeer\Tasks\Deploy'); + $this->assertInstanceOf('Rocketeer\Console\Commands\BaseTaskCommand', $command); + $this->assertInstanceOf('Rocketeer\Tasks\Deploy', $command->getTask()); + } + + public function testCanGetTasksBeforeOrAfterAnotherTask() + { + $task = $this->task('Deploy'); + $before = $this->tasks->getTasksListeners($task, 'before', true); + + $this->assertEquals(['before', 'foobar'], $before); + } + + public function testCanAddTasksViaFacade() + { + $task = $this->task('Deploy'); + $before = $this->tasks->getTasksListeners($task, 'before', true); + + $this->tasks->before('deploy', 'composer install'); + + $newBefore = array_merge($before, array('composer install')); + $this->assertEquals($newBefore, $this->tasks->getTasksListeners($task, 'before', true)); + } + + public function testCanAddMultipleTasksViaFacade() + { + $task = $this->task('Deploy'); + $after = $this->tasks->getTasksListeners($task, 'after', true); + $this->tasks->after('deploy', array( + 'composer install', + 'bower install', + )); + + $newAfter = array_merge($after, array('composer install', 'bower install')); + $this->assertEquals($newAfter, $this->tasks->getTasksListeners($task, 'after', true)); + } + + public function testCanRegisterCustomTask() + { + $this->swapConfig(array( + 'rocketeer::default' => 'production', + )); + + $this->tasks->task('foobar', function ($task) { + $task->runForCurrentRelease('ls'); + }); + + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $this->builder->buildTask('foobar')); + + $this->queue->run('foobar'); + $this->assertHistory([['cd {server}/releases/{release}', 'ls']]); + } + + public function testCanRegisterCustomTaskViaArray() + { + $this->swapConfig(array( + 'rocketeer::default' => 'production', + )); + + $this->tasks->task('foobar', ['ls', 'ls']); + $this->assertInstanceOf('Rocketeer\Tasks\Closure', $this->builder->buildTask('foobar')); + + $this->queue->run('foobar'); + $this->assertHistory([['cd {server}/releases/{release}', 'ls', 'ls']]); + } + + public function testCanAddSurroundTasksToNonExistingTasks() + { + $task = $this->task('Setup'); + $this->tasks->after('setup', 'composer install'); + + $after = array('composer install'); + $this->assertEquals($after, $this->tasks->getTasksListeners($task, 'after', true)); + } + + public function testCanAddSurroundTasksToMultipleTasks() + { + $this->tasks->after(array('cleanup', 'setup'), 'composer install'); + + $after = array('composer install'); + $this->assertEquals($after, $this->tasks->getTasksListeners('setup', 'after', true)); + $this->assertEquals($after, $this->tasks->getTasksListeners('cleanup', 'after', true)); + } + + public function testCangetTasksListenersOrAfterAnotherTaskBySlug() + { + $after = $this->tasks->getTasksListeners('deploy', 'after', true); + + $this->assertEquals(array('after', 'foobar'), $after); + } + + public function testCanAddEventsWithPriority() + { + $this->tasks->before('deploy', 'second', -5); + $this->tasks->before('deploy', 'first'); + + $listeners = $this->tasks->getTasksListeners('deploy', 'before', true); + $this->assertEquals(['before', 'foobar', 'first', 'second'], $listeners); + } + + public function testCanExecuteContextualEvents() + { + $this->swapConfig(array( + 'rocketeer::stages.stages' => array('hasEvent', 'noEvent'), + 'rocketeer::on.stages.hasEvent.hooks' => array('before' => array('check' => 'ls')), + )); + + $this->connections->setStage('hasEvent'); + $this->assertEquals(['ls'], $this->tasks->getTasksListeners('check', 'before', true)); + + $this->connections->setStage('noEvent'); + $this->assertEquals([], $this->tasks->getTasksListeners('check', 'before', true)); + } + + public function testCanbuildTasksFromConfigHook() + { + $tasks = array( + 'npm install', + 'bower install', + ); + + $this->swapConfig(array( + 'rocketeer::hooks' => ['after' => ['deploy' => $tasks]], + )); + + $this->tasks->registerConfiguredEvents(); + $listeners = $this->tasks->getTasksListeners('deploy', 'after', true); + + $this->assertEquals($tasks, $listeners); + } + + public function testCanHaveCustomConnectionHooks() + { + $tasks = array( + 'npm install', + 'bower install', + ); + + $this->swapConfig(array( + 'rocketeer::default' => 'production', + 'rocketeer::hooks' => [], + 'rocketeer::on.connections.staging.hooks' => ['after' => ['deploy' => $tasks]], + )); + $this->tasks->registerConfiguredEvents(); + + $this->connections->setConnection('production'); + $events = $this->tasks->getTasksListeners('deploy', 'after', true); + $this->assertEmpty($events); + + $this->connections->setConnection('staging'); + $events = $this->tasks->getTasksListeners('deploy', 'after', true); + + $this->assertEquals($tasks, $events); + } + + public function testPluginsArentDeregisteredWhenSwitchingConnection() + { + $this->swapConfig(array( + 'rocketeer::hooks' => ['before' => ['deploy' => 'ls']], + )); + + $this->tasks->plugin(new DummyNotifier($this->app)); + + $listeners = $this->tasks->getTasksListeners('deploy', 'before', true); + $this->assertEquals(['ls', 'notify'], $listeners); + + $this->connections->setConnection('production'); + + $listeners = $this->tasks->getTasksListeners('deploy', 'before', true); + $this->assertEquals(['ls', 'notify'], $listeners); + } + + public function testDoesntRegisterPluginsTwice() + { + $this->swapConfig(array( + 'rocketeer::hooks' => [], + )); + + $this->tasks->plugin(new DummyNotifier($this->app)); + $this->tasks->plugin(new DummyNotifier($this->app)); + $this->tasks->plugin(new DummyNotifier($this->app)); + + $listeners = $this->tasks->getTasksListeners('deploy', 'before', true); + $this->assertEquals(['notify'], $listeners); + } +} diff --git a/tests/Strategies/Check/NodeStrategyTest.php b/tests/Strategies/Check/NodeStrategyTest.php new file mode 100644 index 000000000..cfe47578d --- /dev/null +++ b/tests/Strategies/Check/NodeStrategyTest.php @@ -0,0 +1,36 @@ +strategy = $this->builder->buildStrategy('Check', 'Node'); + } + + public function testCanParseLanguageConstraint() + { + $manager = Mockery::mock('Npm', array( + 'getBinary' => 'npm', + 'getManifestContents' => json_encode(['engines' => ['node' => '0.10.30']]), + )); + $this->strategy->setManager($manager); + + $this->mockRemote('0.8.0'); + + $this->assertFalse($this->strategy->language()); + + $this->mockRemote('0.11.0'); + $this->assertTrue($this->strategy->language()); + } +} diff --git a/tests/Strategies/Check/PhpStrategyTest.php b/tests/Strategies/Check/PhpStrategyTest.php new file mode 100644 index 000000000..b5a22e951 --- /dev/null +++ b/tests/Strategies/Check/PhpStrategyTest.php @@ -0,0 +1,62 @@ +strategy = $this->builder->buildStrategy('Check', 'Php'); + } + + public function testCanCheckPhpVersion() + { + $this->mockFiles(function ($mock) { + return $mock + ->shouldReceive('put') + ->shouldReceive('glob')->andReturn(array()) + ->shouldReceive('exists')->andReturn(true) + ->shouldReceive('get')->andReturn('{"require":{"php":">=5.3.0"}}'); + }); + $this->assertTrue($this->strategy->language()); + + // This is is going to come bite me in the ass in 10 years + $this->mockFiles(function ($mock) { + return $mock + ->shouldReceive('put') + ->shouldReceive('glob')->andReturn(array()) + ->shouldReceive('exists')->andReturn(true) + ->shouldReceive('get')->andReturn('{"require":{"php":">=5.9.0"}}'); + }); + $this->assertFalse($this->strategy->language()); + } + + public function testCanCheckPhpExtensions() + { + $this->swapConfig(array( + 'database.default' => 'sqlite', + 'cache.driver' => 'redis', + 'session.driver' => 'apc', + )); + + $this->strategy->extensions(); + + $this->assertHistory(['{php} -m']); + } + + public function testCanCheckForHhvmExtensions() + { + $this->mockRemote('HipHop VM 3.0.1 (rel)'.PHP_EOL.'Some more stuff'); + $exists = $this->strategy->checkPhpExtension('_hhvm'); + + $this->assertTrue($exists); + } +} diff --git a/tests/Strategies/Check/RubyStrategyTest.php b/tests/Strategies/Check/RubyStrategyTest.php new file mode 100644 index 000000000..7562bbca8 --- /dev/null +++ b/tests/Strategies/Check/RubyStrategyTest.php @@ -0,0 +1,35 @@ +strategy = $this->builder->buildStrategy('Check', 'Ruby'); + } + + public function testCanParseLanguageConstraint() + { + $manager = Mockery::mock('Bundler', array( + 'getBinary' => 'bundle', + 'getManifestContents' => '# Some comments'.PHP_EOL."ruby '2.0.0'", + )); + $this->strategy->setManager($manager); + + $this->mockRemote('1.9.3'); + $this->assertFalse($this->strategy->language()); + + $this->mockRemote('2.1.0'); + $this->assertTrue($this->strategy->language()); + } +} diff --git a/tests/Strategies/Dependencies/BowerStrategyTest.php b/tests/Strategies/Dependencies/BowerStrategyTest.php new file mode 100644 index 000000000..63c02e3b5 --- /dev/null +++ b/tests/Strategies/Dependencies/BowerStrategyTest.php @@ -0,0 +1,67 @@ +app); + $bower->setBinary('bower'); + + $this->bower = $this->builder->buildStrategy('Dependencies', 'Bower'); + $this->bower->setManager($bower); + } + + public function testCanInstallDependencies() + { + $this->pretend(); + $this->bower->install(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/{release}', + 'bower install', + ), + )); + } + + public function testCanUpdateDependencies() + { + $this->pretend(); + $this->bower->update(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/{release}', + 'bower update', + ), + )); + } + + public function testUsesAllowRootIfRoot() + { + $this->mock('rocketeer.connections', 'Connections', function ($mock) { + return $mock->shouldReceive('getServerCredentials')->andReturn(['username' => 'root']); + }); + + $this->pretend(); + $this->bower->install(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/{release}', + 'bower install --allow-root', + ), + )); + } +} diff --git a/tests/Strategies/Dependencies/BundlerStrategyTest.php b/tests/Strategies/Dependencies/BundlerStrategyTest.php new file mode 100644 index 000000000..5ee7e6303 --- /dev/null +++ b/tests/Strategies/Dependencies/BundlerStrategyTest.php @@ -0,0 +1,47 @@ +app); + $bundler->setBinary('bundle'); + + $this->bundler = $this->builder->buildStrategy('Dependencies', 'Bundler'); + $this->bundler->setManager($bundler); + } + + public function testCanInstallDependencies() + { + $this->pretend(); + $this->bundler->install(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/{release}', + 'bundle install', + ), + )); + } + + public function testCanUpdateDependencies() + { + $this->pretend(); + $this->bundler->update(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/{release}', + 'bundle update', + ), + )); + } +} diff --git a/tests/Strategies/Dependencies/ComposerStrategyTest.php b/tests/Strategies/Dependencies/ComposerStrategyTest.php new file mode 100644 index 000000000..0c8bb9949 --- /dev/null +++ b/tests/Strategies/Dependencies/ComposerStrategyTest.php @@ -0,0 +1,57 @@ +swapConfig(array( + 'rocketeer::scm' => array( + 'repository' => 'https://github.com/'.$this->repository, + 'username' => '', + 'password' => '', + ), + 'rocketeer::strategies.composer.install' => function ($composer, $task) { + return array( + $composer->selfUpdate(), + $composer->install([], '--prefer-source'), + ); + }, + )); + + $this->pretendTask(); + $composer = $this->builder->buildStrategy('Dependencies', 'Composer'); + $composer->install(); + + $this->assertHistory(array( + array( + "cd {server}/releases/{release}", + "{composer} self-update", + "{composer} install --prefer-source", + ), + )); + } + + public function testCancelsIfInvalidComposerRoutine() + { + $composer = $this->builder->buildStrategy('Dependencies', 'Composer'); + + $this->swapConfig(array( + 'rocketeer::strategies.composer.install' => 'lol', + )); + + $composer->install(); + $this->assertHistory([]); + + $this->swapConfig(array( + 'rocketeer::strategies.composer.install' => function () { + return []; + }, + )); + + $composer->install(); + $this->assertHistory([]); + } +} diff --git a/tests/Strategies/Dependencies/PolyglotStrategyTest.php b/tests/Strategies/Dependencies/PolyglotStrategyTest.php new file mode 100644 index 000000000..755e418f1 --- /dev/null +++ b/tests/Strategies/Dependencies/PolyglotStrategyTest.php @@ -0,0 +1,27 @@ +usesComposer(true); + $this->files->put($this->server.'/current/Gemfile', ''); + + $polyglot = $this->builder->buildStrategy('Dependencies', 'Polyglot'); + $polyglot->install(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/{release}', + '{bundle} install', + ), + array( + 'cd {server}/releases/{release}', + '{composer} install --no-interaction --no-dev --prefer-dist', + ), + )); + } +} diff --git a/tests/Strategies/Deploy/CloneStrategyTest.php b/tests/Strategies/Deploy/CloneStrategyTest.php new file mode 100644 index 000000000..3c3a0c377 --- /dev/null +++ b/tests/Strategies/Deploy/CloneStrategyTest.php @@ -0,0 +1,39 @@ +pretendTask('Deploy'); + $task->getStrategy('Deploy')->deploy(); + + $matcher = array( + 'git clone "{repository}" "{server}/releases/{release}" --branch="master" --depth="1"', + array( + "cd {server}/releases/{release}", + "git submodule update --init --recursive", + ), + ); + + $this->assertHistory($matcher); + } + + public function testCanUpdateRepository() + { + $task = $this->pretendTask('Deploy'); + $task->getStrategy('Deploy')->update(); + + $matcher = array( + array( + "cd $this->server/releases/20000000000000", + "git reset --hard", + "git pull", + ), + ); + + $this->assertHistory($matcher); + } +} diff --git a/tests/Strategies/Deploy/CopyStrategyTest.php b/tests/Strategies/Deploy/CopyStrategyTest.php new file mode 100644 index 000000000..396382035 --- /dev/null +++ b/tests/Strategies/Deploy/CopyStrategyTest.php @@ -0,0 +1,73 @@ +pretend(); + } + + public function testCanCopyPreviousRelease() + { + $this->builder->buildStrategy('Deploy', 'Copy')->deploy(); + + $matcher = array( + 'cp -r {server}/releases/10000000000000 {server}/releases/20000000000000', + array( + "cd {server}/releases/{release}", + "git reset --hard", + "git pull", + ), + ); + + $this->assertHistory($matcher); + } + + public function testClonesIfNoPreviousRelease() + { + $this->mock('rocketeer.releases', 'ReleasesManager', function (MockInterface $mock) { + return $mock->shouldReceive('getReleases')->andReturn([]) + ->shouldReceive('getCurrentReleasePath')->andReturn($this->server.'/releases/10000000000000'); + }); + + $this->builder->buildStrategy('Deploy', 'Copy')->deploy(); + + $matcher = array( + 'git clone "{repository}" "{server}/releases/{release}" --branch="master" --depth="1"', + array( + "cd {server}/releases/{release}", + "git submodule update --init --recursive", + ), + ); + + $this->assertHistory($matcher); + } + + public function testCanCloneIfPreviousReleaseIsInvalid() + { + $this->mock('rocketeer.releases', 'ReleasesManager', function (MockInterface $mock) { + return $mock->shouldReceive('getReleases')->andReturn([10000000000000]) + ->shouldReceive('getPreviousRelease')->andReturn(null) + ->shouldReceive('getPathToRelease')->andReturn(null) + ->shouldReceive('getCurrentReleasePath')->andReturn($this->server.'/releases/10000000000000'); + }); + + $this->builder->buildStrategy('Deploy', 'Copy')->deploy(); + + $matcher = array( + 'git clone "{repository}" "{server}/releases/{release}" --branch="master" --depth="1"', + array( + "cd {server}/releases/{release}", + "git submodule update --init --recursive", + ), + ); + + $this->assertHistory($matcher); + } +} diff --git a/tests/Strategies/Deploy/SyncStrategyTest.php b/tests/Strategies/Deploy/SyncStrategyTest.php new file mode 100644 index 000000000..29140371a --- /dev/null +++ b/tests/Strategies/Deploy/SyncStrategyTest.php @@ -0,0 +1,46 @@ +swapConfig(array( + 'rocketeer::connections' => array( + 'production' => array( + 'host' => 'bar.com', + 'username' => 'foo', + ), + ), + )); + } + + public function testCanDeployRepository() + { + $task = $this->pretendTask('Deploy'); + $task->getStrategy('Deploy', 'Sync')->deploy(); + + $matcher = array( + 'mkdir {server}/releases/{release}', + 'rsync ./ foo@bar.com:{server}/releases/{release} --verbose --recursive --rsh="ssh" --exclude=".git" --exclude="vendor"', + ); + + $this->assertHistory($matcher); + } + + public function testCanUpdateRepository() + { + $task = $this->pretendTask('Deploy'); + $task->getStrategy('Deploy', 'Sync')->update(); + + $matcher = array( + 'rsync ./ foo@bar.com:{server}/releases/{release} --verbose --recursive --rsh="ssh" --exclude=".git" --exclude="vendor"', + ); + + $this->assertHistory($matcher); + } +} diff --git a/tests/Strategies/Test/PhpunitStrategyTest.php b/tests/Strategies/Test/PhpunitStrategyTest.php new file mode 100644 index 000000000..0c3b7f351 --- /dev/null +++ b/tests/Strategies/Test/PhpunitStrategyTest.php @@ -0,0 +1,20 @@ +pretendTask(); + $this->builder->buildStrategy('Test', 'Phpunit')->test(); + + $this->assertHistory(array( + array( + 'cd {server}/releases/20000000000000', + '{phpunit} --stop-on-failure', + ), + )); + } +} diff --git a/tests/Tasks/CheckTest.php b/tests/Tasks/CheckTest.php index b0fdbbce9..e79292366 100644 --- a/tests/Tasks/CheckTest.php +++ b/tests/Tasks/CheckTest.php @@ -5,7 +5,7 @@ class CheckTest extends RocketeerTestCase { - public function testCanDoBasicCheck() + public function testCanCheckScmVersionIfRequired() { $this->assertTaskHistory('Check', array( 'git --version', @@ -13,40 +13,13 @@ public function testCanDoBasicCheck() )); } - public function testCanCheckPhpVersion() - { - $check = new Check($this->app); - - $this->mock('files', 'Filesystem', function ($mock) { - return $mock - ->shouldReceive('put') - ->shouldReceive('glob')->andReturn(array()) - ->shouldReceive('exists')->andReturn(true) - ->shouldReceive('get')->andReturn('{"require":{"php":">=5.3.0"}}'); - }); - $this->assertTrue($check->checkPhpVersion()); - - // This is is going to come bite me in the ass in 10 years - $this->mock('files', 'Filesystem', function ($mock) { - return $mock - ->shouldReceive('put') - ->shouldReceive('glob')->andReturn(array()) - ->shouldReceive('exists')->andReturn(true) - ->shouldReceive('get')->andReturn('{"require":{"php":">=5.9.0"}}'); - }); - $this->assertFalse($check->checkPhpVersion()); - } - - public function testCanCheckPhpExtensions() + public function testSkipsScmCheckIfNotRequired() { $this->swapConfig(array( - 'database.default' => 'sqlite', - 'cache.driver' => 'redis', - 'session.driver' => 'apc', + 'rocketeer::strategies.deploy' => 'sync', )); $this->assertTaskHistory('Check', array( - 'git --version', '{php} -m', )); } diff --git a/tests/Tasks/CleanupTest.php b/tests/Tasks/CleanupTest.php index db541efb1..e0c8f201f 100644 --- a/tests/Tasks/CleanupTest.php +++ b/tests/Tasks/CleanupTest.php @@ -24,16 +24,39 @@ public function testCanPruneAllReleasesIfCleanAll() return $mock ->shouldReceive('getDeprecatedReleases')->never() ->shouldReceive('getNonCurrentReleases')->once()->andReturn(array(1, 2)) + ->shouldReceive('markReleaseAsValid')->once() ->shouldReceive('getPathToRelease')->times(2)->andReturnUsing(function ($release) { return $release; }); }); + ob_start(); + $this->assertTaskOutput('Cleanup', 'Removing 2 releases from the server', $this->getCommand(array(), array( 'clean-all' => true, 'verbose' => true, 'pretend' => false, ))); + + ob_end_clean(); + } + + public function testCanRemoveAllReleasesAtOnce() + { + $this->mockReleases(function ($mock) { + return $mock + ->shouldReceive('getDeprecatedReleases')->never() + ->shouldReceive('getDeprecatedReleases')->once()->andReturn(array(1, 2)) + ->shouldReceive('getPathToRelease')->times(2)->andReturnUsing(function ($release) { + return $release; + }); + }); + + $this->pretendTask('Cleanup')->execute(); + + $this->assertHistory(array( + 'rm -rf {server}/1 {server}/2', + )); } public function testPrintsMessageIfNoCleanup() diff --git a/tests/Tasks/ClosureTest.php b/tests/Tasks/ClosureTest.php new file mode 100644 index 000000000..a912fa451 --- /dev/null +++ b/tests/Tasks/ClosureTest.php @@ -0,0 +1,16 @@ +builder->buildTask(['ls', 'ls'], 'FilesLister'); + + $this->assertEquals('FilesLister', $closure->getName()); + $this->assertEquals('files-lister', $closure->getSlug()); + $this->assertEquals('ls/ls', $closure->getDescription()); + } +} diff --git a/tests/Tasks/DeployTest.php b/tests/Tasks/DeployTest.php index 062703b1f..b56ec0bb4 100644 --- a/tests/Tasks/DeployTest.php +++ b/tests/Tasks/DeployTest.php @@ -1,6 +1,7 @@ assertTaskHistory('Deploy', $matcher, array( 'tests' => true, 'seed' => true, - 'migrate' => true + 'migrate' => true, )); } - public function testCanDisableGitOptions() + public function testStepsRunnerDoesntCancelWithPermissionsAndShared() { $this->swapConfig(array( - 'rocketeer::scm.shallow' => false, - 'rocketeer::scm.submodules' => false, - 'rocketeer::scm' => array( - 'repository' => 'https://github.com/'.$this->repository, - 'username' => '', - 'password' => '', - ) + 'rocketeer::remote.shared' => [], + 'rocketeer::remote.permissions.files' => [], )); $matcher = array( - 'git clone -b master "https://github.com/Anahkiasen/html-object.git" {server}/releases/{release}', + 'git clone "{repository}" "{server}/releases/{release}" --branch="master" --depth="1"', array( "cd {server}/releases/{release}", - exec('which phpunit')." --stop-on-failure " + "git submodule update --init --recursive", ), array( "cd {server}/releases/{release}", - "chmod -R 755 {server}/releases/{release}/tests", - "chmod -R g+s {server}/releases/{release}/tests", - "chown -R www-data:www-data {server}/releases/{release}/tests" + "{phpunit} --stop-on-failure", + ), + array( + "cd {server}/releases/{release}", + "{php} artisan migrate", ), array( "cd {server}/releases/{release}", - "{php} artisan migrate --seed" + "{php} artisan db:seed", ), - "mkdir -p {server}/shared/tests", - "mv {server}/releases/{release}/tests/Elements {server}/shared/tests/Elements", "mv {server}/current {server}/releases/{release}", "rm -rf {server}/current", "ln -s {server}/releases/{release} {server}/current", @@ -85,57 +83,72 @@ public function testCanDisableGitOptions() $this->assertTaskHistory('Deploy', $matcher, array( 'tests' => true, 'seed' => true, - 'migrate' => true + 'migrate' => true, )); } - public function testCanConfigureComposerCommands() + public function testCanDisableGitOptions() { $this->swapConfig(array( - 'rocketeer::scm' => array( + 'rocketeer::scm.shallow' => false, + 'rocketeer::scm.submodules' => false, + 'rocketeer::scm' => array( 'repository' => 'https://github.com/'.$this->repository, 'username' => '', 'password' => '', ), - 'rocketeer::remote.composer' => function ($task) { - return array( - $task->composer('self-update'), - $task->composer('install --prefer-source'), - ); - }, )); $matcher = array( + 'git clone "{repository}" "{server}/releases/{release}" --branch="master"', + array( + "cd {server}/releases/{release}", + '{phpunit} --stop-on-failure', + ), array( "cd {server}/releases/{release}", - "{composer} self-update", - "{composer} install --prefer-source", + "chmod -R 755 {server}/releases/{release}/tests", + "chmod -R g+s {server}/releases/{release}/tests", + "chown -R www-data:www-data {server}/releases/{release}/tests", + ), + array( + "cd {server}/releases/{release}", + "{php} artisan migrate", ), + array( + "cd {server}/releases/{release}", + "{php} artisan db:seed", + ), + "mv {server}/current {server}/releases/{release}", + "rm -rf {server}/current", + "ln -s {server}/releases/{release} {server}/current", ); - $deploy = $this->pretendTask('Deploy'); - $deploy->runComposer(true); - - $this->assertTaskHistory($deploy->getHistory(), $matcher, array( - 'tests' => false, - 'seed' => false, - 'migrate' => false + $this->assertTaskHistory('Deploy', $matcher, array( + 'tests' => true, + 'seed' => true, + 'migrate' => true, )); } public function testCanUseCopyStrategy() { $this->swapConfig(array( - 'rocketeer::remote.strategy' => 'copy', 'rocketeer::scm' => array( 'repository' => 'https://github.com/'.$this->repository, 'username' => '', 'password' => '', - ) + ), + )); + + $this->app['rocketeer.strategies.deploy'] = new CopyStrategy($this->app); + + $this->mockState(array( + '10000000000000' => true, )); $matcher = array( - 'cp {server}/releases/10000000000000 {server}/releases/{release}', + 'cp -r {server}/releases/10000000000000 {server}/releases/{release}', array( 'cd {server}/releases/{release}', 'git reset --hard', @@ -145,10 +158,8 @@ public function testCanUseCopyStrategy() "cd {server}/releases/{release}", "chmod -R 755 {server}/releases/{release}/tests", "chmod -R g+s {server}/releases/{release}/tests", - "chown -R www-data:www-data {server}/releases/{release}/tests" + "chown -R www-data:www-data {server}/releases/{release}/tests", ), - "mkdir -p {server}/shared/tests", - "mv {server}/releases/{release}/tests/Elements {server}/shared/tests/Elements", "mv {server}/current {server}/releases/{release}", "rm -rf {server}/current", "ln -s {server}/releases/{release} {server}/current", @@ -157,30 +168,28 @@ public function testCanUseCopyStrategy() $this->assertTaskHistory('Deploy', $matcher, array( 'tests' => false, 'seed' => false, - 'migrate' => false + 'migrate' => false, )); } public function testCanRunDeployWithSeed() { $matcher = array( - 'git clone --depth 1 -b master "" {server}/releases/{release}', + 'git clone "{repository}" "{server}/releases/{release}" --branch="master" --depth="1"', array( "cd {server}/releases/{release}", - "git submodule update --init --recursive" + "git submodule update --init --recursive", ), array( "cd {server}/releases/{release}", "chmod -R 755 {server}/releases/{release}/tests", "chmod -R g+s {server}/releases/{release}/tests", - "chown -R www-data:www-data {server}/releases/{release}/tests" + "chown -R www-data:www-data {server}/releases/{release}/tests", ), array( "cd {server}/releases/{release}", - "{php} artisan db:seed" + "{php} artisan db:seed", ), - "mkdir -p {server}/shared/tests", - "mv {server}/releases/{release}/tests/Elements {server}/shared/tests/Elements", "mv {server}/current {server}/releases/{release}", "rm -rf {server}/current", "ln -s {server}/releases/{release} {server}/current", diff --git a/tests/Tasks/IgniteTest.php b/tests/Tasks/IgniteTest.php index f53b00879..6f9850f2a 100644 --- a/tests/Tasks/IgniteTest.php +++ b/tests/Tasks/IgniteTest.php @@ -1,21 +1,59 @@ app['path']); + $this->app['path.base'] = 'E:\workspace\test'; + + $provider = new RocketeerServiceProvider($this->app); + $provider->bindPaths(); + + $this->mockFiles(function ($mock) { + return $mock + ->shouldReceive('files')->andReturn([]) + ->shouldReceive('glob')->andReturn([]) + ->shouldReceive('copyDirectory')->once()->with(realpath(__DIR__.'/../../src/config'), 'E:/workspace/test/.rocketeer'); + }); + + $this->pretendTask('Ignite')->execute(); + } + + public function testCanIgniteConfigurationOnWindowsInLaravel() + { + $this->app['path.base'] = 'E:\workspace\test'; + $this->app['path'] = 'E:\workspace\test\app'; + + $provider = new RocketeerServiceProvider($this->app); + $provider->bindPaths(); + + $this->mockFiles(function ($mock) { + return $mock + ->shouldReceive('exists')->andReturn(true) + ->shouldReceive('files')->andReturn([]) + ->shouldReceive('glob')->andReturn([]) + ->shouldReceive('copyDirectory')->once()->with(realpath(__DIR__.'/../../src/config'), 'E:/workspace/test/app/config/packages/anahkiasen/rocketeer'); + }); + + $this->pretendTask('Ignite')->execute(); + } + public function testCanIgniteConfigurationOutsideLaravel() { $command = $this->getCommand(array('ask' => 'foobar')); $server = $this->server; - $this->mock('rocketeer.igniter', 'Igniter', function ($mock) use ($server) { + $this->mock('rocketeer.igniter', 'Configuration', function ($mock) use ($server) { return $mock - ->shouldReceive('getConfigurationPath')->twice() ->shouldReceive('exportConfiguration')->once()->andReturn($server) ->shouldReceive('updateConfiguration')->once()->with($server, array( - 'scm_repository' => '', + 'connection' => 'production', + 'scm_repository' => 'https://github.com/'.$this->repository, 'scm_username' => '', 'scm_password' => '', 'application_name' => 'foobar', @@ -31,12 +69,12 @@ public function testCanIgniteConfigurationInLaravel() $command->shouldReceive('call')->with('config:publish', array('package' => 'anahkiasen/rocketeer'))->andReturn('foobar'); $path = $this->app['path'].'/config/packages/anahkiasen/rocketeer'; - $this->mock('rocketeer.igniter', 'Igniter', function ($mock) use ($path) { + $this->mock('rocketeer.igniter', 'Configuration', function ($mock) use ($path) { return $mock - ->shouldReceive('getConfigurationPath')->twice() ->shouldReceive('exportConfiguration')->never() ->shouldReceive('updateConfiguration')->once()->with($path, array( - 'scm_repository' => '', + 'connection' => 'production', + 'scm_repository' => 'https://github.com/'.$this->repository, 'scm_username' => '', 'scm_password' => '', 'application_name' => '', diff --git a/tests/Tasks/RollbackTest.php b/tests/Tasks/RollbackTest.php index 857b268d4..5846cd5e7 100644 --- a/tests/Tasks/RollbackTest.php +++ b/tests/Tasks/RollbackTest.php @@ -9,6 +9,36 @@ public function testCanRollbackRelease() { $this->task('Rollback')->execute(); - $this->assertEquals(10000000000000, $this->app['rocketeer.releases']->getCurrentRelease()); + $this->assertEquals(10000000000000, $this->releasesManager->getCurrentRelease()); + } + + public function testCanRollbackToSpecificRelease() + { + $this->mockCommand([], ['argument' => 15000000000000]); + $this->command->shouldReceive('option')->andReturn([]); + + $this->task('Rollback')->execute(); + + $this->assertEquals(15000000000000, $this->releasesManager->getCurrentRelease()); + } + + public function testCanGetShownAvailableReleases() + { + $this->command = $this->mockCommand(['list' => true]); + $this->command->shouldReceive('askWith')->andReturn(1); + + $this->task('Rollback')->execute(); + + $this->assertEquals(15000000000000, $this->releasesManager->getCurrentRelease()); + } + + public function testCantRollbackIfNoPreviousRelease() + { + $this->mockReleases(function ($mock) { + return $mock->shouldReceive('getPreviousRelease')->andReturn(null); + }); + + $status = $this->task('Rollback')->execute(); + $this->assertContains('Rocketeer could not rollback as no releases have yet been deployed', $status); } } diff --git a/tests/Tasks/SetupTest.php b/tests/Tasks/SetupTest.php index 88a0fed76..bcb31deef 100644 --- a/tests/Tasks/SetupTest.php +++ b/tests/Tasks/SetupTest.php @@ -14,6 +14,8 @@ public function testCanSetupServer() }); $this->assertTaskHistory('Setup', array( + 'git --version', + '{php} -m', "mkdir {server}/", "mkdir -p {server}/releases", "mkdir -p {server}/current", @@ -33,6 +35,8 @@ public function testCanSetupStages() )); $this->assertTaskHistory('Setup', array( + 'git --version', + '{php} -m', "mkdir {server}/", "mkdir -p {server}/staging/releases", "mkdir -p {server}/staging/current", @@ -43,7 +47,7 @@ public function testCanSetupStages() )); } - public function testRunningSetupKeepsCurrentCongiguredStage() + public function testRunningSetupKeepsCurrentConfiguredStage() { $this->mockReleases(function ($mock) { return $mock @@ -51,12 +55,14 @@ public function testRunningSetupKeepsCurrentCongiguredStage() ->shouldReceive('getCurrentReleasePath')->andReturn('1'); }); $this->swapConfig(array( - 'rocketeer::stages.stages' => array('staging', 'production'), + 'rocketeer::stages.stages' => ['staging', 'production'], )); - $this->app['rocketeer.rocketeer']->setStage('staging'); - $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getStage()); + $this->connections->setStage('staging'); + $this->assertEquals('staging', $this->connections->getStage()); $this->assertTaskHistory('Setup', array( + 'git --version', + '{php} -m', "mkdir {server}/", "mkdir -p {server}/staging/releases", "mkdir -p {server}/staging/current", @@ -67,6 +73,6 @@ public function testRunningSetupKeepsCurrentCongiguredStage() ), array( 'stage' => 'staging', )); - $this->assertEquals('staging', $this->app['rocketeer.rocketeer']->getStage()); + $this->assertEquals('staging', $this->connections->getStage()); } } diff --git a/tests/Tasks/Subtasks/NotifyTest.php b/tests/Tasks/Subtasks/NotifyTest.php new file mode 100644 index 000000000..94dece431 --- /dev/null +++ b/tests/Tasks/Subtasks/NotifyTest.php @@ -0,0 +1,23 @@ +swapConfig(array( + 'rocketeer::hooks' => array(), + )); + + $this->tasks->plugin(new DummyBeforeAfterNotifier($this->app)); + + $this->expectOutputString('before_deployafter_deploy'); + $this->localStorage->set('notifier.name', 'Jean Eude'); + + $this->task('Deploy')->fireEvent('before'); + $this->task('Deploy')->fireEvent('after'); + } +} diff --git a/tests/Tasks/Subtasks/PrimerTest.php b/tests/Tasks/Subtasks/PrimerTest.php new file mode 100644 index 000000000..1cea8276f --- /dev/null +++ b/tests/Tasks/Subtasks/PrimerTest.php @@ -0,0 +1,19 @@ +swapConfig(array( + 'rocketeer::default' => 'production', + 'rocketeer::strategies.primer' => function () { + return 'ls'; + }, + )); + + $this->assertTaskHistory('Primer', ['ls']); + } +} diff --git a/tests/Tasks/TeardownTest.php b/tests/Tasks/TeardownTest.php index 63f8146ca..81ca215df 100644 --- a/tests/Tasks/TeardownTest.php +++ b/tests/Tasks/TeardownTest.php @@ -7,10 +7,10 @@ class TeardownTest extends RocketeerTestCase { public function testCanTeardownServer() { - $this->mock('rocketeer.server', 'Server', function ($mock) { + $this->mock('rocketeer.storage.local', 'LocalStorage', function ($mock) { return $mock ->shouldReceive('getSeparator')->andReturn(DIRECTORY_SEPARATOR) - ->shouldReceive('deleteRepository')->once(); + ->shouldReceive('destroy')->once(); }); $this->assertTaskHistory('Teardown', array( @@ -20,10 +20,10 @@ public function testCanTeardownServer() public function testCanAbortTeardown() { - $this->mock('rocketeer.server', 'Server', function ($mock) { + $this->mock('rocketeer.storage.local', 'LocalStorage', function ($mock) { return $mock ->shouldReceive('getSeparator')->andReturn(DIRECTORY_SEPARATOR) - ->shouldReceive('deleteRepository')->never(); + ->shouldReceive('destroy')->never(); }); $task = $this->pretendTask('Teardown', array(), array('confirm' => false)); diff --git a/tests/Tasks/TestTest.php b/tests/Tasks/TestTest.php deleted file mode 100644 index 0a0e833da..000000000 --- a/tests/Tasks/TestTest.php +++ /dev/null @@ -1,17 +0,0 @@ -assertTaskHistory('Test', array( - array( - 'cd {server}/releases/20000000000000', - '{phpunit} --stop-on-failure ', - ), - )); - } -} diff --git a/tests/Tasks/UpdateTest.php b/tests/Tasks/UpdateTest.php index 08d4fc25c..372952590 100644 --- a/tests/Tasks/UpdateTest.php +++ b/tests/Tasks/UpdateTest.php @@ -9,31 +9,33 @@ public function testCanUpdateRepository() { $task = $this->pretendTask('Update', array( 'migrate' => true, - 'seed' => true + 'seed' => true, )); $matcher = array( array( "cd {server}/releases/20000000000000", "git reset --hard", - "git pull" + "git pull", ), - "mkdir -p {server}/shared/tests", - "mv {server}/releases/20000000000000/tests/Elements {server}/shared/tests/Elements", array( "cd {server}/releases/20000000000000", "chmod -R 755 {server}/releases/20000000000000/tests", "chmod -R g+s {server}/releases/20000000000000/tests", - "chown -R www-data:www-data {server}/releases/20000000000000/tests" + "chown -R www-data:www-data {server}/releases/20000000000000/tests", ), array( - "cd {server}/releases/20000000000000", - "{php} artisan migrate --seed" + "cd {server}/releases/{release}", + "{php} artisan migrate", + ), + array( + "cd {server}/releases/{release}", + "{php} artisan db:seed", ), array( "cd {server}/releases/20000000000000", - "{php} artisan cache:clear" - ) + "{php} artisan cache:clear", + ), ); $this->assertTaskHistory($task, $matcher); diff --git a/tests/TasksHandlerTest.php b/tests/TasksHandlerTest.php deleted file mode 100644 index 15c82fe1b..000000000 --- a/tests/TasksHandlerTest.php +++ /dev/null @@ -1,104 +0,0 @@ -tasksQueue()->add('Rocketeer\Tasks\Deploy'); - $this->assertInstanceOf('Rocketeer\Commands\BaseTaskCommand', $command); - $this->assertInstanceOf('Rocketeer\Tasks\Deploy', $command->getTask()); - } - - public function testCanUseFacadeOutsideOfLaravel() - { - Rocketeer::before('deploy', 'ls'); - $before = Rocketeer::getTasksListeners('deploy', 'before', true); - - $this->assertEquals(array('ls'), $before); - } - - public function testCanGetTasksBeforeOrAfterAnotherTask() - { - $task = $this->task('Deploy'); - $before = $this->tasksQueue()->getTasksListeners($task, 'before', true); - - $this->assertEquals(array('before', 'foobar'), $before); - } - - public function testCanAddTasksViaFacade() - { - $task = $this->task('Deploy'); - $before = $this->tasksQueue()->getTasksListeners($task, 'before', true); - - $this->tasksQueue()->before('deploy', 'composer install'); - - $newBefore = array_merge($before, array('composer install')); - $this->assertEquals($newBefore, $this->tasksQueue()->getTasksListeners($task, 'before', true)); - } - - public function testCanAddMultipleTasksViaFacade() - { - $task = $this->task('Deploy'); - $after = $this->tasksQueue()->getTasksListeners($task, 'after', true); - - $this->tasksQueue()->after('deploy', array( - 'composer install', - 'bower install' - )); - - $newAfter = array_merge($after, array('composer install', 'bower install')); - $this->assertEquals($newAfter, $this->tasksQueue()->getTasksListeners($task, 'after', true)); - } - - public function testCanAddSurroundTasksToNonExistingTasks() - { - $task = $this->task('Setup'); - $this->tasksQueue()->after('setup', 'composer install'); - - $after = array('composer install'); - $this->assertEquals($after, $this->tasksQueue()->getTasksListeners($task, 'after', true)); - } - - public function testCanAddSurroundTasksToMultipleTasks() - { - $this->tasksQueue()->after(array('cleanup', 'setup'), 'composer install'); - - $after = array('composer install'); - $this->assertEquals($after, $this->tasksQueue()->getTasksListeners('setup', 'after', true)); - $this->assertEquals($after, $this->tasksQueue()->getTasksListeners('cleanup', 'after', true)); - } - - public function testCangetTasksListenersOrAfterAnotherTaskBySlug() - { - $after = $this->tasksQueue()->getTasksListeners('deploy', 'after', true); - - $this->assertEquals(array('after', 'foobar'), $after); - } - - public function testCanAddEventsWithPriority() - { - $this->tasksQueue()->before('deploy', 'second', -5); - $this->tasksQueue()->before('deploy', 'first'); - - $listeners = $this->tasksQueue()->getTasksListeners('deploy', 'before', true); - $this->assertEquals(array('before', 'foobar', 'first', 'second'), $listeners); - } - - public function testCanExecuteContextualEvents() - { - $this->swapConfig(array( - 'rocketeer::stages.stages' => array('hasEvent', 'noEvent'), - 'rocketeer::on.stages.hasEvent.hooks' => array('before' => array('check' => 'ls')), - )); - - $this->app['rocketeer.rocketeer']->setStage('hasEvent'); - $this->assertEquals(array('ls'), $this->tasksQueue()->getTasksListeners('check', 'before', true)); - - $this->app['rocketeer.rocketeer']->setStage('noEvent'); - $this->assertEquals(array(), $this->tasksQueue()->getTasksListeners('check', 'before', true)); - } -} diff --git a/tests/TasksQueueTest.php b/tests/TasksQueueTest.php deleted file mode 100644 index 9cbceacdd..000000000 --- a/tests/TasksQueueTest.php +++ /dev/null @@ -1,144 +0,0 @@ -tasksQueue()->buildTaskFromClass('Rocketeer\Tasks\Deploy'); - - $this->assertInstanceOf('Rocketeer\Traits\Task', $task); - } - - public function testCanBuildCustomTaskByName() - { - $tasks = $this->tasksQueue()->buildQueue(array('Rocketeer\Tasks\Check')); - - $this->assertInstanceOf('Rocketeer\Tasks\Check', $tasks[0]); - } - - public function testCanBuildTaskFromString() - { - $string = 'echo "I love ducks"'; - - $string = $this->tasksQueue()->buildTaskFromClosure($string); - $this->assertInstanceOf('Rocketeer\Tasks\Closure', $string); - - $closure = $string->getClosure(); - $this->assertInstanceOf('Closure', $closure); - - $closureReflection = new ReflectionFunction($closure); - $this->assertEquals(array('stringTask' => 'echo "I love ducks"'), $closureReflection->getStaticVariables()); - - $this->assertEquals('I love ducks', $string->execute()); - } - - public function testCanBuildTaskFromClosure() - { - $originalClosure = function ($task) { - return $task->getCommand()->info('echo "I love ducks"'); - }; - - $closure = $this->tasksQueue()->buildTaskFromClosure($originalClosure); - $this->assertInstanceOf('Rocketeer\Tasks\Closure', $closure); - $this->assertEquals($originalClosure, $closure->getClosure()); - } - - public function testCanBuildQueue() - { - $queue = array( - 'foobar', - function ($task) { - return 'lol'; - }, - 'Rocketeer\Tasks\Deploy' - ); - - $queue = $this->tasksQueue()->buildQueue($queue); - - $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[0]); - $this->assertInstanceOf('Rocketeer\Tasks\Closure', $queue[1]); - $this->assertInstanceOf('Rocketeer\Tasks\Deploy', $queue[2]); - } - - public function testCanRunQueue() - { - $this->swapConfig(array( - 'rocketeer::default' => 'production', - )); - - $this->expectOutputString('JOEY DOESNT SHARE FOOD'); - $this->tasksQueue()->run(array( - function ($task) { - print 'JOEY DOESNT SHARE FOOD'; - } - ), $this->getCommand()); - } - - public function testCanRunQueueOnDifferentConnectionsAndStages() - { - $this->swapConfig(array( - 'rocketeer::default' => array('staging', 'production'), - 'rocketeer::stages.stages' => array('first', 'second'), - )); - - $output = array(); - $queue = array( - function ($task) use (&$output) { - $output[] = $task->rocketeer->getConnection(). ' - ' .$task->rocketeer->getStage(); - } - ); - - $queue = $this->tasksQueue()->buildQueue($queue); - $this->tasksQueue()->run($queue, $this->getCommand()); - - $this->assertEquals(array( - 'staging - first', - 'staging - second', - 'production - first', - 'production - second', - ), $output); - } - - public function testCanRunQueueViaExecute() - { - $this->swapConfig(array( - 'rocketeer::default' => 'production', - )); - - $output = $this->tasksQueue()->execute(array( - 'ls -a', - function ($task) { - return 'JOEY DOESNT SHARE FOOD'; - } - )); - - $this->assertEquals(array( - '.'.PHP_EOL.'..'.PHP_EOL.'.gitkeep', - 'JOEY DOESNT SHARE FOOD', - ), $output); - } - - public function testCanRunOnMultipleConnectionsViaOn() - { - $this->swapConfig(array( - 'rocketeer::stages.stages' => array('first', 'second'), - )); - - $output = $this->tasksQueue()->on(array('staging', 'production'), function ($task) { - return $task->rocketeer->getConnection(). ' - ' .$task->rocketeer->getStage(); - }); - - $this->assertEquals(array( - 'staging - first', - 'staging - second', - 'production - first', - 'production - second', - ), $output); - } -} diff --git a/tests/TestCases/ContainerTestCase.php b/tests/TestCases/ContainerTestCase.php index 7b1ad0eda..f46882796 100644 --- a/tests/TestCases/ContainerTestCase.php +++ b/tests/TestCases/ContainerTestCase.php @@ -1,20 +1,37 @@ app->instance('path.base', '/src'); - $this->app->instance('path', '/src/app'); - $this->app->instance('path.public', '/src/public'); + $this->app->instance('path.base', '/src'); + $this->app->instance('path', '/src/app'); + $this->app->instance('path.public', '/src/public'); $this->app->instance('path.storage', '/src/app/storage'); $this->app['files'] = new Filesystem; - $this->app['config'] = $this->getConfig(); - $this->app['remote'] = $this->getRemote(); $this->app['artisan'] = $this->getArtisan(); + $this->app['rocketeer.remote'] = $this->getRemote(); $this->app['rocketeer.command'] = $this->getCommand(); // Rocketeer classes ------------------------------------------- / $serviceProvider = new RocketeerServiceProvider($this->app); - $this->app = $serviceProvider->bindPaths($this->app); - $this->app = $serviceProvider->bindCoreClasses($this->app); - $this->app = $serviceProvider->bindClasses($this->app); - $this->app = $serviceProvider->bindScm($this->app); + $serviceProvider->boot(); + + // Swap some instances with Mockeries -------------------------- / + + $this->app['config'] = $this->getConfig(); } /** @@ -67,13 +84,21 @@ public function tearDown() * @param string $handle * @param string $class * @param Closure $expectations + * @param boolean $partial * * @return Mockery */ - protected function mock($handle, $class, $expectations) + protected function mock($handle, $class = null, Closure $expectations = null, $partial = true) { + $class = $class ?: $handle; $mockery = Mockery::mock($class); - $mockery = $expectations($mockery)->mock(); + if ($partial) { + $mockery = $mockery->shouldIgnoreMissing(); + } + + if ($expectations) { + $mockery = $expectations($mockery)->mock(); + } $this->app[$handle] = $mockery; @@ -94,11 +119,16 @@ protected function getCommand(array $expectations = array(), array $options = ar return $message; }; - $command = Mockery::mock('Command'); - $command->shouldReceive('comment')->andReturnUsing($message); - $command->shouldReceive('error')->andReturnUsing($message); - $command->shouldReceive('line')->andReturnUsing($message); - $command->shouldReceive('info')->andReturnUsing($message); + $command = Mockery::mock('Command')->shouldIgnoreMissing(); + $command->shouldReceive('getOutput')->andReturn(null); + + // Bind the output expectations + $types = ['comment', 'error', 'line', 'info']; + foreach ($types as $type) { + if (!array_key_exists($type, $expectations)) { + $command->shouldReceive($type)->andReturnUsing($message); + } + } // Merge defaults $expectations = array_merge(array( @@ -115,7 +145,8 @@ protected function getCommand(array $expectations = array(), array $options = ar if ($key === 'option') { $command->shouldReceive($key)->andReturn($value)->byDefault(); } else { - $command->shouldReceive($key)->andReturn($value); + $returnMethod = $value instanceof Closure ? 'andReturnUsing' : 'andReturn'; + $command->shouldReceive($key)->$returnMethod($value); } } @@ -141,94 +172,33 @@ protected function getConfig($expectations = array()) $config = Mockery::mock('Illuminate\Config\Repository'); $config->shouldIgnoreMissing(); + $defaults = $this->getFactoryConfiguration(); + $expectations = array_merge($defaults, $expectations); foreach ($expectations as $key => $value) { $config->shouldReceive('get')->with($key)->andReturn($value); } - // Drivers - $config->shouldReceive('get')->with('cache.driver')->andReturn('file'); - $config->shouldReceive('get')->with('database.default')->andReturn('mysql'); - $config->shouldReceive('get')->with('remote.default')->andReturn('production'); - $config->shouldReceive('get')->with('remote.connections')->andReturn(array('production' => array(), 'staging' => array())); - $config->shouldReceive('get')->with('session.driver')->andReturn('file'); - - // Rocketeer - $config->shouldReceive('get')->with('rocketeer::application_name')->andReturn('foobar'); - $config->shouldReceive('get')->with('rocketeer::default')->andReturn(array('production', 'staging')); - $config->shouldReceive('get')->with('rocketeer::logs')->andReturn(false); - $config->shouldReceive('get')->with('rocketeer::connections')->andReturn(array()); - $config->shouldReceive('get')->with('rocketeer::remote.strategy')->andReturn('clone'); - $config->shouldReceive('get')->with('rocketeer::remote.keep_releases')->andReturn(1); - $config->shouldReceive('get')->with('rocketeer::remote.permissions.callback')->andReturn(function ($task, $file) { - return array( - sprintf('chmod -R 755 %s', $file), - sprintf('chmod -R g+s %s', $file), - sprintf('chown -R www-data:www-data %s', $file), - ); - }); - $config->shouldReceive('get')->with('rocketeer::remote.permissions.files')->andReturn(array('tests')); - $config->shouldReceive('get')->with('rocketeer::remote.root_directory')->andReturn(__DIR__.'/../_server/'); - $config->shouldReceive('get')->with('rocketeer::remote.app_directory')->andReturn(null); - $config->shouldReceive('get')->with('rocketeer::remote.shared')->andReturn(array('tests/Elements')); - $config->shouldReceive('get')->with('rocketeer::remote.composer')->andReturn(function ($task) { - return array( - $task->composer('self-update'), - $task->composer('install --no-interaction --no-dev --prefer-dist'), - ); - }); - $config->shouldReceive('get')->with('rocketeer::stages.default')->andReturn(null); - $config->shouldReceive('get')->with('rocketeer::stages.stages')->andReturn(array()); - - // Paths - $config->shouldReceive('get')->with('rocketeer::paths.php')->andReturn(''); - $config->shouldReceive('get')->with('rocketeer::paths.composer')->andReturn(''); - $config->shouldReceive('get')->with('rocketeer::paths.artisan')->andReturn(''); - - // SCM - $config->shouldReceive('get')->with('rocketeer::scm.branch')->andReturn('master'); - $config->shouldReceive('get')->with('rocketeer::scm.repository')->andReturn('https://github.com/'.$this->repository); - $config->shouldReceive('get')->with('rocketeer::scm.scm')->andReturn('git'); - $config->shouldReceive('get')->with('rocketeer::scm.shallow')->andReturn(true); - $config->shouldReceive('get')->with('rocketeer::scm.submodules')->andReturn(true); - - // Tasks - $config->shouldReceive('get')->with('rocketeer::hooks')->andReturn(array( - 'before' => array( - 'deploy' => array( - 'before', - 'foobar' - ), - ), - 'after' => array( - 'check' => array( - 'Rocketeer\Dummies\MyCustomTask', - ), - 'deploy' => array( - 'after', - 'foobar' - ), - ), - )); - return $config; } /** * Swap the current config * - * @param array $config + * @param array $config * * @return void */ protected function swapConfig($config) { - $this->app['rocketeer.rocketeer']->disconnect(); + $this->connections->disconnect(); $this->app['config'] = $this->getConfig($config); } /** * Mock the Remote component * + * @param string|array|null $mockedOutput + * * @return Mockery */ protected function getRemote($mockedOutput = null) @@ -243,9 +213,9 @@ protected function getRemote($mockedOutput = null) }; $remote = Mockery::mock('Illuminate\Remote\Connection'); + $remote->shouldReceive('connected')->andReturn(true); $remote->shouldReceive('into')->andReturn(Mockery::self()); $remote->shouldReceive('status')->andReturn(0)->byDefault(); - $remote->shouldReceive('run')->andReturnUsing($run)->byDefault(); $remote->shouldReceive('runRaw')->andReturnUsing($run)->byDefault(); $remote->shouldReceive('getString')->andReturnUsing(function ($file) { return file_get_contents($file); @@ -257,6 +227,14 @@ protected function getRemote($mockedOutput = null) print $line.PHP_EOL; }); + if (is_array($mockedOutput)) { + foreach ($mockedOutput as $command => $output) { + $remote->shouldReceive('run')->with($command)->andReturn($output); + } + } else { + $remote->shouldReceive('run')->andReturnUsing($run)->byDefault(); + } + return $remote; } @@ -274,4 +252,76 @@ protected function getArtisan() return $artisan; } + + /** + * @return array + */ + protected function getFactoryConfiguration() + { + if ($this->defaults) { + return $this->defaults; + } + + // Base the mocked configuration off the factory values + $defaults = []; + $files = ['config', 'hooks', 'paths', 'remote', 'scm', 'stages', 'strategies']; + foreach ($files as $file) { + $defaults[$file] = $this->config->get('rocketeer::'.$file); + } + + // Build correct keys + $defaults = array_dot($defaults); + $keys = array_keys($defaults); + $keys = array_map(function ($key) { + return 'rocketeer::'.str_replace('config.', null, $key); + }, $keys); + $defaults = array_combine($keys, array_values($defaults)); + + $overrides = array( + 'cache.driver' => 'file', + 'database.default' => 'mysql', + 'remote.default' => 'production', + 'session.driver' => 'file', + 'remote.connections' => array( + 'production' => [], + 'staging' => [], + ), + 'rocketeer::application_name' => 'foobar', + 'rocketeer::logs' => null, + 'rocketeer::remote.permissions.files' => ['tests'], + 'rocketeer::remote.shared' => ['tests/Elements'], + 'rocketeer::remote.keep_releases' => 1, + 'rocketeer::remote.root_directory' => __DIR__.'/../_server/', + 'rocketeer::scm' => array( + 'branch' => 'master', + 'repository' => 'https://github.com/'.$this->repository, + 'scm' => 'git', + 'shallow' => true, + 'submodules' => true, + ), + 'rocketeer::strategies.dependencies' => 'Composer', + 'rocketeer::hooks' => array( + 'before' => array( + 'deploy' => array( + 'before', + 'foobar', + ), + ), + 'after' => array( + 'check' => array( + 'Rocketeer\Dummies\MyCustomTask', + ), + 'deploy' => array( + 'after', + 'foobar', + ), + ), + ), + ); + + // Assign options to expectations + $this->defaults = array_merge($defaults, $overrides); + + return $this->defaults; + } } diff --git a/tests/TestCases/Modules/RocketeerAssertions.php b/tests/TestCases/Modules/RocketeerAssertions.php new file mode 100644 index 000000000..822d1e54f --- /dev/null +++ b/tests/TestCases/Modules/RocketeerAssertions.php @@ -0,0 +1,157 @@ +assertEquals($connection, $this->connections->getConnection()); + } + + /** + * Assert that the current repository equals + * + * @param string $repository + */ + protected function assertRepositoryEquals($repository) + { + $this->assertEquals($repository, $this->connections->getRepositoryEndpoint()); + } + + /** + * Assert an option has a certain value + * + * @param string $value + * @param string $option + */ + protected function assertOptionValueEquals($value, $option) + { + $this->assertEquals($value, $this->rocketeer->getOption($option)); + } + + /** + * Assert a task has a particular output + * + * @param string $task + * @param string $output + * @param \Mockery $command + * + * @return Assertion + */ + protected function assertTaskOutput($task, $output, $command = null) + { + if ($command) { + $this->app['rocketeer.command'] = $command; + } + + return $this->assertContains($output, $this->task($task)->execute()); + } + + /** + * Assert a task's history matches an array + * + * @param string|AbstractTask $task + * @param array $expectedHistory + * @param array $options + * + * @return string + */ + protected function assertTaskHistory($task, array $expectedHistory, array $options = array()) + { + // Create task if needed + if (is_string($task)) { + $task = $this->pretendTask($task, $options); + } + + // Execute task and get history + if (is_array($task)) { + $results = ''; + $taskHistory = $task; + } else { + $results = $task->execute(); + $taskHistory = $task->history->getFlattenedHistory(); + } + + $this->assertHistory($expectedHistory, $taskHistory); + + return $results; + } + + /** + * Assert an history matches another + * + * @param array $expected + * @param array $obtained + */ + public function assertHistory(array $expected, array $obtained = array()) + { + if (!$obtained) { + $obtained = $this->history->getFlattenedHistory(); + } + + // Look for release in history + $release = implode(array_flatten($obtained)); + preg_match_all('/[0-9]{14}/', $release, $releases); + $release = Arr::get($releases, '0.0', date('YmdHis')); + if ($release === '10000000000000') { + $release = Arr::get($releases, '0.1', date('YmdHis')); + } + + // Replace placeholders + $expected = $this->replaceHistoryPlaceholders($expected, $release); + + // Check equality + $this->assertEquals($expected, $obtained); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// HELPERS /////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Replace placeholders in an history + * + * @param array $history + * @param integer|null $release + * + * @return array + */ + protected function replaceHistoryPlaceholders($history, $release = null) + { + $release = $release ?: date('YmdHis'); + $hhvm = defined('HHVM_VERSION'); + + $replaced = []; + foreach ($history as $key => $entries) { + if ($hhvm && $entries == '{php} -m') { + continue; + } + + if (is_array($entries)) { + $replaced[$key] = $this->replaceHistoryPlaceholders($entries, $release); + continue; + } + + $replaced[$key] = strtr($entries, array( + '{php}' => $this->binaries['php'], + '{bundle}' => $this->binaries['bundle'], + '{phpunit}' => $this->binaries['phpunit'], + '{repository}' => 'https://github.com/'.$this->repository, + '{server}' => $this->server, + '{release}' => $release, + '{composer}' => $this->binaries['composer'], + )); + } + + return $replaced; + } +} diff --git a/tests/TestCases/Modules/RocketeerMockeries.php b/tests/TestCases/Modules/RocketeerMockeries.php new file mode 100644 index 000000000..723445643 --- /dev/null +++ b/tests/TestCases/Modules/RocketeerMockeries.php @@ -0,0 +1,114 @@ +server.'/current/composer.json'; + if ($uses) { + $this->files->put($composer, '{}'); + } else { + $this->files->delete($composer); + } + } + + /** + * @param array $state + */ + protected function mockState(array $state) + { + file_put_contents($this->server.'/state.json', json_encode($state)); + } + + /** + * Set Rocketeer in pretend mode + * + * @param array $options + * @param array $expectations + */ + protected function pretend($options = array(), $expectations = array()) + { + $options['pretend'] = true; + + $this->mockCommand($options, $expectations); + } + + ////////////////////////////////////////////////////////////////////// + ////////////////////////////// SERVICES ////////////////////////////// + ////////////////////////////////////////////////////////////////////// + + /** + * Mock the ReleasesManager + * + * @param Closure $expectations + * + * @return Mockery + */ + protected function mockReleases(Closure $expectations) + { + return $this->mock('rocketeer.releases', 'ReleasesManager', $expectations); + } + + /** + * Mock a Command + * + * @param array $options + * @param array $expectations + */ + protected function mockCommand($options = array(), $expectations = array()) + { + // Default options + $options = array_merge(array( + 'pretend' => false, + 'verbose' => false, + 'tests' => false, + 'migrate' => false, + 'seed' => false, + 'stage' => false, + 'parallel' => false, + 'update' => false, + ), $options); + + $this->app['rocketeer.command'] = $this->getCommand($expectations, $options); + } + + /** + * Mock the RemoteHandler + * + * @param string|array|null $expectations + */ + protected function mockRemote($expectations = null) + { + $this->app['rocketeer.remote'] = $this->getRemote($expectations); + } + + /** + * @param Closure|null $expectations + */ + protected function mockFiles(Closure $expectations = null) + { + $this->mock('files', 'Illuminate\Filesystem\Filesystem', $expectations); + } + + /** + * @param array $configuration + */ + public function mockConfig(array $configuration) + { + $this->app['config'] = $this->getConfig($configuration); + } +} diff --git a/tests/TestCases/RocketeerTestCase.php b/tests/TestCases/RocketeerTestCase.php index a5a8fcf89..6b6420e31 100644 --- a/tests/TestCases/RocketeerTestCase.php +++ b/tests/TestCases/RocketeerTestCase.php @@ -1,10 +1,15 @@ server = __DIR__.'/../_server/foobar'; - $this->deploymentsFile = __DIR__.'/../_meta/deployments.json'; + $this->customConfig = $this->server.'/../.rocketeer'; + $this->deploymentsFile = $this->server.'/deployments.json'; - // Bind new Server instance - $meta = dirname($this->deploymentsFile); - $this->app->bind('rocketeer.server', function ($app) use ($meta) { - return new Server($app, 'deployments', $meta); - }); - - // Bind dummy Task + // Bind dummy AbstractTask $this->task = $this->task('Cleanup'); $this->recreateVirtualServer(); - } - - /** - * Recreates the local file server - * - * @return void - */ - protected function recreateVirtualServer() - { - // Recreate deployments file - $this->app['files']->put($this->deploymentsFile, json_encode(array( - 'foo' => 'bar', - 'current_release' => array('production' => 20000000000000), - 'directory_separator' => '/', - 'is_setup' => true, - 'webuser' => array('username' => 'www-data','group' => 'www-data'), - 'line_endings' => "\n", - ))); - $rootPath = $this->server.'/../../..'; - - // Recreate altered local server - $this->app['files']->deleteDirectory($rootPath.'/storage'); - $this->app['files']->deleteDirectory($this->server.'/logs'); - $folders = array('current', 'shared', 'releases','releases/10000000000000', 'releases/15000000000000', 'releases/20000000000000'); - foreach ($folders as $folder) { - $folder = $this->server.'/'.$folder; + // Bind new LocalStorage instance + $this->app->bind('rocketeer.storage.local', function ($app) { + $folder = dirname($this->deploymentsFile); - $this->app['files']->deleteDirectory($folder); - $this->app['files']->delete($folder); - $this->app['files']->makeDirectory($folder, 0777, true); - file_put_contents($folder.'/.gitkeep', ''); - } - file_put_contents($this->server.'/state.json', json_encode(array( - '10000000000000' => true, - '15000000000000' => false, - '20000000000000' => true, - ))); + return new LocalStorage($app, 'deployments', $folder); + }); - // Delete rocketeer config - $binary = $rootPath.'/.rocketeer'; - $this->app['files']->deleteDirectory($binary); + // Cache paths + $this->binaries = array( + 'php' => exec('which php') ?: 'php', + 'bundle' => exec('which bundle') ?: 'bundle', + 'phpunit' => exec('which phpunit') ?: 'phpunit', + 'composer' => exec('which composer') ?: 'composer', + ); } - //////////////////////////////////////////////////////////////////// - ///////////////////////////// ASSERTIONS /////////////////////////// - //////////////////////////////////////////////////////////////////// - /** - * Assert a task has a particular output - * - * @param string $task - * @param string $output - * @param Mockery $command - * - * @return Assertion + * Cleanup tests */ - protected function assertTaskOutput($task, $output, $command = null) + public function tearDown() { - return $this->assertContains($output, $this->task($task, $command)->execute()); - } - - /** - * Assert a task's history matches an array - * - * @param string|Task $task - * @param array $history - * @param array $options - * - * @return string - */ - protected function assertTaskHistory($task, array $expectedHistory, array $options = array()) - { - // Create task if needed - if (is_string($task)) { - $task = $this->pretendTask($task, $options); - } - - // Execute task and get history - if (is_array($task)) { - $results = ''; - $taskHistory = $task; - } else { - $results = $task->execute(); - $taskHistory = $task->getHistory(); - } + parent::tearDown(); - // Look for release in history - $release = join(array_flatten($taskHistory)); - preg_match_all('/[0-9]{14}/', $release, $releases); - $release = array_get($releases, '0.0', date('YmdHis')); - if ($release === '10000000000000') { - $release = array_get($releases, '0.1', date('YmdHis')); - } - - // Replace placeholders - $expectedHistory = $this->replaceHistoryPlaceholders($expectedHistory, $release); - - // Check equality - $this->assertEquals($expectedHistory, $taskHistory); - - return $results; + // Restore superglobals + $_SERVER['HOME'] = $this->home; } /** - * Replace placeholders in an history - * - * @param array $history + * Recreates the local file server * - * @return array + * @return void */ - protected function replaceHistoryPlaceholders($history, $release = null) + protected function recreateVirtualServer() { - $release = $release ?: date('YmdHis'); - - foreach ($history as $key => $entries) { - if (is_array($entries)) { - $history[$key] = $this->replaceHistoryPlaceholders($entries, $release); - continue; - } - - $history[$key] = strtr($entries, array( - '{php}' => exec('which php'), - '{phpunit}' => exec('which phpunit'), - '{server}' => $this->server, - '{release}' => $release, - '{composer}' => exec('which composer'), - )); + // Save superglobals + $this->home = $_SERVER['HOME']; + + // Cleanup files created by tests + $cleanup = array( + realpath(__DIR__.'/../../.rocketeer'), + realpath(__DIR__.'/../.rocketeer'), + realpath($this->server), + realpath($this->customConfig), + ); + array_map([$this->files, 'deleteDirectory'], $cleanup); + if (is_link($this->server.'/current')) { + unlink($this->server.'/current'); } - return $history; - } - - //////////////////////////////////////////////////////////////////// - ////////////////////////////// MOCKERIES /////////////////////////// - //////////////////////////////////////////////////////////////////// - - /** - * Mock the ReleasesManager - * - * @param Closure $expectations - * - * @return Mockery - */ - protected function mockReleases($expectations) - { - return $this->mock('rocketeer.releases', 'ReleasesManager', $expectations); + // Recreate altered local server + $this->files->copyDirectory($this->server.'-stub', $this->server); } //////////////////////////////////////////////////////////////////// @@ -207,66 +117,39 @@ protected function mockReleases($expectations) //////////////////////////////////////////////////////////////////// /** - * Mock the Composer check + * Get a pretend AbstractTask to run bogus commands * - * @param boolean $uses + * @param string $task + * @param array $options + * @param array $expectations * - * @return void - */ - protected function usesComposer($uses = true) - { - $composer = $this->app['path.base'].DIRECTORY_SEPARATOR.'composer.json'; - $this->mock('files', 'Illuminate\Filesystem\Filesystem', function ($mock) use ($composer, $uses) { - return $mock->makePartial()->shouldReceive('exists')->with($composer)->andReturn($uses); - }); - } - - /** - * Get a pretend Task to run bogus commands - * - * @return Task + * @return \Rocketeer\Abstracts\AbstractTask */ protected function pretendTask($task = 'Deploy', $options = array(), array $expectations = array()) { - // Default options - $options = array_merge(array( - 'pretend' => true, - 'verbose' => false, - 'tests' => false, - 'migrate' => false, - 'seed' => false, - ), $options); + $this->pretend($options, $expectations); - return $this->task($task, $this->getCommand($expectations, $options)); + return $this->task($task); } /** - * Get Task instance + * Get AbstractTask instance * - * @param string $task + * @param string $task + * @param array $options * - * @return Task + * @return \Rocketeer\Abstracts\AbstractTask */ - protected function task($task = null, $command = null) + protected function task($task = null, $options = array()) { - if ($command) { - $this->app['rocketeer.command'] = $command; + if ($options) { + $this->mockCommand($options); } if (!$task) { return $this->task; } - return $this->tasksQueue()->buildTaskFromClass('Rocketeer\Tasks\\'.$task); - } - - /** - * Get TasksQueue instance - * - * @return TasksQueue - */ - protected function tasksQueue() - { - return $this->app['rocketeer.tasks']; + return $this->builder->buildTaskFromClass($task); } } diff --git a/tests/Traits/BashModules/BinariesTest.php b/tests/Traits/BashModules/BinariesTest.php index a64361831..0b9674951 100644 --- a/tests/Traits/BashModules/BinariesTest.php +++ b/tests/Traits/BashModules/BinariesTest.php @@ -1,71 +1,87 @@ app['config'] = $this->getConfig(array('rocketeer::paths.composer' => 'foobar')); + $this->mockConfig(['rocketeer::paths.composer' => __FILE__]); - $this->assertEquals('foobar', $this->task->which('composer')); + $this->assertEquals(__FILE__, $this->task->which('composer')); } - public function testCanSetPathToPhpAndArtisan() + public function testStoredPathsAreInvalidatedIfIncorrect() { - $this->app['config'] = $this->getConfig(array( - 'rocketeer::paths.php' => '/usr/local/bin/php', - 'rocketeer::paths.artisan' => './laravel/artisan', - )); + $this->mock('rocketeer.remote', 'Remote', function ($mock) { + return $mock + ->shouldReceive('run')->with(['which composer'], Mockery::any())->andReturn(null) + ->shouldReceive('run')->with(['which '.$this->binaries['composer']], Mockery::any())->andReturn($this->binaries['composer']) + ->shouldReceive('run')->with('[ -e ] && echo "true"', Mockery::any())->andReturn('false') + ->shouldReceive('run')->with('[ -e foobar ] && echo "true"', Mockery::any())->andReturn('false') + ->shouldReceive('runRaw')->andReturn('false'); + }, false); + + $this->localStorage->set('paths.composer', 'foobar'); - $this->assertEquals('/usr/local/bin/php ./laravel/artisan migrate', $this->task->artisan('migrate')); + $this->assertEquals('composer', $this->task->which('composer')); + $this->assertNull($this->localStorage->get('paths.composer')); } - public function testFetchesBinaryIfNotSpecifiedOrNull() + public function testCanSetPathToPhpAndArtisan() { - $this->app['config'] = $this->getConfig(array( - 'rocketeer::paths.php' => '/usr/local/bin/php', + $this->mockConfig(array( + 'rocketeer::paths.php' => $this->binaries['php'], + 'rocketeer::paths.artisan' => $this->binaries['php'], )); - $this->assertEquals('/usr/local/bin/php artisan migrate', $this->task->artisan('migrate')); + $this->assertEquals($this->binaries['php'].' '.$this->binaries['php'].' migrate', $this->task->artisan()->migrate()); } - public function testCanGetBinary() + public function testFetchesBinaryIfNotSpecifiedOrNull() { - $whichGrep = exec('which grep'); - $grep = $this->task->which('grep'); + $this->mockConfig(array( + 'rocketeer::paths.php' => $this->binaries['php'], + )); - $this->assertEquals($whichGrep, $grep); + $this->assertEquals($this->binaries['php'].' artisan migrate', $this->task->artisan()->migrate()); } - public function testCanGetFallbackForBinary() + public function testCanGetBinary() { $whichGrep = exec('which grep'); - $grep = $this->task->which('foobar', $whichGrep); + $grep = $this->task->which('grep'); $this->assertEquals($whichGrep, $grep); - $this->assertFalse($this->task->which('fdsf')); } - public function testDoesntRunComposerIfNotNeeded() + public function testCanRunComposer() { $this->usesComposer(true); $this->mock('rocketeer.command', 'Illuminate\Console\Command', function ($mock) { return $mock - ->shouldReceive('option')->andReturn(array()) - ->shouldReceive('comment')->once(); + ->shouldIgnoreMissing() + ->shouldReceive('line') + ->shouldReceive('option')->andReturn([]); }); - $this->task->runComposer(); + $this->task('Dependencies')->execute(); + $this->assertCount(2, $this->history->getFlattenedHistory()[0]); + } + public function testDoesntRunComposerIfNotNeeded() + { $this->usesComposer(false); $this->mock('rocketeer.command', 'Illuminate\Console\Command', function ($mock) { return $mock - ->shouldReceive('option')->andReturn(array()) - ->shouldReceive('comment')->never(); + ->shouldIgnoreMissing() + ->shouldReceive('line') + ->shouldReceive('option')->andReturn([]); }); - $this->task->runComposer(); + $this->task('Dependencies')->execute(); + $this->assertEmpty($this->history->getFlattenedHistory()); } } diff --git a/tests/Traits/BashModules/CoreTest.php b/tests/Traits/BashModules/CoreTest.php index 40be091bd..ced529020 100644 --- a/tests/Traits/BashModules/CoreTest.php +++ b/tests/Traits/BashModules/CoreTest.php @@ -9,30 +9,71 @@ public function testCanGetArraysFromRawCommands() { $contents = $this->task->runRaw('ls', true, true); - $this->assertCount(12, $contents); + $this->assertCount(11, $contents); } public function testCanCheckStatusOfACommand() { - $this->task->remote = clone $this->getRemote()->shouldReceive('status')->andReturn(1)->mock(); - ob_start(); - $status = $this->task->checkStatus(null, 'error'); - $output = ob_get_clean(); - $this->assertEquals('error'.PHP_EOL, $output); + $this->expectOutputRegex('/.+An error occured: "Oh noes", while running:\ngit clone.+/'); + + $this->app['rocketeer.remote'] = clone $this->getRemote()->shouldReceive('status')->andReturn(1)->mock(); + $this->mockCommand([], array( + 'line' => function ($error) { + echo $error; + }, + )); + + $status = $this->task('Deploy')->checkStatus('Oh noes', 'git clone'); + $this->assertFalse($status); } public function testCanGetTimestampOffServer() { $timestamp = $this->task->getTimestamp(); + $this->assertEquals(date('YmdHis'), $timestamp); } public function testCanGetLocalTimestampIfError() { - $this->app['remote'] = $this->getRemote('NOPE'); + $this->mockRemote('NOPE'); $timestamp = $this->task->getTimestamp(); $this->assertEquals(date('YmdHis'), $timestamp); } + + public function testDoesntAppendEnvironmentToStandardTasks() + { + $this->connections->setStage('staging'); + $commands = $this->pretendTask()->processCommands(array( + 'artisan something', + 'rm readme*', + )); + + $this->assertEquals(array( + 'artisan something --env="staging"', + 'rm readme*', + ), $commands); + } + + public function testCanRemoveCommonPollutingOutput() + { + $this->mockRemote('stdin: is not a tty'.PHP_EOL.'something'); + $result = $this->bash->run('ls'); + + $this->assertEquals('something', $result); + } + + public function testCanRunCommandsLocally() + { + $this->mock('rocketeer.remote', 'Remote', function ($mock) { + return $mock->shouldReceive('run')->never(); + }); + + $this->task->setLocal(true); + $contents = $this->task->runRaw('ls', true, true); + + $this->assertCount(11, $contents); + } } diff --git a/tests/Traits/BashModules/FilesystemTest.php b/tests/Traits/BashModules/FilesystemTest.php index ec06b5684..b974c3923 100644 --- a/tests/Traits/BashModules/FilesystemTest.php +++ b/tests/Traits/BashModules/FilesystemTest.php @@ -7,9 +7,9 @@ class FilesystemTest extends RocketeerTestCase { public function testCancelsSymlinkForUnexistingFolders() { - $task = $this->pretendTask(); - $folder = '{path.storage}/logs'; - $share = $task->share($folder); + $task = $this->pretendTask(); + $folder = '{path.storage}/logs'; + $share = $task->share($folder); $this->assertFalse($share); } @@ -33,7 +33,10 @@ public function testCanListContentsOfAFolder() { $contents = $this->task->listContents($this->server); - $this->assertEquals(array('current', 'releases', 'shared', 'state.json'), $contents); + $this->assertContains('current', $contents); + $this->assertContains('releases', $contents); + $this->assertContains('shared', $contents); + $this->assertContains('state.json', $contents); } public function testCanCheckIfFileExists() @@ -41,4 +44,11 @@ public function testCanCheckIfFileExists() $this->assertTrue($this->task->fileExists($this->server)); $this->assertFalse($this->task->fileExists($this->server.'/nope')); } + + public function testDoesntTryToMoveUnexistingFolders() + { + $this->pretendTask()->move('foobar', 'bazqux'); + + $this->assertEmpty($this->history->getFlattenedOutput()); + } } diff --git a/tests/Traits/BashModules/ScmTest.php b/tests/Traits/BashModules/ScmTest.php index a71adcaa9..b7b7b8798 100644 --- a/tests/Traits/BashModules/ScmTest.php +++ b/tests/Traits/BashModules/ScmTest.php @@ -7,19 +7,21 @@ class ScmTest extends RocketeerTestCase { public function testCanForgetCredentialsIfInvalid() { - $this->app['rocketeer.server']->setValue('credentials', array( - 'repository' => 'https://Anahkiasen@bitbucket.org/Anahkiasen/registry.git', + $this->app['rocketeer.storage.local']->set('credentials', array( + 'repository' => 'https://bitbucket.org/Anahkiasen/registry.git', 'username' => 'Anahkiasen', 'password' => 'baz', )); - // Create fake remote - $remote = clone $this->getRemote(); - $remote->shouldReceive('status')->andReturn(1); + $this->mock('rocketeer.bash', 'Bash', function ($mock) { + return $mock + ->shouldIgnoreMissing() + ->shouldReceive('checkStatus')->andReturn(false); + }); + $task = $this->pretendTask(); - $task->remote = $remote; - $task->cloneRepository($this->server.'/test'); - $this->assertNull($this->app['rocketeer.server']->getValue('credentials')); + $task->getStrategy('Deploy')->deploy($this->server.'/test'); + $this->assertNull($this->app['rocketeer.storage.local']->get('credentials')); } } diff --git a/tests/Traits/ScmTest.php b/tests/Traits/ScmTest.php deleted file mode 100644 index 822b4e9e8..000000000 --- a/tests/Traits/ScmTest.php +++ /dev/null @@ -1,30 +0,0 @@ -app); - $command = $scm->getCommand('foo %s', 'bar'); - - $this->assertEquals('git foo bar', $command); - } - - public function testCanExecuteMethod() - { - $this->mock('rocketeer.bash', 'Bash', function ($mock) { - return $mock->shouldReceive('run')->once()->withAnyArgs()->andReturnUsing(function ($arguments) { - return $arguments; - }); - }); - - $scm = new Git($this->app); - $command = $scm->execute('checkout', $this->server); - - $this->assertEquals('git clone --depth 1 -b master "" '.$this->server, $command); - } -} diff --git a/tests/Traits/StepsRunnerTest.php b/tests/Traits/StepsRunnerTest.php new file mode 100644 index 000000000..20ee453b7 --- /dev/null +++ b/tests/Traits/StepsRunnerTest.php @@ -0,0 +1,34 @@ +task; + $copy = $this->server.'/state2.json'; + $task->steps()->copy($this->server.'/state.json', $copy); + + $results = $task->runSteps(); + + $this->files->delete($copy); + $this->assertTrue($results); + } + + public function testStepsAreClearedOnceRun() + { + $task = $this->task; + $task->steps()->run('ls'); + + $this->assertEquals(array( + ['run', ['ls']], + ), $task->steps()->getSteps()); + $task->runSteps(); + $task->steps()->run('php --version'); + $this->assertEquals(array( + ['run', ['php --version']], + ), $task->steps()->getSteps()); + } +} diff --git a/tests/Traits/TaskTest.php b/tests/Traits/TaskTest.php deleted file mode 100644 index ba9c98df4..000000000 --- a/tests/Traits/TaskTest.php +++ /dev/null @@ -1,77 +0,0 @@ -pretendTask('Deploy'); - $task->updateRepository(); - - $matcher = array( - array( - "cd $this->server/releases/20000000000000", - "git reset --hard", - "git pull", - ), - ); - - $this->assertEquals($matcher, $task->getHistory()); - } - - public function testCanDisplayOutputOfCommandsIfVerbose() - { - $task = $this->pretendTask('Check', array( - 'verbose' => true, - 'pretend' => false - )); - - ob_start(); - $task->run('ls'); - $output = ob_get_clean(); - - $this->assertContains('tests', $output); - } - - public function testCanPretendToRunTasks() - { - $task = $this->pretendTask(); - - $output = $task->run('ls'); - $this->assertEquals('ls', $output); - } - - public function testCanGetDescription() - { - $task = $this->task('Setup'); - - $this->assertNotNull($task->getDescription()); - } - - public function testCanRunMigrations() - { - $task = $this->pretendTask(); - $php = exec('which php'); - - $commands = $task->runMigrations(); - $this->assertEquals($php.' artisan migrate', $commands[1]); - - $commands = $task->runMigrations(true); - $this->assertEquals($php.' artisan migrate --seed', $commands[1]); - } - - public function testCanFireEventsDuringTasks() - { - $this->expectOutputString('foobar'); - - $this->tasksQueue()->listenTo('closure.test.foobar', function ($task) { - echo 'foobar'; - }); - - $task = $this->tasksQueue()->execute(function ($task) { - $task->fireEvent('test.foobar'); - }, 'staging'); - } -} diff --git a/tests/_meta/coverage.txt b/tests/_meta/coverage.txt deleted file mode 100644 index 880dfdbf0..000000000 --- a/tests/_meta/coverage.txt +++ /dev/null @@ -1,68 +0,0 @@ - - -Code Coverage Report: - 2014-03-08 19:50:07 - - Summary: - Classes: 45.16% (14/31) - Methods: 80.30% (163/203) - Lines: 90.04% (967/1074) - -\Rocketeer::Igniter - Methods: 90.00% ( 9/10) Lines: 98.28% ( 57/ 58) -\Rocketeer::LogsHandler - Methods: 100.00% ( 6/ 6) Lines: 100.00% ( 27/ 27) -\Rocketeer::ReleasesManager - Methods: 94.12% (16/17) Lines: 97.14% ( 68/ 70) -\Rocketeer::Rocketeer - Methods: 96.00% (24/25) Lines: 98.31% (116/118) -\Rocketeer::Server - Methods: 69.23% ( 9/13) Lines: 86.49% ( 64/ 74) -\Rocketeer::TasksHandler - Methods: 81.82% ( 9/11) Lines: 91.94% ( 57/ 62) -\Rocketeer::TasksQueue - Methods: 63.64% ( 7/11) Lines: 89.33% ( 67/ 75) -\Rocketeer\Plugins::Notifier - Methods: 75.00% ( 3/ 4) Lines: 87.50% ( 21/ 24) -\Rocketeer\Scm::Git - Methods: 100.00% ( 7/ 7) Lines: 100.00% ( 10/ 10) -\Rocketeer\Tasks::Check - Methods: 75.00% ( 6/ 8) Lines: 92.65% ( 63/ 68) -\Rocketeer\Tasks::Cleanup - Methods: 100.00% ( 2/ 2) Lines: 100.00% ( 11/ 11) -\Rocketeer\Tasks::Closure - Methods: 100.00% ( 5/ 5) Lines: 100.00% ( 9/ 9) -\Rocketeer\Tasks::CurrentRelease - Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 11/ 11) -\Rocketeer\Tasks::Deploy - Methods: 40.00% ( 2/ 5) Lines: 81.40% ( 35/ 43) -\Rocketeer\Tasks::Ignite - Methods: 100.00% ( 4/ 4) Lines: 100.00% ( 20/ 20) -\Rocketeer\Tasks::Rollback - Methods: 50.00% ( 1/ 2) Lines: 61.11% ( 11/ 18) -\Rocketeer\Tasks::Setup - Methods: 50.00% ( 1/ 2) Lines: 96.30% ( 26/ 27) -\Rocketeer\Tasks::Teardown - Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 7/ 7) -\Rocketeer\Tasks::Test - Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 2/ 2) -\Rocketeer\Tasks::Update - Methods: 100.00% ( 1/ 1) Lines: 100.00% ( 10/ 10) -\Rocketeer\Traits::AbstractLocatorClass - Methods: 100.00% ( 4/ 4) Lines: 100.00% ( 19/ 19) -\Rocketeer\Traits::Plugin - Methods: 100.00% ( 2/ 2) Lines: 100.00% ( 5/ 5) -\Rocketeer\Traits::Scm - Methods: 100.00% ( 3/ 3) Lines: 100.00% ( 9/ 9) -\Rocketeer\Traits::Task - Methods: 72.73% ( 8/11) Lines: 88.89% ( 32/ 36) -\Rocketeer\Traits\BashModules::Binaries - Methods: 50.00% ( 4/ 8) Lines: 89.09% ( 49/ 55) -\Rocketeer\Traits\BashModules::Core - Methods: 91.67% (11/12) Lines: 94.12% ( 64/ 68) -\Rocketeer\Traits\BashModules::Filesystem - Methods: 80.00% ( 8/10) Lines: 94.12% ( 32/ 34) -\Rocketeer\Traits\BashModules::Flow - Methods: 100.00% ( 6/ 6) Lines: 100.00% ( 22/ 22) -\Rocketeer\Traits\BashModules::Scm - Methods: 66.67% ( 2/ 3) Lines: 96.88% ( 31/ 32) diff --git a/tests/_meta/deployments.json b/tests/_meta/deployments.json deleted file mode 100644 index 4b0a6a22b..000000000 --- a/tests/_meta/deployments.json +++ /dev/null @@ -1 +0,0 @@ -{"foo":"bar","current_release":{"production":20000000000000},"directory_separator":"\/","is_setup":true,"webuser":{"username":"www-data","group":"www-data"},"line_endings":"\n","hash":"d41d8cd98f00b204e9800998ecf8427e"} \ No newline at end of file diff --git a/tests/_server/foobar/current/.gitkeep b/tests/_server/foobar-stub/current/.gitkeep similarity index 100% rename from tests/_server/foobar/current/.gitkeep rename to tests/_server/foobar-stub/current/.gitkeep diff --git a/tests/_server/foobar-stub/deployments.json b/tests/_server/foobar-stub/deployments.json new file mode 100644 index 000000000..565fc8656 --- /dev/null +++ b/tests/_server/foobar-stub/deployments.json @@ -0,0 +1 @@ +{"foo":"bar","directory_separator":"\/","is_setup":true,"webuser":{"username":"www-data","group":"www-data"},"line_endings":"\n"} diff --git a/tests/_server/foobar/releases/.gitkeep b/tests/_server/foobar-stub/releases/.gitkeep similarity index 100% rename from tests/_server/foobar/releases/.gitkeep rename to tests/_server/foobar-stub/releases/.gitkeep diff --git a/tests/_server/foobar/releases/10000000000000/.gitkeep b/tests/_server/foobar-stub/releases/10000000000000/.gitkeep similarity index 100% rename from tests/_server/foobar/releases/10000000000000/.gitkeep rename to tests/_server/foobar-stub/releases/10000000000000/.gitkeep diff --git a/tests/_server/foobar/releases/15000000000000/.gitkeep b/tests/_server/foobar-stub/releases/15000000000000/.gitkeep similarity index 100% rename from tests/_server/foobar/releases/15000000000000/.gitkeep rename to tests/_server/foobar-stub/releases/15000000000000/.gitkeep diff --git a/tests/_server/foobar/releases/20000000000000/.gitkeep b/tests/_server/foobar-stub/releases/20000000000000/.gitkeep similarity index 100% rename from tests/_server/foobar/releases/20000000000000/.gitkeep rename to tests/_server/foobar-stub/releases/20000000000000/.gitkeep diff --git a/tests/_server/foobar/shared/.gitkeep b/tests/_server/foobar-stub/shared/.gitkeep similarity index 100% rename from tests/_server/foobar/shared/.gitkeep rename to tests/_server/foobar-stub/shared/.gitkeep diff --git a/tests/_server/foobar/state.json b/tests/_server/foobar-stub/state.json similarity index 100% rename from tests/_server/foobar/state.json rename to tests/_server/foobar-stub/state.json