Browse Source

[Galactica] V5 initial scope

pull/456/head
Andrés Montañez 3 months ago
parent
commit
9a46f89266
No known key found for this signature in database
GPG Key ID: 97E9F675F4C03DE2
  1. 32
      .github/workflows/linters.yml
  2. 60
      .github/workflows/tests.yml
  3. 2
      .gitignore
  4. 3
      .travis.yml
  5. 28
      CHANGELOG.md
  6. 25
      CONTRIBUTING.md
  7. 2
      LICENSE
  8. 4
      README.md
  9. 26
      composer.json
  10. 6
      docs/dockers/docker-compose.yml
  11. 4
      docs/dockers/php8.0/Dockerfile
  12. 8
      docs/example-config.yml
  13. 14
      phpcs.xml.dist
  14. 4
      phpstan.neon
  15. 61
      phpunit.xml.dist
  16. 29
      src/Command/AbstractCommand.php
  17. 14
      src/Command/BuiltIn/Config/DumpCommand.php
  18. 14
      src/Command/BuiltIn/Config/EnvironmentsCommand.php
  19. 108
      src/Command/BuiltIn/DeployCommand.php
  20. 31
      src/Command/BuiltIn/Releases/ListCommand.php
  21. 35
      src/Command/BuiltIn/Releases/RollbackCommand.php
  22. 14
      src/Command/BuiltIn/VersionCommand.php
  23. 27
      src/Deploy/Strategy/ReleasesStrategy.php
  24. 27
      src/Deploy/Strategy/RsyncStrategy.php
  25. 42
      src/Deploy/Strategy/StrategyInterface.php
  26. 5
      src/Mage.php
  27. 43
      src/MageApplication.php
  28. 5
      src/Runtime/Exception/RuntimeException.php
  29. 231
      src/Runtime/Runtime.php
  30. 43
      src/Task/AbstractTask.php
  31. 11
      src/Task/BuiltIn/Composer/AbstractComposerTask.php
  32. 9
      src/Task/BuiltIn/Composer/DumpAutoloadTask.php
  33. 11
      src/Task/BuiltIn/Composer/InstallTask.php
  34. 86
      src/Task/BuiltIn/Composer/SelfUpdateTask.php
  35. 7
      src/Task/BuiltIn/Deploy/Release/CleanupTask.php
  36. 7
      src/Task/BuiltIn/Deploy/Release/PrepareTask.php
  37. 9
      src/Task/BuiltIn/Deploy/ReleaseTask.php
  38. 23
      src/Task/BuiltIn/Deploy/RsyncTask.php
  39. 7
      src/Task/BuiltIn/Deploy/Tar/CleanupTask.php
  40. 20
      src/Task/BuiltIn/Deploy/Tar/CopyTask.php
  41. 14
      src/Task/BuiltIn/Deploy/Tar/PrepareTask.php
  42. 13
      src/Task/BuiltIn/ExecTask.php
  43. 15
      src/Task/BuiltIn/FS/AbstractFileTask.php
  44. 14
      src/Task/BuiltIn/FS/ChangeModeTask.php
  45. 14
      src/Task/BuiltIn/FS/CopyTask.php
  46. 14
      src/Task/BuiltIn/FS/LinkTask.php
  47. 11
      src/Task/BuiltIn/FS/MoveTask.php
  48. 14
      src/Task/BuiltIn/FS/RemoveTask.php
  49. 19
      src/Task/BuiltIn/Git/ChangeBranchTask.php
  50. 13
      src/Task/BuiltIn/Git/UpdateTask.php
  51. 11
      src/Task/BuiltIn/Symfony/AbstractSymfonyTask.php
  52. 18
      src/Task/BuiltIn/Symfony/AssetsInstallTask.php
  53. 7
      src/Task/BuiltIn/Symfony/CacheClearTask.php
  54. 17
      src/Task/BuiltIn/Symfony/CachePoolClearTask.php
  55. 7
      src/Task/BuiltIn/Symfony/CachePoolPruneTask.php
  56. 7
      src/Task/BuiltIn/Symfony/CacheWarmupTask.php
  57. 7
      src/Task/Exception/ErrorException.php
  58. 5
      src/Task/Exception/SkipException.php
  59. 1
      src/Task/ExecuteOnRollbackInterface.php
  60. 42
      src/Task/TaskFactory.php
  61. 47
      src/Utils.php
  62. 49
      tests/Command/BuiltIn/DeployCommandWithReleasesTest.php
  63. 2
      tests/MageApplicationMockup.php
  64. 3
      tests/MageApplicationWindowsMockup.php
  65. 9
      tests/Runtime/ProcessMockup.php
  66. 18
      tests/Runtime/RuntimeMockup.php
  67. 2
      tests/Runtime/RuntimeWindowsMockup.php
  68. 8
      tests/Task/AbstractTaskTest.php
  69. 6
      tests/Task/BuiltIn/Composer/BasicComposerTask.php
  70. 174
      tests/Task/BuiltIn/Composer/SelfUpdateTaskTest.php
  71. 6
      tests/Task/Custom/NotInstantiableTask.php
  72. 6
      tests/Task/Custom/ValidTask.php
  73. 6
      tests/Task/CustomTask.php
  74. 6
      tests/Task/TestCaseFailTask.php
  75. 6
      tests/Task/TestCaseTask.php

32
.github/workflows/linters.yml

@ -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

60
.github/workflows/tests.yml

@ -0,0 +1,60 @@
name: PHPUnit
on:
push:
branches:
- master
- galactica
pull_request:
release:
types:
- created
jobs:
unit_tests_80:
name: Unit tests with PHP 8.0
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: Run tests
env:
XDEBUG_MODE: coverage
run: ./vendor/bin/phpunit --testsuite=unit
- name: Run Coveralls
env:
XDEBUG_MODE: coverage
run: ./vendor/bin/php-coveralls -v --coverage_clover build/logs/coverage.xml
unit_tests_81:
name: Unit tests with PHP 8.1
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.1
- name: Run tests
env:
XDEBUG_MODE: coverage
run: ./vendor/bin/phpunit --testsuite=unit
- name: Run Coveralls
env:
XDEBUG_MODE: coverage
run: ./vendor/bin/php-coveralls -v --coverage_clover build/logs/coverage.xml

2
.gitignore vendored

@ -3,4 +3,4 @@
composer.lock
.mage.yml
.phpunit.result.cache
.phpcs-cache

3
.travis.yml

@ -1,8 +1,7 @@
language: php
php:
- '7.2'
- '7.4'
- '8.0'
- '8.1'
install:
- composer install

28
CHANGELOG.md

@ -1,24 +1,8 @@
CHANGELOG for 4.X
CHANGELOG for 5.X
=================
* 4.1.1 (2021-02-20)
* Add `copyDirectory` option
* Bug fixes
* Improve testing and coverage
* 4.1.0 (2021-02-19)
* PHP 8 and Symfony 5 compatibility [PR#448]
* Timeout option for SSH [PR#436]
* Improve compatibility with Windows [PR#434] [PR#429]
* Improve config load [PR#422]
* Bug fixes [PR#448] [PR#424]
* Readme Update [PR#438]
* 4.0.0 (2018-04-02)
* v4 series release
* Refactored for Symfony 4 and PHP 7.1
* Symfony Pool Clear task added
* Symfony Pool Prune task added
* Symfony Assetic task removed
* 5.0.0 (2022-04-15)
* v5 series release
* Refactored for Symfony 6 and PHP 8
* Added strong types
* Removed task `composer/self-update`

25
CONTRIBUTING.md

@ -7,7 +7,7 @@ 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 `nostromo` branch
3. Create your pull request from `galactica` branch
4. Ensure your code is fully covered by tests
----------
@ -28,39 +28,36 @@ In order to have the PRs prioritized name them with the following tags.
[FEATURE] Create new PermissionsTask
[HOTFIX] Exception not caught on deployment
```
All Pull Requests must be done to the `nostromo` branch, only exception are Hotfixes.
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!
# Developing Magallanes
## Branches
The flow is pretty simple.
In most common cases we work on the `nostromo` 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.
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-nostromo` to dependency version in your `composer.json` file:
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-nostromo"
"andres-montanez/magallanes": "dev-galactica"
}
}
```
## Organization and code quality
We use [PSR2](http://www.php-fig.org/psr/psr-2/) as PHP coding standard.
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](https://phpmd.org/)
3. PHP Copy/Paste Detector
4. PHP Dead Code Detector
5. [PHP Coding Standards Fixer](http://cs.sensiolabs.org) with --level=psr2
1. PHPStan `./vendor/bin/phpstan analyse`
2. PHP Code Sniffer `./vendor/bin/phpcs`
## Testing and quality
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:
```
vendor/bin/phpunit --coverage-clover build/logs/coverage.xml
vendor/bin/coveralls -v --coverage_clover build/logs/coverage.xml
```bash
./vendor/bin/phpunit --coverage-clover build/logs/coverage.xml --coverage-text
./vendor/bin/php-coveralls -v --coverage_clover build/logs/coverage.xml
```
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.

2
LICENSE

@ -1,4 +1,4 @@
Copyright (c) 2011 - 2017 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

4
README.md

@ -16,7 +16,7 @@ Simply add the following dependency to your project’s composer.json file:
```json
"require-dev": {
"andres-montanez/magallanes": "^4.0"
"andres-montanez/magallanes": "^5.0"
}
```
Finally you can use **Magallanes** from the vendor's bin:
@ -26,4 +26,4 @@ vendor/bin/mage version
```
### Codename Discovery One
Each new mayor version of **Magallanes** will have a codename (like Ubuntu), version 3 was _Nostromo_, and in the current version it is **_Discovery One_**, in homage to the spaceship from the ground breaking film *2001: A Space Odyssey (1968)*.
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.

26
composer.json

@ -12,18 +12,20 @@
}
],
"require": {
"php": "^7.2 | ^8.0",
"monolog/monolog": "~1.11 | ^2.0",
"symfony/console": "^4.0 | ^5.0",
"symfony/filesystem": "^4.0 | ^5.0",
"symfony/event-dispatcher": "^4.0 | ^5.0",
"symfony/finder": "^4.0 | ^5.0",
"symfony/yaml": "^4.0 | ^5.0",
"symfony/process": "^4.0 | ^5.0"
"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": "^8.0",
"php-coveralls/php-coveralls": "~2.0"
"phpunit/phpunit": "^9.5",
"phpstan/phpstan": "^1.5",
"squizlabs/php_codesniffer": "^3.6",
"php-coveralls/php-coveralls": "^2.5"
},
"suggest": {
"ext-posix": "*"
@ -41,8 +43,8 @@
"bin": ["bin/mage"],
"extra": {
"branch-alias": {
"dev-master": "4.0.x-dev",
"dev-discovery-one": "4.x-dev"
"dev-master": "5.0.x-dev",
"dev-discovery-one": "5.x-dev"
}
}
}

6
docs/dockers/docker-compose.yml

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

4
docs/dockers/php7.4/Dockerfile → docs/dockers/php8.0/Dockerfile

@ -1,11 +1,11 @@
FROM ubuntu:20.04
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 php7.4-cli php-zip php7.4-curl php7.4-xml php7.4-mbstring php7.4-xdebug
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

8
docs/example-config.yml

@ -7,10 +7,8 @@ magephp:
host_path: /var/www/test
releases: 4
exclude:
- vendor
- app/cache
- app/log
- web/app_dev.php
- var/cache
- var/log
hosts:
- webserver1
- webserver2
@ -18,7 +16,7 @@ magephp:
pre-deploy:
- git/update
- composer/install
- composer/generate-autoload
- composer/dump-autoload
on-deploy:
- symfony/cache-warmup: { env: 'prod' }
- symfony/assets-install: { env: 'prod' }

14
phpcs.xml.dist

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="vendor/squizlabs/php_codesniffer/phpcs.xsd">
<arg name="basepath" value="."/>
<arg name="cache" value=".phpcs-cache"/>
<arg name="colors"/>
<arg name="extensions" value="php"/>
<rule ref="PSR12"/>
<file>src/</file>
</ruleset>

4
phpstan.neon

@ -0,0 +1,4 @@
parameters:
level: 6
paths:
- src

61
phpunit.xml.dist

@ -1,37 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.1/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="vendor/autoload.php"
>
<php>
<ini name="error_reporting" value="-1" />
<ini name="intl.default_locale" value="en" />
<ini name="intl.error_level" value="0" />
<ini name="memory_limit" value="-1" />
</php>
<testsuites>
<testsuite name="Magallanes Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>benchmark</group>
<group>intl-data</group>
</exclude>
</groups>
<filter>
<whitelist>
<directory>./src/</directory>
<exclude>
<directory>./tests/</directory>
</exclude>
</whitelist>
</filter>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" backupGlobals="false" colors="true" bootstrap="vendor/autoload.php">
<coverage>
<include>
<directory>./src/</directory>
</include>
<exclude>
<directory>./tests/</directory>
</exclude>
</coverage>
<php>
<ini name="error_reporting" value="-1"/>
<ini name="intl.default_locale" value="en"/>
<ini name="intl.error_level" value="0"/>
<ini name="memory_limit" value="-1"/>
</php>
<testsuites>
<testsuite name="Magallanes Test Suite">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<groups>
<exclude>
<group>benchmark</group>
<group>intl-data</group>
</exclude>
</groups>
</phpunit>

29
src/Command/AbstractCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -23,46 +24,30 @@ use Symfony\Component\Console\Command\Command;
*/
abstract class AbstractCommand extends Command
{
/**
* @var int
*/
protected $statusCode = 0;
/**
* @var Runtime Current Runtime instance
*/
protected $runtime;
protected int $statusCode = 0;
protected Runtime $runtime;
/**
* Set the Runtime configuration
*
* @param Runtime $runtime Runtime container
* @return AbstractCommand
*/
public function setRuntime(Runtime $runtime)
public function setRuntime(Runtime $runtime): self
{
$this->runtime = $runtime;
return $this;
}
/**
* Logs a message
*
* @param string $message
* @param string $level
*/
public function log($message, $level = LogLevel::DEBUG)
public function log(string $message, string $level = LogLevel::DEBUG): void
{
$this->runtime->log($message, $level);
}
/**
* Get the Human friendly Stage name
*
* @return string
*/
protected function getStageName()
protected function getStageName(): string
{
$utils = new Utils();
return $utils->getStageName($this->runtime->getStage());
@ -71,7 +56,7 @@ abstract class AbstractCommand extends Command
/**
* Requires the configuration to be loaded
*/
protected function requireConfig()
protected function requireConfig(): void
{
$app = $this->getApplication();
if ($app instanceof MageApplication) {

14
src/Command/BuiltIn/Config/DumpCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -24,22 +25,17 @@ class DumpCommand extends AbstractCommand
/**
* Configure the Command
*/
protected function configure()
protected function configure(): void
{
$this
->setName('config:dump')
->setDescription('Dumps the Magallanes configuration')
;
->setDescription('Dumps the Magallanes configuration');
}
/**
* Execute the Command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int|mixed
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->requireConfig();
@ -51,6 +47,6 @@ class DumpCommand extends AbstractCommand
$output->writeln('');
$output->writeln('Finished <fg=blue>Magallanes</>');
return 0;
return self::SUCCESS;
}
}

14
src/Command/BuiltIn/Config/EnvironmentsCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -25,22 +26,17 @@ class EnvironmentsCommand extends AbstractCommand
/**
* Configure the Command
*/
protected function configure()
protected function configure(): void
{
$this
->setName('config:environments')
->setDescription('List all Magallanes configured Environments')
;
->setDescription('List all Magallanes configured Environments');
}
/**
* Execute the Command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int|mixed
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->requireConfig();
@ -66,6 +62,6 @@ class EnvironmentsCommand extends AbstractCommand
$output->writeln('');
$output->writeln('Finished <fg=blue>Magallanes</>');
return 0;
return self::SUCCESS;
}
}

108
src/Command/BuiltIn/DeployCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -31,32 +32,30 @@ use Mage\Command\AbstractCommand;
*/
class DeployCommand extends AbstractCommand
{
/**
* @var TaskFactory
*/
protected $taskFactory;
protected TaskFactory $taskFactory;
/**
* Configure the Command
*/
protected function configure()
protected function configure(): void
{
$this
->setName('deploy')
->setDescription('Deploy code to hosts')
->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to')
->addOption('branch', null, InputOption::VALUE_REQUIRED, 'Force to switch to a branch other than the one defined', false)
;
->addOption(
'branch',
null,
InputOption::VALUE_REQUIRED,
'Force to switch to a branch other than the one defined',
false
);
}
/**
* Execute the Command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return integer
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->requireConfig();
@ -103,17 +102,15 @@ class DeployCommand extends AbstractCommand
$output->writeln('Finished <fg=blue>Magallanes</>');
return $this->statusCode;
return intval($this->statusCode);
}
/**
* Run the Deployment Process
*
* @param OutputInterface $output
* @param StrategyInterface $strategy
* @throws RuntimeException
*/
protected function runDeployment(OutputInterface $output, StrategyInterface $strategy)
protected function runDeployment(OutputInterface $output, StrategyInterface $strategy): void
{
// Run "Pre Deploy" Tasks
$this->runtime->setStage(Runtime::PRE_DEPLOY);
@ -140,16 +137,20 @@ class DeployCommand extends AbstractCommand
}
}
protected function runOnHosts(OutputInterface $output, $tasks)
/**
* @param string[] $tasks
*/
protected function runOnHosts(OutputInterface $output, array $tasks): void
{
$hosts = $this->runtime->getEnvOption('hosts');
if (!is_array($hosts) && !$hosts instanceof \Countable) {
$hosts = [];
}
if (count($hosts) == 0) {
if (count($hosts) === 0) {
$output->writeln(sprintf(' No hosts defined, skipping %s tasks', $this->getStageName()));
$output->writeln('');
return true;
return;
}
foreach ($hosts as $host) {
@ -165,21 +166,27 @@ class DeployCommand extends AbstractCommand
/**
* Runs all the tasks
*
* @param OutputInterface $output
* @param $tasks
* @return bool
* @param string[] $tasks
* @throws RuntimeException
*/
protected function runTasks(OutputInterface $output, $tasks)
protected function runTasks(OutputInterface $output, array $tasks): bool
{
if (count($tasks) == 0) {
$output->writeln(sprintf(' No tasks defined for <fg=black;options=bold>%s</> stage', $this->getStageName()));
$output->writeln(
sprintf(' No tasks defined for <fg=black;options=bold>%s</> stage', $this->getStageName())
);
$output->writeln('');
return true;
}
if ($this->runtime->getHostName() !== null) {
$output->writeln(sprintf(' Starting <fg=black;options=bold>%s</> tasks on host <fg=black;options=bold>%s</>:', $this->getStageName(), $this->runtime->getHostName()));
$output->writeln(
sprintf(
' Starting <fg=black;options=bold>%s</> tasks on host <fg=black;options=bold>%s</>:',
$this->getStageName(),
$this->runtime->getHostName()
)
);
} else {
$output->writeln(sprintf(' Starting <fg=black;options=bold>%s</> tasks:', $this->getStageName()));
}
@ -188,7 +195,6 @@ class DeployCommand extends AbstractCommand
$succeededTasks = 0;
foreach ($tasks as $taskName) {
/** @var AbstractTask $task */
$task = $this->taskFactory->get($taskName);
$output->write(sprintf(' Running <fg=magenta>%s</> ... ', $task->getDescription()));
$this->log(sprintf('Running task %s (%s)', $task->getDescription(), $task->getName()));
@ -196,25 +202,48 @@ class DeployCommand extends AbstractCommand
if ($this->runtime->inRollback() && !$task instanceof ExecuteOnRollbackInterface) {
$succeededTasks++;
$output->writeln('<fg=yellow>SKIPPED</>');
$this->log(sprintf('Task %s (%s) finished with SKIPPED, it was in a Rollback', $task->getDescription(), $task->getName()));
$this->log(
sprintf(
'Task %s (%s) finished with SKIPPED, it was in a Rollback',
$task->getDescription(),
$task->getName()
)
);
} else {
try {
if ($task->execute()) {
$succeededTasks++;
$output->writeln('<fg=green>OK</>');
$this->log(sprintf('Task %s (%s) finished with OK', $task->getDescription(), $task->getName()));
$this->log(
sprintf('Task %s (%s) finished with OK', $task->getDescription(), $task->getName())
);
} else {
$output->writeln('<fg=red>FAIL</>');
$this->statusCode = 180;
$this->log(sprintf('Task %s (%s) finished with FAIL', $task->getDescription(), $task->getName()));
$this->log(
sprintf('Task %s (%s) finished with FAIL', $task->getDescription(), $task->getName())
);
}
} catch (SkipException $exception) {
$succeededTasks++;
$output->writeln('<fg=yellow>SKIPPED</>');
$this->log(sprintf('Task %s (%s) finished with SKIPPED, thrown SkipException', $task->getDescription(), $task->getName()));
$this->log(
sprintf(
'Task %s (%s) finished with SKIPPED, thrown SkipException',
$task->getDescription(),
$task->getName()
)
);
} catch (ErrorException $exception) {
$output->writeln(sprintf('<fg=red>ERROR</> [%s]', $exception->getTrimmedMessage()));
$this->log(sprintf('Task %s (%s) finished with FAIL, with Error "%s"', $task->getDescription(), $task->getName(), $exception->getMessage()));
$this->log(
sprintf(
'Task %s (%s) finished with FAIL, with Error "%s"',
$task->getDescription(),
$task->getName(),
$exception->getMessage()
)
);
$this->statusCode = 190;
}
}
@ -229,7 +258,15 @@ class DeployCommand extends AbstractCommand
$alertColor = 'green';
}
$output->writeln(sprintf(' Finished <fg=%s>%d/%d</> tasks for <fg=black;options=bold>%s</>.', $alertColor, $succeededTasks, $totalTasks, $this->getStageName()));
$output->writeln(
sprintf(
' Finished <fg=%s>%d/%d</> tasks for <fg=black;options=bold>%s</>.',
$alertColor,
$succeededTasks,
$totalTasks,
$this->getStageName()
)
);
$output->writeln('');
return ($succeededTasks == $totalTasks);
@ -238,8 +275,11 @@ class DeployCommand extends AbstractCommand
/**
* Exception for halting the the current process
*/
protected function getException()
protected function getException(): RuntimeException
{
return new RuntimeException(sprintf('Stage "%s" did not finished successfully, halting command.', $this->getStageName()), 50);
return new RuntimeException(
sprintf('Stage "%s" did not finished successfully, halting command.', $this->getStageName()),
50
);
}
}

31
src/Command/BuiltIn/Releases/ListCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -25,31 +26,21 @@ use Mage\Command\AbstractCommand;
*/
class ListCommand extends AbstractCommand
{
/**
* @var int
*/
protected $statusCode = 0;
/**
* Configure the Command
*/
protected function configure()
protected function configure(): void
{
$this
->setName('releases:list')
->setDescription('List the releases on an environment')
->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to')
;
->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to');
}
/**
* Execute the Command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int|mixed
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->requireConfig();
@ -102,7 +93,9 @@ class ListCommand extends AbstractCommand
}
if (count($releases) == 0) {
$output->writeln(sprintf(' No releases available on host <fg=black;options=bold>%s</>:', $host));
$output->writeln(
sprintf(' No releases available on host <fg=black;options=bold>%s</>:', $host)
);
} else {
// Get Current Release
$cmdCurrentRelease = sprintf('readlink -f %s/current', $hostPath);
@ -110,7 +103,10 @@ class ListCommand extends AbstractCommand
/** @var Process $process */
$process = $this->runtime->runRemoteCommand($cmdCurrentRelease, false);
if (!$process->isSuccessful()) {
throw new RuntimeException(sprintf('Unable to retrieve current release from host "%s"', $host), 85);
throw new RuntimeException(
sprintf('Unable to retrieve current release from host "%s"', $host),
85
);
}
$currentReleaseId = explode('/', trim($process->getOutput()));
@ -121,7 +117,8 @@ class ListCommand extends AbstractCommand
foreach ($releases as $releaseId) {
$releaseDate = $utils->getReleaseDate($releaseId);
$output->write(sprintf(' Release ID: <fg=magenta>%s</> - Date: <fg=black;options=bold>%s</> [%s]',
$output->write(sprintf(
' Release ID: <fg=magenta>%s</> - Date: <fg=black;options=bold>%s</> [%s]',
$releaseId,
$releaseDate->format('Y-m-d H:i:s'),
$utils->getTimeDiff($releaseDate)
@ -146,6 +143,6 @@ class ListCommand extends AbstractCommand
$output->writeln('Finished <fg=blue>Magallanes</>');
return $this->statusCode;
return intval($this->statusCode);
}
}

35
src/Command/BuiltIn/Releases/RollbackCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -25,32 +26,22 @@ use Mage\Command\BuiltIn\DeployCommand;
*/
class RollbackCommand extends DeployCommand
{
/**
* @var int
*/
protected $statusCode = 0;
/**
* Configure the Command
*/
protected function configure()
protected function configure(): void
{
$this
->setName('releases:rollback')
->setDescription('Rollback to a release on an environment')
->addArgument('environment', InputArgument::REQUIRED, 'Name of the environment to deploy to')
->addArgument('release', InputArgument::REQUIRED, 'The ID or the Index of the release to rollback to')
;
->addArgument('release', InputArgument::REQUIRED, 'The ID or the Index of the release to rollback to');
}
/**
* Execute the Command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int|mixed
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->requireConfig();
@ -68,11 +59,14 @@ class RollbackCommand extends DeployCommand
}
$releaseToRollback = $input->getArgument('release');
if (($releaseId = $this->checkReleaseAvailability($releaseToRollback)) === false) {
throw new RuntimeException(sprintf('Release "%s" is not available on all hosts', $releaseToRollback), 72);
if ($this->checkReleaseAvailability($releaseToRollback) === false) {
throw new RuntimeException(
sprintf('Release "%s" is not available on all hosts', $releaseToRollback),
72
);
}
$this->runtime->setReleaseId($releaseId)->setRollback(true);
$this->runtime->setReleaseId($releaseToRollback)->setRollback(true);
$output->writeln(sprintf(' Environment: <fg=green>%s</>', $this->runtime->getEnvironment()));
$this->log(sprintf('Environment: %s', $this->runtime->getEnvironment()));
@ -95,16 +89,13 @@ class RollbackCommand extends DeployCommand
$output->writeln('Finished <fg=blue>Magallanes</>');
return $this->statusCode;
return intval($this->statusCode);
}
/**
* Check if the provided Release ID is available in all hosts
*
* @param string $releaseToRollback Release ID
* @return bool
*/
protected function checkReleaseAvailability($releaseToRollback)
protected function checkReleaseAvailability(string $releaseToRollback): bool
{
$hosts = $this->runtime->getEnvOption('hosts');
$hostPath = rtrim($this->runtime->getEnvOption('host_path'), '/');
@ -132,7 +123,7 @@ class RollbackCommand extends DeployCommand
}
if ($availableInHosts === count($hosts)) {
return $releaseToRollback;
return (bool) $releaseToRollback;
}
return false;

14
src/Command/BuiltIn/VersionCommand.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -25,25 +26,20 @@ class VersionCommand extends AbstractCommand
/**
* Configure the Command
*/
protected function configure()
protected function configure(): void
{
$this
->setName('version')
->setDescription('Get the version of Magallanes')
;
->setDescription('Get the version of Magallanes');
}
/**
* Executes the Command
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output)
protected function execute(InputInterface $input, OutputInterface $output): int
{
$output->writeln(sprintf('Magallanes v%s [%s]', Mage::VERSION, Mage::CODENAME));
return 0;
return self::SUCCESS;
}
}

27
src/Deploy/Strategy/ReleasesStrategy.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -20,22 +21,19 @@ use Mage\Runtime\Runtime;
*/
class ReleasesStrategy implements StrategyInterface
{
/**
* @var Runtime
*/
protected $runtime;
protected Runtime $runtime;
public function getName()
public function getName(): string
{
return 'Releases';
}
public function setRuntime(Runtime $runtime)
public function setRuntime(Runtime $runtime): void
{
$this->runtime = $runtime;
}
public function getPreDeployTasks()
public function getPreDeployTasks(): array
{
$this->checkStage(Runtime::PRE_DEPLOY);
$tasks = $this->runtime->getTasks();
@ -51,7 +49,7 @@ class ReleasesStrategy implements StrategyInterface
return $tasks;
}
public function getOnDeployTasks()
public function getOnDeployTasks(): array
{
$this->checkStage(Runtime::ON_DEPLOY);
$tasks = $this->runtime->getTasks();
@ -67,7 +65,7 @@ class ReleasesStrategy implements StrategyInterface
return $tasks;
}
public function getOnReleaseTasks()
public function getOnReleaseTasks(): array
{
$this->checkStage(Runtime::ON_RELEASE);
$tasks = $this->runtime->getTasks();
@ -79,7 +77,7 @@ class ReleasesStrategy implements StrategyInterface
return $tasks;
}
public function getPostReleaseTasks()
public function getPostReleaseTasks(): array
{
$this->checkStage(Runtime::POST_RELEASE);
$tasks = $this->runtime->getTasks();
@ -91,7 +89,7 @@ class ReleasesStrategy implements StrategyInterface
return $tasks;
}
public function getPostDeployTasks()
public function getPostDeployTasks(): array
{
$this->checkStage(Runtime::POST_DEPLOY);
$tasks = $this->runtime->getTasks();
@ -110,13 +108,14 @@ class ReleasesStrategy implements StrategyInterface
/**
* Check the runtime stage is correct
*
* @param string $stage
* @throws RuntimeException
*/
private function checkStage($stage)
private function checkStage(string $stage): void
{
if ($this->runtime->getStage() !== $stage) {
throw new RuntimeException(sprintf('Invalid stage, got "%s" but expected "%s"', $this->runtime->getStage(), $stage));
throw new RuntimeException(
sprintf('Invalid stage, got "%s" but expected "%s"', $this->runtime->getStage(), $stage)
);
}
}
}

27
src/Deploy/Strategy/RsyncStrategy.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -20,22 +21,19 @@ use Mage\Runtime\Runtime;
*/
class RsyncStrategy implements StrategyInterface
{
/**
* @var Runtime
*/
protected $runtime;
protected Runtime $runtime;
public function getName()
public function getName(): string
{
return 'Rsync';
}
public function setRuntime(Runtime $runtime)
public function setRuntime(Runtime $runtime): void
{
$this->runtime = $runtime;
}
public function getPreDeployTasks()
public function getPreDeployTasks(): array
{
$this->checkStage(Runtime::PRE_DEPLOY);
$tasks = $this->runtime->getTasks();
@ -47,7 +45,7 @@ class RsyncStrategy implements StrategyInterface
return $tasks;
}
public function getOnDeployTasks()
public function getOnDeployTasks(): array
{
$this->checkStage(Runtime::ON_DEPLOY);
$tasks = $this->runtime->getTasks();
@ -59,17 +57,17 @@ class RsyncStrategy implements StrategyInterface
return $tasks;
}
public function getOnReleaseTasks()
public function getOnReleaseTasks(): array
{
return [];
}
public function getPostReleaseTasks()
public function getPostReleaseTasks(): array
{
return [];
}
public function getPostDeployTasks()
public function getPostDeployTasks(): array
{
$this->checkStage(Runtime::POST_DEPLOY);
$tasks = $this->runtime->getTasks();
@ -84,13 +82,14 @@ class RsyncStrategy implements StrategyInterface
/**
* Check the runtime stage is correct
*
* @param $stage
* @throws RuntimeException
*/
private function checkStage($stage)
private function checkStage(string $stage): void
{
if ($this->runtime->getStage() !== $stage) {
throw new RuntimeException(sprintf('Invalid stage, got "%s" but expected "%s"', $this->runtime->getStage(), $stage));
throw new RuntimeException(
sprintf('Invalid stage, got "%s" but expected "%s"', $this->runtime->getStage(), $stage)
);
}
}
}

42
src/Deploy/Strategy/StrategyInterface.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -19,17 +20,32 @@ use Mage\Runtime\Runtime;
*/
interface StrategyInterface
{
public function getName();
public function setRuntime(Runtime $runtime);
public function getPreDeployTasks();
public function getOnDeployTasks();
public function getOnReleaseTasks();
public function getPostReleaseTasks();
public function getPostDeployTasks();
public function getName(): string;
public function setRuntime(Runtime $runtime): void;
/**
* @return string[]
*/
public function getPreDeployTasks(): array;
/**
* @return string[]
*/
public function getOnDeployTasks(): array;
/**
* @return string[]
*/
public function getOnReleaseTasks(): array;
/**
* @return string[]
*/
public function getPostReleaseTasks(): array;
/**
* @return string[]
*/
public function getPostDeployTasks(): array;
}

5
src/Mage.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -17,6 +18,6 @@ namespace Mage;
*/
class Mage
{
const VERSION = '4.1.1';
const CODENAME = 'Discovery One';
public const VERSION = '5.0.0';
public const CODENAME = 'Galactica';
}

43
src/MageApplication.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -22,7 +23,6 @@ use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Application;
use Symfony\Component\Yaml\Parser;
use Symfony\Component\Yaml\Exception\ParseException;
use ReflectionClass;
use Mage\Runtime\Exception\RuntimeException;
/**
@ -32,13 +32,13 @@ use Mage\Runtime\Exception\RuntimeException;
*/
class MageApplication extends Application
{
protected $runtime;
protected $file;
protected Runtime $runtime;
protected string $file;
/**
* @param string $file The YAML file from which to read the configuration
*/
public function __construct($file)
public function __construct(string $file)
{
parent::__construct('Magallanes', Mage::VERSION);
@ -49,7 +49,9 @@ class MageApplication extends Application
$dispatcher->addListener(ConsoleEvents::ERROR, function (ConsoleErrorEvent $event) {
$output = $event->getOutput();
$command = $event->getCommand();
$output->writeln(sprintf('Oops, exception thrown while running command <info>%s</info>', $command->getName()));
$output->writeln(
sprintf('Oops, exception thrown while running command <info>%s</info>', $command->getName())
);
$exitCode = $event->getExitCode();
$event->setError(new \LogicException('Caught exception', $exitCode, $event->getError()));
});
@ -63,7 +65,7 @@ class MageApplication extends Application
*
* @throws RuntimeException
*/
public function configure()
public function configure(): void
{
if (!file_exists($this->file) || !is_readable($this->file)) {
throw new RuntimeException(sprintf('The file "%s" does not exists or is not readable.', $this->file));
@ -78,15 +80,22 @@ class MageApplication extends Application
if (array_key_exists('magephp', $config) && is_array($config['magephp'])) {
$logger = null;
if (array_key_exists('log_dir', $config['magephp']) && file_exists($config['magephp']['log_dir']) && is_dir($config['magephp']['log_dir'])) {
if (
array_key_exists('log_dir', $config['magephp']) &&
file_exists($config['magephp']['log_dir']) && is_dir($config['magephp']['log_dir'])
) {
$logfile = sprintf('%s/%s.log', $config['magephp']['log_dir'], date('Ymd_His'));
$config['magephp']['log_file'] = $logfile;
$logger = new Logger('magephp');
$logger->pushHandler(new StreamHandler($logfile));
} elseif (array_key_exists('log_dir', $config['magephp']) && !is_dir($config['magephp']['log_dir'])) {
throw new RuntimeException(sprintf('The configured log_dir "%s" does not exists or is not a directory.', $config['magephp']['log_dir']));
throw new RuntimeException(
sprintf(
'The configured log_dir "%s" does not exists or is not a directory.',
$config['magephp']['log_dir']
)
);
}
$this->runtime->setConfiguration($config['magephp']);
@ -94,13 +103,15 @@ class MageApplication extends Application
return;
}
throw new RuntimeException(sprintf('The file "%s" does not have a valid Magallanes configuration.', $this->file));
throw new RuntimeException(
sprintf('The file "%s" does not have a valid Magallanes configuration.', $this->file)
);
}
/**
* Loads the BuiltIn Commands
*/
protected function loadBuiltInCommands()
protected function loadBuiltInCommands(): void
{
$finder = new Finder();
$finder->files()->in(__DIR__ . '/Command/BuiltIn')->name('*Command.php');
@ -109,7 +120,7 @@ class MageApplication extends Application
foreach ($finder as $file) {
$class = substr('\\Mage\\Command\\BuiltIn\\' . str_replace('/', '\\', $file->getRelativePathname()), 0, -4);
if (class_exists($class)) {
$reflex = new ReflectionClass($class);
$reflex = new \ReflectionClass($class);
if ($reflex->isInstantiable()) {
$command = new $class();
if ($command instanceof AbstractCommand) {
@ -123,20 +134,16 @@ class MageApplication extends Application
/**
* Gets the Runtime instance to use
*
* @return Runtime
*/
protected function instantiateRuntime()
protected function instantiateRuntime(): Runtime
{
return new Runtime();
}
/**
* Get the Runtime instance
*
* @return Runtime
*/
public function getRuntime()
public function getRuntime(): Runtime
{
return $this->runtime;
}

5
src/Runtime/Exception/RuntimeException.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -10,13 +11,11 @@
namespace Mage\Runtime\Exception;
use Exception;
/**
* An Error occurred while running
*
* @author Andrés Montañez <andresmontanez@gmail.com>
*/
class RuntimeException extends Exception
class RuntimeException extends \Exception
{
}

231
src/Runtime/Runtime.php

@ -1,4 +1,5 @@
<?php
/*
* This file is part of the Magallanes package.
*
@ -17,6 +18,7 @@ use Psr\Log\LoggerInterface;
use Psr\Log\LogLevel;
use Symfony\Component\Process\Process;
use Mage\Runtime\Exception\RuntimeException;
use Mage\Task\AbstractTask;