Compare commits

...

No commits in common. "1.0.7" and "master" have entirely different histories.

243 changed files with 8803 additions and 12220 deletions

View file

@ -1,2 +0,0 @@
src_dir: Mage
coverage_clover: build/logs/coverage.xml

32
.github/workflows/linters.yml vendored Normal file
View file

@ -0,0 +1,32 @@
name: Linters
on:
push:
branches:
- master
- galactica
pull_request:
release:
types:
- created
jobs:
linters:
name: Linters
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Composer install
uses: php-actions/composer@v5
with:
command: install
args: --ignore-platform-reqs --no-scripts
version: 2
php_version: 8.0
- name: PHPStan
run: ./vendor/bin/phpstan analyse
- name: PHP Code Sniffer
run: ./vendor/bin/phpcs

46
.github/workflows/tests.yml vendored Normal file
View file

@ -0,0 +1,46 @@
name: PHPUnit
on:
push:
branches:
- master
- galactica
pull_request:
release:
types:
- created
jobs:
unit_tests:
name: Unit tests
runs-on: ubuntu-latest
strategy:
matrix:
include:
- php: 8.0
coveralls: true
- php: 8.1
coveralls: false
steps:
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Composer install
uses: php-actions/composer@v5
with:
command: install
args: --ignore-platform-reqs --no-scripts
version: 2
php_version: ${{ matrix.php }}
- name: Run tests
env:
XDEBUG_MODE: coverage
run: |
mkdir .logs
./vendor/bin/phpunit --coverage-clover build/logs/coverage.xml
- name: Run Coveralls
if: ${{ matrix.coveralls }}
env:
COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: ./vendor/bin/php-coveralls -v --coverage_clover build/logs/coverage.xml

22
.gitignore vendored
View file

@ -1,15 +1,7 @@
vendor
mage.phar
bin
!bin/mage
# OS generated files # // GitHub Recommendation
######################
.DS_Store*
ehthumbs.db
Icon?
Thumbs.db
# IDE generated files
.idea
nbproject
/vendor/
/build
composer.lock
.mage.yml
.phpunit.result.cache
.phpcs-cache
.logs

View file

@ -1,17 +1,14 @@
language: php
php:
- 5.4
- 5.5
- 5.6
- '8.0'
- '8.1'
install:
- composer self-update
- composer install --prefer-source
- composer install
script:
- mkdir -p build/logs
- bin/phpunit --coverage-clover build/logs/coverage.xml
- vendor/bin/phpunit --coverage-clover build/logs/coverage.xml
after_script:
- bin/coveralls -v
- vendor/bin/coveralls -v --coverage_clover build/logs/coverage.xml

15
CHANGELOG.md Normal file
View file

@ -0,0 +1,15 @@
CHANGELOG for 5.X
=================
* 5.0.0 (2022-04-15)
* v5 series release.
* Refactored for Symfony 6 and PHP 8.
* Added strong types.
* Removed task `composer/self-update`.
* Allow `exec` task to interpolate `%environment%` and `%release%`.
* Added new `sleep` task to day execution [PR#414].
* Added new `symlink` option to define the name of symbolic link on the Release [PR#425].
* Improved Windows compatibility [PR#427].
* Added new `log_limit` option to limit how many logs are kept [Issue#403].
* Add new deploy option `--tag` to specify deploying a specific tag [Issue#192] [Issue#315].
* Added new `scp_flags` option for the `scp` command when SSH flags are incompatible with [Issue#439].

View file

@ -1,4 +1,4 @@
Contributor guidelines for Magallanes
Contributor Guidelines for Magallanes
=====================================
Welcome to Magallanes! We are much appreciated you've decided to contribute to this project!
Please read the following guidelines to make your and our work easier and cleaner.
@ -7,117 +7,61 @@ Please read the following guidelines to make your and our work easier and cleane
1. Write clean code with no mess left
2. Contribute the docs when adding configurable new feature
3. Create your pull request from `develop` branch
3. Create your pull request from `galactica` branch
4. Ensure your code is fully covered by tests
----------
# Reporting issues
If you have a problem or you've noticed some bug, please feel free to open new issue. However please follow the rules below:
* First, make sure that similar/the same issue doesn't already exist
* If you've already found the solution of the problem you are about to report, please feel free to open a new pull request. Then follow the rules below in **Developing Magallanes** section.
* If you are able to, include some test cases or steps to reproduce the bug for us to examine the problem to reach the problem origin.
# Reporting Issues
If you have a problem or you've noticed a bug, please feel free to open a new issue. However follow the rules below:
* First, make sure that similar/the same issue doesn't already exists
* If you've already found the solution of the problem you are about to report, please feel free to open a new Pull Request. Then follow the rules below in **Developing Magallanes** section.
* If you are able to, include some test cases or steps to reproduce the bug for us to examine the problem and find a solution.
## Opening pull requests
Pull Request are actually some kind of issues with code, so please follow the rules above in **Reporting issues** section before making the pull requests.
Our code isn't so beautiful, tested and testable as we would like it to be but if you're pushing your code please be sure it meets the requirements from **Organization and code quality** chapter. We want to improve the existing code to facilitate extending it and making fixes quicker. So if you are editing some class and you find it ugly, please do not ignore it. Following [The Boy Scout Rule](http://www.informit.com/articles/article.aspx?p=1235624&seqNum=6) - *Leave the campground cleaner than you found it* - we all can improve the existing code.
Keep your git commits as atomic as possible. It brings better history description only by commit messages and allow us to eventually revert the single commits with no affects. Your commit messages should be also descriptive. The first line of commit should be short, try to limit it up to 50 characters. The messages should be written imperatively, like following:
```
Add MyCustomTask
```
If you need to write more about your tasks, please enter the description in the next lines. There you can write whatever you want, like why you made this commit and what it consists of.
```
Add MyCustomTask
## Opening Pull Requests
Pull Request is a very powerful tool, so let's be measured in its usage. Always commit code which has at least 95% of coverage, and if it's a new feature always provide concrete tests.
In order to have the PRs prioritized name them with the following tags.
This task has very important role for the project. I found this very useful for all developers. I think the deploy with it should be a lot easier.
```
Optionally you can tag your messages in square brackets. It can be issue number or simple flag. Examples:
```
[#183] Add new CONTRIBUTING document
[#66] Add new CONTRIBUTING document
[FIX] Set correct permissions on deploy stage
[FEATURE] Create new PermissionsTask
[HOTFIX] Exception not caught on deployment
```
All Pull Requests must be done to the `galactica` branch, only exception are Hotfixes.
Remember of square brackets when adding issue number. If you'd forget adding them, your whole message will be a comment!
## Contributing the documentation
Magallanes is made to deploy your application quick and with no need to write redudant code. Usage is as simple as writing the configuration for target project in YAML files. In the nearest future we would like to make some Wiki with all available options, tasks and commands. For now, the only "documentation" are example files in `docs` directory. If the code you are going to include in your pull requests adds or changes config options, please make sure that you create a new sample in those files. You should also do the same with commands.
# Developing Magallanes
## Branches
The flow is pretty simple.
In most common cases we work on `develop` branch. It's a branch with the newest changes which sometimes need more testing. All pull requests are opened to be merged into that branch. That keeps us safe to not deploy unsafe code into production - `master` branch. When we decide that every changeset in `develop` is tested manually and works as it's intented, we merge it to master.
If the change you commited is pretty hot and needs to be released ASAP, you are allowed to make a pull request to `master` branch. But it's the only case, please try to avoid it. All pull request that are not made on `develop` will be rejected.
If you want to use develop branch in your code, simple pass `dev-develop` to dependency version in your `composer.json` file:
In most common cases we work on the `galactica` branch. It's the branch with the main development for the current major version. All Pull Requests must merge with that branch. The `master` branch is used to move the validated code and generate the releases in an orderly fashion, also we could use it for hotfixes.
If you want to use developing branch in your code, simple pass `dev-galactica` to dependency version in your `composer.json` file:
```json
{
"require": {
"andres-montanez/magallanes": "dev-develop"
"andres-montanez/magallanes": "dev-galactica"
}
}
```
## Organization and code quality
We use [PSR2](http://www.php-fig.org/psr/psr-2/) as PHP coding standard.
Some of the rules we follow that are not included in document above:
* Variables' and properties' names are camelCased (e.g.: `$thisIsMyVariable`)
* Avoid too long or too short variables' and methods' names, like `$thisIsMyAwesomeVariableAndImProudOfIt`
* Names of your properties/methods should be intuitive and self-describing - that means your code should look like a book. Developers who read the code should immediately know what a variable includes or what a method does.
* Let your methods will be verbs. For boolean methods, prefix it with `is`, `has`, and so on. E.g.: `isConfigurable`, `hasChildren`.
* Be [SOLID](http://en.wikipedia.org/wiki/SOLID_%28object-oriented_design%29) and follow [KISS](http://en.wikipedia.org/wiki/KISS_principle) - let the class be responsible only for its tasks.
* Write testable code and if there's a need - easy extendible.
* Avoid duplications
The rules above have been set a long time after the project has started. If you notice some violations, please open a new issue or even pull request with fixes. It'll be much appreciated.
We use [PSR-12](http://www.php-fig.org/psr/psr-12/) as PHP coding standard.
### Tools you can use to ensure your code quality
1. **PHP-CodeSniffer**
2. **PHP Mess Detector**
3. PHP Copy/Paste Detector
4. PHP Dead Code Detector
5. [PHP Coding Standards Fixer](http://cs.sensiolabs.org)
1. PHPStan `./vendor/bin/phpstan analyse`
2. PHP Code Sniffer `./vendor/bin/phpcs`
## Testing and quality
We use PHPUnit to test our code. The whole project is not covered with tests right now but we've been working on it for some time. If you want your code to be merged into Magallanes, we want you to push it with proper tests. We would love to reach and keep at least 90% of line code coverage. In short time we want to configure quality tools to make sure your code is tested properly with minimum coverage. Anyway, try to keep 100% of Code Coverage in your pull requests. To run your tests with code coverage report, you can either run it with:
We use PHPUnit to test our code. Most of the project is covered with tests, so if you want your code to be merged push it with proper testing and coverage (at least 95%). To execute the tests with code coverage report:
```bash
./vendor/bin/phpunit --coverage-clover build/logs/coverage.xml --coverage-text
./vendor/bin/php-coveralls -v --coverage_clover build/logs/coverage.xml
```
bin/phpunit --coverage-text
```
or with more friendly and detailed user graphical representation, into HTML:
```
bin/phpunit --coverate-html report
```
where `report` is the directory where html report files shall be stored.
Tests structure follow the same structure as production code with `Test` suffix in class and file name. All tests should go to `tests` directory in project root. So if you've created a class `Mage\Tasks\BuilIn\NewTask` the testing class should be called `MageTest\Tasks\BuiltIn\NewTaskTest`.
To provide more strict tests, point what the method actually test and omit testing some classes indirectly, remember to add annotations to your tests:
* **`@coversDefaultClass` class annotations**
This prevent to to write full class name each time you write `@covers` for test method (see next point)
```php
Tests structure follow almost the same structure as production code with `Test` suffix in class and file name. Follow the tests already made as guidelines.
/**
* @coversDefaultClass Mage\Console\Colors
*/
class ColorsTest extends PHPUnit_Framework_TestCase
{
```
* **`@covers` methods annotations**
```php
/**
* @covers ::add
*/
public function testAddOnePlusOne()
{
// ...
}
```
**Note:** If you omit `coversDefaultClass` for test class, you need to write full class name in `@covers` annotation.
# Last Words
Thank you for using Magallanes, and special thanks for making it better. When adding features always have in mind the main goal *Deploy code from A to B, and run some tasks*, and think if the feature is aiming at that.
**Test class musn't test more than one class and any other classes than class being actually tested**
## Configuration
Magallanes configuration is kept in YAML files. Please follow those rules while adding or changing the configuration:
* Keep 2 spaces indentation in each level
* Multi-word config keys should be joined with dash (`-`), like `my-custom-task`
* If your contribution includes new config key, please be sure that you've documented it in configuration documentation.

View file

@ -1,4 +1,4 @@
Copyright (c) 2011 - 2015 Andrés Montañez
Copyright (c) 2011 - 2022 Andrés Montañez
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
@ -21,7 +21,3 @@ OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------
The Yaml Library Parser is (c) by Fabien Potencier, and belongs to the Symfony Proyect
--------

View file

@ -1,19 +0,0 @@
Copyright (c) 2004-2014 Fabien Potencier
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -1,69 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage;
/**
* Magallanes custom Autoload for BuiltIn and Userspace Commands and Tasks.
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Autoload
{
/**
* Autoload a Class by it's Class Name
* @param string $className
* @return boolean
*/
public function autoLoad($className)
{
$className = ltrim($className, '/');
$postfix = '/' . str_replace(array('_', '\\'), '/', $className . '.php');
// Change BaseDir according to Namespace
if (strpos($className, 'Task\\') === 0) {
$baseDir = getcwd() . '/.mage/tasks';
$postfix = substr($postfix, 5);
} elseif (strpos($className, 'Command\\') === 0) {
$baseDir = getcwd() . '/.mage/commands';
$postfix = substr($postfix, 8);
} else {
$baseDir = dirname(dirname(__FILE__));
}
//Try to load a normal Mage class (or Task). Think that Mage component is compiled to .phar
$classFileWithinPhar = $baseDir . $postfix;
if ($this->isReadable($classFileWithinPhar)) {
/** @noinspection PhpIncludeInspection */
require_once $classFileWithinPhar;
return true;
}
//Try to load a custom Task or Class. Notice that the path is absolute to CWD
$classFileOutsidePhar = getcwd() . '/.mage/tasks' . $postfix;
if ($this->isReadable($classFileOutsidePhar)) {
/** @noinspection PhpIncludeInspection */
require_once $classFileOutsidePhar;
return true;
}
return false;
}
/**
* Checks if a file can be read.
* @param string $filePath
* @return boolean
*/
public function isReadable($filePath)
{
return is_readable($filePath);
}
}

View file

@ -1,191 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command;
use Mage\Config;
/**
* Abstract Class for a Magallanes Command
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
abstract class AbstractCommand
{
/**
* Instance of the loaded Configuration.
*
* @var \Mage\Config
*/
protected $config = null;
/**
* Command's help message
*
* @var string
*/
private $helpMessage;
/**
* Usage examples.
*
* @var array
*/
private $usageExamples = array();
/**
* Command's syntax message
*
* @var string
*/
private $syntaxMessage;
/**
* Command name
*
* @var string
*/
private $name;
/**
* Runs the Command
* @return integer exit code
* @throws \Exception
*/
abstract public function run();
/**
* Sets the Loaded Configuration.
*
* @param Config $config
*/
public function setConfig(Config $config)
{
$this->config = $config;
}
/**
* Gets the Loaded Configuration.
*
* @return Config
*/
public function getConfig()
{
return $this->config;
}
/**
* Sets command name
*
* @param string $name Command name
* @return $this
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Sets command's help message
*
* @param string $message Command's help message
* @return $this
*/
public function setHelpMessage($message)
{
$this->helpMessage = $message;
return $this;
}
/**
* Adds command's usage example
*
* @param string $snippet Example's snippet
* @param string $description Example's description
* @return $this
*/
public function addUsageExample($snippet, $description = '')
{
array_push($this->usageExamples, array($snippet, $description));
return $this;
}
/**
* Sets command's syntax message
*
* @param string $message Syntax message
* @return $this
*/
public function setSyntaxMessage($message)
{
$this->syntaxMessage = $message;
return $this;
}
/**
* Returns formatted command info
*
* @return string
*/
public function getInfoMessage()
{
$indent = str_repeat(" ", 4);
$output = "";
if (!empty($this->name)) {
$output .= "\n";
$output .= "<cyan><bold>Command: </bold></cyan>";
$output .= $this->name;
}
if (!empty($this->helpMessage)) {
$output .= "\n";
$output .= "<light_blue>{$this->helpMessage}</light_blue>\n";
}
if (!empty($this->syntaxMessage)) {
$output .= "\n";
$output .= "<light_gray><bold>Syntax:</bold></light_gray>\n";
$output .= "$indent<light_green>{$this->syntaxMessage}</light_green>";
$output .= "\n";
}
if (!empty($this->usageExamples)) {
$output .= "\n";
$output .= "<light_gray><bold>Usage examples:</bold></light_gray>\n";
foreach ($this->usageExamples as $example) {
$snippet = $example[0];
$description = $example[1];
$output .= "$indent* ";
if (!empty($description)) {
$description = rtrim($description, ': ') . ":";
$output .= $description;
$output .= "\n$indent$indent";
}
$output .= "<green>$snippet</green>";
$output .= "\n";
}
}
if (empty($output)) {
$output .= "\n";
$output .= "<red><bold>Sorry, there's no help for this command at the moment.</bold></red>";
$output .= "\n";
}
return $output;
}
}

View file

@ -1,116 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
use Exception;
/**
* Command for Adding elements to the Configuration.
* Currently elements allowed to add:
* - environments
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class AddCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('Add command')
->setHelpMessage('Generates new config for Magallanes. For now, only adding a environment is possible')
->setSyntaxMessage('mage add [environment] [--name=env_name] [--enableReleases]')
->addUsageExample(
'mage add environment --name=production',
'Add a production environment'
)
->addUsageExample(
'mage add environment --name=qa --enableReleases',
'Add a QA environment and enable releasing'
);
}
/**
* Adds new Configuration Elements
* @see \Mage\Command\AbstractCommand::run()
* @throws Exception
*/
public function run()
{
$subCommand = $this->getConfig()->getArgument(1);
try {
switch ($subCommand) {
case 'environment':
$this->addEnvironment();
break;
default;
throw new Exception('The Type of Add is needed.');
break;
}
} catch (Exception $exception) {
Console::output('<red>' . $exception->getMessage() . '</red>', 1, 2);
}
}
/**
* Adds an Environment
*
* @throws Exception
*/
protected function addEnvironment()
{
$withReleases = $this->getConfig()->getParameter('enableReleases', false);
$environmentName = strtolower($this->getConfig()->getParameter('name'));
if ($environmentName == '') {
throw new Exception('You must specify a name for the environment.');
}
$environmentConfigFile = getcwd() . '/.mage/config/environment/' . $environmentName . '.yml';
if (file_exists($environmentConfigFile)) {
throw new Exception('The environment already exists.');
}
Console::output('Adding new environment: <bold>' . $environmentName . '</bold>');
$releasesConfig = 'releases:' . PHP_EOL
. ' enabled: true' . PHP_EOL
. ' max: 10' . PHP_EOL
. ' symlink: current' . PHP_EOL
. ' directory: releases' . PHP_EOL;
$baseConfig = '#' . $environmentName . PHP_EOL
. 'deployment:' . PHP_EOL
. ' user: dummy' . PHP_EOL
. ' from: ./' . PHP_EOL
. ' to: /var/www/vhosts/example.com/www' . PHP_EOL
. ' excludes:' . PHP_EOL
. ($withReleases ? $releasesConfig : '')
. 'hosts:' . PHP_EOL
. 'tasks:' . PHP_EOL
. ' pre-deploy:' . PHP_EOL
. ' on-deploy:' . PHP_EOL
. ($withReleases ? (' post-release:' . PHP_EOL) : '')
. ' post-deploy:' . PHP_EOL;
$result = file_put_contents($environmentConfigFile, $baseConfig);
if ($result) {
Console::output('<light_green>Success!!</light_green> Environment config file for <bold>' . $environmentName . '</bold> created successfully at <blue>' . $environmentConfigFile . '</blue>');
Console::output('<bold>So please! Review and adjust its configuration.</bold>', 2, 2);
} else {
Console::output('<light_red>Error!!</light_red> Unable to create config file for environment called <bold>' . $environmentName . '</bold>', 1, 2);
}
}
}

View file

@ -1,68 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
use Mage\Compiler;
/**
* Command for Compile Magallanes into a PHAR executable
*
* @author Ismael Ambrosi<ismaambrosi@gmail.com>
*/
class CompileCommand extends AbstractCommand
{
/**
* @var Compiler
*/
private $compiler;
public function __construct(Compiler $compiler = null)
{
if ($compiler === null) {
$compiler = new Compiler();
}
$this->compiler = $compiler;
$this->setName('Compile command')
->setHelpMessage('Compiles Magallanes to mage.phar file')
->setSyntaxMessage('mage compile');
}
/**
* @see \Mage\Compile::compile()
*/
public function run()
{
if (ini_get('phar.readonly')) {
Console::output(
'The <purple>php.ini</purple> variable <light_red>phar.readonly</light_red>'
. ' must be <yellow>Off</yellow>.',
1,
2
);
return 200;
}
$this->compiler->compile();
Console::output(
'<light_purple>mage.phar</light_purple> compiled <light_green>successfully</light_green>',
0,
2
);
return 0;
}
}

View file

@ -1,663 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
* (c) Alex V Kotelnikov <gudron@gudron.me>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Command\RequiresEnvironment;
use Mage\Task\Factory;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\SkipOnOverride;
use Mage\Task\ErrorWithMessageException;
use Mage\Task\RollbackException;
use Mage\Task\SkipException;
use Mage\Console;
use Mage\Config;
use Mage\Mailer;
use Exception;
/**
* Command for Deploying
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class DeployCommand extends AbstractCommand implements RequiresEnvironment
{
const DEFAULT_RELEASE_IS_ENABLED = false;
const DEPLOY_STRATEGY_DISABLED = 'disabled';
const DEPLOY_STRATEGY_RSYNC = 'rsync';
const DEPLOY_STRATEGY_TARGZ = 'targz';
const DEPLOY_STRATEGY_GIT_REBASE = 'git-rebase';
const DEPLOY_STRATEGY_GIT_REMOTE_CACHE = 'git-remote-cache';
const DEPLOY_STRATEGY_GUESS = 'guess';
const DEFAULT_DEPLOY_STRATEGY = self::DEPLOY_STRATEGY_GUESS;
/**
* Deploy has Failed
* @var string
*/
const FAILED = 'failed';
/**
* Deploy has Succeded
* @var string
*/
const SUCCEDED = 'succeded';
/**
* Deploy is in progress
* @var string
*/
const IN_PROGRESS = 'in_progress';
/**
* Stage where possible throw Rollback Exception
* @var array
*/
public $acceptedStagesToRollback = array(
AbstractTask::STAGE_POST_RELEASE,
AbstractTask::STAGE_POST_DEPLOY
);
/**
* Time the Deployment has Started
* @var integer
*/
protected $startTime = null;
/**
* Time the Deployment has Started to the current Host
* @var integer
*/
protected $startTimeHosts = null;
/**
* Time the Deployment to the Hosts has Finished
* @var integer
*/
protected $endTimeHosts = null;
/**
* Quantity of Hosts to Deploy to.
* @var integer
*/
protected $hostsCount = 0;
/**
* Current Status of the Deployment (in progress, succeded, failed)
* @var string
*/
protected static $deployStatus = 'in_progress';
/**
* Total of Failed tasks
* @var integer
*/
protected static $failedTasks = 0;
public function __construct()
{
$this->setName('Deploy command')
->setHelpMessage('Deploys the project into target environment')
->setSyntaxMessage('mage deploy to:[environment_name]')
->addUsageExample(
'mage deploy to:production',
'Deploy the project into <bold>production</bold> environment'
)
->addUsageExample(
'mage deploy to:production --overrideRelease',
'Deploy the project into <bold>production</bold> environment '
. 'but skip <bold>SkipOnOverride</bold> aware tasks'
);
}
/**
* Returns the Status of the Deployment
*
* @return string
*/
public static function getStatus()
{
return self::$deployStatus;
}
/**
* Deploys the Application
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$exitCode = 240;
// Check if Environment is not Locked
$lockFile = getcwd() . '/.mage/' . $this->getConfig()->getEnvironment() . '.lock';
if (file_exists($lockFile)) {
Console::output('<red>This environment is locked!</red>', 1, 2);
echo file_get_contents($lockFile);
return 231;
}
// Check for running instance and Lock
if (file_exists(getcwd() . '/.mage/~working.lock')) {
Console::output('<red>There is already an instance of Magallanes running!</red>', 1, 2);
return 230;
} else {
touch(getcwd() . '/.mage/~working.lock');
}
// Release ID
$this->getConfig()->setReleaseId(date('YmdHis'));
// Deploy Summary
Console::output('<bold>Deploy summary</bold>', 1, 1);
// Deploy Summary - Environment
Console::output('<bold>Environment:</bold> <purple>' . $this->getConfig()->getEnvironment() . '</purple>', 2, 1);
// Deploy Summary - Releases
if ($this->getConfig()->release('enabled', false)) {
Console::output('<bold>Release ID:</bold> <purple>' . $this->getConfig()->getReleaseId() . '</purple>', 2, 1);
}
// Deploy Summary - SCM
if ($this->getConfig()->deployment('scm', false)) {
$scmConfig = $this->getConfig()->deployment('scm');
if (isset($scmConfig['branch'])) {
Console::output('<bold>SCM Branch:</bold> <purple>' . $scmConfig['branch'] . '</purple>', 2, 1);
}
}
// Deploy Summary - Separator Line
Console::output('', 0, 1);
$this->startTime = time();
// Run Pre-Deployment Tasks
$this->runNonDeploymentTasks(AbstractTask::STAGE_PRE_DEPLOY, $this->getConfig(), 'Pre-Deployment');
// Check Status
if (self::$failedTasks > 0) {
self::$deployStatus = self::FAILED;
Console::output('A total of <bold>' . self::$failedTasks . '</bold> deployment tasks failed: <red>ABORTING</red>', 1, 2);
} else {
// Run Deployment Tasks
$this->runDeploymentTasks();
// Check Status
if (self::$failedTasks > 0) {
self::$deployStatus = self::FAILED;
Console::output('A total of <bold>' . self::$failedTasks . '</bold> deployment tasks failed: <red>ABORTING</red>', 1, 2);
}
// Run Post-Deployment Tasks
$this->runNonDeploymentTasks(AbstractTask::STAGE_POST_DEPLOY, $this->getConfig(), 'Post-Deployment');
}
// Time Information Hosts
if ($this->hostsCount > 0) {
$timeTextHost = $this->transcurredTime($this->endTimeHosts - $this->startTimeHosts);
Console::output('Time for deployment: <bold>' . $timeTextHost . '</bold>.');
$timeTextPerHost = $this->transcurredTime(round(($this->endTimeHosts - $this->startTimeHosts) / $this->hostsCount));
Console::output('Average time per host: <bold>' . $timeTextPerHost . '</bold>.');
}
// Time Information General
$timeText = $this->transcurredTime(time() - $this->startTime);
Console::output('Total time: <bold>' . $timeText . '</bold>.', 1, 2);
// Send Notifications
$this->sendNotification(self::$deployStatus === self::SUCCEDED);
// Unlock
if (file_exists(getcwd() . '/.mage/~working.lock')) {
unlink(getcwd() . '/.mage/~working.lock');
}
if (self::$failedTasks === 0) {
$exitCode = 0;
}
if (self::$deployStatus === self::FAILED) {
$exitCode = 1;
}
return $exitCode;
}
/**
* Execute Pre and Post Deployment Tasks
*
* @param string $stage
* @param Config $config
* @param string $title
*/
protected function runNonDeploymentTasks($stage, Config $config, $title)
{
$tasksToRun = $config->getTasks($stage);
// PreDeployment Hook
if ($stage == AbstractTask::STAGE_PRE_DEPLOY) {
// Look for Remote Source
if (is_array($config->deployment('source', null))) {
array_unshift($tasksToRun, 'scm/clone');
}
// Change Branch
if ($config->deployment('scm', false)) {
array_unshift($tasksToRun, 'scm/change-branch');
}
}
// PostDeployment Hook
if ($stage == AbstractTask::STAGE_POST_DEPLOY) {
// If Deploy failed, clear post deploy tasks
if (self::$deployStatus == self::FAILED) {
$tasksToRun = array();
}
// Change Branch Back
if ($config->deployment('scm', false)) {
array_push($tasksToRun, 'scm/change-branch');
$config->addParameter('_changeBranchRevert');
}
// Remove Remote Source
if (is_array($config->deployment('source', null))) {
array_push($tasksToRun, 'scm/remove-clone');
}
}
if (count($tasksToRun) == 0) {
Console::output('<bold>No </bold><light_cyan>' . $title . '</light_cyan> <bold>tasks defined.</bold>', 1, 3);
} else {
Console::output('Starting <bold>' . $title . '</bold> tasks:');
$tasks = 0;
$completedTasks = 0;
self::$failedTasks = 0;
foreach ($tasksToRun as $taskData) {
$tasks++;
$task = Factory::get($taskData, $config, false, $stage);
if ($this->runTask($task)) {
$completedTasks++;
} else {
self::$failedTasks++;
}
}
if ($completedTasks == $tasks) {
$tasksColor = 'green';
} else {
$tasksColor = 'red';
}
Console::output('Finished <bold>' . $title . '</bold> tasks: <' . $tasksColor . '>' . $completedTasks . '/' . $tasks . '</' . $tasksColor . '> tasks done.', 1, 3);
}
}
protected function runDeploymentTasks()
{
if (self::$deployStatus == self::FAILED) {
return;
}
// Run Tasks for Deployment
$hosts = $this->getConfig()->getHosts();
$this->hostsCount = count($hosts);
self::$failedTasks = 0;
if ($this->hostsCount == 0) {
Console::output('<light_purple>Warning!</light_purple> <bold>No hosts defined, skipping deployment tasks.</bold>', 1, 3);
} else {
$this->startTimeHosts = time();
foreach ($hosts as $hostKey => $host) {
// Check if Host has specific configuration
$hostConfig = null;
if (is_array($host)) {
$hostConfig = $host;
$host = $hostKey;
}
// Set Host and Host Specific Config
$this->getConfig()->setHost($host);
$this->getConfig()->setHostConfig($hostConfig);
// Prepare Tasks
$tasks = 0;
$completedTasks = 0;
Console::output('Deploying to <bold>' . $this->getConfig()->getHost() . '</bold>');
$tasksToRun = $this->getConfig()->getTasks();
$deployStrategy = $this->chooseDeployStrategy();
array_unshift($tasksToRun, $deployStrategy);
if (count($tasksToRun) == 0) {
Console::output('<light_purple>Warning!</light_purple> <bold>No </bold><light_cyan>Deployment</light_cyan> <bold>tasks defined.</bold>', 2);
Console::output('Deployment to <bold>' . $host . '</bold> skipped!', 1, 3);
} else {
foreach ($tasksToRun as $taskData) {
$tasks++;
$task = Factory::get($taskData, $this->getConfig(), false, AbstractTask::STAGE_DEPLOY);
if ($this->runTask($task)) {
$completedTasks++;
} else {
self::$failedTasks++;
}
}
if ($completedTasks == $tasks) {
$tasksColor = 'green';
} else {
$tasksColor = 'red';
}
Console::output('Deployment to <bold>' . $this->getConfig()->getHost() . '</bold> completed: <' . $tasksColor . '>' . $completedTasks . '/' . $tasks . '</' . $tasksColor . '> tasks done.', 1, 3);
}
// Reset Host Config
$this->getConfig()->setHostConfig(null);
}
$this->endTimeHosts = time();
if (self::$failedTasks > 0) {
self::$deployStatus = self::FAILED;
} else {
self::$deployStatus = self::SUCCEDED;
}
// Releasing
if (self::$deployStatus == self::SUCCEDED && $this->getConfig()->release('enabled', false) === true) {
// Execute the Releases
Console::output('Starting the <bold>Releasing</bold>');
$completedTasks = 0;
foreach ($hosts as $hostKey => $host) {
// Check if Host has specific configuration
$hostConfig = null;
if (is_array($host)) {
$hostConfig = $host;
$host = $hostKey;
}
// Set Host
$this->getConfig()->setHost($host);
$this->getConfig()->setHostConfig($hostConfig);
$task = Factory::get($this->chooseReleaseStrategy(), $this->getConfig(), false, AbstractTask::STAGE_DEPLOY);
if ($this->runTask($task, 'Releasing on host <purple>' . $host . '</purple> ... ')) {
$completedTasks++;
}
// Reset Host Config
$this->getConfig()->setHostConfig(null);
}
Console::output('Finished the <bold>Releasing</bold>', 1, 3);
// Execute the Post-Release Tasks
foreach ($hosts as $hostKey => $host) {
// Check if Host has specific configuration
$hostConfig = null;
if (is_array($host)) {
$hostConfig = $host;
$host = $hostKey;
}
// Set Host
$this->getConfig()->setHost($host);
$this->getConfig()->setHostConfig($hostConfig);
$tasksToRun = $this->getConfig()->getTasks(AbstractTask::STAGE_POST_RELEASE);
$tasks = count($tasksToRun);
$completedTasks = 0;
if (count($tasksToRun) > 0) {
Console::output('Starting <bold>Post-Release</bold> tasks for <bold>' . $host . '</bold>:');
foreach ($tasksToRun as $task) {
$task = Factory::get($task, $this->getConfig(), false, AbstractTask::STAGE_POST_RELEASE);
if ($this->runTask($task)) {
$completedTasks++;
}
}
if ($completedTasks == $tasks) {
$tasksColor = 'green';
} else {
$tasksColor = 'red';
}
Console::output('Finished <bold>Post-Release</bold> tasks for <bold>' . $host . '</bold>: <' . $tasksColor . '>' . $completedTasks . '/' . $tasks . '</' . $tasksColor . '> tasks done.', 1, 3);
}
// Reset Host Config
$this->getConfig()->setHostConfig(null);
}
}
}
}
protected function runRollbackTask(AbstractTask $task)
{
$this->getConfig()->reload();
$hosts = $this->getConfig()->getHosts();
Console::output("", 1, 2);
Console::output("Starting the <bold>rollback</bold>", 1, 1);
if (!in_array($task->getStage(), $this->acceptedStagesToRollback)) {
$stagesString = implode(', ', $this->acceptedStagesToRollback);
Console::output("<light_purple>Warning!</light_purple> <bold>Rollback during deployment can be called only at the stages: $stagesString <bold>", 1);
Console::output("<bold>Rollback:<bold> <red>ABORTING</red>", 1, 3);
} elseif (count($hosts) == 0) {
Console::output('<light_purple>Warning!</light_purple> <bold>No hosts defined, unable to get releases.</bold>', 1, 3);
} else {
$result = true;
foreach ($hosts as $hostKey => $host) {
$hostConfig = null;
if (is_array($host)) {
$hostConfig = $host;
$host = $hostKey;
}
// Set Host and Host Specific Config
$this->getConfig()->setHost($host);
$this->getConfig()->setHostConfig($hostConfig);
$this->getConfig()->setReleaseId(-1);
$task = Factory::get(array(
'name'=>'releases/rollback',
'parameters' => array('inDeploy'=>true)
),
$this->getConfig(),
false,
$task->getStage()
);
$task->init();
$result = $task->run() && $result;
}
return $result;
}
return false;
}
/**
* Runs a Task
*
* @param AbstractTask $task
* @param string $title
* @return boolean
*/
protected function runTask(AbstractTask $task, $title = null)
{
$task->init();
if ($title === null) {
$title = 'Running <purple>' . $task->getName() . '</purple> ... ';
}
Console::output($title, 2, 0);
$runTask = true;
if (($task instanceof SkipOnOverride) && $this->getConfig()->getParameter('overrideRelease', false)) {
$runTask = false;
}
if ($runTask === true) {
try {
$result = $task->run();
if ($result === true) {
Console::output('<green>OK</green>', 0);
$result = true;
} else {
Console::output('<red>FAIL</red>', 0);
$result = false;
}
} catch (RollbackException $e) {
Console::output('<red>FAIL, Rollback catched</red> [Message: ' . $e->getMessage() . ']', 0);
$this->runRollbackTask($task);
$result = false;
} catch (ErrorWithMessageException $e) {
Console::output('<red>FAIL</red> [Message: ' . $e->getMessage() . ']', 0);
$result = false;
} catch (SkipException $e) {
Console::output('<yellow>SKIPPED</yellow>', 0);
$result = true;
} catch (Exception $e) {
Console::output('<red>FAIL</red>', 0);
$result = false;
}
} else {
Console::output('<yellow>SKIPPED</yellow>', 0);
$result = true;
}
return $result;
}
/**
* Humanize Transcurred time
*
* @param integer $time
* @return string
*/
protected function transcurredTime($time)
{
$hours = floor($time / 3600);
$minutes = floor(($time - ($hours * 3600)) / 60);
$seconds = $time - ($minutes * 60) - ($hours * 3600);
$timeText = array();
if ($hours > 0) {
$timeText[] = $hours . ' hours';
}
if ($minutes > 0) {
$timeText[] = $minutes . ' minutes';
}
if ($seconds >= 0) {
$timeText[] = $seconds . ' seconds';
}
return implode(' ', $timeText);
}
/**
* Send Email Notification if enabled
* @param boolean $result
* @return boolean
*/
protected function sendNotification($result)
{
$projectName = $this->getConfig()->general('name', false);
$projectEmail = $this->getConfig()->general('email', false);
$notificationsEnabled = $this->getConfig()->general('notifications', false);
// We need notifications enabled, and a project name and email to send the notification
if (!$projectName || !$projectEmail || !$notificationsEnabled) {
return false;
}
$mailer = new Mailer;
$mailer->setAddress($projectEmail)
->setProject($projectName)
->setLogFile(Console::getLogFile())
->setEnvironment($this->getConfig()->getEnvironment())
->send($result);
return true;
}
/**
* @return string
*/
protected function chooseDeployStrategy()
{
// Guess a Deploy Strategy
switch ($this->getConfig()->deployment('strategy', self::DEFAULT_DEPLOY_STRATEGY)) {
case self::DEPLOY_STRATEGY_DISABLED:
$deployStrategy = 'deployment/strategy/disabled';
break;
case self::DEPLOY_STRATEGY_RSYNC:
$deployStrategy = 'deployment/strategy/rsync';
break;
case self::DEPLOY_STRATEGY_TARGZ:
$deployStrategy = 'deployment/strategy/tar-gz';
break;
case self::DEPLOY_STRATEGY_GIT_REBASE:
$deployStrategy = 'deployment/strategy/git-rebase';
break;
case self::DEPLOY_STRATEGY_GIT_REMOTE_CACHE:
$deployStrategy = 'deployment/strategy/git-remote-cache';
break;
case self::DEPLOY_STRATEGY_GUESS:
default:
if ($this->getConfig()->release('enabled', false) === true) {
$deployStrategy = 'deployment/strategy/tar-gz';
} else {
$deployStrategy = 'deployment/strategy/rsync';
}
break;
}
return $deployStrategy;
}
/**
* @return string
*/
protected function chooseReleaseStrategy()
{
if ($this->getConfig()->release('enabled', self::DEFAULT_RELEASE_IS_ENABLED)
&& $this->getConfig()->deployment('strategy', self::DEFAULT_DEPLOY_STRATEGY) !== self::DEPLOY_STRATEGY_DISABLED
) {
$strategy = 'deployment/release';
} else {
$strategy = 'deployment/strategy/disabled';
}
return $strategy;
}
}

View file

@ -1,126 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
/**
* Initializes a Magallanes Configuration into a Proyect
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class InitCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('Initialize command')
->setHelpMessage('Initialize Magallanes project, create .mage directory with starter configs')
->setSyntaxMessage('mage init --name=[project_name] [--email=[author_email]]')
->addUsageExample(
'mage init --name="My awesome project"',
'Initialize "My awesome project" configuration'
)
->addUsageExample(
'mage init --name="My project" --email="john.smith@example.com"',
'Initialize "My project" configuration with email notification enabled for john.smith@example.com'
);
}
/**
* Command for Initalize a new Configuration Proyect
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$exitCode = 50;
$configDir = getcwd() . '/.mage';
Console::output('Initiating managing process for application with <bold>Magallanes</bold>');
// Check if there is already a config dir
if (file_exists($configDir)) {
Console::output('<light_red>Error!!</light_red> Already exists <bold>.mage</bold> directory.', 1, 2);
} else {
$results = array();
$results[] = mkdir($configDir);
$results[] = mkdir($configDir . '/logs');
$results[] = file_put_contents($configDir . '/logs/.gitignore', "*\n!.gitignore");
$results[] = mkdir($configDir . '/tasks');
$results[] = touch($configDir . '/tasks/.gitignore');
$results[] = mkdir($configDir . '/config');
$results[] = mkdir($configDir . '/config/environment');
$results[] = touch($configDir . '/config/environment/.gitignore');
$results[] = file_put_contents($configDir . '/config/general.yml', $this->getGeneralConfig());
if (!in_array(false, $results)) {
Console::output('<light_green>Success!!</light_green> The configuration for <bold>Magallanes</bold> has been generated at <blue>.mage</blue> directory.');
Console::output('<bold>Please!! Review and adjust the configuration.</bold>', 2, 2);
$exitCode = 0;
} else {
Console::output('<light_red>Error!!</light_red> Unable to generate the configuration.', 1, 2);
}
return $exitCode;
}
}
/**
* Returns the Global Configuration
* @return string
*/
protected function getGeneralConfig()
{
// Assamble Global Settings
$projectName = $this->getConfig()->getParameter('name', '');
$notificationEmail = $this->getConfig()->getParameter('email', '');
$notificationEnabled = ($notificationEmail != '') ? 'true' : 'false';
$globalSettings = str_replace(
array(
'%projectName%',
'%notificationEmail%',
'%notificationEnabled%',
'%loggingEnabled%',
'%maxlogs%',
'%ssh_needs_tty%',
),
array(
$projectName,
$notificationEmail,
$notificationEnabled,
'true',
30,
'false'
),
$this->getGeneralConfigTemplate()
);
return $globalSettings;
}
/**
* Returns the YAML Template for the Global Configuration
* @return string
*/
protected function getGeneralConfigTemplate()
{
$template = '# global settings' . PHP_EOL
. 'name: %projectName%' . PHP_EOL
. 'email: %notificationEmail%' . PHP_EOL
. 'notifications: %notificationEnabled%' . PHP_EOL
. 'logging: %loggingEnabled%' . PHP_EOL
. 'maxlogs: %maxlogs%' . PHP_EOL
. 'ssh_needs_tty: %ssh_needs_tty%' . PHP_EOL;
return $template;
}
}

View file

@ -1,139 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
/**
* Installs Magallanes in the Local System
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class InstallCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('Install command')
->setHelpMessage(
'Installs Magallanes system-widely.'
. ' By default, Magallanes\' going to be installed in /opt/magallanes'
)
->setSyntaxMessage('mage install [--installDir=[install_directory]] [--systemWide]')
->addUsageExample(
'mage install --installDir=/src/projects/Magellanes',
'Install Magallanes at /src/projects/Magallanes directory'
)
->addUsageExample(
'mage install --systemWide',
'Install Magallanes at default directory and creates a symlink in /usr/bin/mage'
);
}
/**
* Installs Magallanes
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$exitCode = 88;
Console::output('Installing <bold>Magallanes</bold>... ', 1, 0);
// Vars
$installDir = $this->getConfig()->getParameter('installDir', '/opt/magallanes');
$systemWide = $this->getConfig()->getParameter('systemWide', false);
// Clean vars
$baseDir = realpath(dirname($installDir));
$installDir = basename($installDir);
// Check if install dir is available
if (!is_dir($baseDir) || !is_writable($baseDir)) {
Console::output('<red>Failure: install directory is invalid.</red>', 0, 2);
// Chck if it is a system wide install the user is root
} elseif ($systemWide && (getenv('LOGNAME') != 'root')) {
Console::output('<red>Failure: you have to be root to perform a system wide install.</red>', 0, 2);
} else {
$destinationDir = $baseDir . '/' . $installDir;
if (!is_dir($destinationDir)) {
mkdir($destinationDir);
}
// Copy
$this->recursiveCopy(MAGALLANES_DIRECTORY, $destinationDir . '/' . MAGALLANES_VERSION);
// Check if there is already a symlink
if (file_exists($destinationDir . '/' . 'latest')
&& is_link($destinationDir . '/' . 'latest')
) {
unlink($destinationDir . '/' . 'latest');
}
// Create "latest" symlink
symlink(
$destinationDir . '/' . MAGALLANES_VERSION,
$destinationDir . '/' . 'latest'
);
chmod($destinationDir . '/' . MAGALLANES_VERSION . '/bin/mage', 0755);
if ($systemWide) {
if (!file_exists('/usr/bin/mage')) {
symlink($destinationDir . '/latest/bin/mage', '/usr/bin/mage');
}
}
Console::output('<light_green>Success!</light_green>', 0, 2);
$exitCode = 0;
}
return $exitCode;
}
/**
* Copy Files
* @param string $from
* @param string $to
* @return boolean
*/
protected function recursiveCopy($from, $to)
{
if (is_dir($from)) {
mkdir($to);
$files = scandir($from);
if (count($files) > 0) {
foreach ($files as $file) {
if (strpos($file, '.') === 0) {
continue;
}
if (is_dir($from . DIRECTORY_SEPARATOR . $file)) {
$this->recursiveCopy(
$from . DIRECTORY_SEPARATOR . $file,
$to . DIRECTORY_SEPARATOR . $file
);
} else {
copy(
$from . DIRECTORY_SEPARATOR . $file,
$to . DIRECTORY_SEPARATOR . $file
);
}
}
}
return true;
} elseif (is_file($from)) {
return copy($from, $to);
} else {
return false;
}
}
}

View file

@ -1,92 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
use Exception;
/**
* Adds elements to the Configuration.
* Currently elements allowed to add:
* - environments
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class ListCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('List command')
->setHelpMessage('List available configurations. For now, only environments listing available')
->setSyntaxMessage('mage list [environments]')
->addUsageExample(
'mage list environments',
'List currently configured environments'
);
}
/**
* Command for Listing Configuration Elements
* @see \Mage\Command\AbstractCommand::run()
* @throws Exception
*/
public function run()
{
$exitCode = 221;
$subCommand = $this->getConfig()->getArgument(1);
try {
switch ($subCommand) {
case 'environments':
$exitCode = $this->listEnvironments();
break;
default:
throw new Exception('The Type of Elements to List is needed.');
break;
}
} catch (Exception $e) {
Console::output('<red>' . $e->getMessage() . '</red>', 1, 2);
}
return $exitCode;
}
/**
* Lists the Environments
*/
protected function listEnvironments()
{
$exitCode = 220;
$environments = array();
$content = scandir(getcwd() . '/.mage/config/environment/');
foreach ($content as $file) {
if (strpos($file, '.yml') !== false) {
$environments[] = str_replace('.yml', '', $file);
}
}
sort($environments);
if (count($environments) > 0) {
Console::output('<bold>These are your configured environments:</bold>', 1, 1);
foreach ($environments as $environment) {
Console::output('* <light_red>' . $environment . '</light_red>', 2, 1);
}
Console::output('', 1, 1);
$exitCode = 0;
} else {
Console::output('<bold>You don\'t have any environment configured.</bold>', 1, 2);
}
return $exitCode;
}
}

View file

@ -1,76 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Command\RequiresEnvironment;
use Mage\Console;
/**
* Command for Locking the Deployment to an Environment
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class LockCommand extends AbstractCommand implements RequiresEnvironment
{
public function __construct()
{
$this->setName('Lock command')
->setHelpMessage(
"Locks the deployment to given environment and creates a lock file "
. "with lock reason and lock performer.\n"
. "You are going to be prompted to provide this information"
)
->setSyntaxMessage('mage lock to:[environment_name]')
->addUsageExample(
'mage lock to:production',
'Create a lock to production environment deployment'
);
}
/**
* Locks the Deployment to a Environment
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
Console::output('Your name (enter to leave blank): ', 0, 0);
$name = Console::readInput();
Console::output('Your email (enter to leave blank): ', 0, 0);
$email = Console::readInput();
Console::output('Reason of lock (enter to leave blank): ', 0, 0);
$reason = Console::readInput();
$lockmsg = PHP_EOL;
if ($name) {
$lockmsg .= 'Locked by ' . $name . ' ';
}
if ($email) {
$lockmsg .= '(' . $email . ')';
}
if ($reason) {
$lockmsg .= PHP_EOL . $reason . PHP_EOL;
}
$lockFile = getcwd() . '/.mage/' . $this->getConfig()->getEnvironment() . '.lock';
file_put_contents($lockFile, 'Locked environment at date: ' . date('Y-m-d H:i:s') . $lockmsg);
Console::output(
'Locked deployment to <light_purple>'
. $this->getConfig()->getEnvironment()
. '</light_purple> environment',
1,
2
);
return 0;
}
}

View file

@ -1,126 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Command\RequiresEnvironment;
use Mage\Task\Factory;
use Mage\Console;
/**
* Command for Managing the Releases
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class ReleasesCommand extends AbstractCommand implements RequiresEnvironment
{
public function __construct()
{
$this->setName('Releases management command')
->setHelpMessage('Manages releases')
->setSyntaxMessage(
'mage releases [list|rollback [--release=[release_id]]] '
. 'to:[environment_name] [--deleteCurrent]'
)
->addUsageExample(
'mage releases list to:production',
'List releases on production environment'
)
->addUsageExample(
'mage releases rollback --release=20120101172148 to:production',
'Rollback 20120101172148 release on production environment'
)
->addUsageExample(
'mage releases rollback --release=-1 to:production',
'Rollback <bold>list release -1</bold> release on production environment'
)
->addUsageExample(
'mage releases rollback --release=0 to:production',
'Rollback last release on production environment'
)
->addUsageExample(
'mage releases rollback -1 to:production --deleteCurrent',
'Rollbacks the <bold>last release -1</bold> release and removes current release'
);
}
/**
* List the Releases, Rollback to a Release
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$exitCode = 100;
$subCommand = $this->getConfig()->getArgument(1);
// Run Tasks for Deployment
$hosts = $this->getConfig()->getHosts();
if (count($hosts) == 0) {
Console::output(
'<light_purple>Warning!</light_purple> <bold>No hosts defined, unable to get releases.</bold>',
1, 3
);
return 101;
}
$result = true;
foreach ($hosts as $hostKey => $host) {
// Check if Host has specific configuration
$hostConfig = null;
if (is_array($host)) {
$hostConfig = $host;
$host = $hostKey;
}
// Set Host and Host Specific Config
$this->getConfig()->setHost($host);
$this->getConfig()->setHostConfig($hostConfig);
switch ($subCommand) {
case 'list':
$task = Factory::get('releases/list', $this->getConfig());
$task->init();
$result = $task->run() && $result;
break;
case 'rollback':
if (!is_numeric($this->getConfig()->getParameter('release', ''))) {
Console::output('<red>Missing required releaseid.</red>', 1, 2);
return 102;
}
$lockFile = getcwd() . '/.mage/' . $this->getConfig()->getEnvironment() . '.lock';
if (file_exists($lockFile)) {
Console::output('<red>This environment is locked!</red>', 1, 2);
echo file_get_contents($lockFile);
return 103;
}
$releaseId = $this->getConfig()->getParameter('release', '');
$this->getConfig()->setReleaseId($releaseId);
$task = Factory::get('releases/rollback', $this->getConfig());
$task->init();
$result = $task->run() && $result;
break;
}
}
if ($result) {
$exitCode = 0;
}
return $exitCode;
}
}

View file

@ -1,96 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Command\RequiresEnvironment;
use Mage\Task\Factory;
use Mage\Console;
/**
* This is an Alias of "release rollback"
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class RollbackCommand extends AbstractCommand implements RequiresEnvironment
{
public function __construct()
{
$this->setName('Rollback command')
->setHelpMessage('Rollbacks the release by given release id or index')
->setSyntaxMessage('mage rollback [releaseId] to:[environment_name]')
->addUsageExample(
'mage rollback 20120101172148 to:production',
'Rollbacks the 20120101172148 release on production environment'
)
->addUsageExample(
'mage rollback -1 to:production',
'Rollbacks the <bold>last release -1</bold> release'
)
->addUsageExample(
'mage rollback -1 to:production --deleteCurrent',
'Rollbacks the <bold>last release -1</bold> release and removes current release'
);
}
/**
* Rollback a release
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$exitCode = 105;
$releaseId = $this->getConfig()->getArgument(1);
if (!is_numeric($releaseId)) {
Console::output('<red>This release is mandatory.</red>', 1, 2);
return 104;
}
$lockFile = getcwd() . '/.mage/' . $this->getConfig()->getEnvironment() . '.lock';
if (file_exists($lockFile)) {
Console::output('<red>This environment is locked!</red>', 1, 2);
echo file_get_contents($lockFile);
return 106;
}
// Run Tasks for Deployment
$hosts = $this->getConfig()->getHosts();
if (count($hosts) == 0) {
Console::output('<light_purple>Warning!</light_purple> <bold>No hosts defined, unable to get releases.</bold>', 1, 3);
} else {
$result = true;
foreach ($hosts as $hostKey => $host) {
// Check if Host has specific configuration
$hostConfig = null;
if (is_array($host)) {
$hostConfig = $host;
$host = $hostKey;
}
// Set Host and Host Specific Config
$this->getConfig()->setHost($host);
$this->getConfig()->setHostConfig($hostConfig);
$this->getConfig()->setReleaseId($releaseId);
$task = Factory::get('releases/rollback', $this->getConfig());
$task->init();
$result = $task->run() && $result;
}
if ($result) {
$exitCode = 0;
}
}
return $exitCode;
}
}

View file

@ -1,55 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Command\RequiresEnvironment;
use Mage\Console;
/**
* Command for Unlocking an Environment
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class UnlockCommand extends AbstractCommand implements RequiresEnvironment
{
public function __construct()
{
$this->setName('Unlock command')
->setHelpMessage('Unlocks deployment for given environment')
->setSyntaxMessage('mage unlock to:[environment_name]')
->addUsageExample(
'mage unlock to:production',
'Removes the lock form production environment deployment'
);
}
/**
* Unlocks an Environment
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$lockFile = getcwd() . '/.mage/' . $this->getConfig()->getEnvironment() . '.lock';
if (file_exists($lockFile)) {
@unlink($lockFile);
}
Console::output(
'Unlocked deployment to <light_purple>'
. $this->getConfig()->getEnvironment() . '</light_purple> environment',
1,
2
);
return 0;
}
}

View file

@ -1,54 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Task\Factory;
use Mage\Console;
/**
* Updates the SCM Base Code
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class UpdateCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('Update command')
->setHelpMessage('Updates the SCM base code')
->setSyntaxMessage('mage update');
}
/**
* Updates the SCM Base Code
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
$exitCode = 200;
$task = Factory::get('scm/update', $this->getConfig());
$task->init();
Console::output('Updating application via ' . $task->getName() . ' ... ', 1, 0);
$result = $task->run();
if ($result === true) {
Console::output('<green>OK</green>' . PHP_EOL, 0);
$exitCode = 0;
} else {
Console::output('<red>FAIL</red>' . PHP_EOL, 0);
}
return $exitCode;
}
}

View file

@ -1,105 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
/**
* Upgrades the Magallanes Version on the Local System
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class UpgradeCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('Upgrade command')
->setHelpMessage('Upgrades Magallanes')
->setSyntaxMessage('mage upgrade');
}
/**
* Source for downloading
* @var string
*/
const DOWNLOAD = 'http://download.magephp.com/magallanes.{version}.tar.gz';
/**
* JSON for Upgrade
* @var string
*/
const UPGRADE = 'http://download.magephp.com/upgrade.json';
/**
* Command for Upgrading Magallanes
* @see \Mage\Command\BuiltIn\InstallCommand::run()
*/
public function run()
{
$exitCode = 99;
Console::output('Upgrading <bold>Magallanes</bold> ... ', 1, 0);
$user = '';
// Check if user is root
Console::executeCommand('whoami', $user);
$owner = posix_getpwuid(fileowner(__FILE__));
$owner = $owner['name'];
if ($user != 'root' && $user != $owner) {
Console::output('<red>FAIL</red>', 0, 1);
Console::output('You need to be the <bold>' . $owner . '</bold> user to perform the upgrade, or <bold>root</bold>.', 2);
} else {
// Check version
$version = json_decode(file_get_contents(self::UPGRADE));
if ($version !== false && $version !== null) {
$versionCompare = version_compare(MAGALLANES_VERSION, $version->latest);
if ($versionCompare == 0) {
Console::output('<yellow>SKIP</yellow>', 0, 1);
Console::output('Your current version is up to date.', 2);
$exitCode = 0;
} elseif ($versionCompare == 1) {
Console::output('<yellow>SKIP</yellow>', 0, 1);
Console::output('Your current version is newer.', 2);
$exitCode = 0;
} elseif ($versionCompare == -1) {
// Download Package
$tarball = file_get_contents(str_replace('{version}', $version->latest, self::DOWNLOAD));
if ($tarball === false) {
Console::output('<red>FAIL</red>', 0, 1);
Console::output('Corrupted download.', 2);
} else {
$tarballFile = tempnam('/tmp', 'magallanes_download');
rename($tarballFile, $tarballFile . '.tar.gz');
$tarballFile .= '.tar.gz';
file_put_contents($tarballFile, $tarball);
Console::executeCommand('rm -rf ' . MAGALLANES_DIRECTORY);
Console::executeCommand('cd ' . dirname($tarballFile) . ' && tar xfz ' . $tarballFile);
Console::executeCommand('mv ' . dirname($tarballFile) . '/magallanes ' . MAGALLANES_DIRECTORY);
Console::output('<green>OK</green>', 0, 1);
$exitCode = 0;
}
} else {
Console::output('<red>FAIL</red>', 0, 1);
Console::output('Invalid version.', 2);
}
} else {
Console::output('<red>FAIL</red>', 0, 1);
Console::output('Invalid version.', 2);
}
}
return $exitCode;
}
}

View file

@ -1,39 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command\BuiltIn;
use Mage\Command\AbstractCommand;
use Mage\Console;
/**
* Command for displaying the Version of Magallanes
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class VersionCommand extends AbstractCommand
{
public function __construct()
{
$this->setName('Version command')
->setHelpMessage('Displays the current version of Magallanes')
->setSyntaxMessage('mage version');
}
/**
* Display the Magallanes Version
* @see \Mage\Command\AbstractCommand::run()
*/
public function run()
{
Console::output('Running <blue>Magallanes</blue> version <bold>' . MAGALLANES_VERSION . '</bold>', 0, 2);
return 0;
}
}

View file

@ -1,59 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command;
use Mage\Config;
use Exception;
/**
* Loads a Magallanes Command.
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Factory
{
/**
* Gets an instance of a Command.
*
* @param string $commandName
* @param Config $config
* @return AbstractCommand
* @throws Exception
*/
public static function get($commandName, Config $config)
{
$instance = null;
$commandName = ucwords(str_replace('-', ' ', $commandName));
$commandName = str_replace(' ', '', $commandName);
$commandName = str_replace(' ', '_', ucwords(str_replace('/', ' ', $commandName)));
$className = 'Mage\\Command\\BuiltIn\\' . $commandName . 'Command';
if (!class_exists($className)) {
// try a custom command
$className = 'Command\\' . $commandName;
if (!class_exists($className)) {
throw new Exception('Command "' . $commandName . '" not found.');
}
}
/** @var AbstractCommand $instance */
$instance = new $className;
if (! $instance instanceof AbstractCommand) {
throw new Exception('The command ' . $commandName . ' must be an instance of Mage\Command\AbstractCommand.');
}
$instance->setConfig($config);
return $instance;
}
}

View file

@ -1,20 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Command;
/**
* Indicates that a Command depends of an Environment.
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
interface RequiresEnvironment
{
}

View file

@ -1,66 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage;
use Phar;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
/**
* Compiles the library into a .phar file
*
* @author Ismael Ambrosi<ismaambrosi@gmail.com>
*/
class Compiler
{
/**
* Compiles the library
*
* @param string $file
*/
public function compile($file = 'mage.phar')
{
if (file_exists($file)) {
unlink($file);
}
$phar = new Phar($file, 0, 'mage.phar');
$phar->setSignatureAlgorithm(Phar::SHA1);
$phar->startBuffering();
$iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator(__DIR__), RecursiveIteratorIterator::CHILD_FIRST);
/** @var \SplFileInfo $path */
foreach ($iterator as $path) {
if ($path->isFile()) {
$phar->addFromString(str_replace(dirname(__DIR__) . '/', '', $path->getPathname()), file_get_contents($path));
}
}
$binary = file(__DIR__ . '/../bin/mage');
unset($binary[0]);
$binary = implode(PHP_EOL, $binary);
$phar->addFromString('mage', str_replace(
'$baseDir = dirname(dirname(__FILE__));',
'$baseDir = __DIR__;',
$binary
));
$phar->setStub("#!/usr/bin/env php\n<?php Phar::mapPhar('mage.phar'); require 'phar://mage.phar/mage'; __HALT_COMPILER();");
$phar->stopBuffering();
unset($phar);
chmod($file, 0755);
}
}

View file

@ -1,616 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage;
use Mage\Config\ConfigNotFoundException;
use Mage\Config\RequiredConfigNotFoundException;
use Mage\Yaml\Yaml;
use Exception;
/**
* Magallanes Configuration
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Config
{
const HOST_NAME_LENGTH = 1000;
/**
* Arguments loaded
* @var array
*/
private $arguments = array();
/**
* Parameters loaded
* @var array
*/
private $parameters = array();
/**
* Environment
* @var string|boolean
*/
private $environment = false;
/**
* The current Host
* @var string
*/
private $host = null;
/**
* Custom Configuration for the current Host
* @var array
*/
private $hostConfig = array();
/**
* The Release ID
* @var integer
*/
private $releaseId = null;
/**
* Magallanes Global and Environment configuration
*/
private $generalConfig = array();
private $environmentConfig = array();
/**
* Parse the Command Line options
* @param $arguments
*/
protected function parse($arguments)
{
foreach ($arguments as $argument) {
if (preg_match('/to:[\w]+/i', $argument)) {
$this->environment = str_replace('to:', '', $argument);
} elseif (preg_match('/--[\w]+/i', $argument)) {
$optionValue = explode('=', substr($argument, 2));
if (count($optionValue) == 1) {
$this->parameters[$optionValue[0]] = true;
} elseif (count($optionValue) == 2) {
if (strtolower($optionValue[1]) == 'true') {
$this->parameters[$optionValue[0]] = true;
} elseif (strtolower($optionValue[1]) == 'false') {
$this->parameters[$optionValue[0]] = false;
} else {
$this->parameters[$optionValue[0]] = $optionValue[1];
}
}
} else {
$this->arguments[] = $argument;
}
}
}
/**
* Initializes the General Configuration
*/
protected function initGeneral()
{
try {
$this->generalConfig = $this->loadGeneral(getcwd() . '/.mage/config/general.yml');
} catch (ConfigNotFoundException $e) {
// normal situation
}
}
/**
* Load general config from the given file
*
* @param $filePath
*
* @return array
* @throws Config\ConfigNotFoundException
*/
protected function loadGeneral($filePath)
{
return $this->parseConfigFile($filePath);
}
/**
* Loads the Environment configuration
* @param $filePath string
*
* @throws Exception
* @return boolean
*/
protected function loadEnvironment($filePath)
{
$settings = $this->parseConfigFile($filePath);
return $settings;
}
/**
* Initializes the Environment configuration
*
* @throws Exception
* @return boolean
*/
protected function initEnvironment()
{
$environment = $this->getEnvironment();
if (!empty($environment)) {
$configFilePath = getcwd() . '/.mage/config/environment/' . $environment . '.yml';
try {
$this->environmentConfig = $this->loadEnvironment($configFilePath);
} catch (ConfigNotFoundException $e) {
throw new RequiredConfigNotFoundException("Not found required config $configFilePath for environment $environment", 0, $e);
}
}
}
/**
*
* @param array $parameters
* @return boolean
*/
protected function isRunInSpecialMode(array $parameters)
{
if (empty($parameters)) {
return true;
}
foreach ($parameters as $parameter) {
if (isset(Console::$paramsNotRequiringEnvironment[$parameter])) {
return true;
}
}
return false;
}
/**
* Load the Configuration and parses the Arguments
*
* @param array $arguments
*/
public function load($arguments)
{
$this->parse($arguments);
$this->initGeneral();
$this->initEnvironment();
}
/**
* Reloads the configuration
*/
public function reload()
{
$this->initGeneral();
$this->initEnvironment();
}
/**
* Return the invocation argument based on a position
* 0 = Invoked Command Name
*
* @param integer $position
* @return mixed
*/
public function getArgument($position = 0)
{
if (isset($this->arguments[$position])) {
return $this->arguments[$position];
} else {
return false;
}
}
/**
* Returns all the invocation arguments
*
* @return array
*/
public function getArguments()
{
return $this->arguments;
}
/**
* Return the a parameter
*
* @param string $name
* @param mixed $default
* @param array $extraParameters
* @return mixed
*/
public function getParameter($name, $default = null, $extraParameters = array())
{
if (isset($this->parameters[$name])) {
return $this->parameters[$name];
} elseif (isset($extraParameters[$name])) {
return $extraParameters[$name];
} else {
return $default;
}
}
/**
* Returns all the invocation arguments
*
* @return array
*/
public function getParameters()
{
return $this->parameters;
}
/**
* Adds (or replaces) a parameter
* @param string $name
* @param mixed $value
*/
public function addParameter($name, $value = true)
{
$this->parameters[$name] = $value;
}
/**
* Returns the Current environment
*
* @return mixed
*/
public function getEnvironment()
{
return $this->environment;
}
/**
* Get the Tasks to execute
*
* @param string $stage
* @return array
*/
public function getTasks($stage = 'deploy')
{
if ($stage == 'deploy') {
$configStage = 'on-deploy';
} else {
$configStage = $stage;
}
$tasks = array();
$config = $this->getEnvironmentOption('tasks', array());
// Host Config
if (is_array($this->hostConfig) && isset($this->hostConfig['tasks'])) {
if (isset($this->hostConfig['tasks'][$configStage])) {
$config[$configStage] = $this->hostConfig['tasks'][$configStage];
}
}
if (isset($config[$configStage])) {
$tasksData = ($config[$configStage] ? (array)$config[$configStage] : array());
foreach ($tasksData as $taskData) {
if (is_array($taskData)) {
$tasks[] = array(
'name' => key($taskData),
'parameters' => current($taskData),
);
} else {
$tasks[] = $taskData;
}
}
}
return $tasks;
}
/**
* Get the current Hosts to deploy
*
* @return array
*/
public function getHosts()
{
$hosts = array();
$envConfig = $this->getEnvironmentConfig();
if (isset($envConfig['hosts'])) {
if (is_array($envConfig['hosts'])) {
$hosts = (array)$envConfig['hosts'];
} elseif (is_string($envConfig['hosts']) && file_exists($envConfig['hosts']) && is_readable($envConfig['hosts'])) {
$hosts = $this->getHostsFromFile($envConfig['hosts']);
}
}
return $hosts;
}
/**
* Set the current host
*
* @param string $host
* @return \Mage\Config
*/
public function setHost($host)
{
$this->host = $host;
return $this;
}
/**
* Set the host specific configuration
*
* @param array $hostConfig
* @return \Mage\Config
*/
public function setHostConfig($hostConfig = null)
{
$this->hostConfig = $hostConfig;
return $this;
}
/**
* Get the current host name
*
* @return string
*/
public function getHostName()
{
$info = explode(':', $this->host);
return $info[0];
}
/**
* Get the current Host Port
*
* @return integer
*/
public function getHostPort()
{
$info = explode(':', $this->host);
$info[] = $this->deployment('port', '22');
return $info[1];
}
/**
* Get the general Host Identity File Option
*
* @return string
*/
public function getHostIdentityFileOption()
{
return $this->deployment('identity-file') ? ('-i ' . $this->deployment('identity-file') . ' ') : '';
}
/**
* Get the ConnectTimeout option
*
* @return string
*/
public function getConnectTimeoutOption()
{
return $this->environmentConfig('connect-timeout') ? ('-o ConnectTimeout=' . $this->environmentConfig('connect-timeout') . ' ') : '';
}
/**
* Get the current Host
*
* @return string
*/
public function getHost()
{
return $this->host;
}
/**
* Gets General Configuration
*
* @param string $option
* @param bool|string $default
* @return mixed
*/
public function general($option, $default = false)
{
$config = $this->getGeneralConfig();
if (isset($config[$option])) {
if (is_array($default) && ($config[$option] == '')) {
return $default;
} else {
return $config[$option];
}
} else {
return $default;
}
}
/**
* Gets Environments Full Configuration
*
* @param string $option
* @param bool|string $default
* @return mixed
*/
public function environmentConfig($option, $default = false)
{
return $this->getEnvironmentOption($option, $default);
}
/**
* Get deployment configuration
*
* @param string $option
* @param bool|string $default
* @return string
*/
public function deployment($option, $default = false)
{
// Host Config
if (is_array($this->hostConfig) && isset($this->hostConfig['deployment'])) {
if (isset($this->hostConfig['deployment'][$option])) {
return $this->hostConfig['deployment'][$option];
}
}
// Global Config
$config = $this->getEnvironmentOption('deployment', array());
if (isset($config[$option])) {
if (is_array($default) && ($config[$option] == '')) {
return $default;
} else {
return $config[$option];
}
} else {
return $default;
}
}
public function setSourceTemporal($directory)
{
$this->environmentConfig['deployment']['source']['temporal'] = $directory;
}
/**
* Returns Releasing Options
*
* @param string $option
* @param bool|string $default
* @return mixed
*/
public function release($option, $default = false)
{
// Host Config
if (is_array($this->hostConfig) && isset($this->hostConfig['releases'])) {
if (isset($this->hostConfig['releases'][$option])) {
return $this->hostConfig['releases'][$option];
}
}
$config = $this->getEnvironmentOption('releases', array());
if (isset($config[$option])) {
if (is_array($default) && ($config[$option] == '')) {
return $default;
} else {
return $config[$option];
}
} else {
return $default;
}
}
/**
* Set From Deployment Path
*
* @param string $from
* @return \Mage\Config
*/
public function setFrom($from)
{
$this->environmentConfig['deployment']['from'] = $from;
return $this;
}
/**
* Sets the Current Release ID
*
* @param integer $id
* @return \Mage\Config
*/
public function setReleaseId($id)
{
$this->releaseId = $id;
return $this;
}
/**
* Gets the Current Release ID
*
* @return integer
*/
public function getReleaseId()
{
return $this->releaseId;
}
/**
* Get Environment root option
*
* @param string $option
* @param mixed $default
* @return mixed
*/
public function getEnvironmentOption($option, $default = array())
{
$config = $this->getEnvironmentConfig();
if (isset($config[$option])) {
return $config[$option];
} else {
return $default;
}
}
/**
* Utility methods. TODO To be extracted into own Class
*/
public function parseConfigFile($filePath)
{
if (!file_exists($filePath)) {
throw new ConfigNotFoundException("Cannot find the file at path $filePath");
}
return $this->parseConfigText(file_get_contents($filePath));
}
public function parseConfigText($input)
{
return Yaml::parse($input);
}
/**
* @return array
*/
protected function getGeneralConfig()
{
return $this->generalConfig;
}
/**
* @return array
*/
protected function getEnvironmentConfig()
{
return $this->environmentConfig;
}
/**
* @param string $filePath
*
* @return array
*/
protected function getHostsFromFile($filePath)
{
$handle = fopen($filePath, 'r');
$hosts = array();
try {
$fileContent = stream_get_contents($handle);
$hosts = json_decode($fileContent);
} catch (Exception $e) {
rewind($handle);
//do it old-style: one host per line
while (($host = stream_get_line($handle, self::HOST_NAME_LENGTH)) !== false) {
$host = trim($host);
if (!empty($host)) {
$hosts[] = $host;
}
}
}
return $hosts;
}
}

View file

@ -1,13 +0,0 @@
<?php
namespace Mage\Config;
use Mage\Yaml\Exception\RuntimeException;
/**
*
* @author Vladimir Grigor <vgrigor@gmail.com>
*/
class ConfigNotFoundException extends RuntimeException
{
}

View file

@ -1,13 +0,0 @@
<?php
namespace Mage\Config;
use Mage\Yaml\Exception\RuntimeException;
/**
*
* @author Vladimir Grigor <vgrigor@gmail.com>
*/
class OptionalConfigNotFoundException extends RuntimeException
{
}

View file

@ -1,13 +0,0 @@
<?php
namespace Mage\Config;
use Mage\Yaml\Exception\RuntimeException;
/**
*
* @author Vladimir Grigor <vgrigor@gmail.com>
*/
class RequiredConfigNotFoundException extends RuntimeException
{
}

View file

@ -1,313 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage;
use Mage\Command\Factory;
use Mage\Command\RequiresEnvironment;
use Mage\Console\Colors;
use Exception;
use RecursiveDirectoryIterator;
use SplFileInfo;
/**
* Magallanes interface between the Tasks and Commands and the User's Console.
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Console
{
/**
* @var array
*/
public static $paramsNotRequiringEnvironment = array('install' => 'install', 'upgrade' => 'upgrade', 'version' => 'version');
/**
* Handler to the current Log File.
* @var mixed
*/
private static $log = null;
/**
* The current logfile
* @var string
*/
private static $logFile = null;
/**
* Enables or Disables Logging
* @var boolean
*/
private static $logEnabled = true;
/**
* Enables or disables verbose logging
* @var boolean
*/
private static $verboseLogEnabled = false;
/**
* String Buffer for the screen output
* @var string
*/
private static $screenBuffer = '';
/**
* Output of executed commands
* @var string
*/
private static $commandsOutput = '';
/**
* Configuration
* @var \Mage\Config
*/
private static $config;
/**
* Runns a Magallanes Command
* @throws Exception
*/
public function run($arguments)
{
$exitCode = 10;
// Declare a Shutdown Closure
register_shutdown_function(function () {
// Only Unlock if there was an error
if (error_get_last() !== null) {
if (file_exists(getcwd() . '/.mage/~working.lock')) {
unlink(getcwd() . '/.mage/~working.lock');
}
}
});
$config = self::$config = new Config;
$configError = false;
try {
// Load configuration
$config->load($arguments);
} catch (Exception $exception) {
$configError = $exception->getMessage();
}
// Command Option
$commandName = $config->getArgument(0);
// Logging
$showGreetings = true;
if (in_array($commandName, self::$paramsNotRequiringEnvironment)) {
self::$logEnabled = false;
$showGreetings = false;
} else {
self::$logEnabled = $config->general('logging', false);
}
self::$verboseLogEnabled = self::isVerboseLoggingEnabled();
// Greetings
if ($showGreetings) {
if (!self::$logEnabled) {
self::output('Starting <blue>Magallanes</blue>', 0, 2);
} else {
self::output('Starting <blue>Magallanes</blue>', 0, 1);
self::log("Logging enabled");
self::output('<bold>Logging enabled:</bold> <purple>' . self::getLogFile() . '</purple>', 1, 2);
}
}
// Run Command - Check if there is a Configuration Error
if ($configError !== false) {
self::output('<red>' . $configError . '</red>', 1, 2);
} else {
// Run Command and check for Command Requirements
try {
$command = Factory::get($commandName, $config);
if ($config->getParameter('help')) {
self::output($command->getInfoMessage(), 2);
return 0;
}
if ($command instanceof RequiresEnvironment && $config->getEnvironment() === false) {
throw new Exception('You must specify an environment for this command.');
}
// Run the Command
$exitCode = $command->run();
// Check for errors
if (is_int($exitCode) && $exitCode !== 0) {
throw new Exception('Command execution failed with following exit code: ' . $exitCode, $exitCode);
} elseif (is_bool($exitCode) && !$exitCode) {
$exitCode = 1;
throw new Exception('Command execution failed.', $exitCode);
}
} catch (Exception $exception) {
self::output('<red>' . $exception->getMessage() . '</red>', 1, 2);
}
}
if ($showGreetings) {
self::output('Finished <blue>Magallanes</blue>', 0, 2);
if (file_exists(getcwd() . '/.mage/~working.lock')) {
unlink(getcwd() . '/.mage/~working.lock');
}
}
// Check if logs need to be deleted
self::checkLogs($config);
return $exitCode;
}
/**
* Outputs a message to the user's screen.
*
* @param string $message
* @param integer $tabs
* @param integer $newLine
*/
public static function output($message, $tabs = 1, $newLine = 1)
{
self::log(strip_tags($message));
if (!self::$verboseLogEnabled) {
self::$screenBuffer .= str_repeat("\t", $tabs)
. strip_tags($message)
. str_repeat(PHP_EOL, $newLine);
$output = str_repeat("\t", $tabs)
. Colors::color($message, self::$config)
. str_repeat(PHP_EOL, $newLine);
echo $output;
}
}
/**
* Executes a Command on the Shell
*
* @param string $command
* @param string $output
* @return boolean
*/
public static function executeCommand($command, &$output = null)
{
self::log('---------------------------------');
self::log('---- Executing: $ ' . $command);
$return = 1;
$log = array();
exec($command . ' 2>&1', $log, $return);
$log = implode(PHP_EOL, $log);
if (!$return) {
$output = trim($log);
}
self::$commandsOutput .= PHP_EOL . trim($log) . PHP_EOL;
self::log($log);
self::log('---------------------------------');
return !$return;
}
/**
* Log a message to the logfile.
*
* @param string $message
*/
public static function log($message)
{
if (self::$logEnabled) {
if (self::$log === null) {
self::$logFile = realpath(getcwd() . '/.mage/logs') . '/log-' . date('Ymd-His') . '.log';
self::$log = fopen(self::$logFile, 'w');
}
$message = date('Y-m-d H:i:s -- ') . $message;
fwrite(self::$log, $message . PHP_EOL);
if (self::$verboseLogEnabled) {
echo $message . PHP_EOL;
}
}
}
/**
* Return the screen buffer
* @return string
*/
public static function getOutput()
{
return self::$screenBuffer;
}
/**
* Returns the Log File
* @return string
*/
public static function getLogFile()
{
return self::$logFile;
}
/**
* Read String From Prompt
*/
public static function readInput()
{
$fp = fopen("php://stdin", "r");
$line = fgets($fp);
return rtrim($line);
}
/**
* Check Logs
* @param \Mage\Config $config
*/
private static function checkLogs(Config $config)
{
if (self::$logEnabled) {
$maxLogs = $config->general('maxlogs', 30);
$logs = array();
foreach (new RecursiveDirectoryIterator(getcwd() . '/.mage/logs', RecursiveDirectoryIterator::SKIP_DOTS) as $log) {
/* @var $log SplFileInfo */
if (strpos($log->getFilename(), 'log-') === 0) {
$logs[] = $log->getFilename();
}
}
sort($logs);
if (count($logs) > $maxLogs) {
$logsToDelete = array_slice($logs, 0, count($logs) - $maxLogs);
foreach ($logsToDelete as $logToDeelte) {
unlink(getcwd() . '/.mage/logs/' . $logToDeelte);
}
}
}
}
/**
* Check if verbose logging is enabled
* @return boolean
*/
protected static function isVerboseLoggingEnabled()
{
return self::$config->getParameter('verbose', false)
|| self::$config->general('verbose_logging')
|| self::$config->environmentConfig('verbose_logging', false);
}
}

View file

@ -1,78 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Console;
use Mage\Config;
/**
* Parses the different colors available for the Terminal or Console.
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Colors
{
/**
* List of Colors and they Terminal/Console representation.
* @var array
*/
private static $foregroundColors = array(
'black' => '0;30',
'bold' => '1',
'blue' => '0;34',
'light_blue' => '1;34',
'green' => '0;32',
'light_green' => '1;32',
'cyan' => '0;36',
'light_cyan' => '1;36',
'red' => '0;31',
'light_red' => '1;31',
'purple' => '0;35',
'light_purple' => '1;35',
'brown' => '0;33',
'yellow' => '1;33',
'light_gray' => '0;37',
'white' => '1;37'
);
/**
* Parses a Text to represent Colors in the Terminal/Console.
*
* @param string $string
* @param Config $config
* @return string
*/
public static function color($string, Config $config)
{
$disabled = $config->getParameter('no-color', !$config->general('colors', true));
if ($disabled) {
$string = strip_tags($string);
return $string;
}
foreach (self::$foregroundColors as $key => $code) {
$replaceFrom = array(
'<' . $key . '>',
'</' . $key . '>'
);
$replaceTo = array(
"\033[" . $code . 'm',
"\033[0m"
);
$string = str_replace($replaceFrom, $replaceTo, $string);
}
return $string;
}
}

View file

@ -1,88 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage;
/**
* Mailer Helper.
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Mailer
{
const EOL = "\r\n";
const SUBJECT = '[Magallanes] Deployment of {project} to {environment}: {result}';
protected $address;
protected $project;
protected $environment;
protected $logFile;
public function setAddress($address)
{
$this->address = $address;
return $this;
}
public function setProject($project)
{
$this->project = $project;
return $this;
}
public function setEnvironment($environment)
{
$this->environment = $environment;
return $this;
}
public function setLogFile($logFile)
{
$this->logFile = $logFile;
return $this;
}
public function send($result)
{
$boundary = md5(date('r', time()));
$headers = 'From: ' . $this->address
. self::EOL
. 'Reply-To: ' . $this->address
. self::EOL
. 'MIME-Version: 1.0'
. self::EOL
. 'Content-Type: multipart/mixed; boundary=Mage-mixed-' . $boundary;
$subject = str_replace(
array('{project}', '{environment}', '{result}'),
array($this->project, $this->environment, $result ? 'SUCCESS' : 'FAILURE'),
self::SUBJECT
);
$attachment = chunk_split(base64_encode(file_get_contents($this->logFile)));
$message = 'This is a multi-part message in MIME format.' . self::EOL
. '--Mage-mixed-' . $boundary . self::EOL
. 'Content-Type: text/plain; charset=iso-8859-1' . self::EOL
. 'Content-Transfer-Encoding: quoted-printable' . self::EOL
. self::EOL
. strip_tags(Console::getOutput()) . self::EOL
. self::EOL
. '--Mage-mixed-' . $boundary . self::EOL
. 'Content-Type: text/plain; name="log.txt"' . self::EOL
. 'Content-Transfer-Encoding: base64' . self::EOL
. 'Content-Disposition: attachment' . self::EOL
. self::EOL
. $attachment . self::EOL
. '--Mage-mixed-' . $boundary . '--' . self::EOL;
mail($this->address, $subject, $message, $headers);
}
}

View file

@ -1,360 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task;
use Mage\Console;
use Mage\Config;
use Mage\Task\Releases\IsReleaseAware;
use Exception;
/**
* Abstract Class for a Magallanes Task
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
abstract class AbstractTask
{
/**
* Stage Constant for Pre Deployment
* @var string
*/
const STAGE_PRE_DEPLOY = 'pre-deploy';
/**
* Stage Constant for Deployment
* @var string
*/
const STAGE_DEPLOY = 'deploy';
/**
* Stage Constant for Post Deployment
* @var string
*/
const STAGE_POST_DEPLOY = 'post-deploy';
/**
* Stage Constant for Post Release
* @var string
*/
const STAGE_POST_RELEASE = 'post-release';
/**
* Configuration
* @var Config;
*/
protected $config = null;
/**
* Indicates if the Task is running in a Rollback
* @var boolean
*/
protected $inRollback = false;
/**
* Indicates the Stage the Task is running ing
* @var string
*/
protected $stage = null;
/**
* Extra parameters
* @var array
*/
protected $parameters = array();
/**
* Returns the Title of the Task
* @return string
*/
abstract public function getName();
/**
* Runs the task
*
* @return boolean
* @throws Exception
* @throws ErrorWithMessageException
* @throws SkipException
*/
abstract public function run();
/**
* Task Constructor
*
* @param Config $config
* @param boolean $inRollback
* @param string $stage
* @param array $parameters
*/
final public function __construct(Config $config, $inRollback = false, $stage = null, $parameters = array())
{
$this->config = $config;
$this->inRollback = $inRollback;
$this->stage = $stage;
$this->parameters = $parameters;
}
/**
* Indicates if the Task is running in a Rollback operation
* @return boolean
*/
public function inRollback()
{
return $this->inRollback;
}
/**
* Gets the Stage of the Deployment:
* - pre-deploy
* - deploy
* - post-deploy
* - post-release
* @return string
*/
public function getStage()
{
return $this->stage;
}
/**
* Gets the Configuration
* @return Config;
*/
public function getConfig()
{
return $this->config;
}
/**
* Initializes the Task, optional to implement
*/
public function init()
{
}
/**
* Returns a Parameter, or a default if not found
*
* @param string $name
* @param mixed $default
* @return mixed
*/
public function getParameter($name, $default = null)
{
return $this->getConfig()->getParameter($name, $default, $this->getParameters());
}
/**
* @return array
*/
protected function getParameters()
{
return $this->parameters;
}
/**
* Runs a Shell Command Localy
* @param string $command
* @param string $output
* @return boolean
*/
final protected function runCommandLocal($command, &$output = null)
{
return Console::executeCommand($command, $output);
}
/**
* Runs a Shell Command on the Remote Host
* @param string $command
* @param string $output
* @param boolean $cdToDirectoryFirst
* @return boolean
*/
final protected function runCommandRemote($command, &$output = null, $cdToDirectoryFirst = true)
{
if ($this->getConfig()->release('enabled', false) === true) {
if ($this instanceof IsReleaseAware) {
$releasesDirectory = '';
} else {
$releasesDirectory = '/'
. $this->getConfig()->release('directory', 'releases')
. '/'
. $this->getConfig()->getReleaseId();
}
} else {
$releasesDirectory = '';
}
// if general.yml includes "ssy_needs_tty: true", then add "-t" to the ssh command
$needs_tty = ($this->getConfig()->general('ssh_needs_tty', false) ? '-t' : '');
$localCommand = 'ssh ' . $this->getConfig()->getHostIdentityFileOption() . $needs_tty . ' -p ' . $this->getConfig()->getHostPort() . ' '
. '-q -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no '
. $this->getConfig()->getConnectTimeoutOption()
. ($this->getConfig()->deployment('user') != '' ? $this->getConfig()->deployment('user') . '@' : '')
. $this->getConfig()->getHostName();
$remoteCommand = str_replace('"', '\"', $command);
if ($cdToDirectoryFirst) {
$remoteCommand = 'cd ' . rtrim($this->getConfig()->deployment('to'), '/') . $releasesDirectory . ' && ' . $remoteCommand;
}
$localCommand .= ' ' . '"sh -c \"' . $remoteCommand . '\""';
Console::log('Run remote command ' . $remoteCommand);
return $this->runCommandLocal($localCommand, $output);
}
/**
* Runs a Shell Command Localy or in the Remote Host based on the Task Stage.
* If the stage is "deploy" then it will be executed in the remote host.
* @param string $command
* @param string $output
* @return boolean
*/
final protected function runCommand($command, &$output = null)
{
if ($this->getStage() == self::STAGE_DEPLOY || $this->getStage() == self::STAGE_POST_RELEASE) {
return $this->runCommandRemote($command, $output);
} else {
return $this->runCommandLocal($command, $output);
}
}
/**
* adds a cd to the needed release if we work with releases.
*
* @param string $command
* @return string
*/
protected function getReleasesAwareCommand($command)
{
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$deployToDirectory = $releasesDirectory . '/' . $this->getConfig()->getReleaseId();
return 'cd ' . $deployToDirectory . ' && ' . $command;
}
return $command;
}
/**
* @param integer $releaseId
* @return bool
*/
protected function tarRelease($releaseId)
{
$result = true;
// for given release, check if tarred
$output = '';
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$currentReleaseDirectory = $releasesDirectory . '/' . $releaseId;
$currentReleaseDirectoryTemp = $currentReleaseDirectory . '_tmp/';
$currentRelease = $currentReleaseDirectory . '/' . $releaseId . '.tar.gz';
$command = 'test -e ' . $currentRelease . ' && echo "true" || echo ""';
$this->runCommandRemote($command, $output);
// if not, do so
if (!$output) {
$commands = array();
$commands[] = 'mv ' . $currentReleaseDirectory . ' ' . $currentReleaseDirectoryTemp;
$commands[] = 'mkdir ' . $currentReleaseDirectory;
$commands[] = 'tar cfz ' . $currentRelease . ' ' . $currentReleaseDirectoryTemp;
$commands[] = 'rm -rf ' . $currentReleaseDirectoryTemp;
$command = implode(' && ', $commands);
$result = $this->runCommandRemote($command, $output);
return $result;
}
return $result;
}
protected function untarRelease($releaseId)
{
$result = true;
// for given release, check if tarred
$output = '';
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$currentReleaseDirectory = $releasesDirectory . '/' . $releaseId;
$currentReleaseDirectoryTemp = $currentReleaseDirectory . '_tmp/';
$currentRelease = $currentReleaseDirectory . '/' . $releaseId . '.tar.gz';
$command = 'test -e ' . $currentRelease . ' && echo "true" || echo ""';
$this->runCommandRemote($command, $output);
// if tarred, untar now
if ($output) {
$commands = array();
$commands[] = 'tar xfz ' . $currentRelease;
$commands[] = 'rm -rf ' . $currentReleaseDirectory;
$commands[] = 'mv ' . $currentReleaseDirectoryTemp . ' ' . $currentReleaseDirectory;
$command = implode(' && ', $commands);
$result = $this->runCommandRemote($command, $output);
return $result;
}
return $result;
}
/**
* Returns the array of environment variables
* Returned array contains both system variables and variables set in config
* WARNING: To access system's variables you need to set proper value in your php.ini at variables_order key
* @see http://php.net/manual/en/ini.core.php#ini.variables-order
*
* @return array
*/
protected function getEnvVariables()
{
$configVars = array_merge(
$this->getConfig()->general('env', array()),
$this->getConfig()->environmentConfig('env', array()),
$this->getConfig()->getParameter('env', array()),
array(
'variables' => $this->getConfig()->getParameter('env.variables', array())
)
);
if (isset($configVars['variables'])) {
$configVars = $configVars['variables'];
}
$envVariables = array_merge(
$_ENV,
$configVars
);
return $envVariables;
}
/**
* Returns ready to inject environment string
* The string is build from env vars array in schema:
* key1=value1 key2=value3 ...
*
* @return string
*/
protected function getEnvVarsString()
{
$envVarsArray = $this->getEnvVariables();
$envVars = array_map(
function ($key, $value) {
return "$key=$value";
},
array_keys($envVarsArray),
$this->getEnvVariables()
);
return join(' ', $envVars);
}
}

View file

@ -1,27 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Composer;
use Mage\Task\AbstractTask;
/**
* Abstract Task for Composer
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
abstract class ComposerAbstractTask extends AbstractTask
{
protected function getComposerCmd()
{
$composerCmd = $this->getParameter('composer_cmd', $this->getConfig()->general('composer_cmd', 'php composer.phar'));
return $this->getConfig()->general('composer_cmd', $composerCmd);
}
}

View file

@ -1,27 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Composer;
use Mage\Task\ErrorWithMessageException;
class GenerateAutoloadTask extends ComposerAbstractTask
{
/**
* Returns the Title of the Task
* @return string
*/
public function getName()
{
return 'Generate autoload via Composer [built-in]';
}
/**
* Runs the task
*
* @return boolean
* @throws ErrorWithMessageException
*/
public function run()
{
return $this->runCommand($this->getComposerCmd() . ' dumpautoload --optimize');
}
}

View file

@ -1,28 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Composer;
use Mage\Task\ErrorWithMessageException;
class InstallTask extends ComposerAbstractTask
{
/**
* Returns the Title of the Task
* @return string
*/
public function getName()
{
return 'Install vendors via Composer [built-in]';
}
/**
* Runs the task
*
* @return boolean
* @throws ErrorWithMessageException
*/
public function run()
{
$dev = $this->getParameter('dev', true);
return $this->runCommand($this->getComposerCmd() . ' install' . ($dev ? ' --dev' : ' --no-dev'));
}
}

View file

@ -1,164 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Deployment;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\IsReleaseAware;
use Mage\Task\Releases\SkipOnOverride;
/**
* Task for Releasing a Deploy
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class ReleaseTask extends AbstractTask implements IsReleaseAware, SkipOnOverride
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Releasing [built-in]';
}
/**
* Releases a Deployment: points the current symbolic link to the release directory
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$resultFetch = false;
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$symlink = $this->getConfig()->release('symlink', 'current');
if (substr($symlink, 0, 1) == '/') {
$releasesDirectory = rtrim($this->getConfig()->deployment('to'), '/') . '/' . $releasesDirectory;
}
$releaseId = $this->getConfig()->getReleaseId();
$currentCopy = $releasesDirectory . '/' . $releaseId;
//Check if target user:group is specified
$userGroup = $this->getConfig()->deployment('owner');
// Fetch the user and group from base directory; defaults usergroup to 33:33
if (empty($userGroup)) {
$user = '33';
$group = '33';
$directoryInfos = '';
// Get raw directory info and parse it in php.
// "stat" command don't behave the same on different systems, ls output format also varies
// and awk parameters need special care depending on the executing shell
$resultFetch = $this->runCommandRemote("ls -ld .", $directoryInfos);
if (!empty($directoryInfos)) {
// break $directoryInfos by line break
// to exclude unwanted line(s)
$lines = explode("\n", $directoryInfos);
$filtered_lines = array();
foreach ($lines as $line) {
// exclude line that starts with 'Identity added'
// e.g.: from ssh with ProxyCommand / proxy jump
if (stripos($line, "Identity added") !== FALSE) {
continue;
}
$filtered_lines[] = $line;
}
// reconstruct $directoryInfos using the filtered lines
$directoryInfos = implode("\n", $filtered_lines);
//uniformize format as it depends on the system deployed on
$directoryInfos = trim(str_replace(array(" ", "\t"), ' ', $directoryInfos));
$infoArray = explode(' ', $directoryInfos);
if (!empty($infoArray[2])) {
$user = $infoArray[2];
}
if (!empty($infoArray[3])) {
$group = $infoArray[3];
}
}
$userGroup = $user . ':' . $group;
}
$command = 'chown -R ' . $userGroup . ' ' . $currentCopy
. ' && '
. 'chown ' . $userGroup . ' ' . $releasesDirectory;
$result = $this->runCommandRemote($command);
if (!$result) {
return $result;
}
// Switch symlink and change owner
$tmplink = $symlink . '.tmp';
$command = "ln -sfn {$currentCopy} {$tmplink}";
if ($resultFetch && $userGroup != '') {
$command.= " && chown -h {$userGroup} {$tmplink}";
}
$command.= " && mv -fT {$tmplink} {$symlink}";
$result = $this->runCommandRemote($command);
if ($result) {
$this->cleanUpReleases();
}
return $result;
} else {
return false;
}
}
/**
* Removes old releases
*/
protected function cleanUpReleases()
{
// Count Releases
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$symlink = $this->getConfig()->release('symlink', 'current');
if (substr($symlink, 0, 1) == '/') {
$releasesDirectory = rtrim($this->getConfig()->deployment('to'), '/') . '/' . $releasesDirectory;
}
$maxReleases = $this->getConfig()->release('max', false);
if (($maxReleases !== false) && ($maxReleases > 0)) {
$releasesList = '';
$countReleasesFetch = $this->runCommandRemote('ls -1 ' . $releasesDirectory, $releasesList);
$releasesList = trim($releasesList);
if ($countReleasesFetch && $releasesList != '') {
$releasesList = explode(PHP_EOL, $releasesList);
if (count($releasesList) > $maxReleases) {
$releasesToDelete = array_diff($releasesList, array($this->getConfig()->getReleaseId()));
sort($releasesToDelete);
$releasesToDeleteCount = count($releasesToDelete) - $maxReleases;
$releasesToDelete = array_slice($releasesToDelete, 0, $releasesToDeleteCount + 1);
foreach ($releasesToDelete as $releaseIdToDelete) {
$directoryToDelete = $releasesDirectory . '/' . $releaseIdToDelete;
if ($directoryToDelete != '/') {
$command = 'rm -rf ' . $directoryToDelete;
$this->runCommandRemote($command);
}
}
}
}
}
}
}
}

View file

@ -1,65 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Deployment\Strategy;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\IsReleaseAware;
/**
* Abstract Base task to concentrate common code for Deployment Tasks
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
abstract class BaseStrategyTaskAbstract extends AbstractTask implements IsReleaseAware
{
/**
* Checks if there is an override underway
*
* @return bool
*/
protected function checkOverrideRelease()
{
$overrideRelease = $this->getParameter('overrideRelease', false);
$symlink = $this->getConfig()->release('symlink', 'current');
if ($overrideRelease === true) {
$releaseToOverride = false;
$resultFetch = $this->runCommandRemote('ls -ld ' . $symlink . ' | cut -d"/" -f2', $releaseToOverride);
if ($resultFetch && is_numeric($releaseToOverride)) {
$this->getConfig()->setReleaseId($releaseToOverride);
}
}
return $overrideRelease;
}
/**
* Gathers the files to exclude
*
* @return array
*/
protected function getExcludes()
{
$excludes = array(
'.git',
'.svn',
'.mage',
'.gitignore',
'.gitkeep',
'nohup.out'
);
// Look for User Excludes
$userExcludes = $this->getConfig()->deployment('excludes', array());
return array_merge($excludes, $userExcludes);
}
}

View file

@ -1,41 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Deployment\Strategy;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\IsReleaseAware;
use Mage\Task\SkipException;
/**
* Deployment Strategy is Disabled
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class DisabledTask extends AbstractTask implements IsReleaseAware
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Disabled Deployment [built-in]';
}
/**
* Deployment Strategy is Disabled
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
throw new SkipException;
}
}

View file

@ -1,98 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Deployment\Strategy;
use Mage\Task\Releases\IsReleaseAware;
/**
* Task for using Git Working Copy as the Deployed Code
*
* @author Oscar Reales <oreales@gmail.com>
*/
class GitRebaseTask extends BaseStrategyTaskAbstract implements IsReleaseAware
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Deploy via Git Rebase [built-in]';
}
/**
* Rebases the Git Working Copy as the Deployed Code
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$this->checkOverrideRelease();
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$deployToDirectory = rtrim($this->getConfig()->deployment('to'), '/')
. '/' . $releasesDirectory
. '/' . $this->getConfig()->getReleaseId();
$this->runCommandRemote('mkdir -p ' . $deployToDirectory);
}
$branch = $this->getParameter('branch', 'master');
$remote = $this->getParameter('remote', 'origin');
// Fetch Remote
$command = $this->getReleasesAwareCommand('git fetch ' . $remote);
$result = $this->runCommandRemote($command);
if ($result === false) {
$repository = $this->getConfig()->deployment('repository');
if ($repository) {
$command = $this->getReleasesAwareCommand('git clone ' . $repository . ' .');
$result = $this->runCommandRemote($command);
$command = $this->getReleasesAwareCommand('git fetch ' . $remote);
$result = $this->runCommandRemote($command);
}
}
// Checkout
$command = $this->getReleasesAwareCommand('git checkout ' . $branch);
$result = $this->runCommandRemote($command) && $result;
// Check Working Copy status
$stashed = false;
$status = '';
$command = $this->getReleasesAwareCommand('git checkout ' . $branch);
$result = $this->runCommandRemote($command) && $result;
// Stash if Working Copy is not clean
if (!$status) {
$stashResult = '';
$command = $this->getReleasesAwareCommand('git stash');
$result = $this->runCommandRemote($command, $stashResult) && $result;
if ($stashResult != "No local changes to save") {
$stashed = true;
}
}
// Rebase
$command = $this->getReleasesAwareCommand('git rebase ' . $remote . '/' . $branch);
$result = $this->runCommandRemote($command) && $result;
// If Stashed, restore.
if ($stashed) {
$command = $this->getReleasesAwareCommand('git stash pop');
$result = $this->runCommandRemote($command) && $result;
}
return $result;
}
}

View file

@ -1,106 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Deployment\Strategy;
use Exception;
use Mage\Task\AbstractTask;
use Mage\Task\ErrorWithMessageException;
use Mage\Task\Releases\IsReleaseAware;
use Mage\Task\SkipException;
/**
* The git remote cache deployment task.
*
* This tasks uses a remote checkout on the server to provide the release.
* In our use case this remote cache resides in $to/$shared/git-remote-cache,
* $shared is substituted with "shared" by default. At this time, the remote cache
* is not build automatically, you need to provide a clean checkout before you can
* start using it.
*
* @package Mage\Task\BuiltIn\Deployment\Strategy
* @author Mario Mueller <mueller@freshcells.de>
*/
class GitRemoteCacheTask extends AbstractTask implements IsReleaseAware
{
/**
* Returns the Title of the Task
* @return string
*/
public function getName()
{
return 'Deploy via remote cached git repository [built-in]';
}
/**
* Runs the task
*
* @return boolean
* @throws Exception
* @throws ErrorWithMessageException
* @throws SkipException
*/
public function run()
{
$overrideRelease = $this->getParameter('overrideRelease', false);
if ($overrideRelease === true) {
$releaseToOverride = false;
$resultFetch = $this->runCommandRemote('ls -ld current | cut -d"/" -f2', $releaseToOverride);
if ($resultFetch && is_numeric($releaseToOverride)) {
$this->getConfig()->setReleaseId($releaseToOverride);
}
}
$excludes = array(
'.git',
'.svn',
'.mage',
'.gitignore',
'.gitkeep',
'nohup.out'
);
// Look for User Excludes
$userExcludes = $this->getConfig()->deployment('excludes', array());
$deployToDirectory = $this->getConfig()->deployment('to');
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$deployToDirectory = rtrim($this->getConfig()->deployment('to'), '/')
. '/' . $releasesDirectory
. '/' . $this->getConfig()->getReleaseId();
$this->runCommandRemote('mkdir -p ' . $releasesDirectory . '/' . $this->getConfig()->getReleaseId());
}
$branch = $this->getParameter('branch');
$remote = $this->getParameter('remote', 'origin');
$remoteCacheParam = $this->getParameter('remote_cache', 'shared/git-remote-cache');
$remoteCacheFolder = rtrim($this->getConfig()->deployment('to'), '/') . '/' . $remoteCacheParam;
// Don't use -C as git 1.7 does not support it
$command = 'cd ' . $remoteCacheFolder . ' && /usr/bin/env git fetch ' . $remote;
$result = $this->runCommandRemote($command);
$command = 'cd ' . $remoteCacheFolder . ' && /usr/bin/env git checkout ' . $branch;
$result = $this->runCommandRemote($command) && $result;
$command = 'cd ' . $remoteCacheFolder . ' && /usr/bin/env git pull --rebase ' . $branch;
$result = $this->runCommandRemote($command) && $result;
$excludes = array_merge($excludes, $userExcludes);
$excludeCmd = '';
foreach ($excludes as $excludeFile) {
$excludeCmd .= ' --exclude=' . $excludeFile;
}
$command = 'cd ' . $remoteCacheFolder . ' && /usr/bin/env git archive ' . $branch . ' | tar -x -C ' . $deployToDirectory . ' ' . $excludeCmd;
$result = $this->runCommandRemote($command) && $result;
if ($result) {
$this->cleanUpReleases();
}
return $result;
}
}

View file

@ -1,139 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Deployment\Strategy;
use Mage\Console;
use Mage\Task\Releases\IsReleaseAware;
/**
* Task for Sync the Local Code to the Remote Hosts via RSYNC
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class RsyncTask extends BaseStrategyTaskAbstract implements IsReleaseAware
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
if ($this->getConfig()->release('enabled', false) === true) {
if ($this->getConfig()->getParameter('overrideRelease', false) === true) {
return 'Deploy via Rsync (with Releases override) [built-in]';
} else {
$rsync_copy = $this->getConfig()->deployment("rsync");
if ($rsync_copy && is_array($rsync_copy) && $rsync_copy['copy']) {
return 'Deploy via Rsync (with Releases) [built-in, incremental]';
} else {
return 'Deploy via Rsync (with Releases) [built-in]';
}
}
} else {
return 'Deploy via Rsync [built-in]';
}
}
/**
* Syncs the Local Code to the Remote Host
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$this->checkOverrideRelease();
$excludes = $this->getExcludes();
$excludesListFilePath = $this->getConfig()->deployment('excludes_file', '');
// If we are working with releases
$deployToDirectory = $this->getConfig()->deployment('to');
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$symlink = $this->getConfig()->release('symlink', 'current');
$currentRelease = false;
$deployToDirectory = rtrim($this->getConfig()->deployment('to'), '/')
. '/' . $releasesDirectory
. '/' . $this->getConfig()->getReleaseId();
Console::log('Deploy to ' . $deployToDirectory);
$resultFetch = $this->runCommandRemote('ls -ld ' . $symlink . ' | cut -d"/" -f2', $currentRelease);
if ($resultFetch && $currentRelease) {
// If deployment configuration is rsync, include a flag to simply sync the deltas between the prior release
// rsync: { copy: yes }
$rsync_copy = $this->getConfig()->deployment('rsync');
// If copy_tool_rsync, use rsync rather than cp for finer control of what is copied
if ($rsync_copy && is_array($rsync_copy) && $rsync_copy['copy'] && $this->runCommandRemote('test -d ' . $releasesDirectory . '/' . $currentRelease)) {
if (isset($rsync_copy['copy_tool_rsync'])) {
$this->runCommandRemote("rsync -a {$this->excludes(array_merge($excludes, $rsync_copy['rsync_excludes']))} "
. "$releasesDirectory/$currentRelease/ $releasesDirectory/{$this->getConfig()->getReleaseId()}");
} else {
$this->runCommandRemote('cp -R ' . $releasesDirectory . '/' . $currentRelease . ' ' . $releasesDirectory . '/' . $this->getConfig()->getReleaseId());
}
} else {
$this->runCommandRemote('mkdir -p ' . $releasesDirectory . '/' . $this->getConfig()->getReleaseId());
}
}
}
// Strategy Flags
$strategyFlags = $this->getConfig()->deployment('strategy_flags', $this->getConfig()->general('strategy_flags', array()));
if (isset($strategyFlags['rsync'])) {
$strategyFlags = $strategyFlags['rsync'];
} else {
$strategyFlags = '';
}
$command = 'rsync -avz '
. $strategyFlags . ' '
. '--rsh="ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no ' . $this->getConfig()->getHostIdentityFileOption() . '-p' . $this->getConfig()->getHostPort() . '" '
. $this->excludes($excludes) . ' '
. $this->excludesListFile($excludesListFilePath) . ' '
. $this->getConfig()->deployment('from') . ' '
. ($this->getConfig()->deployment('user') ? $this->getConfig()->deployment('user') . '@' : '')
. $this->getConfig()->getHostName() . ':' . $deployToDirectory;
$result = $this->runCommandLocal($command);
return $result;
}
/**
* Generates the Excludes for rsync
* @param array $excludes
* @return string
*/
protected function excludes(Array $excludes)
{
$excludesRsync = '';
foreach ($excludes as $exclude) {
$excludesRsync .= ' --exclude=' . escapeshellarg($exclude) . ' ';
}
$excludesRsync = trim($excludesRsync);
return $excludesRsync;
}
/**
* Generates the Exclude from file for rsync
* @param string $excludesFile
* @return string
*/
protected function excludesListFile($excludesFile)
{
$excludesListFileRsync = '';
if (!empty($excludesFile) && file_exists($excludesFile) && is_file($excludesFile) && is_readable($excludesFile)) {
$excludesListFileRsync = ' --exclude-from=' . $excludesFile;
}
return $excludesListFileRsync;
}
}

View file

@ -1,142 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Deployment\Strategy;
use Mage\Task\Releases\IsReleaseAware;
/**
* Task for Sync the Local Code to the Remote Hosts via Tar GZ
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class TarGzTask extends BaseStrategyTaskAbstract implements IsReleaseAware
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
if ($this->getConfig()->release('enabled', false) === true) {
if ($this->getConfig()->getParameter('overrideRelease', false) === true) {
return 'Deploy via TarGz (with Releases override) [built-in]';
} else {
return 'Deploy via TarGz (with Releases) [built-in]';
}
} else {
return 'Deploy via TarGz [built-in]';
}
}
/**
* Syncs the Local Code to the Remote Host
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$this->checkOverrideRelease();
$excludes = $this->getExcludes();
$excludesListFilePath = $this->getConfig()->deployment('excludes_file', '');
// If we are working with releases
$deployToDirectory = $this->getConfig()->deployment('to');
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$deployToDirectory = rtrim($this->getConfig()->deployment('to'), '/')
. '/' . $releasesDirectory
. '/' . $this->getConfig()->getReleaseId();
$output = null;
$this->runCommandRemote('mkdir -p ' . $deployToDirectory, $output, false);
}
// Create Tar Gz
$localTarGz = tempnam(sys_get_temp_dir(), 'mage');
$remoteTarGz = basename($localTarGz);
$excludeCmd = '';
foreach ($excludes as $excludeFile) {
if (strpos($excludeFile, '*') !== false) {
$excludeFile = '"' . $excludeFile . '"';
}
$excludeCmd .= ' --exclude=' . $excludeFile;
}
$excludeFromFileCmd = $this->excludesListFile($excludesListFilePath);
// Strategy Flags
$strategyFlags = $this->getConfig()->deployment('strategy_flags', $this->getConfig()->general('strategy_flags', array()));
if (isset($strategyFlags['targz']) && isset($strategyFlags['targz']['create'])) {
$strategyFlags = $strategyFlags['targz']['create'];
} else {
$strategyFlags = '';
}
// remove h option only if dump-symlinks is allowed in the release config part
$dumpSymlinks = $this->getConfig()->release('dump-symlinks') ? '' : 'h';
$command = 'tar cfz'. $dumpSymlinks . $strategyFlags . ' ' . $localTarGz . '.tar.gz ' . $excludeCmd . $excludeFromFileCmd . ' -C ' . $this->getConfig()->deployment('from') . ' .';
$result = $this->runCommandLocal($command);
// Strategy Flags
$strategyFlags = $this->getConfig()->deployment('strategy_flags', $this->getConfig()->general('strategy_flags', array()));
if (isset($strategyFlags['targz']) && isset($strategyFlags['targz']['exctract'])) {
$strategyFlags = $strategyFlags['targz']['exctract'];
} else {
$strategyFlags = '';
}
// Copy Tar Gz to Remote Host
$command = 'scp ' . $strategyFlags . ' ' . $this->getConfig()->getHostIdentityFileOption()
. $this->getConfig()->getConnectTimeoutOption() . '-P ' . $this->getConfig()->getHostPort()
. " -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no "
. ' ' . $localTarGz . '.tar.gz '
. $this->getConfig()->deployment('user') . '@' . $this->getConfig()->getHostName() . ':'
. $deployToDirectory;
$result = $this->runCommandLocal($command) && $result;
// Strategy Flags
$strategyFlags = $this->getConfig()->deployment('strategy_flags', $this->getConfig()->general('strategy_flags', array()));
if (isset($strategyFlags['targz']) && isset($strategyFlags['targz']['scp'])) {
$strategyFlags = $strategyFlags['targz']['scp'];
} else {
$strategyFlags = '';
}
// Extract Tar Gz
$command = $this->getReleasesAwareCommand('tar xfz' . $strategyFlags . ' ' . $remoteTarGz . '.tar.gz');
$result = $this->runCommandRemote($command) && $result;
// Delete Tar Gz from Remote Host
$command = $this->getReleasesAwareCommand('rm -f ' . $remoteTarGz . '.tar.gz');
$result = $this->runCommandRemote($command) && $result;
// Delete Tar Gz from Local
$command = 'rm -f ' . $localTarGz . ' ' . $localTarGz . '.tar.gz';
$result = $this->runCommandLocal($command) && $result;
return $result;
}
/**
* Generates the Exclude from file for rsync
* @param string $excludesFile
* @return string
*/
protected function excludesListFile($excludesFile)
{
$excludesListFileRsync = '';
if (!empty($excludesFile) && file_exists($excludesFile) && is_file($excludesFile) && is_readable($excludesFile)) {
$excludesListFileRsync = ' --exclude-from=' . $excludesFile;
}
return $excludesListFileRsync;
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Filesystem;
use Mage\Task\AbstractTask;
use Mage\Task\SkipException;
use Mage\Task\Releases\IsReleaseAware;
class ApplyFaclsTask extends AbstractTask implements IsReleaseAware
{
/**
* Returns the Title of the Task
* @return string
*/
public function getName()
{
return 'Set file ACLs on remote system [built-in]';
}
/**
* Runs the task
*
* @return boolean
* @throws SkipException
*/
public function run()
{
if ($this->getConfig()->release('enabled')) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$currentCopy = $releasesDirectory.'/'.$this->getConfig()->getReleaseId();
} else {
$currentCopy = $this->getConfig()->deployment('to', '.');
}
$aclParam = $this->getParameter('acl_param', '');
if (empty($aclParam)) {
throw new SkipException('Parameter acl_param not set.');
}
$folders = $this->getParameter('folders', array());
$recursive = $this->getParameter('recursive', false) ? ' -R ' : ' ';
foreach ($folders as $folder) {
$this->runCommandRemote("setfacl$recursive-m $aclParam $currentCopy/$folder", $output);
}
return true;
}
}

View file

@ -1,151 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Filesystem;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\IsReleaseAware;
use Mage\Task\SkipException;
/**
* Class LinkSharedFilesTask
*
* @package Mage\Task\BuiltIn\Filesystem
* @author Andrey Kolchenko <andrey@kolchenko.me>
*/
class LinkSharedFilesTask extends AbstractTask implements IsReleaseAware
{
/**
* Linked folders parameter name
*/
const LINKED_FILES = 'linked_files';
/**
* Linked folders parameter name
*/
const LINKED_FOLDERS = 'linked_folders';
/**
* Linking strategy parameter name
*/
const LINKED_STRATEGY = 'linking_strategy';
/**
* Absolute linked strategy
*/
const ABSOLUTE_LINKING = 'absolute';
/**
* Relative linked strategy
*/
const RELATIVE_LINKING = 'relative';
/**
* @var array
*/
private static $linkingStrategies = array(
self::ABSOLUTE_LINKING,
self::RELATIVE_LINKING
);
/**
* Returns the Title of the Task
*
* @return string
*/
public function getName()
{
return 'Linking files/folders from the shared folder into the current release [built-in]';
}
/**
* Runs the task
*
* @return boolean
* @throws SkipException
*/
public function run()
{
$linkedFiles = $this->getParameter(self::LINKED_FILES, array());
$linkedFolders = $this->getParameter(self::LINKED_FOLDERS, array());
$linkedEntities = array_merge($linkedFiles, $linkedFolders);
if (empty($linkedEntities)) {
throw new SkipException('No files and folders configured for sym-linking.');
}
$remoteDirectory = rtrim($this->getConfig()->deployment('to'), '/') . '/';
$sharedFolderPath = $remoteDirectory . $this->getParameter('shared', 'shared');
$releasesDirectoryPath = $remoteDirectory . $this->getConfig()->release('directory', 'releases');
$currentCopy = $releasesDirectoryPath . '/' . $this->getConfig()->getReleaseId();
foreach ($linkedEntities as $ePath) {
list($entityPath, $strategy) = $this->getPath($ePath);
if ($strategy === self::RELATIVE_LINKING) {
$dirName = dirname($currentCopy . '/' . $entityPath);
$target = $this->makePathRelative($sharedFolderPath, $dirName) . $entityPath;
} else {
$target = $sharedFolderPath . '/' . $entityPath;
}
$command = 'mkdir -p ' . escapeshellarg(in_array($ePath, $linkedFolders) ? $target : dirname($target));
$this->runCommandRemote($command);
$command = 'ln -nfs ' . escapeshellarg($target) . ' ' . escapeshellarg($currentCopy . '/' . $entityPath);
$this->runCommandRemote($command);
}
return true;
}
/**
* Given an existing path, convert it to a path relative to a given starting path
*
* @param string $endPath Absolute path of target
* @param string $startPath Absolute path where traversal begins
*
* @return string Path of target relative to starting path
*
* @author Fabien Potencier <fabien@symfony.com>
* @see https://github.com/symfony/Filesystem/blob/v2.6.1/Filesystem.php#L332
*/
private function makePathRelative($endPath, $startPath)
{
// Normalize separators on Windows
if (defined('PHP_WINDOWS_VERSION_MAJOR')) {
$endPath = strtr($endPath, '\\', '/');
$startPath = strtr($startPath, '\\', '/');
}
// Split the paths into arrays
$startPathArr = explode('/', trim($startPath, '/'));
$endPathArr = explode('/', trim($endPath, '/'));
// Find for which directory the common path stops
$index = 0;
while (isset($startPathArr[$index]) && isset($endPathArr[$index]) && $startPathArr[$index] === $endPathArr[$index]) {
$index++;
}
// Determine how deep the start path is relative to the common path (ie, "web/bundles" = 2 levels)
$depth = count($startPathArr) - $index;
// Repeated "../" for each level need to reach the common path
$traverser = str_repeat('../', $depth);
$endPathRemainder = implode('/', array_slice($endPathArr, $index));
// Construct $endPath from traversing to the common path, then to the remaining $endPath
$relativePath = $traverser . (strlen($endPathRemainder) > 0 ? $endPathRemainder . '/' : '');
return (strlen($relativePath) === 0) ? './' : $relativePath;
}
/**
* @param array|string $linkedEntity
*
* @return array [$path, $strategy]
*/
private function getPath($linkedEntity)
{
$linkingStrategy = $this->getParameter(self::LINKED_STRATEGY, self::ABSOLUTE_LINKING);
if (is_array($linkedEntity)) {
list($path, $strategy) = each($linkedEntity);
if (!in_array($strategy, self::$linkingStrategies)) {
$strategy = $linkingStrategy;
}
} else {
$strategy = $linkingStrategy;
$path = $linkedEntity;
}
return array($path, $strategy);
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Filesystem;
use Mage\Task\SkipException;
/**
* Task for giving only to web server read permissions on given paths.
*
* Usage :
* pre-deploy:
* - filesystem/permissions-readable-only-by-web-server: {paths: /var/www/myapp/app/config/config.yml:/var/www/myapp/app/config/parameters.yml, recursive: false, checkPathsExist: true}
* - filesystem/permissions-readable-only-by-web-server:
* paths:
* - /var/www/myapp/app/config/config.yml
* - /var/www/myapp/app/config/parameters.yml
* recursive: false
* checkPathsExist: true
* on-deploy:
* - filesystem/permissions-readable-only-by-web-server: {paths: app/config/config.yml:app/config/parameters.yml, recursive: false, checkPathsExist: true}
*
* @author Jérémy Huet <jeremy.huet@gmail.com>
*/
class PermissionsReadableOnlyByWebServerTask extends PermissionsTask
{
/**
* Set group with web server user and give group write permissions.
*/
public function init()
{
parent::init();
$this->setGroup($this->getParameter('group') ? $this->getParameter('group') : $this->getWebServerUser())
->setRights('040');
}
/**
* @return string
*/
public function getName()
{
return "Giving read permissions only to web server user for given paths [built-in]";
}
/**
* Tries to guess the web server user by going thru the running processes.
*
* @return string
* @throws SkipException
*/
protected function getWebServerUser()
{
$this->runCommand("ps aux | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1", $webServerUser);
if (empty($webServerUser)) {
throw new SkipException("Can't guess web server user. Please check if it is running or force it by setting the group parameter");
}
return $webServerUser;
}
}

View file

@ -1,324 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Filesystem;
use Mage\Task\AbstractTask;
use Mage\Task\SkipException;
/**
* Task for setting permissions on given paths. Change will be done on local or
* remote host depending on the stage of the deployment.
*
* Usage :
* pre-deploy:
* - filesystem/permissions: {paths: /var/www/myapp/app/cache:/var/www/myapp/app/cache, recursive: false, checkPathsExist: true, owner: www-data, group: www-data, rights: 775}
* - filesystem/permissions:
* paths:
* - /var/www/myapp/app/cache
* - /var/www/myapp/app/logs
* recursive: false
* checkPathsExist: true
* owner: www-data:www-data
* rights: 775
* on-deploy:
* - filesystem/permissions: {paths: app/cache:app/logs, recursive: false, checkPathsExist: true, owner: www-data, group: www-data, rights: 775}
*
* @author Jérémy Huet <jeremy.huet@gmail.com>
*/
class PermissionsTask extends AbstractTask
{
/**
* Paths to change of permissions in an array or a string separated by
* PATH_SEPARATOR.
*
* If the stage is on local host you should give full paths. If on remote
* you may give full or relative to the current release directory paths.
*
* @var string
*/
private $paths;
/**
* If set to true, will check existance of given paths on the host and
* throw SkipException if at least one does not exist.
*
* @var boolean
*/
private $checkPathsExist = true;
/**
* Owner to set for the given paths (ex : "www-data" or "www-data:www-data"
* to set both owner and group at the same time)
*
* @var string
*/
private $owner;
/**
* Group to set for the given paths (ex : "www-data")
*
* @var string
*/
private $group;
/**
* Rights to set for the given paths (ex: "755" or "g+w")
*
* @var string
*/
private $rights;
/**
* If set to true, will recursively change permissions on given paths.
*
* @var string
*/
private $recursive = false;
/**
* Initialize parameters.
*
* @throws SkipException
*/
public function init()
{
parent::init();
if (! is_null($this->getParameter('checkPathsExist'))) {
$this->setCheckPathsExist($this->getParameter('checkPathsExist'));
}
if (! $this->getParameter('paths')) {
throw new SkipException('Param paths is mandatory');
}
$this->setPaths(is_array($this->getParameter('paths')) ? $this->getParameter('paths') : explode(PATH_SEPARATOR, $this->getParameter('paths', '')));
if (! is_null($owner = $this->getParameter('owner'))) {
if (strpos($owner, ':') !== false) {
$this->setOwner(array_shift(explode(':', $owner)));
$this->setGroup(array_pop(explode(':', $owner)));
} else {
$this->setOwner($owner);
}
}
if (! is_null($group = $this->getParameter('group'))) {
$this->setGroup($group);
}
if (! is_null($rights = $this->getParameter('rights'))) {
$this->setRights($rights);
}
if (! is_null($recursive = $this->getParameter('recursive'))) {
$this->setRecursive($recursive);
}
}
/**
* @return string
*/
public function getName()
{
return "Changing rights / owner / group for given paths [built-in]";
}
/**
* @return boolean
*/
public function run()
{
$commands = array();
if ($this->paths && $this->owner) {
$commands []= 'chown '. $this->getOptionsForCmd() .' ' . $this->owner . ' ' . $this->getPathsForCmd();
}
if ($this->paths && $this->group) {
$commands []= 'chgrp '. $this->getOptionsForCmd() .' ' . $this->group . ' ' . $this->getPathsForCmd();
}
if ($this->paths && $this->rights) {
$commands []= 'chmod '. $this->getOptionsForCmd() .' ' . $this->rights . ' ' . $this->getPathsForCmd();
}
$result = $this->runCommand(implode(' && ', $commands));
return $result;
}
/**
* Returns the options for the commands to run. Only supports -R for now.
*
* @return string
*/
protected function getOptionsForCmd()
{
$optionsForCmd = '';
$options = array(
'R' => $this->recursive
);
foreach ($options as $option => $apply) {
if ($apply === true) {
$optionsForCmd .= $option;
}
}
return $optionsForCmd ? '-' . $optionsForCmd : '';
}
/**
* Transforms paths array to a string separated by 1 space in order to use
* it in a command line.
*
* @return string
*/
protected function getPathsForCmd($paths = null)
{
if (is_null($paths)) {
$paths = $this->paths;
}
return implode(' ', $paths);
}
/**
* Set paths. Will check if they exist on the host depending on
* checkPathsExist flag.
*
* @param array $paths
* @return PermissionsTask
* @throws SkipException
*/
protected function setPaths(array $paths)
{
if ($this->checkPathsExist === true) {
$commands = array();
foreach ($paths as $path) {
$commands[] = '(([ -f ' . $path . ' ]) || ([ -d ' . $path . ' ]))';
}
$command = implode(' && ', $commands);
if (! $this->runCommand($command)) {
throw new SkipException('Make sure all paths given exist on the host : ' . $this->getPathsForCmd($paths));
}
}
$this->paths = $paths;
return $this;
}
/**
* @return string
*/
protected function getPaths()
{
return $this->paths;
}
/**
* Set checkPathsExist.
*
* @param boolean $checkPathsExist
* @return PermissionsTask
*/
protected function setCheckPathsExist($checkPathsExist)
{
$this->checkPathsExist = (bool) $checkPathsExist;
return $this;
}
/**
* @return boolean
*/
protected function getCheckPathsExist()
{
return $this->checkPathsExist;
}
/**
* Set owner.
*
* @param string $owner
* @return PermissionsTask
*/
protected function setOwner($owner)
{
$this->owner = $owner;
return $this;
}
/**
* @return string
*/
protected function getOwner()
{
return $this->owner;
}
/**
* Set group.
*
* @param string $group
* @return PermissionsTask
*/
protected function setGroup($group)
{
$this->group = $group;
return $this;
}
/**
* @return string
*/
protected function getGroup()
{
return $this->group;
}
/**
* Set rights.
*
* @param string $rights
* @return PermissionsTask
*/
protected function setRights($rights)
{
$this->rights = $rights;
return $this;
}
/**
* @return string
*/
protected function getRights()
{
return $this->rights;
}
/**
* Set recursive.
*
* @param boolean $recursive
* @return PermissionsTask
*/
protected function setRecursive($recursive)
{
$this->recursive = (bool) $recursive;
return $this;
}
/**
* @return boolean
*/
protected function getRecursive()
{
return $this->recursive;
}
}

View file

@ -1,60 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Filesystem;
use Mage\Task\SkipException;
/**
* Task for giving web server write permissions on given paths.
*
* Usage :
* pre-deploy:
* - filesystem/permissions-writable-by-web-server: {paths: /var/www/myapp/app/cache:/var/www/myapp/app/logs, recursive: false, checkPathsExist: true}
* - filesystem/permissions-writable-by-web-server:
* paths:
* - /var/www/myapp/app/cache
* - /var/www/myapp/app/logs
* recursive: false
* checkPathsExist: true
* on-deploy:
* - filesystem/permissions-writable-by-web-server: {paths: app/cache:app/logs, recursive: false, checkPathsExist: true}
*
* @author Jérémy Huet <jeremy.huet@gmail.com>
*/
class PermissionsWritableByWebServerTask extends PermissionsTask
{
/**
* Set group with web server user and give group write permissions.
*/
public function init()
{
parent::init();
$this->setGroup($this->getParameter('group') ? $this->getParameter('group') : $this->getWebServerUser())
->setRights('g+w');
}
/**
* @return string
*/
public function getName()
{
return "Giving write permissions to web server user for given paths [built-in]";
}
/**
* Tries to guess the web server user by going thru the running processes.
*
* @return string
* @throws SkipException
*/
protected function getWebServerUser()
{
$this->runCommand("ps aux | grep -E '[a]pache|[h]ttpd|[_]www|[w]ww-data|[n]ginx' | grep -v root | head -1 | cut -d\ -f1", $webServerUser);
if (empty($webServerUser)) {
throw new SkipException("Can't guess web server user. Please check if it is running or force it by setting the group parameter");
}
return $webServerUser;
}
}

View file

@ -1,139 +0,0 @@
<?php
namespace Mage\Task\BuiltIn\Filesystem;
use Mage\Task\AbstractTask;
use Mage\Config\RequiredConfigNotFoundException;
/**
* Task for creating a symbolic link. Change will be done on local or
* remote host depending on the stage of the deployment.
*
* Usage :
* pre-deploy:
* - filesystem/symlink: {link:/path/to/symlink, target: /path/to/target}
* on-deploy:
* - filesystem/symlink: {link:/path/to/symlink, target: /path/to/target}
*
* @author Jérémy Huet <jeremy.huet@gmail.com>
*/
class SymlinkTask extends AbstractTask
{
/**
* The name of the symlink including full path to it.
*
* If the stage is on local host you should give full paths. If on remote
* you may give full or relative to the current release directory paths.
*
* @var string
*/
private $link;
/**
* The target to wich the symlink should point to including full path to it.
*
* If the stage is on local host you should give full paths. If on remote
* you may give full or relative to the current release directory paths.
*
* @var string
*/
private $target;
/**
* Initialize parameters.
*
* @throws RequiredConfigNotFoundException
*/
public function init()
{
parent::init();
if (! $this->getParameter('target')) {
throw new RequiredConfigNotFoundException('Missing required target link.');
}
$this->setTarget($this->getParameter('target'));
if (!is_null($this->getParameter('link'))) {
$this->setLink($this->getParameter('link'));
}
}
/**
* @return string
*/
public function getName()
{
return "Creating symbolic link [built-in]";
}
/**
* @return boolean
*/
public function run()
{
$command = 'ln -fs ' . $this->getAbsolutPath($this->getTarget());
if ($this->getLink()) {
$command .= ' ' . $this->getAbsolutPath($this->getLink());
}
$result = $this->runCommand($command);
return $result;
}
/**
* @param string $path
* @return string
*/
public function getAbsolutPath($path)
{
// For release
if ($this->getStage() != 'pre-deploy' && $path[0] != '/' && $this->getConfig()->deployment('to')) {
$releasesDirectory = trim($this->getConfig()->release('directory', 'releases'), '/') . '/' . $this->getConfig()->getReleaseId();
return rtrim($this->getConfig()->deployment('to'), '/') . '/' . $releasesDirectory . '/' . ltrim($path, '/');
}
return $path;
}
/**
* Set link.
*
* @param string $link
* @return SymlinkTask
*/
protected function setLink($link)
{
$this->link = $link;
return $this;
}
/**
* @return string
*/
protected function getLink()
{
return $this->link;
}
/**
* Set target.
*
* @param string $target
* @return SymlinkTask
*/
protected function setTarget($target)
{
$this->target = $target;
return $this;
}
/**
* @return string
*/
protected function getTarget()
{
return $this->target;
}
}

View file

@ -1,59 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\General;
use Mage\Task\AbstractTask;
/**
* Task for running multiple custom commands setting them manually
*
* Example of usage:
*
* tasks:
* on-deploy:
* - scm/force-update
* - general/manually:
* - find . -type d -exec chmod 755 {} \;
* - find . -type f -exec chmod 644 {} \;
* - chmod +x bin/console
* - find var/logs -maxdepth 1 -type f -name '*.log' -exec chown apache:apache {} \;
* - symfony2/cache-clear
*
* @author Samuel Chiriluta <samuel4x4@gmail.com>
*/
class ManuallyTask extends AbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Manually multiple custom tasks';
}
/**
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$result = true;
$commands = $this->getParameters();
foreach ($commands as $command) {
$result = $result && $this->runCommand($command);
}
return $result;
}
}

View file

@ -1,810 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Ioncube;
use Mage\Task\AbstractTask;
use Mage\Console;
use Mage\Task\ErrorWithMessageException;
/**
* This allows intergrating IonCube PHP
* encoder into deployment system
* It takes the source path renames
* it to .raw creates a fresh source
* path and runs ioncube encoder placing
* encoded files into source folder.
* Afterwards it removes the old .raw
* folder This means that we dont have
* to change the source path within the
* main scripts and allows the built
* in rsync and other tasks to operate
* on the encrypted files.
*
* IonCube PHP Encoder can be downloaded from
* http://www.actweb.info/ioncube.html
*
* Example enviroment.yaml file at end
*
*
* (c) ActWeb 2013
* (c) Matt Lowe (marl.scot.1@googlemail.com)
*
* Extends Magallanes (c) Andrés Montañez <andres@andresmontanez.com>
*
*/
class EncryptTask extends AbstractTask
{
/**
* Name of the task
*
* @var string
*/
private $name = 'IonCube Encoder';
/**
* Array of default Ioncube
* options
*
* @var array
*/
private $default = array();
/**
* Array of YAML Ioncube
* options
*
* @var array
*/
private $yaml = array();
/**
* Array of file Ioncube
* options (taken from additional
* external config file if supplied)
*
* @var array
*/
private $file = array();
/**
* Source directory as used by
* main scripts
*
* @var string
*/
private $source = '';
/**
* Name of tempory folder
* for source code to be moved
* to.
*
* @var string
*/
private $ionSource = '';
/**
* How the default/yaml/project
* files interact with each other
*
* @var string
*/
private $ionOverRide = '';
/**
* Config options from the
* enviroment config file
*
* @var array
*/
private $mageConfig = array();
/**
* Final version of the IonCube
* options, after merging all
* sources together
*
* @var array
*/
private $ionCubeConfig = array();
/**
* Default encoder version to use
* for the ioncube encoder
*
* @var string
*/
private $encoder = 'ioncube_encoder54';
/**
* Name of tempory IonCube Project
* file, used when running encoder
*
* @var string
*/
private $projectFile = '';
/**
* If true then run a check on
* all files after encoding and
* report which ones are not encoded
* if any are found to not be encoded
* then prompt if we should continue
* with the process
* If not then clean up the temp files
* and exit with cancled code.
*
* @var bool
*/
private $checkEncoding = false;
/**
* List of file extensions to exclude
* from encrypted/encoded test
*
* @var array
*/
private $checkIgnoreExtens = array();
/**
* List of paths to exclude from
* encrypted/encoded test
* Paths must begin with '/'
* and are all relative to the
* project root
*
* @var array
*/
private $checkIgnorePaths = array();
/**
* (non-PHPdoc)
*
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return $this->name;
}
/**
* (non-PHPdoc)
*
* @see \Mage\Task\AbstractTask::init()
*/
public function init()
{
// Set the default extensions to ignore
$this->checkIgnoreExtens = array(
'jpg',
'jpeg',
'png',
'js',
'gif',
'css',
'ttf',
'svg',
'map',
'ico',
);
// Get any options specfic to this task
$this->mageConfig = $this->getConfig()->environmentConfig('ioncube');
/*
* Get all our IonCube config options
*/
$this->getAllIonCubeConfigs();
/*
* get the source code location
*/
$this->source = $this->getConfig()->deployment('from');
/*
* remove trailing slash if present
*/
if (substr($this->source, -1) == DIRECTORY_SEPARATOR) {
$this->source = substr($this->source, 0, -1);
}
/*
* Set the name of the folder that the un-encrypted
* files will be moved into
*/
$this->ionSource = $this->source . '.raw';
/*
* set the filename for the ioncube project build file
*/
$this->projectFile = $this->source . '.prj';
/*
* Check if we have been given an encoder script
* If not then we will just use the default
*/
if (isset($this->mageConfig ['encoder'])) {
$this->encoder = $this->mageConfig ['encoder'];
}
/*
* Check if a differant merge type has been
* supplied, this defines how the 3 differant
* config files will be merged together.
*/
if (isset($this->mageConfig ['override'])) {
$this->ionOverRide = $this->mageConfig ['override'];
}
/*
* Check if we have been asked to
* confirm all encodings
*/
if (isset($this->mageConfig ['checkencoding'])) {
$this->checkEncoding = true;
}
/*
* Check if we have been passed any extra
* file extensions to exclude from
* encrypt/encode file check
*
*/
if (isset($this->mageConfig ['checkignoreextens'])) {
$this->checkIgnoreExtens = array_merge($this->checkIgnoreExtens, $this->mageConfig['checkignoreextens']);
}
/*
* Check if we have been passed any extra
* file paths/files to exclude from
* encrypt/encode file check
*
*/
if (isset($this->mageConfig ['checkignorepaths'])) {
$this->checkIgnorePaths = array_merge($this->checkIgnorePaths, $this->mageConfig['checkignorepaths']);
}
/*
* now merge all the config options together
*/
$this->ionCubeConfig = $this->mergeConfigFiles();
}
/**
* This gets all the Ioncube configs
* Basicly it gets the default options contianed within this script
* It reads any project options from the enviroment.yaml config file
* It reads any additional options from external project file if set
*
* @return void
*/
private function getAllIonCubeConfigs()
{
/*
* Get a set of default IonCube options
*/
$this->default = $this->getOptionsDefault();
/*
* Check if there is a 'project' section,
* if so then get the options from there
*/
if (isset($this->mageConfig ['project'])) {
$this->yaml = $this->getOptionsFromYaml($this->mageConfig ['project']);
} else {
$this->yaml = array(
's' => array(),
'p' => array()
);
}
/*
* Check if a seperate projectfile has been specified, and if so
* then read the options from there.
*/
if (isset($this->mageConfig ['projectfile'])) {
$this->file = $this->getOptionsFromFile($this->mageConfig ['projectfile']);
} else {
$this->file = array(
's' => array(),
'p' => array()
);
}
}
/**
* Encrypt the project
* Steps are as follows :
* Switch our current source dir to the ioncube srouce dir and create new empty dir to encrypt into
* Write the IonCube project file (this is the file that controls IonCube encoder)
* Run IonCube encoder
* Delete the temporary files that we created (so long as we hadn't set 'keeptemp')
* Return the result of the IonCube encoder
*
* @see \Mage\Task\AbstractTask::run()
*
* @return bool
* @throws \Mage\Task\ErrorWithMessageException
*/
public function run()
{
$this->switchSrcToTmp();
$this->writeProjectFile();
$result = $this->runIonCube();
Console::output("Encoding result :" . ($result ? '<green>OK</green>' : '<red>Bad!</red>') . "\n", 0, 2);
if (!$result) {
$this->deleteTmpFiles();
throw new ErrorWithMessageException('Ioncube failed to encode your project :' . $result);
}
if (($this->checkEncoding) && (!$this->checkEncoding())) {
$this->deleteTmpFiles();
throw new ErrorWithMessageException('Operation canceled by user.');
}
$this->deleteTmpFiles();
return $result;
}
/**
* Runs through all files in the encoded
* folders and lists any that are not
* encoded. If any are found then prompt
* user to continue or quit.
* If user quites, then clean out encoded
* files, and return true to indicate error
*
* @return bool
*/
private function checkEncoding()
{
$src = $this->source;
// $ask holds flag to indicate we need to prompt the end user
$ask = false;
// scan through the directory
$rit = new \RecursiveDirectoryIterator($src);
foreach (new \RecursiveIteratorIterator($rit) as $filename => $cur) {
// get the 'base dir' for the project, eg. relative to the temp folder
$srcFileName = (str_replace($this->source, '', $filename));
/*
* Scan through the ignor directorys array
* and if it matches the current path/filename
* then mark the file to be skipped from testing
*/
$skip = false;
foreach ($this->checkIgnorePaths as $path) {
if (fnmatch($path, $srcFileName)) {
$skip = true;
}
}
// check if we should test this file
if (!$skip) {
// get the file exten for this file and compare to our fileexten exclude array
$exten = pathinfo($filename, PATHINFO_EXTENSION);
if (!in_array(strtolower($exten), $this->checkIgnoreExtens)) {
// ok, this extension needs to be checked
if ($this->checkFileCoding($filename)) {
// file was encrypted/encoded
} else {
// file was not encrypted/encoded
Console::output("<blue>File :" . $srcFileName . "</blue> -> <red>Was not encrypted</red>", 0, 1);
$ask = true;
}
}
}
}
if ($ask) {
// ok lets ask the user if they want to procede
Console::output("\n\nDo you wish to procede (y/N):", 0, 0);
if (!$this->promptYn()) {
return false;
}
}
return true;
}
/**
* This simply for user to enter
* 'y' or 'Y' and press enter, if
* a single 'y' is not entered then
* false is returned, otherwise
* true is returned.
*
* @return bool True if 'y' pressed
*/
private function promptYn()
{
$handle = fopen("php://stdin", "r");
$line = strtolower(fgets($handle));
if (trim($line) != 'y') {
return false;
}
return true;
}
/**
* This will take the passed file and try to
* work out if it is an encoded/encrypted
* ioncube file.
* It dosent test the file exten, as it
* expects the calling method to have done
* that before.
*
* @param string $filename Filename, with path, to check
*
* @return boolean True if file was encoded/encrypted
*/
private function checkFileCoding($filename)
{
// check to see if this is an encrypted file
$ioncube = ioncube_read_file($filename);
if (is_int($ioncube)) {
// we got an error from ioncube, so its encrypted
return true;
}
// read first line of file
$f = fopen($filename, 'r');
$line = trim(fgets($f, 32));
fclose($f);
// if first line is longer than 30, then this isnt a php file
if (strlen($line) > 30) {
return false;
}
// if first line starts '<?php //0' then we can be pretty certain its encoded
if (substr($line, 0, 9) == '<?php //0') {
return true;
}
// otherwise its most likley un-encrypted/encoded
return false;
}
/**
* Deletes tempory folder and project file
* if 'keeptemp' is set then skips delete
* process
*
* @throws ErrorWithMessageException If there was a problem with deleting the tempory files
*
* @return void
*/
private function deleteTmpFiles()
{
if (isset($this->mageConfig ['keeptemp'])) {
return;
}
Console::log('Deleting tempory files :');
$ret1 = Console::executeCommand('rm -Rf ' . $this->ionSource, $out1);
$ret2 = Console::executeCommand('rm ' . $this->projectFile, $out2);
if ($ret1 && $ret2) {
return;
}
throw new ErrorWithMessageException('Error deleting temp files :' . $out1 . ' : ' . $out2, 40);
}
/**
* Builds the ioncube command
* and runs it, returning the result
*
* @return bool
*/
private function runIonCube()
{
$cli = $this->encoder . ' --project-file ' . $this->projectFile . ' ' . $this->ionSource . DIRECTORY_SEPARATOR . '*';
$ret = Console::executeCommand($cli, $out);
return $ret;
}
/**
* Write the config options into
* a project file ready for use
* with ioncube cli
*
* @throws ErrorWithMessageException If it cant write the project file
*
* @return void
*/
private function writeProjectFile()
{
// array used to build config file into
$out = array();
// set the project destination
$out [] = '--into ' . $this->source . PHP_EOL;
// output the switches
foreach ($this->ionCubeConfig ['s'] as $key => $value) {
if ($value) {
// switch was set to true, so output it
$out [] = '--' . $key . PHP_EOL;
}
}
// output the options
foreach ($this->ionCubeConfig ['p'] as $key => $value) {
// check if we have an array of values
if (is_array($value)) {
foreach ($value as $entry) {
$out [] = '--' . $key . ' "' . $entry . '"' . PHP_EOL;
}
} else {
// ok just a normal single option
if (strlen($value) > 0) {
$out [] = '--' . $key . ' "' . $value . '"' . PHP_EOL;
}
}
}
$ret = file_put_contents($this->projectFile, $out);
if (!$ret) {
// something went wrong
$this->deleteTmpFiles();
throw new ErrorWithMessageException('Unable to create project file.', 20);
}
}
/**
* This merges the 3 config arrays
* depending on the 'override' option
*
* @return array Final config array
*/
private function mergeConfigFiles()
{
/*
* Options are the order the arrays are in
* F - Project File
* Y - YAML config options (enviroment file)
* D - Default options as stored in script
*
* more options could be added to make this a bit more flexable
*
*/
$s = array();
$p = array();
switch (strtolower($this->ionOverRide)) {
case 'fyd' :
// FILE / YAML / DEFAULT
$s = array_merge($this->file ['s'], $this->yaml ['s'], $this->default ['s']);
$p = array_merge($this->file ['p'], $this->yaml ['p'], $this->default ['p']);
break;
case 'yfd' :
// YAML / FILE / DEFAULT
$s = array_merge($this->yaml ['s'], $this->file ['s'], $this->default ['s']);
$p = array_merge($this->yaml ['p'], $this->file ['p'], $this->default ['p']);
break;
case 'dyf' :
// DEFAULT / YAML / FILE
$s = array_merge($this->default ['s'], $this->yaml ['s'], $this->file ['s']);
$p = array_merge($this->default ['p'], $this->yaml ['p'], $this->file ['p']);
break;
case 'd' :
default :
// Use defaults only
$s = $this->default ['s'];
$p = $this->default ['p'];
break;
}
return array(
's' => $s,
'p' => $p
);
}
/**
* Switches the original source
* code dir to tempory name
* and recreates orginal dir
* allows encryption to be done
* into source dir, so other functions
* work without changing
*
* @throws ErrorWithMessageException If source dir can't be renamed
* @throws ErrorWithMessageException If original source dir cant be created
*
* @return bool
*/
private function switchSrcToTmp()
{
$ret = Console::executeCommand('mv ' . $this->source . ' ' . $this->ionSource, $out);
if (!$ret) {
throw new ErrorWithMessageException('Cant create tmp dir :' . $out, $ret);
}
$ret = Console::executeCommand('mkdir -p ' . $this->source, $out);
if (!$ret) {
throw new ErrorWithMessageException('Cant re-create dir :' . $out, $ret);
}
return true;
}
/**
* Reads a set of options taken from the YAML config
* Compares keys against the default ioncube settings
* if a key doesnt appear in the default options, it
* is ignored
*
* return array
*/
private function getOptionsFromYaml($options)
{
$s = array();
$p = array();
foreach ($options as $key => $value) {
if (array_key_exists($key, $this->default ['s'])) {
$s [$key] = true;
}
if (array_key_exists($key, $this->default ['p'])) {
$p [$key] = $value;
}
}
return array(
's' => $s,
'p' => $p
);
}
/**
* reads an existing ioncube project
* file.
*
* @param $fileName
* @return array
*/
private function getOptionsFromFile($fileName)
{
$s = array();
$p = array();
$fileContents = file_get_contents($fileName);
/*
* split the config file on every occurance of '--' at start of a line
* Adds a PHP_EOL at the start, so we can catch the first '--'
*/
$entrys = explode(PHP_EOL . '--', PHP_EOL . $fileContents);
foreach ($entrys as $line) {
$line = trim($line);
if ($line != '') {
/*
* get position of first space
* so we can split the options out
*/
$str = strpos($line, ' ');
if ($str === false) {
/*
* Ok, no spaces found, so take this as a single line
*/
$str = strlen($line);
}
$key = substr($line, $str);
$value = substr($line, $str + 1);
if ((array_key_exists($key, $this->default ['s']))) {
/*
* ok this key appears in the switch config
* so store it as a switch
*/
$s [$key] = true;
}
if ((array_key_exists($key, $this->default ['p']))) {
/*
* Ok this key exists in the parameter section,
* So store it allong with its value
*/
$p [$key] = $this->splitParam($value);
}
}
}
return array(
's' => $s,
'p' => $p
);
}
/**
* Takes supplied line and splits it if required
* into an array
* returns ether the array, or a plain
* string.
* Allows options to be spread accross several lines
*
* @param $string String to split
*
* @return mixed
*/
private function splitParam($string)
{
$split = explode(PHP_EOL, $string);
if ($split === false) {
// nothing found, so return a blank string
return '';
}
if (count($split) == 1) {
return $split [0];
} else {
return $split;
}
}
/**
* returns an array of default ioncube options
* This is also used as a 'filter' for the YAML
* and Config files, if an option hasnt got an
* entry in this list, then it can not be set
* via the VAML or Config files
*
* @return array
*/
private function getOptionsDefault()
{
$s = array();
$p = array();
// Set the switches
$s ['allow-encoding-into-source'] = false;
$s ['ascii'] = false;
$s ['binary'] = true;
$s ['replace-target'] = true;
$s ['merge-target'] = false;
$s ['rename-target'] = false;
$s ['update-target'] = false;
$s ['only-include-encoded-files'] = false;
$s ['use-hard-links'] = false;
$s ['without-keeping-file-perms'] = false;
$s ['without-keeping-file-times'] = false;
$s ['without-keeping-file-owner'] = false;
$s ['no-short-open-tags'] = false;
$s ['ignore-strict-warnings'] = false;
$s ['ignore-deprecated-warnings'] = false;
$s ['without-runtime-loader-support'] = false;
$s ['without-loader-check'] = false;
$s ['disable-auto-prepend-append'] = true;
$s ['no-doc-comments'] = true;
// Now set the params
$p ['encrypt'] [] = '*.tpl.html';
$p ['encode'] = array();
$p ['copy'] = array();
$p ['ignore'] = array(
'.git',
'.svn',
getcwd() . '/.mage',
'.gitignore',
'.gitkeep',
'nohup.out'
);
$p ['keep'] = array();
$p ['obfuscate'] = '';
$p ['obfuscation-key'] = '';
$p ['obfuscation-exclusion-file'] = '';
$p ['expire-in'] = '7d';
$p ['expire-on'] = '';
$p ['allowed-server'] = '';
$p ['with-license'] = 'license.txt';
$p ['passphrase'] = '';
$p ['license-check'] = '';
$p ['apply-file-user'] = '';
$p ['apply-file-group'] = '';
$p ['register-autoglobal'] = array();
$p ['message-if-no-loader'] = '';
$p ['action-if-no-loader'] = '';
$p ['loader-path'] = '';
$p ['preamble-file'] = '';
$p ['add-comment'] = array();
$p ['add-comments'] = '';
$p ['loader-event'] = array();
$p ['callback-file'] = '';
$p ['property'] = '';
$p ['propertys'] = '';
$p ['include-if-property'] = array();
$p ['optimise'] = 'max';
$p ['shell-script-line'] = '';
$p ['min-loader-version'] = '';
return array(
's' => $s,
'p' => $p
);
}
}

View file

@ -1,42 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Magento;
use Mage\Task\AbstractTask;
/**
* Task for Clearing Cache
*
* @author Oscar Reales <oreales@gmail.com>
*/
class ClearCacheTask extends AbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Magento - Clean Cache [built-in]';
}
/**
* Clears Cache
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$command = 'rm -rf var/cache/*';
$result = $this->runCommand($command);
return $result;
}
}

View file

@ -1,42 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Magento;
use Mage\Task\AbstractTask;
/**
* Task for Clearing Full Page Cache
*
* @author Oscar Reales <oreales@gmail.com>
*/
class ClearFullPageCacheTask extends AbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Magento - Clean Full Page Cache [built-in]';
}
/**
* Clears Full Page Cache
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$command = 'rm -rf var/full_page_cache/*';
$result = $this->runCommand($command);
return $result;
}
}

View file

@ -1,143 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Releases;
use Mage\Console;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\IsReleaseAware;
use DateTime;
/**
* Task for Listing Available Releases on an Environment
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class ListTask extends AbstractTask implements IsReleaseAware
{
public function getName()
{
return 'Listing releases [built-in]';
}
/**
* List the Available Releases on an Environment
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$symlink = $this->getConfig()->release('symlink', 'current');
Console::output('Releases available on <bold>' . $this->getConfig()->getHost() . '</bold>');
// Get Releases
$output = '';
$result = $this->runCommandRemote('ls -1 ' . $releasesDirectory, $output);
$releases = ($output == '') ? array() : explode(PHP_EOL, $output);
// Get Current
$result = $this->runCommandRemote('ls -l ' . $symlink, $output) && $result;
$currentRelease = explode('/', $output);
$currentRelease = trim(array_pop($currentRelease));
if (count($releases) == 0) {
Console::output('<bold>No releases available</bold> ... ', 2);
} else {
rsort($releases);
$releases = array_slice($releases, 0, 10);
foreach ($releases as $releaseIndex => $release) {
$release = trim($release);
$releaseIndex = str_pad($releaseIndex * -1, 2, ' ', STR_PAD_LEFT);
$releaseDate = $release[0] . $release[1] . $release[2] . $release[3]
. '-'
. $release[4] . $release[5]
. '-'
. $release[6] . $release[7]
. ' '
. $release[8] . $release[9]
. ':'
. $release[10] . $release[11]
. ':'
. $release[12] . $release[13];
$isCurrent = '';
if ($currentRelease == $release) {
$isCurrent = ' <- current';
}
$dateDiff = $this->dateDiff($releaseDate);
Console::output(
'Release: <purple>' . $release . '</purple> '
. '- Date: <bold>' . $releaseDate . '</bold> '
. '- Index: <bold>' . $releaseIndex . '</bold>' . $dateDiff . $isCurrent, 2);
}
}
Console::output('');
return $result;
} else {
Console::output('');
return false;
}
}
/**
* Calculates a Human Readable Time Difference
* @param string $releaseDate
* @return string
*/
protected function dateDiff($releaseDate)
{
$textDiff = '';
$releaseDate = new DateTime($releaseDate);
$now = new DateTime();
$diff = $now->diff($releaseDate);
if ($diff->format('%a') <= 7) {
if ($diff->format('%d') == 7) {
$textDiff = ' [a week ago] ';
} elseif ($diff->format('%d') > 0 && $diff->format('%d') < 7) {
$days = $diff->format('%d');
if ($days <= 1) {
$textDiff = ' [one day ago] ';
} else {
$textDiff = ' [' . $days . ' days ago] ';
}
} elseif ($diff->format('%d') == 0 && $diff->format('%h') > 0) {
$hours = $diff->format('%h');
if ($hours <= 1) {
$textDiff = ' [one hour ago] ';
} else {
$textDiff = ' [' . $hours . ' hours ago] ';
}
} elseif ($diff->format('%d') == 0 && $diff->format('%h') == 0) {
$minutes = $diff->format('%i');
if ($minutes <= 1) {
$textDiff = ' [one minute ago] ';
} else {
$textDiff = ' [' . $minutes . ' minutes ago] ';
}
} elseif ($diff->format('%d') == 0 && $diff->format('%h') == 0 && $diff->format('%i') == 0) {
$seconds = $diff->format('%s');
if ($seconds < 10) {
$textDiff = ' [just now!] ';
} else {
$textDiff = ' [' . $seconds . ' seconds ago] ';
}
}
}
return $textDiff;
}
}

View file

@ -1,197 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Releases;
use Mage\Console;
use Mage\Task\Factory;
use Mage\Task\AbstractTask;
use Mage\Task\Releases\IsReleaseAware;
use Mage\Task\Releases\RollbackAware;
/**
* Task for Performing a Rollback Operation
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class RollbackTask extends AbstractTask implements IsReleaseAware
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Rollback release [built-in]';
}
/**
* Gets the Release ID to Rollback To
* @return integer
*/
public function getReleaseId()
{
return $this->getConfig()->getReleaseId();
}
/**
* Performs a Rollback Operation
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
if ($this->getConfig()->release('enabled', false) === true) {
$releasesDirectory = $this->getConfig()->release('directory', 'releases');
$symlink = $this->getConfig()->release('symlink', 'current');
if (substr($symlink, 0, 1) == '/') {
$releasesDirectory = rtrim($this->getConfig()->deployment('to'), '/') . '/' . $releasesDirectory;
}
$output = '';
$result = $this->runCommandRemote('ls -1 ' . $releasesDirectory, $output);
$releases = ($output == '') ? array() : explode(PHP_EOL, $output);
if (count($releases) == 0) {
Console::output('Release are not available for <bold>' . $this->getConfig()->getHost() . '</bold> ... <red>FAIL</red>');
} else {
rsort($releases);
$deleteCurrent = $this->getConfig()->getParameter('deleteCurrent',
$this->getConfig()->deployment('delete-on-rollback',
$this->getConfig()->general('delete-on-rollback', false)
)
);
$releaseIsAvailable = false;
if ($this->getReleaseId() == '') {
$releaseId = $releases[0];
$releaseIsAvailable = true;
} elseif ($this->getReleaseId() <= 0) {
$index = $this->getReleaseId() * -1;
if (isset($releases[$index])) {
$releaseId = $releases[$index];
$releaseIsAvailable = true;
}
} else {
if (in_array($this->getReleaseId(), $releases)) {
$releaseId = $this->getReleaseId();
$releaseIsAvailable = true;
}
}
$currentCopy = rtrim($releasesDirectory, '/') . '/' . $releaseId;
if (!$releaseIsAvailable) {
Console::output('Release <bold>' . $this->getReleaseId() . '</bold> is invalid or unavailable for <bold>' . $this->getConfig()->getHost() . '</bold> ... <red>FAIL</red>');
} else {
Console::output('Rollback release on <bold>' . $this->getConfig()->getHost() . '</bold>');
$rollbackTo = $releasesDirectory . '/' . $releaseId;
// Get Current Release
if ($deleteCurrent) {
$result = $this->runCommandRemote('ls -l ' . $symlink, $output) && $result;
$currentRelease = explode('/', $output);
$currentRelease = trim(array_pop($currentRelease));
}
// Tasks
$tasks = 1;
$completedTasks = 0;
$tasksToRun = $this->getConfig()->getTasks();
$this->getConfig()->setReleaseId($releaseId);
// Run Deploy Tasks
foreach ($tasksToRun as $taskData) {
$task = Factory::get($taskData, $this->getConfig(), true, self::STAGE_DEPLOY);
$task->init();
Console::output('Running <purple>' . $task->getName() . '</purple> ... ', 2, false);
if ($task instanceof RollbackAware) {
/* @var $task AbstractTask */
$tasks++;
$result = $task->run();
if ($result === true) {
Console::output('<green>OK</green>', 0);
$completedTasks++;
} else {
Console::output('<red>FAIL</red>', 0);
}
} else {
Console::output('<yellow>SKIPPED</yellow>', 0);
}
}
// Changing Release
Console::output('Running <purple>Rollback Release [id=' . $releaseId . ']</purple> ... ', 2, false);
$userGroup = '';
$resultFetch = $this->runCommandRemote('ls -ld ' . $rollbackTo . ' | awk \'{print \$3":"\$4}\'', $userGroup);
$tmplink = $symlink . '.tmp';
$command = "ln -sfn {$currentCopy} {$tmplink}";
if ($resultFetch && $userGroup) {
$command .= " && chown -h {$userGroup} ${tmplink}";
}
$command .= " && mv -T {$tmplink} {$symlink}";
$result = $this->runCommandRemote($command);
if ($result) {
Console::output('<green>OK</green>', 0);
$completedTasks++;
// Delete Old Current Release
if ($deleteCurrent && $currentRelease) {
$this->runCommandRemote('rm -rf ' . $releasesDirectory . '/' . $currentRelease, $output);
}
} else {
Console::output('<red>FAIL</red>', 0);
}
// Run Post Release Tasks
$tasksToRun = $this->getConfig()->getTasks(AbstractTask::STAGE_POST_DEPLOY);
foreach ($tasksToRun as $taskData) {
$task = Factory::get($taskData, $this->getConfig(), true, self::STAGE_POST_DEPLOY);
$task->init();
Console::output('Running <purple>' . $task->getName() . '</purple> ... ', 2, false);
if ($task instanceof RollbackAware) {
/* @var $task AbstractTask */
$tasks++;
$result = $task->run();
if ($result === true) {
Console::output('<green>OK</green>', 0);
$completedTasks++;
} else {
Console::output('<red>FAIL</red>', 0);
}
} else {
Console::output('<yellow>SKIPPED</yellow>', 0);
}
}
if ($completedTasks == $tasks) {
$tasksColor = 'green';
} else {
$tasksColor = 'red';
}
Console::output('Release rollback on <bold>' . $this->getConfig()->getHost() . '</bold> compted: <' . $tasksColor . '>' . $completedTasks . '/' . $tasks . '</' . $tasksColor . '> tasks done.', 1, 3);
}
}
return $result;
} else {
return false;
}
}
}

View file

@ -1,108 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Scm;
use Mage\Task\AbstractTask;
use Mage\Task\SkipException;
use Mage\Task\ErrorWithMessageException;
/**
* Task for Changing the Branch of the SCM
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class ChangeBranchTask extends AbstractTask
{
/**
* Branch the executiong began with
* @var string
*/
protected static $startingBranch = 'master';
/**
* Name of the Task
* @var string
*/
private $name = 'SCM Changing branch [built-in]';
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return $this->name;
}
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::init()
*/
public function init()
{
$scmType = $this->getConfig()->general('scm');
switch ($scmType) {
case 'git':
$this->name = 'SCM Changing branch (GIT) [built-in]';
break;
}
}
/**
* Changes the Branch of the SCM
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$preCommand = 'cd ' . $this->getConfig()->deployment('from', './') . '; ';
switch ($this->getConfig()->general('scm')) {
case 'git':
if ($this->getParameter('_changeBranchRevert', false)) {
$command = $preCommand . 'git checkout ' . self::$startingBranch;
$result = $this->runCommandLocal($command);
} else {
$command = $preCommand . 'git branch | grep \'*\' | cut -d\' \' -f 2';
$currentBranch = 'master';
$result = $this->runCommandLocal($command, $currentBranch);
self::$startingBranch = $currentBranch;
$scmData = $this->getConfig()->deployment('scm', false);
if ($result && is_array($scmData) && isset($scmData['branch']) && $scmData['branch'] != $currentBranch) {
$command = $preCommand . 'git branch | grep \'' . $scmData['branch'] . '\' | tr -s \' \' | sed \'s/^[ ]//g\'';
$isBranchTracked = '';
$result = $this->runCommandLocal($command, $isBranchTracked);
if ($isBranchTracked == '') {
throw new ErrorWithMessageException('The branch <purple>' . $scmData['branch'] . '</purple> must be tracked.');
}
$branch = $this->getParameter('branch', $scmData['branch']);
$command = $preCommand . 'git checkout ' . $branch;
$result = $this->runCommandLocal($command) && $result;
} else {
throw new SkipException;
}
}
break;
default:
throw new SkipException;
break;
}
$this->getConfig()->reload();
return $result;
}
}

View file

@ -1,92 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Scm;
use Mage\Task\AbstractTask;
use Mage\Task\SkipException;
/**
* Task for Clonning a Repository
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class CloneTask extends AbstractTask
{
/**
* Name of the Task
* @var string
*/
private $name = 'SCM Clone [built-in]';
/**
* Source of the Repo
* @var string
*/
private $source = null;
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return $this->name;
}
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::init()
*/
public function init()
{
$this->source = $this->getConfig()->deployment('source');
switch ($this->source['type']) {
case 'git':
$this->name = 'SCM Clone (GIT) [built-in]';
break;
}
// Create temporal directory for clone
if (is_array($this->source)) {
if (!isset($this->source['temporal']) || trim($this->source['temporal']) == '') {
$this->source['temporal'] = sys_get_temp_dir();
}
$this->source['temporal'] = rtrim($this->source['temporal'], '/') . '/' . md5(microtime()) . '/';
$this->getConfig()->setSourceTemporal($this->source['temporal']);
}
}
/**
* Clones a Repository
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$this->runCommandLocal('mkdir -p ' . $this->source['temporal']);
switch ($this->source['type']) {
case 'git':
// Fast clone Repo form Branch
$command = 'cd ' . $this->source['temporal'] . ' ; '
. 'git clone --depth 1 -q -b ' . $this->source['from']
. ' ' . $this->source['repository'] . ' . ';
$result = $this->runCommandLocal($command);
$this->getConfig()->setFrom($this->source['temporal']);
break;
default:
throw new SkipException;
break;
}
return $result;
}
}

View file

@ -1,86 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Scm;
use Mage\Task\AbstractTask;
use Mage\Task\SkipException;
/**
* Task for Force Updating a Working Copy
*
* 'git fetch' downloads the latest from remote without trying to merge or rebase anything.
* 'git reset' resets the master branch to what you just fetched.
* The '--hard' option changes all the files in your working tree to match the files in origin/master,
* so if you have any local changes, they will be lost.
*
* @author Samuel Chiriluta <samuel4x4@gmail.com>
*/
class ForceUpdateTask extends AbstractTask
{
/**
* Name of the Task
* @var string
*/
private $name = 'SCM Force Update [built-in]';
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return $this->name;
}
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::init()
*/
public function init()
{
switch ($this->getConfig()->general('scm')) {
case 'git':
$this->name = 'SCM Force Update (GIT) [built-in]';
break;
}
}
/**
* Force Updates the Working Copy
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
switch ($this->getConfig()->general('scm')) {
case 'git':
$branch = $this->getParameter('branch', 'master');
$remote = $this->getParameter('remote', 'origin');
$command = 'git fetch ' . $remote . ' ' . $branch;
$result = $this->runCommand($command);
$command = 'git reset --hard ' . $remote . '/' . $branch;
$result = $result && $this->runCommand($command);
$command = 'git pull ' . $remote . ' ' . $branch;
$result = $result && $this->runCommand($command);
break;
default:
throw new SkipException;
break;
}
$this->getConfig()->reload();
return $result;
}
}

View file

@ -1,61 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Scm;
use Mage\Task\AbstractTask;
/**
* Task for Removing an used Cloned Repository
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class RemoveCloneTask extends AbstractTask
{
/**
* Name of the Task
* @var string
*/
private $name = 'SCM Remove Clone [built-in]';
/**
* Source of the Repo
* @var string
*/
private $source = null;
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return $this->name;
}
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::init()
*/
public function init()
{
$this->source = $this->getConfig()->deployment('source');
switch ($this->source['type']) {
case 'git':
$this->name = 'SCM Remove Clone (GIT) [built-in]';
break;
}
}
public function run()
{
return $this->runCommandLocal('rm -rf ' . $this->source['temporal']);
}
}

View file

@ -1,73 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Scm;
use Mage\Task\AbstractTask;
use Mage\Task\SkipException;
/**
* Task for Updating a Working Copy
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class UpdateTask extends AbstractTask
{
/**
* Name of the Task
* @var string
*/
private $name = 'SCM Update [built-in]';
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return $this->name;
}
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::init()
*/
public function init()
{
switch ($this->getConfig()->general('scm')) {
case 'git':
$this->name = 'SCM Update (GIT) [built-in]';
break;
}
}
/**
* Updates the Working Copy
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$command = 'cd ' . $this->getConfig()->deployment('from', './') . '; ';
switch ($this->getConfig()->general('scm')) {
case 'git':
$command .= 'git pull';
break;
default:
throw new SkipException;
break;
}
$result = $this->runCommandLocal($command);
$this->getConfig()->reload();
return $result;
}
}

View file

@ -1,43 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Symfony2;
/**
* Task for Dumping Assetics
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class AsseticDumpTask extends SymfonyAbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Symfony v2 - Assetic Dump [built-in]';
}
/**
* Dumps Assetics
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
// Options
$env = $this->getParameter('env', 'dev');
$command = $this->getAppPath() . ' assetic:dump --env=' . $env;
$result = $this->runCommand($command);
return $result;
}
}

View file

@ -1,50 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Symfony2;
/**
* Task for Installing Assets
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class AssetsInstallTask extends SymfonyAbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Symfony v2 - Assets Install [built-in]';
}
/**
* Installs Assets
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
// Options
$target = $this->getParameter('target', 'web');
$symlink = $this->getParameter('symlink', false);
$relative = $this->getParameter('relative', false);
$env = $this->getParameter('env', 'dev');
if ($relative) {
$symlink = true;
}
$command = $this->getAppPath() . ' assets:install ' . ($symlink ? '--symlink' : '') . ' ' . ($relative ? '--relative' : '') . ' --env=' . $env . ' ' . $target;
$result = $this->runCommand($command);
return $result;
}
}

View file

@ -1,50 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Symfony2;
/**
* Task for Clearing the Cache
*
* Example of usage:
* symfony2/cache-clear: { env: dev }
* symfony2/cache-clear: { env: dev, optional: --no-warmup }
*
* @author Andrés Montañez <andres@andresmontanez.com>
* @author Samuel Chiriluta <samuel4x4@gmail.com>
*/
class CacheClearTask extends SymfonyAbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Symfony v2 - Cache Clear [built-in]';
}
/**
* Clears the Cache
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
// Options
$env = $this->getParameter('env', 'dev');
$optional = $this->getParameter('optional', '');
$command = $this->getAppPath() . ' cache:clear --env=' . $env . ' ' . $optional;
$result = $this->runCommand($command);
return $result;
}
}

View file

@ -1,43 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Symfony2;
/**
* Task for Warming Up the Cache
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class CacheWarmupTask extends SymfonyAbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Symfony v2 - Cache Warmup [built-in]';
}
/**
* Warms Up Cache
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
// Options
$env = $this->getParameter('env', 'dev');
$command = $this->getAppPath() . ' cache:warmup --env=' . $env;
$result = $this->runCommand($command);
return $result;
}
}

View file

@ -1,40 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Symfony2;
/**
* Task for Doctrine migrations
*/
class DoctrineMigrateTask extends SymfonyAbstractTask
{
/**
* (non-PHPdoc)
* @see \Mage\Task\AbstractTask::getName()
*/
public function getName()
{
return 'Symfony v2 - Migrate doctrine entities [built-in]';
}
/**
* Migrates Doctrine entities
*
* @see \Mage\Task\AbstractTask::run()
*/
public function run()
{
$env = $this->getParameter('env', 'dev');
$command = $this->getAppPath() . ' doctrine:migrations:migrate -n --env=' . $env;
return $this->runCommand($command);
}
}

View file

@ -1,23 +0,0 @@
<?php
namespace Task;
use Mage\Task\AbstractTask;
/**
* @author Muhammad Surya Ihsanuddin <surya.kejawen@gmail.com>
*/
class FrontControllerCleanTask extends AbstractTask
{
public function getName()
{
return 'Cleaning Project';
}
public function run()
{
$command = 'rm -rf web/app_*.php';
$result = $this->runCommandRemote($command);
return $result;
}
}

View file

@ -1,32 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\BuiltIn\Symfony2;
use Mage\Task\AbstractTask;
/**
* Abstract Task for Symfony2
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
abstract class SymfonyAbstractTask extends AbstractTask
{
protected function getAppPath()
{
if ($this->getConfig()->general('symfony_version', '2') == '3') {
$defaultAppPath = 'bin/console';
} else {
$defaultAppPath = 'app/console';
}
return $this->getConfig()->general('symfony_app_path', $defaultAppPath);
}
}

View file

@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task;
use Exception;
/**
* Exception that indicates that the Task has an Error and also a Message indicating the Error
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class ErrorWithMessageException extends Exception
{
}

View file

@ -1,65 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task;
use Mage\Config;
use Exception;
/**
* Task Factory
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class Factory
{
/**
* Gets an instance of a Task.
*
* @param string|array $taskData
* @param \Mage\Config $taskConfig
* @param boolean $inRollback
* @param string $stage
* @return \Mage\Task\AbstractTask
* @throws \Exception
*/
public static function get($taskData, Config $taskConfig, $inRollback = false, $stage = null)
{
if (is_array($taskData)) {
$taskName = $taskData['name'];
$taskParameters = $taskData['parameters'];
} else {
$taskName = $taskData;
$taskParameters = array();
}
$instance = null;
$taskName = ucwords(str_replace('-', ' ', $taskName));
$taskName = str_replace(' ', '', $taskName);
if (strpos($taskName, '/') === false) {
$className = 'Task\\' . $taskName;
} else {
$className = 'Mage\\Task\\BuiltIn\\' . str_replace(' ', '\\', ucwords(str_replace('/', ' ', $taskName))) . 'Task';
}
if (!class_exists($className)) {
throw new Exception('Task "' . $taskName . '" not found.');
}
$instance = new $className($taskConfig, $inRollback, $stage, $taskParameters);
if (!($instance instanceof AbstractTask)) {
throw new Exception('The Task ' . $taskName . ' must be an instance of Mage\Task\AbstractTask.');
}
return $instance;
}
}

View file

@ -1,20 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\Releases;
/**
* Indicates that the Task is Relase Aware/Dependant
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
interface IsReleaseAware
{
}

View file

@ -1,20 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\Releases;
/**
* Indicates that the Task is Aware of Rollbacks
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
interface RollbackAware
{
}

View file

@ -1,20 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task\Releases;
/**
* Indicates that the Task will be Skipped on Relase Override
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
interface SkipOnOverride
{
}

View file

@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Alex V Kotelnikov <gudron@gudron.me>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task;
use Exception;
/**
* Exception that indicates that the Task was Failed and rollback needed
*
* @author Alex V Kotelnikov <gudron@gudron.me>
*/
class RollbackException extends Exception
{
}

View file

@ -1,22 +0,0 @@
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mage\Task;
use Exception;
/**
* Exception that indicates that the Task was Skipped
*
* @author Andrés Montañez <andres@andresmontanez.com>
*/
class SkipException extends Exception
{
}

View file

@ -1,73 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml;
/**
* Dumper dumps PHP variables to YAML strings.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Dumper
{
/**
* The amount of spaces to use for indentation of nested nodes.
*
* @var int
*/
protected $indentation = 4;
/**
* Sets the indentation.
*
* @param int $num The amount of spaces to use for indentation of nested nodes.
*/
public function setIndentation($num)
{
$this->indentation = (int)$num;
}
/**
* Dumps a PHP value to YAML.
*
* @param mixed $input The PHP value
* @param int $inline The level where you switch to inline YAML
* @param int $indent The level of indentation (used internally)
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
*
* @return string The YAML representation of the PHP value
*/
public function dump($input, $inline = 0, $indent = 0, $exceptionOnInvalidType = false, $objectSupport = false)
{
$output = '';
$prefix = $indent ? str_repeat(' ', $indent) : '';
if ($inline <= 0 || !is_array($input) || empty($input)) {
$output .= $prefix . Inline::dump($input, $exceptionOnInvalidType, $objectSupport);
} else {
$isAHash = array_keys($input) !== range(0, count($input) - 1);
foreach ($input as $key => $value) {
$willBeInlined = $inline - 1 <= 0 || !is_array($value) || empty($value);
$output .= sprintf('%s%s%s%s',
$prefix,
$isAHash ? Inline::dump($key, $exceptionOnInvalidType, $objectSupport) . ':' : '-',
$willBeInlined ? ' ' : "\n",
$this->dump($value, $inline - 1, $willBeInlined ? 0 : $indent + $this->indentation, $exceptionOnInvalidType, $objectSupport)
) . ($willBeInlined ? "\n" : '');
}
}
return $output;
}
}

View file

@ -1,89 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml;
/**
* Escaper encapsulates escaping rules for single and double-quoted
* YAML strings.
*
* @author Matthew Lewinski <matthew@lewinski.org>
*/
class Escaper
{
// Characters that would cause a dumped string to require double quoting.
const REGEX_CHARACTER_TO_ESCAPE = "[\\x00-\\x1f]|\xc2\x85|\xc2\xa0|\xe2\x80\xa8|\xe2\x80\xa9";
// Mapping arrays for escaping a double quoted string. The backslash is
// first to ensure proper escaping because str_replace operates iteratively
// on the input arrays. This ordering of the characters avoids the use of strtr,
// which performs more slowly.
private static $escapees = array('\\\\', '\\"', '"',
"\x00", "\x01", "\x02", "\x03", "\x04", "\x05", "\x06", "\x07",
"\x08", "\x09", "\x0a", "\x0b", "\x0c", "\x0d", "\x0e", "\x0f",
"\x10", "\x11", "\x12", "\x13", "\x14", "\x15", "\x16", "\x17",
"\x18", "\x19", "\x1a", "\x1b", "\x1c", "\x1d", "\x1e", "\x1f",
"\xc2\x85", "\xc2\xa0", "\xe2\x80\xa8", "\xe2\x80\xa9");
private static $escaped = array('\\"', '\\\\', '\\"',
"\\0", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
"\\b", "\\t", "\\n", "\\v", "\\f", "\\r", "\\x0e", "\\x0f",
"\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
"\\x18", "\\x19", "\\x1a", "\\e", "\\x1c", "\\x1d", "\\x1e", "\\x1f",
"\\N", "\\_", "\\L", "\\P");
/**
* Determines if a PHP value would require double quoting in YAML.
*
* @param string $value A PHP value
*
* @return bool True if the value would require double quotes.
*/
public static function requiresDoubleQuoting($value)
{
return preg_match('/' . self::REGEX_CHARACTER_TO_ESCAPE . '/u', $value);
}
/**
* Escapes and surrounds a PHP value with double quotes.
*
* @param string $value A PHP value
*
* @return string The quoted, escaped string
*/
public static function escapeWithDoubleQuotes($value)
{
return sprintf('"%s"', str_replace(self::$escapees, self::$escaped, $value));
}
/**
* Determines if a PHP value would require single quoting in YAML.
*
* @param string $value A PHP value
*
* @return bool True if the value would require single quotes.
*/
public static function requiresSingleQuoting($value)
{
return preg_match('/[ \s \' " \: \{ \} \[ \] , & \* \# \?] | \A[ \- ? | < > = ! % @ ` ]/x', $value);
}
/**
* Escapes and surrounds a PHP value with single quotes.
*
* @param string $value A PHP value
*
* @return string The quoted, escaped string
*/
public static function escapeWithSingleQuotes($value)
{
return sprintf("'%s'", str_replace('\'', '\'\'', $value));
}
}

View file

@ -1,23 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml\Exception;
/**
* Exception class thrown when an error occurs during dumping.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
class DumpException extends RuntimeException
{
}

View file

@ -1,23 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml\Exception;
/**
* Exception interface for all exceptions thrown by the component.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
interface ExceptionInterface
{
}

View file

@ -1,148 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml\Exception;
if (!defined('JSON_UNESCAPED_UNICODE')) {
define('JSON_UNESCAPED_SLASHES', 64);
define('JSON_UNESCAPED_UNICODE', 256);
}
/**
* Exception class thrown when an error occurs during parsing.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
class ParseException extends RuntimeException
{
private $parsedFile;
private $parsedLine;
private $snippet;
private $rawMessage;
/**
* Constructor.
*
* @param string $message The error message
* @param int $parsedLine The line where the error occurred
* @param int $snippet The snippet of code near the problem
* @param string $parsedFile The file name where the error occurred
* @param \Exception $previous The previous exception
*/
public function __construct($message, $parsedLine = -1, $snippet = null, $parsedFile = null, \Exception $previous = null)
{
$this->parsedFile = $parsedFile;
$this->parsedLine = $parsedLine;
$this->snippet = $snippet;
$this->rawMessage = $message;
$this->updateRepr();
parent::__construct($this->message, 0, $previous);
}
/**
* Gets the snippet of code near the error.
*
* @return string The snippet of code
*/
public function getSnippet()
{
return $this->snippet;
}
/**
* Sets the snippet of code near the error.
*
* @param string $snippet The code snippet
*/
public function setSnippet($snippet)
{
$this->snippet = $snippet;
$this->updateRepr();
}
/**
* Gets the filename where the error occurred.
*
* This method returns null if a string is parsed.
*
* @return string The filename
*/
public function getParsedFile()
{
return $this->parsedFile;
}
/**
* Sets the filename where the error occurred.
*
* @param string $parsedFile The filename
*/
public function setParsedFile($parsedFile)
{
$this->parsedFile = $parsedFile;
$this->updateRepr();
}
/**
* Gets the line where the error occurred.
*
* @return int The file line
*/
public function getParsedLine()
{
return $this->parsedLine;
}
/**
* Sets the line where the error occurred.
*
* @param int $parsedLine The file line
*/
public function setParsedLine($parsedLine)
{
$this->parsedLine = $parsedLine;
$this->updateRepr();
}
private function updateRepr()
{
$this->message = $this->rawMessage;
$dot = false;
if ('.' === substr($this->message, -1)) {
$this->message = substr($this->message, 0, -1);
$dot = true;
}
if (null !== $this->parsedFile) {
$this->message .= sprintf(' in %s', json_encode($this->parsedFile, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
}
if ($this->parsedLine >= 0) {
$this->message .= sprintf(' at line %d', $this->parsedLine);
}
if ($this->snippet) {
$this->message .= sprintf(' (near "%s")', $this->snippet);
}
if ($dot) {
$this->message .= '.';
}
}
}

View file

@ -1,23 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml\Exception;
/**
* Exception class thrown when an error occurs during parsing.
*
* @author Romain Neutron <imprec@gmail.com>
*
* @api
*/
class RuntimeException extends \RuntimeException implements ExceptionInterface
{
}

View file

@ -1,503 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml;
use Mage\Yaml\Exception\ParseException;
use Mage\Yaml\Exception\DumpException;
/**
* Inline implements a YAML parser/dumper for the YAML inline syntax.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
/** @noinspection PhpUndefinedClassInspection */
class Inline
{
const REGEX_QUOTED_STRING = '(?:"([^"\\\\]*(?:\\\\.[^"\\\\]*)*)"|\'([^\']*(?:\'\'[^\']*)*)\')';
private static $exceptionOnInvalidType = false;
private static $objectSupport = false;
private static $objectForMap = false;
/**
* Converts a YAML string to a PHP array.
*
* @param string $value A YAML string
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $objectForMap true if maps should return a stdClass instead of array()
*
* @return array A PHP array representing the YAML string
*
* @throws ParseException
*/
public static function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
{
self::$exceptionOnInvalidType = $exceptionOnInvalidType;
self::$objectSupport = $objectSupport;
self::$objectForMap = $objectForMap;
$value = trim($value);
if (0 == strlen($value)) {
return '';
}
if (function_exists('mb_internal_encoding') && ((int)ini_get('mbstring.func_overload')) & 2) {
$mbEncoding = mb_internal_encoding();
mb_internal_encoding('ASCII');
}
$i = 0;
switch ($value[0]) {
case '[':
$result = self::parseSequence($value, $i);
++$i;
break;
case '{':
$result = self::parseMapping($value, $i);
++$i;
break;
default:
$result = self::parseScalar($value, null, array('"', "'"), $i);
}
// some comments are allowed at the end
if (preg_replace('/\s+#.*$/A', '', substr($value, $i))) {
throw new ParseException(sprintf('Unexpected characters near "%s".', substr($value, $i)));
}
if (isset($mbEncoding)) {
mb_internal_encoding($mbEncoding);
}
return $result;
}
/**
* Dumps a given PHP variable to a YAML string.
*
* @param mixed $value The PHP variable to convert
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
*
* @return string The YAML string representing the PHP array
*
* @throws DumpException When trying to dump PHP resource
*/
public static function dump($value, $exceptionOnInvalidType = false, $objectSupport = false)
{
switch (true) {
case is_resource($value):
if ($exceptionOnInvalidType) {
throw new DumpException(sprintf('Unable to dump PHP resources in a YAML file ("%s").', get_resource_type($value)));
}
return 'null';
case is_object($value):
if ($objectSupport) {
return '!!php/object:' . serialize($value);
}
if ($exceptionOnInvalidType) {
throw new DumpException('Object support when dumping a YAML file has been disabled.');
}
return 'null';
case is_array($value):
return self::dumpArray($value, $exceptionOnInvalidType, $objectSupport);
case null === $value:
return 'null';
case true === $value:
return 'true';
case false === $value:
return 'false';
case ctype_digit($value):
return is_string($value) ? "'$value'" : (int)$value;
case is_numeric($value):
$locale = setlocale(LC_NUMERIC, 0);
if (false !== $locale) {
setlocale(LC_NUMERIC, 'C');
}
$repr = is_string($value) ? "'$value'" : (is_infinite($value) ? str_ireplace('INF', '.Inf', strval($value)) : strval($value));
if (false !== $locale) {
setlocale(LC_NUMERIC, $locale);
}
return $repr;
case Escaper::requiresDoubleQuoting($value):
return Escaper::escapeWithDoubleQuotes($value);
case Escaper::requiresSingleQuoting($value):
return Escaper::escapeWithSingleQuotes($value);
case '' == $value:
return "''";
case preg_match(self::getTimestampRegex(), $value):
case in_array(strtolower($value), array('null', '~', 'true', 'false')):
return "'$value'";
default:
return $value;
}
}
/**
* Dumps a PHP array to a YAML string.
*
* @param array $value The PHP array to dump
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
*
* @return string The YAML string representing the PHP array
*/
private static function dumpArray($value, $exceptionOnInvalidType, $objectSupport)
{
// array
$keys = array_keys($value);
if ((1 == count($keys) && '0' == $keys[0])
|| (count($keys) > 1 && array_reduce($keys, function ($v, $w) {
return (int)$v + $w;
}, 0) == count($keys) * (count($keys) - 1) / 2)
) {
$output = array();
foreach ($value as $val) {
$output[] = self::dump($val, $exceptionOnInvalidType, $objectSupport);
}
return sprintf('[%s]', implode(', ', $output));
}
// mapping
$output = array();
foreach ($value as $key => $val) {
$output[] = sprintf('%s: %s', self::dump($key, $exceptionOnInvalidType, $objectSupport), self::dump($val, $exceptionOnInvalidType, $objectSupport));
}
return sprintf('{ %s }', implode(', ', $output));
}
/**
* Parses a scalar to a YAML string.
*
* @param scalar $scalar
* @param string $delimiters
* @param array $stringDelimiters
* @param int &$i
* @param bool $evaluate
*
* @return string A YAML string
*
* @throws ParseException When malformed inline YAML string is parsed
*/
public static function parseScalar($scalar, $delimiters = null, $stringDelimiters = array('"', "'"), &$i = 0, $evaluate = true)
{
if (in_array($scalar[$i], $stringDelimiters)) {
// quoted scalar
$output = self::parseQuotedScalar($scalar, $i);
if (null !== $delimiters) {
$tmp = ltrim(substr($scalar, $i), ' ');
if (!in_array($tmp[0], $delimiters)) {
throw new ParseException(sprintf('Unexpected characters (%s).', substr($scalar, $i)));
}
}
} else {
// "normal" string
if (!$delimiters) {
$output = substr($scalar, $i);
$i += strlen($output);
// remove comments
if (false !== $strpos = strpos($output, ' #')) {
$output = rtrim(substr($output, 0, $strpos));
}
} elseif (preg_match('/^(.+?)(' . implode('|', $delimiters) . ')/', substr($scalar, $i), $match)) {
$output = $match[1];
$i += strlen($output);
} else {
throw new ParseException(sprintf('Malformed inline YAML string (%s).', $scalar));
}
if ($evaluate) {
$output = self::evaluateScalar($output);
}
}
return $output;
}
/**
* Parses a quoted scalar to YAML.
*
* @param string $scalar
* @param int &$i
*
* @return string A YAML string
*
* @throws ParseException When malformed inline YAML string is parsed
*/
private static function parseQuotedScalar($scalar, &$i)
{
if (!preg_match('/' . self::REGEX_QUOTED_STRING . '/Au', substr($scalar, $i), $match)) {
throw new ParseException(sprintf('Malformed inline YAML string (%s).', substr($scalar, $i)));
}
$output = substr($match[0], 1, strlen($match[0]) - 2);
$unescaper = new Unescaper();
if ('"' == $scalar[$i]) {
$output = $unescaper->unescapeDoubleQuotedString($output);
} else {
$output = $unescaper->unescapeSingleQuotedString($output);
}
$i += strlen($match[0]);
return $output;
}
/**
* Parses a sequence to a YAML string.
*
* @param string $sequence
* @param int &$i
*
* @return string A YAML string
*
* @throws ParseException When malformed inline YAML string is parsed
*/
private static function parseSequence($sequence, &$i = 0)
{
$output = array();
$len = strlen($sequence);
$i += 1;
// [foo, bar, ...]
while ($i < $len) {
switch ($sequence[$i]) {
case '[':
// nested sequence
$output[] = self::parseSequence($sequence, $i);
break;
case '{':
// nested mapping
$output[] = self::parseMapping($sequence, $i);
break;
case ']':
return $output;
case ',':
case ' ':
break;
default:
$isQuoted = in_array($sequence[$i], array('"', "'"));
$value = self::parseScalar($sequence, array(',', ']'), array('"', "'"), $i);
if (!$isQuoted && false !== strpos($value, ': ')) {
// embedded mapping?
try {
$value = self::parseMapping('{' . $value . '}');
} catch (\InvalidArgumentException $e) {
// no, it's not
}
}
$output[] = $value;
--$i;
}
++$i;
}
throw new ParseException(sprintf('Malformed inline YAML string %s', $sequence));
}
/**
* Parses a mapping to a YAML string.
*
* @param string $mapping
* @param int &$i
*
* @return string A YAML string
*
* @throws ParseException When malformed inline YAML string is parsed
*/
private static function parseMapping($mapping, &$i = 0)
{
$output = array();
$len = strlen($mapping);
$i += 1;
// {foo: bar, bar:foo, ...}
while ($i < $len) {
switch ($mapping[$i]) {
case ' ':
case ',':
++$i;
continue 2;
case '}':
if (self::$objectForMap) {
return (object)$output;
}
return $output;
}
// key
$key = self::parseScalar($mapping, array(':', ' '), array('"', "'"), $i, false);
// value
$done = false;
while ($i < $len) {
switch ($mapping[$i]) {
case '[':
// nested sequence
$value = self::parseSequence($mapping, $i);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($output[$key])) {
$output[$key] = $value;
}
$done = true;
break;
case '{':
// nested mapping
$value = self::parseMapping($mapping, $i);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($output[$key])) {
$output[$key] = $value;
}
$done = true;
break;
case ':':
case ' ':
break;
default:
$value = self::parseScalar($mapping, array(',', '}'), array('"', "'"), $i);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($output[$key])) {
$output[$key] = $value;
}
$done = true;
--$i;
}
++$i;
if ($done) {
continue 2;
}
}
}
throw new ParseException(sprintf('Malformed inline YAML string %s', $mapping));
}
/**
* Evaluates scalars and replaces magic values.
*
* @param string $scalar
*
* @throws Exception\ParseException
* @return string A YAML string
*/
private static function evaluateScalar($scalar)
{
$scalar = trim($scalar);
$scalarLower = strtolower($scalar);
switch (true) {
case 'null' === $scalarLower:
case '' === $scalar:
case '~' === $scalar:
/** @noinspection PhpInconsistentReturnPointsInspection */
return;
case 'true' === $scalarLower:
return true;
case 'false' === $scalarLower:
return false;
// Optimise for returning strings.
/** @noinspection PhpMissingBreakStatementInspection */
case $scalar[0] === '+' || $scalar[0] === '-' || $scalar[0] === '.' || $scalar[0] === '!' || is_numeric($scalar[0]):
switch (true) {
case 0 === strpos($scalar, '!str'):
return (string)substr($scalar, 5);
case 0 === strpos($scalar, '! '):
return intval(self::parseScalar(substr($scalar, 2)));
case 0 === strpos($scalar, '!!php/object:'):
if (self::$objectSupport) {
return unserialize(substr($scalar, 13));
}
if (self::$exceptionOnInvalidType) {
throw new ParseException('Object support when parsing a YAML file has been disabled.');
}
/** @noinspection PhpInconsistentReturnPointsInspection */
return;
case ctype_digit($scalar):
$raw = $scalar;
$cast = intval($scalar);
return '0' == $scalar[0] ? octdec($scalar) : (((string)$raw == (string)$cast) ? $cast : $raw);
case '-' === $scalar[0] && ctype_digit(substr($scalar, 1)):
$raw = $scalar;
$cast = intval($scalar);
return '0' == $scalar[1] ? octdec($scalar) : (((string)$raw == (string)$cast) ? $cast : $raw);
case is_numeric($scalar):
return '0x' == $scalar[0] . $scalar[1] ? hexdec($scalar) : floatval($scalar);
case '.inf' === $scalarLower:
case '.nan' === $scalarLower:
return -log(0);
case '-.inf' === $scalarLower:
return log(0);
case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $scalar):
return floatval(str_replace(',', '', $scalar));
case preg_match(self::getTimestampRegex(), $scalar):
return strtotime($scalar);
}
default:
return (string)$scalar;
}
}
/**
* Gets a regex that matches a YAML date.
*
* @return string The regular expression
*
* @see http://www.yaml.org/spec/1.2/spec.html#id2761573
*/
private static function getTimestampRegex()
{
return <<<EOF
~^
(?P<year>[0-9][0-9][0-9][0-9])
-(?P<month>[0-9][0-9]?)
-(?P<day>[0-9][0-9]?)
(?:(?:[Tt]|[ \t]+)
(?P<hour>[0-9][0-9]?)
:(?P<minute>[0-9][0-9])
:(?P<second>[0-9][0-9])
(?:\.(?P<fraction>[0-9]*))?
(?:[ \t]*(?P<tz>Z|(?P<tz_sign>[-+])(?P<tz_hour>[0-9][0-9]?)
(?::(?P<tz_minute>[0-9][0-9]))?))?)?
$~x
EOF;
}
}

View file

@ -1,657 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml;
use Mage\Yaml\Exception\ParseException;
/**
* Parser parses YAML strings to convert them to PHP arrays.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Parser
{
const FOLDED_SCALAR_PATTERN = '(?P<separator>\||>)(?P<modifiers>\+|\-|\d+|\+\d+|\-\d+|\d+\+|\d+\-)?(?P<comments> +#.*)?';
private $offset = 0;
private $lines = array();
private $currentLineNb = -1;
private $currentLine = '';
private $refs = array();
/**
* Constructor
*
* @param int $offset The offset of YAML document (used for line numbers in error messages)
*/
public function __construct($offset = 0)
{
$this->offset = $offset;
}
/**
* Parses a YAML string to a PHP value.
*
* @param string $value A YAML string
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
* @param bool $objectForMap true if maps should return a stdClass instead of array()
*
* @return mixed A PHP value
*
* @throws ParseException If the YAML is not valid
*/
public function parse($value, $exceptionOnInvalidType = false, $objectSupport = false, $objectForMap = false)
{
$this->currentLineNb = -1;
$this->currentLine = '';
$this->lines = explode("\n", $this->cleanup($value));
if (!preg_match('//u', $value)) {
throw new ParseException('The YAML value does not appear to be valid UTF-8.');
}
if (function_exists('mb_internal_encoding') && ((int)ini_get('mbstring.func_overload')) & 2) {
$mbEncoding = mb_internal_encoding();
mb_internal_encoding('UTF-8');
}
$data = array();
$context = null;
while ($this->moveToNextLine()) {
if ($this->isCurrentLineEmpty()) {
continue;
}
// tab?
if ("\t" === $this->currentLine[0]) {
throw new ParseException('A YAML file cannot contain tabs as indentation.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
$isRef = $isInPlace = $isProcessed = false;
if (preg_match('#^\-((?P<leadspaces>\s+)(?P<value>.+?))?\s*$#u', $this->currentLine, $values)) {
if ($context && 'mapping' == $context) {
throw new ParseException('You cannot define a sequence item when in a mapping');
}
$context = 'sequence';
if (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
$isRef = $matches['ref'];
$values['value'] = $matches['value'];
}
// array
if (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c);
$parser->refs =& $this->refs;
$data[] = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
} else {
if (isset($values['leadspaces'])
&& ' ' == $values['leadspaces']
&& preg_match('#^(?P<key>' . Inline::REGEX_QUOTED_STRING . '|[^ \'"\{\[].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $values['value'], $matches)
) {
// this is a compact notation element, add to next block and parse
$c = $this->getRealCurrentLineNb();
$parser = new self($c);
$parser->refs =& $this->refs;
$block = $values['value'];
if ($this->isNextLineIndented()) {
$block .= "\n" . $this->getNextEmbedBlock($this->getCurrentLineIndentation() + 2);
}
$data[] = $parser->parse($block, $exceptionOnInvalidType, $objectSupport, $objectForMap);
} else {
$data[] = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap);
}
}
} elseif (preg_match('#^(?P<key>' . Inline::REGEX_QUOTED_STRING . '|[^ \'"\[\{].*?) *\:(\s+(?P<value>.+?))?\s*$#u', $this->currentLine, $values) && false === strpos($values['key'], ' #')) {
if ($context && 'sequence' == $context) {
throw new ParseException('You cannot define a mapping item when in a sequence');
}
$context = 'mapping';
// force correct settings
Inline::parse(null, $exceptionOnInvalidType, $objectSupport, $objectForMap);
try {
$key = Inline::parseScalar($values['key']);
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);
throw $e;
}
if ('<<' === $key) {
if (isset($values['value']) && 0 === strpos($values['value'], '*')) {
$isInPlace = substr($values['value'], 1);
if (!array_key_exists($isInPlace, $this->refs)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $isInPlace), $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
} else {
if (isset($values['value']) && $values['value'] !== '') {
$value = $values['value'];
} else {
$value = $this->getNextEmbedBlock();
}
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c);
$parser->refs =& $this->refs;
$parsed = $parser->parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
$merged = array();
if (!is_array($parsed)) {
throw new ParseException('YAML merge keys used with a scalar value instead of an array.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
} elseif (isset($parsed[0])) {
// Numeric array, merge individual elements
foreach (array_reverse($parsed) as $parsedItem) {
if (!is_array($parsedItem)) {
throw new ParseException('Merge items must be arrays.', $this->getRealCurrentLineNb() + 1, $parsedItem);
}
$merged = array_merge($parsedItem, $merged);
}
} else {
// Associative array, merge
$merged = array_merge($merged, $parsed);
}
$isProcessed = $merged;
}
} elseif (isset($values['value']) && preg_match('#^&(?P<ref>[^ ]+) *(?P<value>.*)#u', $values['value'], $matches)) {
$isRef = $matches['ref'];
$values['value'] = $matches['value'];
}
if ($isProcessed) {
// Merge keys
$data = $isProcessed;
// hash
} elseif (!isset($values['value']) || '' == trim($values['value'], ' ') || 0 === strpos(ltrim($values['value'], ' '), '#')) {
// if next line is less indented or equal, then it means that the current value is null
if (!$this->isNextLineIndented() && !$this->isNextLineUnIndentedCollection()) {
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($data[$key])) {
$data[$key] = null;
}
} else {
$c = $this->getRealCurrentLineNb() + 1;
$parser = new self($c);
$parser->refs =& $this->refs;
$value = $parser->parse($this->getNextEmbedBlock(), $exceptionOnInvalidType, $objectSupport, $objectForMap);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($data[$key])) {
$data[$key] = $value;
}
}
} else {
if ($isInPlace) {
$data = $this->refs[$isInPlace];
} else {
$value = $this->parseValue($values['value'], $exceptionOnInvalidType, $objectSupport, $objectForMap);
// Spec: Keys MUST be unique; first one wins.
// Parser cannot abort this mapping earlier, since lines
// are processed sequentially.
if (!isset($data[$key])) {
$data[$key] = $value;
}
}
}
} else {
// 1-liner optionally followed by newline
$lineCount = count($this->lines);
if (1 === $lineCount || (2 === $lineCount && empty($this->lines[1]))) {
try {
$value = Inline::parse($this->lines[0], $exceptionOnInvalidType, $objectSupport, $objectForMap);
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);
throw $e;
}
if (is_array($value)) {
$first = reset($value);
if (is_string($first) && 0 === strpos($first, '*')) {
$data = array();
foreach ($value as $alias) {
$data[] = $this->refs[substr($alias, 1)];
}
$value = $data;
}
}
if (isset($mbEncoding)) {
mb_internal_encoding($mbEncoding);
}
return $value;
}
switch (preg_last_error()) {
case PREG_INTERNAL_ERROR:
$error = 'Internal PCRE error.';
break;
case PREG_BACKTRACK_LIMIT_ERROR:
$error = 'pcre.backtrack_limit reached.';
break;
case PREG_RECURSION_LIMIT_ERROR:
$error = 'pcre.recursion_limit reached.';
break;
case PREG_BAD_UTF8_ERROR:
$error = 'Malformed UTF-8 data.';
break;
case PREG_BAD_UTF8_OFFSET_ERROR:
$error = 'Offset doesn\'t correspond to the begin of a valid UTF-8 code point.';
break;
default:
$error = 'Unable to parse.';
}
throw new ParseException($error, $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
if ($isRef) {
$this->refs[$isRef] = end($data);
}
}
if (isset($mbEncoding)) {
mb_internal_encoding($mbEncoding);
}
return empty($data) ? null : $data;
}
/**
* Returns the current line number (takes the offset into account).
*
* @return int The current line number
*/
private function getRealCurrentLineNb()
{
return $this->currentLineNb + $this->offset;
}
/**
* Returns the current line indentation.
*
* @return int The current line indentation
*/
private function getCurrentLineIndentation()
{
return strlen($this->currentLine) - strlen(ltrim($this->currentLine, ' '));
}
/**
* Returns the next embed block of YAML.
*
* @param int $indentation The indent level at which the block is to be read, or null for default
*
* @return string A YAML string
*
* @throws ParseException When indentation problem are detected
*/
private function getNextEmbedBlock($indentation = null)
{
$this->moveToNextLine();
if (null === $indentation) {
$newIndent = $this->getCurrentLineIndentation();
$unindentedEmbedBlock = $this->isStringUnIndentedCollectionItem($this->currentLine);
if (!$this->isCurrentLineEmpty() && 0 === $newIndent && !$unindentedEmbedBlock) {
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
} else {
$newIndent = $indentation;
}
$data = array(substr($this->currentLine, $newIndent));
$isItUnindentedCollection = $this->isStringUnIndentedCollectionItem($this->currentLine);
// Comments must not be removed inside a string block (ie. after a line ending with "|")
$removeCommentsPattern = '~' . self::FOLDED_SCALAR_PATTERN . '$~';
$removeComments = !preg_match($removeCommentsPattern, $this->currentLine);
while ($this->moveToNextLine()) {
$indent = $this->getCurrentLineIndentation();
if ($indent === $newIndent) {
$removeComments = !preg_match($removeCommentsPattern, $this->currentLine);
}
if ($isItUnindentedCollection && !$this->isStringUnIndentedCollectionItem($this->currentLine)) {
$this->moveToPreviousLine();
break;
}
if ($this->isCurrentLineBlank()) {
$data[] = substr($this->currentLine, $newIndent);
continue;
}
if ($removeComments && $this->isCurrentLineComment()) {
continue;
}
if ($indent >= $newIndent) {
$data[] = substr($this->currentLine, $newIndent);
} elseif (0 == $indent) {
$this->moveToPreviousLine();
break;
} else {
throw new ParseException('Indentation problem.', $this->getRealCurrentLineNb() + 1, $this->currentLine);
}
}
return implode("\n", $data);
}
/**
* Moves the parser to the next line.
*
* @return bool
*/
private function moveToNextLine()
{
if ($this->currentLineNb >= count($this->lines) - 1) {
return false;
}
$this->currentLine = $this->lines[++$this->currentLineNb];
return true;
}
/**
* Moves the parser to the previous line.
*/
private function moveToPreviousLine()
{
$this->currentLine = $this->lines[--$this->currentLineNb];
}
/**
* Parses a YAML value.
*
* @param string $value A YAML value
* @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
* @param bool $objectSupport True if object support is enabled, false otherwise
* @param bool $objectForMap true if maps should return a stdClass instead of array()
*
* @return mixed A PHP value
*
* @throws ParseException When reference does not exist
*/
private function parseValue($value, $exceptionOnInvalidType, $objectSupport, $objectForMap)
{
if (0 === strpos($value, '*')) {
if (false !== $pos = strpos($value, '#')) {
$value = substr($value, 1, $pos - 2);
} else {
$value = substr($value, 1);
}
if (!array_key_exists($value, $this->refs)) {
throw new ParseException(sprintf('Reference "%s" does not exist.', $value), $this->currentLine);
}
return $this->refs[$value];
}
if (preg_match('/^' . self::FOLDED_SCALAR_PATTERN . '$/', $value, $matches)) {
$modifiers = isset($matches['modifiers']) ? $matches['modifiers'] : '';
return $this->parseFoldedScalar($matches['separator'], preg_replace('#\d+#', '', $modifiers), intval(abs($modifiers)));
}
try {
return Inline::parse($value, $exceptionOnInvalidType, $objectSupport, $objectForMap);
} catch (ParseException $e) {
$e->setParsedLine($this->getRealCurrentLineNb() + 1);
$e->setSnippet($this->currentLine);
throw $e;
}
}
/**
* Parses a folded scalar.
*
* @param string $separator The separator that was used to begin this folded scalar (| or >)
* @param string $indicator The indicator that was used to begin this folded scalar (+ or -)
* @param int $indentation The indentation that was used to begin this folded scalar
*
* @return string The text value
*/
private function parseFoldedScalar($separator, $indicator = '', $indentation = 0)
{
$notEOF = $this->moveToNextLine();
if (!$notEOF) {
return '';
}
$isCurrentLineBlank = $this->isCurrentLineBlank();
$text = '';
// leading blank lines are consumed before determining indentation
while ($notEOF && $isCurrentLineBlank) {
// newline only if not EOF
if ($notEOF = $this->moveToNextLine()) {
$text .= "\n";
$isCurrentLineBlank = $this->isCurrentLineBlank();
}
}
// determine indentation if not specified
if (0 === $indentation) {
if (preg_match('/^ +/', $this->currentLine, $matches)) {
$indentation = strlen($matches[0]);
}
}
if ($indentation > 0) {
$pattern = sprintf('/^ {%d}(.*)$/', $indentation);
while (
$notEOF && (
$isCurrentLineBlank ||
preg_match($pattern, $this->currentLine, $matches)
)
) {
if ($isCurrentLineBlank) {
$text .= substr($this->currentLine, $indentation);
} else {
/** @noinspection PhpUndefinedVariableInspection */
$text .= $matches[1];
}
// newline only if not EOF
if ($notEOF = $this->moveToNextLine()) {
$text .= "\n";
$isCurrentLineBlank = $this->isCurrentLineBlank();
}
}
} elseif ($notEOF) {
$text .= "\n";
}
if ($notEOF) {
$this->moveToPreviousLine();
}
// replace all non-trailing single newlines with spaces in folded blocks
if ('>' === $separator) {
preg_match('/(\n*)$/', $text, $matches);
$text = preg_replace('/(?<!\n)\n(?!\n)/', ' ', rtrim($text, "\n"));
$text .= $matches[1];
}
// deal with trailing newlines as indicated
if ('' === $indicator) {
$text = preg_replace('/\n+$/s', "\n", $text);
} elseif ('-' === $indicator) {
$text = preg_replace('/\n+$/s', '', $text);
}
return $text;
}
/**
* Returns true if the next line is indented.
*
* @return bool Returns true if the next line is indented, false otherwise
*/
private function isNextLineIndented()
{
$currentIndentation = $this->getCurrentLineIndentation();
$EOF = !$this->moveToNextLine();
while (!$EOF && $this->isCurrentLineEmpty()) {
$EOF = !$this->moveToNextLine();
}
if ($EOF) {
return false;
}
$ret = false;
if ($this->getCurrentLineIndentation() > $currentIndentation) {
$ret = true;
}
$this->moveToPreviousLine();
return $ret;
}
/**
* Returns true if the current line is blank or if it is a comment line.
*
* @return bool Returns true if the current line is empty or if it is a comment line, false otherwise
*/
private function isCurrentLineEmpty()
{
return $this->isCurrentLineBlank() || $this->isCurrentLineComment();
}
/**
* Returns true if the current line is blank.
*
* @return bool Returns true if the current line is blank, false otherwise
*/
private function isCurrentLineBlank()
{
return '' == trim($this->currentLine, ' ');
}
/**
* Returns true if the current line is a comment line.
*
* @return bool Returns true if the current line is a comment line, false otherwise
*/
private function isCurrentLineComment()
{
//checking explicitly the first char of the trim is faster than loops or strpos
$ltrimmedLine = ltrim($this->currentLine, ' ');
return $ltrimmedLine[0] === '#';
}
/**
* Cleanups a YAML string to be parsed.
*
* @param string $value The input YAML string
*
* @return string A cleaned up YAML string
*/
private function cleanup($value)
{
$value = str_replace(array("\r\n", "\r"), "\n", $value);
// strip YAML header
$count = 0;
$value = preg_replace('#^\%YAML[: ][\d\.]+.*\n#su', '', $value, -1, $count);
$this->offset += $count;
// remove leading comments
$trimmedValue = preg_replace('#^(\#.*?\n)+#s', '', $value, -1, $count);
if ($count == 1) {
// items have been removed, update the offset
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
$value = $trimmedValue;
}
// remove start of the document marker (---)
$trimmedValue = preg_replace('#^\-\-\-.*?\n#s', '', $value, -1, $count);
if ($count == 1) {
// items have been removed, update the offset
$this->offset += substr_count($value, "\n") - substr_count($trimmedValue, "\n");
$value = $trimmedValue;
// remove end of the document marker (...)
$value = preg_replace('#\.\.\.\s*$#s', '', $value);
}
return $value;
}
/**
* Returns true if the next line starts unindented collection
*
* @return bool Returns true if the next line starts unindented collection, false otherwise
*/
private function isNextLineUnIndentedCollection()
{
$currentIndentation = $this->getCurrentLineIndentation();
$notEOF = $this->moveToNextLine();
while ($notEOF && $this->isCurrentLineEmpty()) {
$notEOF = $this->moveToNextLine();
}
if (false === $notEOF) {
return false;
}
$ret = false;
if (
$this->getCurrentLineIndentation() == $currentIndentation
&&
$this->isStringUnIndentedCollectionItem($this->currentLine)
) {
$ret = true;
}
$this->moveToPreviousLine();
return $ret;
}
/**
* Returns true if the string is un-indented collection item
*
* @return bool Returns true if the string is un-indented collection item, false otherwise
*/
private function isStringUnIndentedCollectionItem()
{
return (0 === strpos($this->currentLine, '- '));
}
}

View file

@ -1,142 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml;
/**
* Unescaper encapsulates unescaping rules for single and double-quoted
* YAML strings.
*
* @author Matthew Lewinski <matthew@lewinski.org>
*/
class Unescaper
{
// Parser and Inline assume UTF-8 encoding, so escaped Unicode characters
// must be converted to that encoding.
// @deprecated since 2.5, to be removed in 3.0
const ENCODING = 'UTF-8';
// Regex fragment that matches an escaped character in a double quoted
// string.
const REGEX_ESCAPED_CHARACTER = "\\\\([0abt\tnvfre \\\"\\/\\\\N_LP]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8})";
/**
* Unescapes a single quoted string.
*
* @param string $value A single quoted string.
*
* @return string The unescaped string.
*/
public function unescapeSingleQuotedString($value)
{
return str_replace('\'\'', '\'', $value);
}
/**
* Unescapes a double quoted string.
*
* @param string $value A double quoted string.
*
* @return string The unescaped string.
*/
public function unescapeDoubleQuotedString($value)
{
$self = $this;
$callback = function ($match) use ($self) {
return $self->unescapeCharacter($match[0]);
};
// evaluate the string
return preg_replace_callback('/' . self::REGEX_ESCAPED_CHARACTER . '/u', $callback, $value);
}
/**
* Unescapes a character that was found in a double-quoted string
*
* @param string $value An escaped character
*
* @return string The unescaped character
*/
/** @noinspection PhpInconsistentReturnPointsInspection */
public function unescapeCharacter($value)
{
switch ($value{1}) {
case '0':
return "\x0";
case 'a':
return "\x7";
case 'b':
return "\x8";
case 't':
return "\t";
case "\t":
return "\t";
case 'n':
return "\n";
case 'v':
return "\xB";
case 'f':
return "\xC";
case 'r':
return "\r";
case 'e':
return "\x1B";
case ' ':
return ' ';
case '"':
return '"';
case '/':
return '/';
case '\\':
return '\\';
case 'N':
// U+0085 NEXT LINE
return "\xC2\x85";
case '_':
// U+00A0 NO-BREAK SPACE
return "\xC2\xA0";
case 'L':
// U+2028 LINE SEPARATOR
return "\xE2\x80\xA8";
case 'P':
// U+2029 PARAGRAPH SEPARATOR
return "\xE2\x80\xA9";
case 'x':
return self::utf8chr(hexdec(substr($value, 2, 2)));
case 'u':
return self::utf8chr(hexdec(substr($value, 2, 4)));
case 'U':
return self::utf8chr(hexdec(substr($value, 2, 8)));
}
}
/**
* Get the UTF-8 character for the given code point.
*
* @param int $c The unicode code point
*
* @return string The corresponding UTF-8 character
*/
private static function utf8chr($c)
{
if (0x80 > $c %= 0x200000) {
return chr($c);
}
if (0x800 > $c) {
return chr(0xC0 | $c >> 6) . chr(0x80 | $c & 0x3F);
}
if (0x10000 > $c) {
return chr(0xE0 | $c >> 12) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F);
}
return chr(0xF0 | $c >> 18) . chr(0x80 | $c >> 12 & 0x3F) . chr(0x80 | $c >> 6 & 0x3F) . chr(0x80 | $c & 0x3F);
}
}

View file

@ -1,100 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE_YAML
* file that was distributed with this source code.
*/
namespace Mage\Yaml;
use Mage\Yaml\Exception\ParseException;
/**
* Yaml offers convenience methods to load and dump YAML.
*
* @author Fabien Potencier <fabien@symfony.com>
*
* @api
*/
class Yaml
{
/**
* Parses YAML into a PHP array.
*
* The parse method, when supplied with a YAML stream (string or file),
* will do its best to convert YAML in a file into a PHP array.
*
* Usage:
* <code>
* $array = Yaml::parse('config.yml');
* print_r($array);
* </code>
*
* As this method accepts both plain strings and file names as an input,
* you must validate the input before calling this method. Passing a file
* as an input is a deprecated feature and will be removed in 3.0.
*
* @param string $input Path to a YAML file or a string containing YAML
* @param bool $exceptionOnInvalidType True if an exception must be thrown on invalid types false otherwise
* @param bool $objectSupport True if object support is enabled, false otherwise
*
* @return array The YAML converted to a PHP array
*
* @throws ParseException If the YAML is not valid
*
* @api
*/
public static function parse($input, $exceptionOnInvalidType = false, $objectSupport = false)
{
// if input is a file, process it
$file = '';
if (strpos($input, "\n") === false && is_file($input)) {
if (false === is_readable($input)) {
throw new ParseException(sprintf('Unable to parse "%s" as the file is not readable.', $input));
}
$file = $input;
$input = file_get_contents($file);
}
$yaml = new Parser();
try {
return $yaml->parse($input, $exceptionOnInvalidType, $objectSupport);
} catch (ParseException $e) {
if ($file) {
$e->setParsedFile($file);
}
throw $e;
}
}
/**
* Dumps a PHP array to a YAML string.
*
* The dump method, when supplied with an array, will do its best
* to convert the array into friendly YAML.
*
* @param array $array PHP array
* @param int $inline The level where you switch to inline YAML
* @param int $indent The amount of spaces to use for indentation of nested nodes.
* @param bool $exceptionOnInvalidType true if an exception must be thrown on invalid types (a PHP resource or object), false otherwise
* @param bool $objectSupport true if object support is enabled, false otherwise
*
* @return string A YAML string representing the original PHP array
*
* @api
*/
public static function dump($array, $inline = 2, $indent = 4, $exceptionOnInvalidType = false, $objectSupport = false)
{
$yaml = new Dumper();
$yaml->setIndentation($indent);
return $yaml->dump($array, $inline, 0, $exceptionOnInvalidType, $objectSupport);
}
}

View file

@ -1,75 +1,29 @@
# Magallanes #
# Magallanes
![Linters](https://github.com/andres-montanez/Magallanes/actions/workflows/linters.yml/badge.svg?)
![Tests](https://github.com/andres-montanez/Magallanes/actions/workflows/tests.yml/badge.svg?)
[![Coverage Status](https://img.shields.io/coveralls/andres-montanez/Magallanes/master.svg)](https://coveralls.io/github/andres-montanez/Magallanes?branch=master)
[![Code Quality](https://img.shields.io/scrutinizer/g/andres-montanez/Magallanes.svg)](https://scrutinizer-ci.com/g/andres-montanez/Magallanes/)
[![Latest Stable Version](https://img.shields.io/packagist/v/andres-montanez/magallanes.svg?label=stable)](https://packagist.org/packages/andres-montanez/magallanes)
[![Pre Release](https://img.shields.io/packagist/vpre/andres-montanez/magallanes.svg?label=dev)](https://packagist.org/packages/andres-montanez/magallanes)
[![Total Downloads](https://img.shields.io/packagist/dt/andres-montanez/magallanes.svg)](https://packagist.org/packages/andres-montanez/magallanes)
[![License](https://img.shields.io/packagist/l/andres-montanez/magallanes.svg)](https://packagist.org/packages/andres-montanez/magallanes)
[![SensioLabsInsight](https://insight.sensiolabs.com/projects/ed0de53a-a12e-459b-9464-34def5907b56/mini.png)](https://insight.sensiolabs.com/projects/ed0de53a-a12e-459b-9464-34def5907b56)
[![Build Status](https://travis-ci.org/andres-montanez/Magallanes.svg?branch=master)](https://travis-ci.org/andres-montanez/Magallanes)
[![Coverage Status](https://coveralls.io/repos/andres-montanez/Magallanes/badge.svg?branch=master)](https://coveralls.io/r/andres-montanez/Magallanes?branch=master)
### What's Magallanes? ###
Magallanes is a deployment tool for PHP applications; it's quite simple to use and manage.
It will get your application to a safe harbor.
### So, What can it do? ###
You can instruct Magallanes to deploy your code to all the servers you want (via rsync over ssh),
and run tasks for that freshly deployed code.
### How can I install it via composer? ###
### What's Magallanes?
**Magallanes** is a deployment tool for made with PHP for PHP applications; it's quite simple to use and manage. For more information and documentation please visit [magephp.com](https://www.magephp.com/)
### Installing
Simply add the following dependency to your projects composer.json file:
```js
```json
"require-dev": {
// ...
"andres-montanez/magallanes": "~1.0"
// ...
"andres-montanez/magallanes": "^5.0"
}
```
Now tell we update the vendors:
Finally you can use **Magallanes** from the vendor's bin:
```bash
$ php composer update andres-montanez/magallanes
vendor/bin/mage version
```
And finally we can use Magallanes from the vendor's bin:
```bash
$ bin/mage version
```
### System-wide installation with composer ###
```bash
$ composer global require "andres-montanez/magallanes=~1.0"
```
Make sure you have ~/.composer/vendor/bin/ in your path.
You can now use Magallanes by using the ````mage```` command.
### Can you give me some examples/ideas? ###
**Sure!**
Suppose you have a checkout of your app and you have to deploy it to four servers;
and after each deploy you have to run some boring tasks, like fixing file permissions, creating symlinks, etc.
You can define all this on Magallanes and with *just one command* you can do all this at once!
Like this:
```
$ mage deploy to:production
```
### What's this sorcery?! ###
Easy boy. It's not sorcery, just some *technomagick*!
In Magallanes you define environments like *testing*, *staging*, or *production* like on the example above.
Then, on that environment, you can configure a setup specifying to which hosts you want to deploy and what tasks to run (*after*, *on*, and *before* deploying).
And you are done!
### This is awesome! Where can I learn more? ###
You can read the whole source code (naaah!); or checkout the documentation at: http://magephp.com
Enjoy your magic trip with **Magallanes** to the land of the easily deployable apps!!
### "develop" branch ###
Please, all pull request now must be on the develop branch. Thanks!
### Codename Galactica
Each new mayor version of **Magallanes** will have a codename (like Ubuntu), version 3 was _Nostromo_, version 4 was _Discovery One_, and in the current version it is **_Galactica_**, in homage to the space battleship from the TV series Battlestar Galactica, both the '70s and the mind blowing revision of 2005.

View file

@ -1,38 +1,20 @@
#!/usr/bin/env php
<?php
/*
* This file is part of the Magallanes package.
*
* (c) Andrés Montañez <andres@andresmontanez.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
$timezone = ini_get('date.timezone');
if (empty($timezone)) {
date_default_timezone_set('UTC');
date_default_timezone_set('UTC');
if (file_exists(__DIR__ . '/../../../autoload.php')) {
require __DIR__ . '/../../../autoload.php';
} elseif (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require __DIR__ . '/../vendor/autoload.php';
}
$baseDir = dirname(dirname(__FILE__));
use Mage\MageApplication;
define('MAGALLANES_VERSION', '1.0.7');
define('MAGALLANES_DIRECTORY', $baseDir);
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
require_once __DIR__ . '/../vendor/autoload.php';
} else if (file_exists(__DIR__ . '/../../../autoload.php')) {
require_once __DIR__ . '/../../../autoload.php';
try {
$file = sprintf('%s/.mage.yml', getcwd());
$mage = new MageApplication($file);
$mage->run();
} catch (Exception $exception) {
printf('Error: %s' . PHP_EOL, $exception->getMessage());
exit(9);
}
require_once $baseDir . '/Mage/Autoload.php';
$loader = new \Mage\Autoload();
spl_autoload_register(array($loader, 'autoLoad'));
// Clean arguments
array_shift($argv);
// Run Magallanes
$console = new Mage\Console;
$exitCode = $console->run($argv);
exit((integer) $exitCode);

View file

@ -1,16 +0,0 @@
{
"files": ["LICENSE"],
"finder": [
{
"name": "*.php",
"exclude": [
"docs"
],
"in": "Mage"
}
],
"git-version": "git_tag",
"main": "bin/mage",
"output": "mage.phar",
"stub": true
}

File diff suppressed because it is too large Load diff

View file

@ -1,34 +1,50 @@
{
"name": "andres-montanez/magallanes",
"description": "A Deployment Tool for PHP Applications",
"homepage": "http://magephp.com",
"license": "MIT",
"type": "library",
"keywords": ["deployment"],
"require": {
"php": ">=5.4"
},
"require-dev": {
"phpunit/phpunit": "4.8.*",
"satooshi/php-coveralls": "~1.0",
"php-mock/php-mock": "^0.1"
},
"autoload": {
"psr-4": {
"Mage\\": "./Mage",
"Task\\": [".mage/tasks", "../../../.mage/tasks"],
"Command\\": [".mage/tasks", "../../../.mage/commands"]
}
},
"autoload-dev": {
"psr-4": {
"MageTest\\": "./tests/MageTest"
}
},
"config": {
"bin-dir": "bin"
},
"bin": [
"bin/mage"
]
"name": "andres-montanez/magallanes",
"description": "The Deployment Tool for PHP Applications",
"homepage": "https://magephp.com",
"license": "MIT",
"type": "library",
"keywords": ["deployment"],
"authors": [
{
"name": "Andrés Montañez",
"email": "andresmontanez@gmail.com"
}
],
"require": {
"php": "^8.0",
"monolog/monolog": "^2.5",
"symfony/console": "^6.0",
"symfony/filesystem": "^6.0",
"symfony/event-dispatcher": "^6.0",
"symfony/finder": "^6.0",
"symfony/yaml": "^6.0",
"symfony/process": "^6.0"
},
"require-dev": {
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.5",
"squizlabs/php_codesniffer": "^3.6",
"php-coveralls/php-coveralls": "^2.5"
},
"suggest": {
"ext-posix": "*"
},
"autoload": {
"psr-4": {
"Mage\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Mage\\Tests\\": "tests/"
}
},
"bin": ["bin/mage"],
"extra": {
"branch-alias": {
"dev-master": "5.0.x-dev",
"dev-galactica": "5.x-dev"
}
}
}

View file

@ -1,61 +0,0 @@
### List of Commands ###
# Installs Magallanes on the system
sudo mage install
# Displays Magallanes version
mage version
# Creats a Magallanes instance configuration
mage init
# Creates a compiled version of Magallanes using phar
mage compile
# Upgrades Magallanes itself
mage upgrade
# Add a new Environment configuration
mage add environment --name=production
# Add a new Environment configuration with releases enabled
mage add environment --name=production --enableReleases
# Performs a SCM Update, if configured
mage update
# Deploys Application to Production environment
mage deploy to:production
# Deploys Application to Production environment, overriding the current release
mage deploy to:production --overrideRelease
# Locks deployment to Production environment
mage lock to:production
# Unlocks deployment to Production environment
mage unlock to:production
# Lists all Releases on the Production environment
mage releases list to:production
# Rollback to the last Release on the Production environment
mage releases rollback to:production
mage releases rollback --release=0 to:production
# Rollback to the first, second, or thrith Release before the current Release on the Production environment
mage releases rollback --release=-1 to:production
mage releases rollback --release=-2 to:production
mage releases rollback --release=-3 to:production
# Rollback to a specific Release on the Production environment
# mage releases rollback --release=20120101172148 to:production
# Output logs by adding verbose option to ANY command
mage deploy to:production --verbose
### List of UPCOMING Commands ###
# mage config add host s05.example.com to:[production]
# mage config git git://github.com/andres-montanez/Zend-Framework-Twig-example-app.git
# mage config svn svn://example.com/repo
# mage task:deployment/rsync to:production

View file

@ -0,0 +1,7 @@
version: '2'
services:
php8.0:
container_name: mage-php8.0
build: ./php8.0
volumes:
- ../../:/home/magephp

View file

@ -0,0 +1,12 @@
FROM ubuntu:21.10
ENV DEBIAN_FRONTEND=noninteractive \
TZ=UTC
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y vim curl git unzip
RUN apt-get install -y php8.0-cli php8.0-zip php8.0-curl php8.0-xml php8.0-mbstring php8.0-xdebug
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer
WORKDIR /home/magephp

29
docs/example-config.yml Normal file
View file

@ -0,0 +1,29 @@
magephp:
environments:
production:
user: app
branch: test
from: ./
host_path: /var/www/test
releases: 4
exclude:
- var/cache
- var/log
hosts:
- webserver1
- webserver2
- webserver3
pre-deploy:
- git/update
- composer/install
- composer/dump-autoload
on-deploy:
- symfony/cache-warmup: { env: 'prod' }
- symfony/assets-install: { env: 'prod' }
- symfony/assetic-dump: { env: 'prod' }
on-release:
post-release:
post-deploy:
- magic
custom_tasks:
- App\Deployment\MagicTask

View file

@ -1,72 +0,0 @@
#master
deployment:
user: marl
from: ./
to: /var/www/test1
source:
type: git
repository: git@bitbucket.org:myuser/myproject.git
from: master
ioncube: test
releases:
enabled: true
symlink: current
directory: releases
hosts:
- localhost
tasks:
pre-deploy:
- ioncube/encrypt
on-deply:
post-deploy:
ioncube:
override: dyf
keeptemp:
encoder: ioncube_encoder54
checkencoding: true
checkignorepaths:
- /public/js/*
- /public/css/*
projfile: project.prj
project:
replace-target:
binary:
ignore-deprecated-warnings:
ignore-strict-warnings:
ignore:
- _*
- templates_c/*
- *~
- database.md
- specs/
- composer.json
- README.md
- .git/
- .project
- .settings/
- .buildpath
message-if-no-loader: "System error No Loader"
passphrase: "My really secure passphrase"
encrypt:
- templates/*
add-comment:
- 'Comment 1'
- 'Comment 2'
- "(c) ACTweb 2013"
- "Draft Version"
loader-event:
- corrupt-file=Corupted files
- expired-file=System needs updated
- no-permissions=Not allowed on this server
- clock-skew=Time incorect
- license-not-found=License not installed
- license-corrupt=Something wrong with your license
- license-expired=Out of time
- license-property-invalid=Invalid license data
- license-header-invalid=Files corupted
- license-server-invalid=Server problem
- unauth-including-file=Sorry these files can only be used within defined software
- unauth-included-file=Crtical Software Error
- unauth-append-prepend-file=System can not be used with PHP Prepend/Append set

View file

@ -1,27 +0,0 @@
#production
deployment:
user: root
from: ./
to: /var/www/vhosts/example.com/www
excludes:
- application/data/cache/twig/*
releases:
enabled: true
max: 5
symlink: current
directory: releases
hosts:
- s01.example.com
- s02.example.com
tasks:
pre-deploy:
- scm/update
on-deploy:
- symfony2/cache-warmup: { env: prod }
- privileges
- sampleTask
- sampleTaskRollbackAware
verbose_logging: true
env:
variables:
symfony_env: prod

View file

@ -1,38 +0,0 @@
#production
deployment:
user: root
from: ./
# source:
# type: git
# repository: git://github.com/andres-montanez/Magallanes.git
# from: master
# temporal: /tmp/myAppClone
to: /var/www/vhosts/example.com/www
excludes:
- application/data/cache/twig/*
releases:
enabled: true
max: 5
symlink: current
directory: releases
hosts:
- s01.example.com
- s02.example.com
# s02.example.com:
# deployment:
# user: toor
# to: /home/web/public
# releases:
# max: 10
# tasks:
# on-deploy:
# - privileges
tasks:
pre-deploy:
- scm/update
on-deploy:
- symfony2/cache-warmup: { env: prod }
- privileges
- sampleTask
- sampleTaskRollbackAware
#post-deploy:

View file

@ -1,43 +0,0 @@
#production
deployment:
user: root
from: ./
# source:
# type: git
# repository: git://github.com/andres-montanez/Magallanes.git
# from: master
# temporal: /tmp/myAppClone
to: /var/www/vhosts/example.com/www
excludes:
- application/data/cache/twig/*
releases:
enabled: true
max: 5
symlink: current
directory: releases
# This option allows to dump the symlink with the TarGz strategy and use the symlink on the deployment host.
# This is useful, if the files the symlink point to only exist on the deployment host and not on the host who runs the mage command.
# The default value is false to keep bc.
# See : http://linux.die.net/man/1/tar -h, --dereference
dump-symlinks: true
hosts:
- s01.example.com
- s02.example.com
# s02.example.com:
# deployment:
# user: toor
# to: /home/web/public
# releases:
# max: 10
# tasks:
# on-deploy:
# - privileges
tasks:
pre-deploy:
- scm/update
on-deploy:
- symfony2/cache-warmup: { env: prod }
- privileges
- sampleTask
- sampleTaskRollbackAware
#post-deploy:

View file

@ -1,32 +0,0 @@
#staging
deployment:
user: root
from: ./
to: /var/www
scm:
branch: master
releases:
enabled: true
max: 5
symlink: current
directory: releases
hosts:
- localhost
- 127.0.0.1
tasks:
pre-deploy:
# - sampleTask
# - failTask
- scm/update
on-deploy:
- privileges
# - sampleTask
- sampleTaskRollbackAware
- taskWithParameters:
booleanOption: true
- taskWithParameters
post-release:
# - sampleTask
post-deploy:
- sampleTask
verbose_logging: false

Some files were not shown because too many files have changed in this diff Show more