diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..5a67aed --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: Fix Code Style + +on: [push] + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: true + matrix: + php: [8.4] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: json, dom, curl, libxml, mbstring + coverage: none + + - name: Install Pint + run: composer global require laravel/pint + + - name: Run Pint + run: pint + + - name: Commit linted files + uses: stefanzweifel/git-auto-commit-action@v5 diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml new file mode 100644 index 0000000..8b884e1 --- /dev/null +++ b/.github/workflows/phpstan.yml @@ -0,0 +1,37 @@ +name: PHPStan + +on: + pull_request: + types: [opened, synchronize, reopened] + +jobs: + phpstan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + coverage: none + tools: composer + + - name: Get Composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Setup cache + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: + composer install --prefer-dist --no-suggest --no-progress + + - name: Run PHPStan + run: ./vendor/bin/phpstan analyse diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 3365e04..391d045 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -9,14 +9,12 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [7.4, 8.0] - laravel: [8.*, 7.*] + php: [8.3, 8.4] + laravel: [11.*] dependency-version: [prefer-lowest, prefer-stable] include: - - laravel: 8.* - testbench: 6.* - - laravel: 7.* - testbench: 5.* + - laravel: 11.* + testbench: 9.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} - ${{ matrix.os }} @@ -28,7 +26,7 @@ jobs: uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv coverage: none - name: Install dependencies diff --git a/README.md b/README.md index 72215a4..88bbfcb 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@ The purpose of this package is to introduce local zero-downtime deployments into ## Requirements -* Laravel 7 | 8 -* PHP ^7.4 | ^8.0 +* Laravel >= 11.x+ +* PHP >= 8.3 ## Installation diff --git a/composer.json b/composer.json index 9898d1d..b1f0c11 100644 --- a/composer.json +++ b/composer.json @@ -11,13 +11,15 @@ } ], "require": { - "php": "^7.4|^8.0" + "php": "^8.3" }, "require-dev": { - "laravel/framework": "^8.12", - "mockery/mockery": "^1.4", - "phpunit/phpunit": "^9.3.3", - "orchestra/testbench": "^6.9" + "laravel/framework": "^11.0", + "mockery/mockery": "^1.6.10", + "phpunit/phpunit": "^10.5.35|^11.3.6", + "orchestra/testbench": "^9.0", + "laravel/pint": "^1.20", + "phpstan/phpstan": "^2.1" }, "autoload": { "psr-4": { diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..2621711 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +parameters: + + paths: + - src + + # The level 9 is the highest level + level: 5 + + universalObjectCratesClasses: + - Illuminate\Http\Resources\Json\JsonResource diff --git a/phpunit.xml b/phpunit.xml index c18e235..ab8ec23 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,27 +1,23 @@ - - - - ./tests/Unit - - - ./tests/Integration - - - - - ./app - - - - - - - - - - + + + + ./tests/Unit + + + ./tests/Feature + + + + + + + + + + + + ./app + + diff --git a/src/AtomicDeploymentsServiceProvider.php b/src/AtomicDeploymentsServiceProvider.php index 459c106..4d19162 100644 --- a/src/AtomicDeploymentsServiceProvider.php +++ b/src/AtomicDeploymentsServiceProvider.php @@ -10,12 +10,12 @@ class AtomicDeploymentsServiceProvider extends ServiceProvider { - public function boot() + public function boot(): void { $this->loadMigrationsFrom(__DIR__.'/../database/migrations'); } - public function register() + public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/atomic-deployments.php', 'atomic-deployments'); $this->registerPublishables(); @@ -24,7 +24,7 @@ public function register() $this->app->bind(DeploymentInterface::class, config('atomic-deployments.deployment-class')); $this->app->bind(AtomicDeploymentService::class, function ($app, $params) { - if (empty($params) || (count($params) && !is_a($params[0], DeploymentInterface::class))) { + if (empty($params) || (count($params) && ! is_a($params[0], DeploymentInterface::class))) { array_unshift($params, $app->make(DeploymentInterface::class)); } diff --git a/src/Commands/DeployCommand.php b/src/Commands/DeployCommand.php index 8644b57..b1e6594 100644 --- a/src/Commands/DeployCommand.php +++ b/src/Commands/DeployCommand.php @@ -8,17 +8,18 @@ use JTMcC\AtomicDeployments\Services\AtomicDeploymentService; use JTMcC\AtomicDeployments\Services\Deployment; use JTMcC\AtomicDeployments\Services\Output; +use Throwable; class DeployCommand extends BaseCommand { - protected $signature = 'atomic-deployments:deploy - {--hash= : Specify a previous deployments commit hash/deploy-dir to deploy } - {--directory= : Define your deploy folder name. Defaults to current HEAD hash } - {--dry-run : Test and log deployment steps }'; + protected $signature = 'atomic-deployments:deploy + {--hash= : Specify a previous deployments commit hash/deploy-dir to deploy } + {--directory= : Define your deploy folder name. Defaults to current HEAD hash } + {--dry-run : Test and log deployment steps }'; protected $description = 'Deploy a clone of your latest build and attach symlink'; - public function handle() + public function handle(): void { Output::alert('Running Atomic Deployment'); @@ -26,46 +27,63 @@ public function handle() $dryRun = $this->option('dry-run'); if ($hash = $this->option('hash')) { - Output::info("Updating symlink to previous build: {$hash}"); - - $deploymentModel = AtomicDeployment::successful()->where('commit_hash', $hash)->first(); - - if (!$deploymentModel || !$deploymentModel->hasDeployment) { - Output::warn("Build not found for hash: {$hash}"); - } else { - $atomicDeployment = AtomicDeploymentService::create( - new Deployment($deploymentModel), - $migrate, - $dryRun - ); - - try { - $atomicDeployment->getDeployment()->link(); - $atomicDeployment->confirmSymbolicLink(); - DeploymentSuccessful::dispatch($atomicDeployment, $deploymentModel); - } catch (\Throwable $e) { - $atomicDeployment->fail(); - Output::throwable($e); - } - } + $this->deployPreviousBuild($hash, $migrate, $dryRun); } else { - $atomicDeployment = AtomicDeploymentService::create($migrate, $dryRun); - - Output::info('Running Deployment...'); - - try { - if ($deployDir = trim($this->option('directory'))) { - Output::info("Deployment directory option set - Deployment will use directory: {$deployDir} "); - $atomicDeployment->getDeployment()->setDirectory($deployDir); - } - $atomicDeployment->deploy(fn () => $atomicDeployment->cleanBuilds(config('atomic-deployments.build-limit'))); - } catch (\Throwable $e) { - $atomicDeployment->fail(); - Output::throwable($e); - } + $this->deployCurrentBuild($migrate, $dryRun); } Output::info('Finished'); ConsoleOutput::line(''); } + + private function deployPreviousBuild(string $hash, array $migrate, bool $dryRun): void + { + Output::info("Updating symlink to previous build: {$hash}"); + + /** @var null|AtomicDeployment $deploymentModel */ + $deploymentModel = AtomicDeployment::successful()->where('commit_hash', $hash)->first(); + + if (! $deploymentModel?->has_deployment) { + Output::warn("Build not found for hash: {$hash}"); + + return; + } + + $atomicDeployment = AtomicDeploymentService::create( + new Deployment($deploymentModel), + $migrate, + $dryRun + ); + + try { + $atomicDeployment->getDeployment()->link(); + $atomicDeployment->confirmSymbolicLink(); + DeploymentSuccessful::dispatch($atomicDeployment, $deploymentModel); + } catch (Throwable $e) { + $atomicDeployment->fail(); + Output::throwable($e); + } + } + + private function deployCurrentBuild(array $migrate, bool $dryRun): void + { + $atomicDeployment = AtomicDeploymentService::create($migrate, $dryRun); + + Output::info('Running Deployment...'); + + try { + if ($deployDir = trim($this->option('directory'))) { + Output::info("Deployment directory option set - Deployment will use directory: {$deployDir}"); + $atomicDeployment->getDeployment()->setDirectory($deployDir); + } + + $atomicDeployment + ->deploy( + fn () => $atomicDeployment->cleanBuilds(config('atomic-deployments.build-limit')) + ); + } catch (Throwable $e) { + $atomicDeployment->fail(); + Output::throwable($e); + } + } } diff --git a/src/Commands/ListCommand.php b/src/Commands/ListCommand.php index 0e7528a..9852610 100644 --- a/src/Commands/ListCommand.php +++ b/src/Commands/ListCommand.php @@ -4,7 +4,6 @@ use JTMcC\AtomicDeployments\Helpers\ConsoleOutput; use JTMcC\AtomicDeployments\Models\AtomicDeployment; -use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus; class ListCommand extends BaseCommand { @@ -12,26 +11,29 @@ class ListCommand extends BaseCommand protected $description = 'List currently available deployments'; - public function handle() + public function handle(): void { ConsoleOutput::line(''); ConsoleOutput::alert('Available Deployments'); - $deployments = AtomicDeployment::select( - 'id', - 'commit_hash', - 'deployment_path', - 'deployment_link', - 'deployment_status', - 'created_at', - )->get()->map(function ($deployment) { - $deployment->append('isCurrentlyDeployed'); - $deployment->deployment_status = DeploymentStatus::getNameFromValue($deployment->deployment_status); - - return $deployment; - }); - - if (!$deployments->count()) { + $deployments = AtomicDeployment::query() + ->select([ + 'id', + 'commit_hash', + 'deployment_path', + 'deployment_link', + 'deployment_status', + 'created_at', + ]) + ->get() + // @phpstan-ignore-next-line + ->map(function (AtomicDeployment $deployment) { + $deployment->append('is_currently_deployed'); + + return $deployment; + }); + + if (! $deployments->count()) { ConsoleOutput::info('No deployments found'); return; @@ -39,6 +41,7 @@ public function handle() $titles = ['ID', 'Commit Hash', 'Path', 'SymLink', 'Status', 'Created', 'Live']; + // @phpstan-ignore-next-line ConsoleOutput::table($titles, $deployments); ConsoleOutput::line(''); } diff --git a/src/Events/DeploymentFailed.php b/src/Events/DeploymentFailed.php index e028d83..ce2c761 100644 --- a/src/Events/DeploymentFailed.php +++ b/src/Events/DeploymentFailed.php @@ -14,15 +14,13 @@ class DeploymentFailed implements ShouldQueue use SerializesModels; public AtomicDeploymentService $deploymentService; + public ?AtomicDeployment $deployment = null; /** * DeploymentSuccessful constructor. - * - * @param AtomicDeploymentService $deploymentService - * @param AtomicDeployment|null $deployment */ - public function __construct(AtomicDeploymentService $deploymentService, AtomicDeployment $deployment = null) + public function __construct(AtomicDeploymentService $deploymentService, ?AtomicDeployment $deployment = null) { $this->deploymentService = $deploymentService; $this->deployment = $deployment; diff --git a/src/Events/DeploymentSuccessful.php b/src/Events/DeploymentSuccessful.php index ec93aa5..8fb5d7e 100644 --- a/src/Events/DeploymentSuccessful.php +++ b/src/Events/DeploymentSuccessful.php @@ -14,15 +14,13 @@ class DeploymentSuccessful implements ShouldQueue use SerializesModels; public AtomicDeploymentService $deploymentService; + public ?AtomicDeployment $deployment = null; /** * DeploymentSuccessful constructor. - * - * @param AtomicDeploymentService $deploymentService - * @param AtomicDeployment|null $deployment */ - public function __construct(AtomicDeploymentService $deploymentService, AtomicDeployment $deployment = null) + public function __construct(AtomicDeploymentService $deploymentService, ?AtomicDeployment $deployment = null) { $this->deploymentService = $deploymentService; $this->deployment = $deployment; diff --git a/src/Exceptions/AreYouInsaneException.php b/src/Exceptions/AreYouInsaneException.php index 99d53c9..fd9c477 100644 --- a/src/Exceptions/AreYouInsaneException.php +++ b/src/Exceptions/AreYouInsaneException.php @@ -7,7 +7,7 @@ class AreYouInsaneException extends Exception { - public function __construct($message = '', $code = 0, Throwable $previous = null) + public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) { parent::__construct("(╯°□°)╯︵ ┻━┻: {$message}", $code, $previous); } diff --git a/src/Exceptions/ExecuteFailedException.php b/src/Exceptions/ExecuteFailedException.php index cbc33f1..478753e 100644 --- a/src/Exceptions/ExecuteFailedException.php +++ b/src/Exceptions/ExecuteFailedException.php @@ -7,7 +7,7 @@ class ExecuteFailedException extends Exception { - public function __construct($message = '', $code = 0, Throwable $previous = null) + public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) { parent::__construct("exec failed: {$message}", $code, $previous); } diff --git a/src/Exceptions/InvalidPathException.php b/src/Exceptions/InvalidPathException.php index 9600692..d054273 100644 --- a/src/Exceptions/InvalidPathException.php +++ b/src/Exceptions/InvalidPathException.php @@ -7,7 +7,7 @@ class InvalidPathException extends Exception { - public function __construct($message = '', $code = 0, Throwable $previous = null) + public function __construct(string $message = '', int $code = 0, ?Throwable $previous = null) { parent::__construct("Invalid Path: {$message}", $code, $previous); } diff --git a/src/Helpers/ConsoleOutput.php b/src/Helpers/ConsoleOutput.php index 61de03c..98a3ae7 100644 --- a/src/Helpers/ConsoleOutput.php +++ b/src/Helpers/ConsoleOutput.php @@ -3,26 +3,28 @@ namespace JTMcC\AtomicDeployments\Helpers; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Collection; +/** + * @method static void line(string $string) + * @method static void warn(string $string) + * @method static void alert(string $string) + * @method static void error(string $string) + * @method static void info(string $string) + * @method static void table(string[] $titles, Collection $rows) + */ class ConsoleOutput { public static ?Command $runningCommand = null; - /** - * @param Command $runningCommand - */ - public function setOutput(Command $runningCommand) + public function setOutput(Command $runningCommand): void { static::$runningCommand = $runningCommand; } - /** - * @param string $method - * @param $arguments - */ - public static function __callStatic(string $method, $arguments) + public static function __callStatic(string $method, array $arguments) { - if (!static::$runningCommand) { + if (! static::$runningCommand) { return; } diff --git a/src/Helpers/FileHelper.php b/src/Helpers/FileHelper.php index 2e37d77..f480a25 100644 --- a/src/Helpers/FileHelper.php +++ b/src/Helpers/FileHelper.php @@ -12,16 +12,12 @@ class FileHelper { /** - * @param string ...$paths - * * @throws InvalidPathException - * - * @return bool */ public static function confirmPathsExist(string ...$paths): bool { foreach ($paths as $path) { - if (!File::exists($path)) { + if (! File::exists($path)) { throw new InvalidPathException("{$path} does not exist"); } } @@ -32,19 +28,18 @@ public static function confirmPathsExist(string ...$paths): bool /** * Recursively update symbolic links with new endpoint. * - * @param $from - * @param $to - * * @throws ExecuteFailedException */ - public static function recursivelyUpdateSymlinks($from, $to) + public static function recursivelyUpdateSymlinks(string $from, string $to): void { $dir = new RecursiveDirectoryIterator($to); + foreach (new RecursiveIteratorIterator($dir) as $file) { if (is_link($file)) { $link = $file->getPathName(); $target = $file->getLinkTarget(); $newPath = str_replace($from, $to, $target); + if ($target !== $newPath) { Exec::ln($link, $newPath); } diff --git a/src/Interfaces/DeploymentInterface.php b/src/Interfaces/DeploymentInterface.php index bde32ab..f9e384c 100644 --- a/src/Interfaces/DeploymentInterface.php +++ b/src/Interfaces/DeploymentInterface.php @@ -2,25 +2,34 @@ namespace JTMcC\AtomicDeployments\Interfaces; +use JTMcC\AtomicDeployments\Models\AtomicDeployment; +use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus; + interface DeploymentInterface { - public function getBuildPath(); + public function getBuildPath(): string; + + public function setDirectory(string $name = ''): void; + + public function setPath(): void; + + public function getPath(): string; - public function setDirectory(string $name = ''); + public function getCurrentPath(): string; - public function setPath(); + public function copyContents(): void; - public function getPath(); + public function link(): void; - public function getCurrentPath(); + public function getLink(): string; - public function copyContents(); + public function getModel(): AtomicDeployment; - public function link(); + public function updateStatus(DeploymentStatus $status): void; - public function getLink(); + public function isDeployed(): bool; - public function getModel(); + public function getDirectoryName(): string; - public function updateStatus(int $status); + public function createDirectory(): void; } diff --git a/src/Models/AtomicDeployment.php b/src/Models/AtomicDeployment.php index 8dfcf9e..249aa81 100644 --- a/src/Models/AtomicDeployment.php +++ b/src/Models/AtomicDeployment.php @@ -2,6 +2,8 @@ namespace JTMcC\AtomicDeployments\Models; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Support\Facades\File; @@ -9,6 +11,19 @@ use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus; use JTMcC\AtomicDeployments\Services\Exec; +/** + * @mixin Builder + * + * @method static Builder successful() + * + * @property-read bool $has_deployment + * @property-read bool $is_currently_deployed + * @property string $commit_hash + * @property int $deployment_status + * @property string $build_path + * @property string $deployment_path + * @property string $deployment_link + */ class AtomicDeployment extends Model { use SoftDeletes; @@ -21,47 +36,46 @@ class AtomicDeployment extends Model 'deployment_link', ]; - protected static function boot() + protected $casts = [ + 'deployment_status' => DeploymentStatus::class, + ]; + + protected static function boot(): void { parent::boot(); - static::deleting(function ($model) { - if ($model->isCurrentlyDeployed) { + + static::deleting(function (AtomicDeployment $model) { + if ($model->is_currently_deployed) { throw new AreYouInsaneException('Cannot delete live deployment'); } + $model->deleteDeployment(); }); } - public function scopeSuccessful($query) + public function scopeSuccessful($query): Builder { return $query->where('deployment_status', DeploymentStatus::SUCCESS); } - public function getHasDeploymentAttribute() + protected function hasDeployment(): Attribute { - return File::isDirectory($this->deployment_path); + return Attribute::make( + get: fn () => File::isDirectory($this->deployment_path), + ); } - /** - * @throws \JTMcC\AtomicDeployments\Exceptions\ExecuteFailedException - * - * @return bool - */ - public function getIsCurrentlyDeployedAttribute() + protected function isCurrentlyDeployed(): Attribute { - if (!$this->hasDeployment) { - return false; - } - - return Exec::readlink($this->deployment_link) === $this->deployment_path; + return Attribute::make( + get: fn () => $this->has_deployment && Exec::readlink($this->deployment_link) === $this->deployment_path, + ); } - public function deleteDeployment() + public function deleteDeployment(): void { - if ($this->hasDeployment) { + if ($this->has_deployment) { File::deleteDirectory($this->deployment_path); } - - return $this; } } diff --git a/src/Models/Enums/DeploymentStatus.php b/src/Models/Enums/DeploymentStatus.php index dae8caf..00590ba 100644 --- a/src/Models/Enums/DeploymentStatus.php +++ b/src/Models/Enums/DeploymentStatus.php @@ -2,9 +2,9 @@ namespace JTMcC\AtomicDeployments\Models\Enums; -class DeploymentStatus extends Enum +enum DeploymentStatus: int { - const FAILED = 0; - const RUNNING = 1; - const SUCCESS = 2; + case FAILED = 0; + case RUNNING = 1; + case SUCCESS = 2; } diff --git a/src/Models/Enums/Enum.php b/src/Models/Enums/Enum.php deleted file mode 100644 index 69e05b5..0000000 --- a/src/Models/Enums/Enum.php +++ /dev/null @@ -1,41 +0,0 @@ -getConstants(); - } - - return self::$constCacheArray[$calledClass]; - } - - /** - * @param int $value - * - * @throws \ReflectionException - * - * @return false|int|string|null - */ - public static function getNameFromValue(int $value) - { - return array_search($value, self::getConstants(), true) ?? null; - } -} diff --git a/src/Services/AtomicDeploymentService.php b/src/Services/AtomicDeploymentService.php index be523ea..144d79b 100644 --- a/src/Services/AtomicDeploymentService.php +++ b/src/Services/AtomicDeploymentService.php @@ -5,6 +5,7 @@ namespace JTMcC\AtomicDeployments\Services; use Closure; +use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Facades\File; use Illuminate\Support\Pluralizer; use JTMcC\AtomicDeployments\Events\DeploymentFailed; @@ -14,31 +15,26 @@ use JTMcC\AtomicDeployments\Interfaces\DeploymentInterface; use JTMcC\AtomicDeployments\Models\AtomicDeployment; use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus; +use Throwable; class AtomicDeploymentService { protected DeploymentInterface $deployment; protected bool $dryRun; + protected array $migrate; protected string $initialDeploymentPath = ''; /** - * @param mixed ...$args - * - * @return self + * @param mixed ...$args */ - public static function create(...$args) + public static function create(...$args): self { return app(static::class, $args); } - /** - * @param DeploymentInterface $deployment - * @param array $migrate - * @param bool $dryRun - */ public function __construct(DeploymentInterface $deployment, array $migrate = [], bool $dryRun = false) { $this->deployment = $deployment; @@ -50,28 +46,16 @@ public function __construct(DeploymentInterface $deployment, array $migrate = [] $this->initialDeploymentPath = $deployment->getCurrentPath(); } - /** - * @return DeploymentInterface - */ - public function getDeployment() + public function getDeployment(): DeploymentInterface { return $this->deployment; } - /** - * @return string - */ public function getInitialDeploymentPath(): string { return $this->initialDeploymentPath; } - /** - * Run full deployment. - * - * @param Closure|null $successCallback - * @param Closure|null $failedCallback - */ public function deploy(?Closure $successCallback = null, ?Closure $failedCallback = null): void { try { @@ -82,7 +66,7 @@ public function deploy(?Closure $successCallback = null, ?Closure $failedCallbac Output::info('Checking for previous deployment'); Output::info($this->initialDeploymentPath ? - "Previous deployment detected at {$this->initialDeploymentPath}" : + sprintf('Previous deployment detected at %s', $this->initialDeploymentPath) : 'No previous deployment detected for this link'); $this->updateDeploymentStatus(DeploymentStatus::RUNNING); @@ -101,19 +85,17 @@ public function deploy(?Closure $successCallback = null, ?Closure $failedCallbac if ($successCallback) { $successCallback($this); } - } catch (\Throwable $e) { + } catch (Throwable $e) { $this->fail(); Output::throwable($e); + if ($failedCallback) { $failedCallback($this); } } } - /** - * @param int $status - */ - public function updateDeploymentStatus(int $status): void + public function updateDeploymentStatus(DeploymentStatus $status): void { if ($this->isDryRun()) { Output::warn('Dry run - Skipping deployment status update'); @@ -125,7 +107,7 @@ public function updateDeploymentStatus(int $status): void public function linkDeployment(): void { - Output::info("Creating symbolic link: {$this->deployment->getLink()} -> {$this->deployment->getPath()}"); + Output::info(sprintf('Creating symbolic link: %s -> %s', $this->deployment->getLink(), $this->deployment->getPath())); if ($this->isDryRun()) { Output::warn('Dry run - Skipping symbolic link deployment'); @@ -137,8 +119,6 @@ public function linkDeployment(): void /** * @throws ExecuteFailedException - * - * @return bool */ public function confirmSymbolicLink(): bool { @@ -150,11 +130,13 @@ public function confirmSymbolicLink(): bool return true; } - if (!$this->deployment->isDeployed()) { + if (! $this->deployment->isDeployed()) { throw new ExecuteFailedException( - 'Expected deployment link to direct to '. - $this->deployment->getPath().' but found '. - $this->deployment->getCurrentPath() + sprintf( + 'Expected deployment link to direct to %s but found %s', + $this->deployment->getPath(), + $this->deployment->getCurrentPath() + ) ); } @@ -165,7 +147,7 @@ public function confirmSymbolicLink(): bool public function createDeploymentDirectory(): void { - Output::info("Creating directory at {$this->deployment->getPath()}"); + Output::info(sprintf('Creating directory at %s', $this->deployment->getPath())); if ($this->isDryRun()) { Output::warn('Dry run - Skipping creating deployment directory'); @@ -198,14 +180,14 @@ public function copyDeploymentContents(): void */ public function copyMigrationContents(): void { - if (!empty($this->initialDeploymentPath) && count($this->migrate)) { + if (! empty($this->initialDeploymentPath) && count($this->migrate)) { if ($this->isDryRun()) { Output::warn('Dry run - skipping migrations'); } - collect($this->migrate)->each(function ($pattern) { - if (!$this->isDryRun()) { - Output::info("Running migration for pattern {$pattern}"); + collect($this->migrate)->each(function (string $pattern): void { + if (! $this->isDryRun()) { + Output::info(sprintf('Running migration for pattern %s', $pattern)); } $rootFrom = rtrim($this->initialDeploymentPath, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR; @@ -214,7 +196,7 @@ public function copyMigrationContents(): void foreach (File::glob($rootFrom.$pattern) as $from) { $dir = $from; - if (!File::isDirectory($dir)) { + if (! File::isDirectory($dir)) { $dir = File::dirname($dir); } @@ -222,8 +204,9 @@ public function copyMigrationContents(): void $to = str_replace($rootFrom, $rootTo, $from); if ($this->isDryRun()) { - Output::warn("Dry run - migrate: \r\n - {$from}\r\n - {$to}"); + Output::warn(sprintf("Dry run - migrate: \r\n - %s\r\n - %s", $from, $to)); Output::line(); + continue; } @@ -232,8 +215,8 @@ public function copyMigrationContents(): void Exec::rsync($from, $to); } - if (!$this->isDryRun()) { - Output::info("Finished migration for pattern {$pattern}"); + if (! $this->isDryRun()) { + Output::info(sprintf('Finished migration for pattern %s', $pattern)); } }); } @@ -242,7 +225,7 @@ public function copyMigrationContents(): void /** * @throws ExecuteFailedException */ - public function updateSymlinks() + public function updateSymlinks(): void { Output::info('Correcting old symlinks that still reference the build directory'); @@ -264,18 +247,18 @@ public function rollback(): void { Output::warn('Atomic deployment rollback has been requested'); - if (!$this->isDryRun()) { + if (! $this->isDryRun()) { $currentPath = $this->deployment->getCurrentPath(); if ( - //confirm if we need to revert the link + // confirm if we need to revert the link $this->initialDeploymentPath && $this->initialDeploymentPath !== $currentPath ) { - Output::emergency("Attempting to link deployment at {$this->initialDeploymentPath}"); + Output::emergency(sprintf('Attempting to link deployment at %s', $this->initialDeploymentPath)); try { - //attempt to revert link to our original path + // attempt to revert link to our original path Exec::ln($this->deployment->getLink(), $this->initialDeploymentPath); if ($this->deployment->getCurrentPath() === $this->initialDeploymentPath) { Output::info('Successfully rolled back symbolic link'); @@ -309,8 +292,9 @@ public function fail(): void public function shutdown(): void { - if ($error = error_get_last()) { + if (error_get_last()) { Output::error('Error detected during shutdown, requesting rollback'); + $this->fail(); } } @@ -318,21 +302,23 @@ public function shutdown(): void public function cleanBuilds(int $limit): void { Output::alert('Running Build Cleanup'); - Output::info("Max deployment directories allowed set to {$limit}"); + Output::info(sprintf('Max deployment directories allowed set to %d', $limit)); $buildIDs = AtomicDeployment::successful() ->orderBy('id', 'desc') ->limit($limit) ->pluck('id'); - $buildsToRemove = AtomicDeployment::whereNotIn('id', $buildIDs)->get(); + /* @var Collection $buildsToRemove */ + $buildsToRemove = AtomicDeployment::query()->whereNotIn('id', $buildIDs)->get(); $countOfBuildsToRemove = $buildsToRemove->count(); - Output::info('Found '.$countOfBuildsToRemove.' '.Pluralizer::plural('folder', $countOfBuildsToRemove).' to be removed'); + Output::info(sprintf('Found %d %s to be removed', $countOfBuildsToRemove, Pluralizer::plural('folder', $countOfBuildsToRemove))); foreach ($buildsToRemove as $deployment) { - if ($deployment->isCurrentlyDeployed) { + + if ($deployment->is_currently_deployed) { Output::warn('Current linked path has appeared in the directory cleaning logic'); Output::warn('This either means you currently have an old build deployed or there is a problem with your deployment data'); Output::warn('Skipping deletion'); @@ -340,9 +326,10 @@ public function cleanBuilds(int $limit): void return; } - Output::info("Deleting {$deployment->commit_hash}"); + Output::info(sprintf('Deleting %s', $deployment->commit_hash)); - if (!$this->isDryRun()) { + if (! $this->isDryRun()) { + // @phpstan-ignore-next-line $deployment->delete(); Output::info('Deployment deleted'); } else { diff --git a/src/Services/Deployment.php b/src/Services/Deployment.php index 719b250..1585d6b 100644 --- a/src/Services/Deployment.php +++ b/src/Services/Deployment.php @@ -12,23 +12,26 @@ use JTMcC\AtomicDeployments\Helpers\FileHelper; use JTMcC\AtomicDeployments\Interfaces\DeploymentInterface; use JTMcC\AtomicDeployments\Models\AtomicDeployment; +use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus; class Deployment implements DeploymentInterface { protected AtomicDeployment $model; protected string $buildPath; + protected string $deploymentLink; + protected string $deploymentsPath; + protected string $directoryNaming; protected string $deploymentPath = ''; + protected string $deploymentDirectory = ''; /** * Deployment constructor. - * - * @param AtomicDeployment $model */ public function __construct(AtomicDeployment $model) { @@ -67,13 +70,10 @@ public function setDirectory(string $name = ''): void { $this->deploymentDirectory = trim($name); - //update deployment path to use new directory + // update deployment path to use new directory $this->setPath(); } - /** - * @return string - */ public function getDirectory(): string { return $this->deploymentDirectory; @@ -83,8 +83,6 @@ public function getDirectory(): string * Get the current symlinked deployment path. * * @throws ExecuteFailedException - * - * @return string */ public function getCurrentPath(): string { @@ -98,20 +96,14 @@ public function getCurrentPath(): string /** * @throws ExecuteFailedException - * - * @return string */ - public function getDirectoryName() + public function getDirectoryName(): string { - switch ($this->directoryNaming) { - case 'datetime': - return Carbon::now()->format('Y-m-d_H-i-s'); - case 'rand': - return Str::random(5).time(); - case 'git': - default: - return Exec::getGitHash(); - } + return match ($this->directoryNaming) { + 'datetime' => Carbon::now()->format('Y-m-d_H-i-s'), + 'rand' => Str::random(5).time(), + default => Exec::getGitHash(), + }; } /** @@ -138,8 +130,6 @@ public function setPath(): void * * @throws ExecuteFailedException * @throws InvalidPathException - * - * @return string */ public function getPath(): string { @@ -151,19 +141,17 @@ public function getPath(): string } /** - * @param int $status - * * @throws ExecuteFailedException * @throws InvalidPathException */ - public function updateStatus(int $status): void + public function updateStatus(DeploymentStatus $status): void { $this->model->updateOrCreate( ['deployment_path' => $this->getPath()], [ - 'commit_hash' => $this->deploymentDirectory, - 'build_path' => $this->buildPath, - 'deployment_link' => $this->deploymentLink, + 'commit_hash' => $this->deploymentDirectory, + 'build_path' => $this->buildPath, + 'deployment_link' => $this->deploymentLink, 'deployment_status' => $status, ] ); @@ -173,7 +161,7 @@ public function updateStatus(int $status): void * @throws ExecuteFailedException * @throws InvalidPathException */ - public function copyContents() + public function copyContents(): void { FileHelper::confirmPathsExist( $this->buildPath, @@ -183,25 +171,16 @@ public function copyContents() Exec::rsync("{$this->buildPath}/", "{$this->deploymentPath}/"); } - /** - * @return AtomicDeployment - */ public function getModel(): AtomicDeployment { return $this->model; } - /** - * @return string - */ public function getBuildPath(): string { return $this->buildPath; } - /** - * @return string - */ public function getLink(): string { return $this->deploymentLink; @@ -209,8 +188,6 @@ public function getLink(): string /** * @throws ExecuteFailedException - * - * @return bool */ public function isDeployed(): bool { diff --git a/src/Services/Exec.php b/src/Services/Exec.php index d5b39c6..4ae60ca 100644 --- a/src/Services/Exec.php +++ b/src/Services/Exec.php @@ -7,77 +7,52 @@ class Exec { /** - * @param string $command - * @param array $arguments - * * @throws ExecuteFailedException - * - * @return string */ - private static function run(string $command, array $arguments = []) + private static function run(string $command, array $arguments = []): string { - $arguments = array_map(fn ($argument) => escapeshellarg($argument), $arguments); - + $arguments = array_map('escapeshellarg', $arguments); $command = escapeshellcmd(count($arguments) ? sprintf($command, ...$arguments) : $command); $output = []; $status = null; - $result = trim(exec($command, $output, $status)); - //non zero status means execution failed - //see https://www.linuxtopia.org/online_books/advanced_bash_scripting_guide/exitcodes.html if ($status) { - throw new ExecuteFailedException("resulted in exit code {$status}"); + throw new ExecuteFailedException(sprintf('Command resulted in exit code %d', $status)); } return $result; } /** - * @param $link - * * @throws ExecuteFailedException - * - * @return string */ - public static function readlink($link) + public static function readlink(string $link): string { return self::run('readlink -f %s', [$link]); } /** - * @param string $link - * @param string $path - * * @throws ExecuteFailedException - * - * @return string */ - public static function ln(string $link, string $path) + public static function ln(string $link, string $path): string { return self::run('ln -sfn %s %s', [$path, $link]); } /** - * @param string $from - * @param string $to - * * @throws ExecuteFailedException - * - * @return string */ - public static function rsync(string $from, string $to) + public static function rsync(string $from, string $to): string { return self::run('rsync -aW --no-compress %s %s', [$from, $to]); } /** * @throws ExecuteFailedException - * - * @return string */ - public static function getGitHash() + public static function getGitHash(): string { return self::run('git log --pretty="%h" -n1'); } diff --git a/src/Services/Output.php b/src/Services/Output.php index caec160..f705ded 100644 --- a/src/Services/Output.php +++ b/src/Services/Output.php @@ -9,8 +9,6 @@ class Output { /** * Print throwable to console | log. - * - * @param \Throwable $obj */ public static function throwable(\Throwable $obj): void { @@ -29,9 +27,6 @@ public static function throwable(\Throwable $obj): void ); } - /** - * @param string $message - */ public static function alert(string $message): void { ConsoleOutput::line(''); @@ -39,36 +34,24 @@ public static function alert(string $message): void Log::info($message); } - /** - * @param string $message - */ public static function error(string $message): void { ConsoleOutput::error($message); Log::error($message); } - /** - * @param string $message - */ public static function emergency(string $message): void { ConsoleOutput::error($message); Log::emergency($message); } - /** - * @param $message - */ public static function info(string $message): void { ConsoleOutput::info($message); Log::info($message); } - /** - * @param $message - */ public static function warn(string $message): void { ConsoleOutput::warn($message); diff --git a/tests/Integration/Commands/DeployCommandTest.php b/tests/Feature/Commands/DeployCommandTest.php similarity index 69% rename from tests/Integration/Commands/DeployCommandTest.php rename to tests/Feature/Commands/DeployCommandTest.php index c6f8074..dc0fc60 100644 --- a/tests/Integration/Commands/DeployCommandTest.php +++ b/tests/Feature/Commands/DeployCommandTest.php @@ -4,6 +4,7 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Event; use JTMcC\AtomicDeployments\Events\DeploymentFailed; use JTMcC\AtomicDeployments\Events\DeploymentSuccessful; use JTMcC\AtomicDeployments\Exceptions\InvalidPathException; @@ -15,13 +16,12 @@ class DeployCommandTest extends TestCase { use RefreshDatabase; - /** - * @test - */ - public function it_allows_dry_run_with_no_mutations() + public function test_it_allows_dry_run_with_no_mutations() { + // Act Artisan::call('atomic-deployments:deploy --dry-run --directory=test-dir-1'); + // Assert $this->seeInConsoleOutput([ 'Deployment directory option set - Deployment will use directory: test-dir-1', 'Running Deployment...', @@ -34,24 +34,21 @@ public function it_allows_dry_run_with_no_mutations() ]); $this->dontSeeInConsoleOutput('Atomic deployment rollback has been requested'); - $this->assertFalse($this->fileSystem->exists($this->deploymentsPath.'/test-dir-1/')); $this->assertFalse($this->fileSystem->exists($this->deploymentLink)); $this->assertEmpty(AtomicDeployment::all()); } - /** - * @test - */ - public function it_does_not_migrate_on_dry_run() + public function test_it_does_not_migrate_on_dry_run() { + // Collect Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); - - //add file to 'deployment' that does not exist in 'build' for migrate test $this->fileSystem->ensureDirectoryExists($this->deploymentsPath.'/test-dir-1/migration/test-folder'); + // Act Artisan::call('atomic-deployments:deploy --dry-run --directory=test-dir-2'); + // Assert $this->seeInConsoleOutput([ 'Deployment directory option set - Deployment will use directory: test-dir-2', 'Running Deployment...', @@ -60,18 +57,16 @@ public function it_does_not_migrate_on_dry_run() ]); $this->dontSeeInConsoleOutput('Atomic deployment rollback has been requested'); - $this->assertTrue($this->fileSystem->exists($this->deploymentsPath.'/test-dir-1/migration/test-folder')); $this->assertFalse($this->fileSystem->exists($this->deploymentsPath.'/test-dir-2/migration/test-folder')); } - /** - * @test - */ - public function it_allows_run_with_mutations() + public function test_it_allows_run_with_mutations() { + // Act Artisan::call('atomic-deployments:deploy --directory=test-dir'); + // Assert $this->seeInConsoleOutput([ 'Deployment directory option set - Deployment will use directory: test-dir', 'Running Deployment...', @@ -89,23 +84,20 @@ public function it_allows_run_with_mutations() $deployment = AtomicDeployment::first(); $this->assertNotEmpty($deployment); - $this->assertTrue((int) $deployment->deployment_status === DeploymentStatus::SUCCESS); + $this->assertTrue($deployment->deployment_status === DeploymentStatus::SUCCESS); } - /** - * @test - */ - public function it_allows_migrate_on_run() + public function test_it_allows_migrate_on_run() { + // Collect Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); - - //add file to 'deployment' that does not exist in 'build' for migrate test $this->fileSystem->ensureDirectoryExists($this->deploymentsPath.'/test-dir-1/migration/test-folder'); - $this->assertFalse($this->fileSystem->exists($this->deploymentsPath.'/test-dir-2/migration/test-folder')); + // Act Artisan::call('atomic-deployments:deploy --directory=test-dir-2'); + // Assert $this->seeInConsoleOutput([ 'Deployment directory option set - Deployment will use directory: test-dir-2', 'Running Deployment...', @@ -114,118 +106,119 @@ public function it_allows_migrate_on_run() ]); $this->dontSeeInConsoleOutput('Atomic deployment rollback has been requested'); - $this->assertTrue($this->fileSystem->exists($this->deploymentsPath.'/test-dir-1/migration/test-folder')); - - //confirm migrate logic copied test folder $this->assertTrue($this->fileSystem->exists($this->deploymentsPath.'/test-dir-2/migration/test-folder')); } - /** - * @test - */ - public function it_allows_swapping_between_deployments() + public function test_it_allows_swapping_between_deployments() { - - //create two builds + // Collect Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); Artisan::call('atomic-deployments:deploy --directory=test-dir-2'); + $deployment1 = AtomicDeployment::where('commit_hash', 'test-dir-1')->first()->append( + 'is_currently_deployed' + )->toArray(); + $deployment2 = AtomicDeployment::where('commit_hash', 'test-dir-2')->first()->append( + 'is_currently_deployed' + )->toArray(); - $deployment1 = AtomicDeployment::where('commit_hash', 'test-dir-1')->first()->append('isCurrentlyDeployed')->toArray(); - $deployment2 = AtomicDeployment::where('commit_hash', 'test-dir-2')->first()->append('isCurrentlyDeployed')->toArray(); - - //confirm our last build is currently deployed - $this->assertFalse($deployment1['isCurrentlyDeployed']); - $this->assertTrue($deployment2['isCurrentlyDeployed']); + $this->assertFalse($deployment1['is_currently_deployed']); + $this->assertTrue($deployment2['is_currently_deployed']); + // Act Artisan::call('atomic-deployments:deploy --hash=test-dir-fake'); - //confirm build must exist when attempting to swap + // Assert $this->seeInConsoleOutput([ 'Updating symlink to previous build: test-dir-fake', 'Build not found for hash: test-dir-fake', ]); + // Act Artisan::call('atomic-deployments:deploy --hash=test-dir-1'); - //swap build to our first deployment + // Assert $this->seeInConsoleOutput([ 'Updating symlink to previous build: test-dir-1', 'Build link confirmed', ]); - $deployment1 = AtomicDeployment::where('commit_hash', 'test-dir-1')->first()->append('isCurrentlyDeployed')->toArray(); - $deployment2 = AtomicDeployment::where('commit_hash', 'test-dir-2')->first()->append('isCurrentlyDeployed')->toArray(); + $deployment1 = AtomicDeployment::where('commit_hash', 'test-dir-1')->first()->append( + 'is_currently_deployed' + )->toArray(); + $deployment2 = AtomicDeployment::where('commit_hash', 'test-dir-2')->first()->append( + 'is_currently_deployed' + )->toArray(); - //confirm first deployment is now live and second is not - $this->assertTrue($deployment1['isCurrentlyDeployed']); - $this->assertFalse($deployment2['isCurrentlyDeployed']); + $this->assertTrue($deployment1['is_currently_deployed']); + $this->assertFalse($deployment2['is_currently_deployed']); } - /** - * @test - */ - public function it_cleans_old_build_folders_based_on_build_limit() + public function test_it_cleans_old_build_folders_based_on_build_limit() { - $this->app['config']->set('atomic-deployments.build-limit', 1); - - Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); - Artisan::call('atomic-deployments:deploy --directory=test-dir-2'); - - $this->assertTrue(AtomicDeployment::all()->count() === 1); - $this->assertTrue(AtomicDeployment::withTrashed()->get()->count() === 2); - - AtomicDeployment::truncate(); - + // Collect $this->app['config']->set('atomic-deployments.build-limit', 3); + // Act Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); Artisan::call('atomic-deployments:deploy --directory=test-dir-2'); Artisan::call('atomic-deployments:deploy --directory=test-dir-3'); Artisan::call('atomic-deployments:deploy --directory=test-dir-4'); Artisan::call('atomic-deployments:deploy --directory=test-dir-5'); + // Assert $this->assertTrue(AtomicDeployment::all()->count() === 3); $this->assertTrue(AtomicDeployment::withTrashed()->get()->count() === 5); } - /** - * @test - */ - public function it_dispatches_deployment_successful_event_on_build() + public function test_it_dispatches_deployment_successful_event_on_build() { - $this->expectsEvents(DeploymentSuccessful::class); + // Collect + Event::fake(); + + // Act Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); + + // Assert + Event::assertDispatched(DeploymentSuccessful::class); } - /** - * @test - */ - public function it_dispatches_deployment_successful_event_on_deployment_swap() + public function test_it_dispatches_deployment_successful_event_on_deployment_swap() { + // Collect + Event::fake(); Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); Artisan::call('atomic-deployments:deploy --directory=test-dir-2'); + $deployment = AtomicDeployment::where('commit_hash', 'test-dir-2')->first()->append( + 'is_currently_deployed' + )->toArray(); + $this->assertTrue($deployment['is_currently_deployed']); - $deployment = AtomicDeployment::where('commit_hash', 'test-dir-2')->first()->append('isCurrentlyDeployed')->toArray(); - $this->assertTrue($deployment['isCurrentlyDeployed']); - - $this->expectsEvents(DeploymentSuccessful::class); - + // Act Artisan::call('atomic-deployments:deploy --hash=test-dir-1'); - $deployment = AtomicDeployment::where('commit_hash', 'test-dir-1')->first()->append('isCurrentlyDeployed')->toArray(); - $this->assertTrue($deployment['isCurrentlyDeployed']); + + // Assert + $deployment = AtomicDeployment::where('commit_hash', 'test-dir-1')->first()->append( + 'is_currently_deployed' + )->toArray(); + $this->assertTrue($deployment['is_currently_deployed']); + Event::assertDispatched(DeploymentSuccessful::class); } - /** - * @test - */ - public function it_dispatches_deployment_failed_event_on_build_fail() + public function test_it_dispatches_deployment_failed_event_on_build_fail() { - //force invalid path exception - $this->expectException(InvalidPathException::class); + // Collect + Event::fake(); $this->app['config']->set('atomic-deployments.build-path', $this->buildPath); $this->app['config']->set('atomic-deployments.deployments-path', $this->buildPath.'/deployments'); - $this->expectsEvents(DeploymentFailed::class); + + // Assert + $this->expectException(InvalidPathException::class); + + // Act Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); + + // Assert + Event::assertDispatched(DeploymentFailed::class); } } diff --git a/tests/Integration/Commands/ListCommandTest.php b/tests/Feature/Commands/ListCommandTest.php similarity index 81% rename from tests/Integration/Commands/ListCommandTest.php rename to tests/Feature/Commands/ListCommandTest.php index f8108f1..19a399f 100644 --- a/tests/Integration/Commands/ListCommandTest.php +++ b/tests/Feature/Commands/ListCommandTest.php @@ -4,28 +4,24 @@ use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Artisan; -use JTMcC\AtomicDeployments\Models\AtomicDeployment; use Tests\TestCase; class ListCommandTest extends TestCase { use RefreshDatabase; - /** - * @test - */ - public function it_lists_available_builds() + public function test_it_lists_available_builds() { - Artisan::call('atomic-deployments:deploy --directory=test-dir-1'); + // Collect Artisan::call('atomic-deployments:deploy --directory=test-dir-2'); Artisan::call('atomic-deployments:deploy --directory=test-dir-3'); Artisan::call('atomic-deployments:deploy --directory=test-dir-4'); Artisan::call('atomic-deployments:deploy --directory=test-dir-5'); - AtomicDeployment::find(1)->delete(); - + // Act Artisan::call('atomic-deployments:list'); + // Assert $this->seeInConsoleOutput([ 'ID', 'Commit Hash', diff --git a/tests/Integration/Services/AtomicDeploymentServiceTest.php b/tests/Feature/Services/AtomicDeploymentServiceTest.php similarity index 70% rename from tests/Integration/Services/AtomicDeploymentServiceTest.php rename to tests/Feature/Services/AtomicDeploymentServiceTest.php index a1b286a..5c1e89d 100644 --- a/tests/Integration/Services/AtomicDeploymentServiceTest.php +++ b/tests/Feature/Services/AtomicDeploymentServiceTest.php @@ -3,6 +3,7 @@ namespace Tests\Integration\Services; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Facades\Event; use JTMcC\AtomicDeployments\Events\DeploymentFailed; use JTMcC\AtomicDeployments\Events\DeploymentSuccessful; use JTMcC\AtomicDeployments\Exceptions\InvalidPathException; @@ -14,93 +15,106 @@ class AtomicDeploymentServiceTest extends TestCase { use RefreshDatabase; - /** - * @test - */ - public function it_links_deployment() + public function test_it_links_deployment() { + // Collect $atomicDeployment = self::getAtomicDeployment(); + + // Act $atomicDeployment->linkDeployment(); + + // Assert $this->assertTrue($atomicDeployment->getDeployment()->isDeployed()); } - /** - * @test - */ - public function it_registers_previous_deployment_on_boot() + public function test_it_registers_previous_deployment_on_boot() { + // Collect $atomicDeployment = self::getAtomicDeployment(); $this->assertTrue(empty($atomicDeployment->getInitialDeploymentPath())); + + // Act $atomicDeployment->linkDeployment(); $atomicDeployment = self::getAtomicDeployment(); - $this->assertTrue(!empty($atomicDeployment->getInitialDeploymentPath())); + + // Assert + $this->assertTrue(! empty($atomicDeployment->getInitialDeploymentPath())); } - /** - * @test - */ - public function it_creates_a_deployment_directory() + public function test_it_creates_a_deployment_directory() { + // Collect $atomicDeployment = self::getAtomicDeployment(); + + // Act $atomicDeployment->createDeploymentDirectory(); + + // Assert $this->assertTrue($this->fileSystem->exists($atomicDeployment->getDeployment()->getPath())); } - /** - * @test - */ - public function it_copies_deployment_contents_to_deployment_directory() + public function test_it_copies_deployment_contents_to_deployment_directory() { + // Collect $atomicDeployment = self::getAtomicDeployment('abc123'); $atomicDeployment->createDeploymentDirectory(); + + // Act $atomicDeployment->copyDeploymentContents(); + + // Assert $this->assertTrue($this->fileSystem->exists($atomicDeployment->getDeployment()->getPath().'/build-contents-folder')); } - /** - * @test - */ - public function it_updates_deployment_status_record() + public function test_it_updates_deployment_status_record() { + // Collect $hash = '123abc'; $this->assertEmpty(AtomicDeployment::where('commit_hash', $hash)->first()); + + // Act $atomicDeployment = self::getAtomicDeployment($hash); $atomicDeployment->updateDeploymentStatus(DeploymentStatus::RUNNING); + + // Assert $record = AtomicDeployment::where('commit_hash', $hash)->first(); - $this->assertTrue((int) $record->deployment_status === DeploymentStatus::RUNNING); + $this->assertTrue($record->deployment_status === DeploymentStatus::RUNNING); } - /** - * @test - */ - public function it_confirms_symbolic_link() + public function test_it_confirms_symbolic_link() { + // Collect $hash = '123abc'; + + // Act $atomicDeployment = self::getAtomicDeployment($hash); $atomicDeployment->linkDeployment(); + + // Assert $this->assertTrue($atomicDeployment->confirmSymbolicLink()); } - /** - * @test - */ - public function it_doesnt_allow_deployments_folder_to_be_subdirectory_of_build_folder() + public function test_it_doesnt_allow_deployments_folder_to_be_subdirectory_of_build_folder() { + // Collect $this->app['config']->set('atomic-deployments.build-path', $this->buildPath); $this->app['config']->set('atomic-deployments.deployments-path', $this->buildPath.'/deployments'); - $this->expectException(InvalidPathException::class); $atomicDeployment = self::getAtomicDeployment(); + + // Act + $this->expectException(InvalidPathException::class); + + // Act $atomicDeployment->createDeploymentDirectory(); } - /** - * @test - */ - public function it_rolls_back_symbolic_link_to_deployment_detected_on_boot() + public function test_it_rolls_back_symbolic_link_to_deployment_detected_on_boot() { + // Collect $atomicDeployment1 = self::getAtomicDeployment(); $atomicDeployment1->createDeploymentDirectory(); $atomicDeployment1->linkDeployment(); + $this->assertTrue($atomicDeployment1->getDeployment()->isDeployed()); $atomicDeployment2 = self::getAtomicDeployment('abc123'); @@ -110,38 +124,47 @@ public function it_rolls_back_symbolic_link_to_deployment_detected_on_boot() $this->assertTrue($atomicDeployment2->getDeployment()->isDeployed()); $this->assertFalse($atomicDeployment1->getDeployment()->isDeployed()); + // Act $atomicDeployment2->rollback(); + // Assert $this->assertTrue($atomicDeployment1->getDeployment()->isDeployed()); } - /** - * @test - */ - public function it_calls_closure_on_success() + public function test_it_calls_closure_on_success() { - $this->expectsEvents(DeploymentSuccessful::class); + // Collect + Event::fake(); $success = false; + + // Act self::getAtomicDeployment()->deploy(function () use (&$success) { $success = true; }); + + // Assert $this->assertTrue($success); + Event::assertDispatched(DeploymentSuccessful::class); } - /** - * @test - */ - public function it_calls_closure_on_failure() + public function test_it_calls_closure_on_failure() { + // Collect + Event::fake(); $this->app['config']->set('atomic-deployments.build-path', $this->buildPath); $this->app['config']->set('atomic-deployments.deployments-path', $this->buildPath.'/deployments'); - $this->expectsEvents(DeploymentFailed::class); - $this->expectException(InvalidPathException::class); $failed = false; $atomicDeployment = self::getAtomicDeployment(); + + $this->expectException(InvalidPathException::class); + + // Act $atomicDeployment->deploy(fn () => '', function () use (&$failed) { $failed = true; }); + + // Assert $this->assertTrue($failed); + Event::assertDispatched(DeploymentFailed::class); } } diff --git a/tests/Integration/Services/DeploymentTest.php b/tests/Feature/Services/DeploymentTest.php similarity index 66% rename from tests/Integration/Services/DeploymentTest.php rename to tests/Feature/Services/DeploymentTest.php index b453ea2..d813cf5 100644 --- a/tests/Integration/Services/DeploymentTest.php +++ b/tests/Feature/Services/DeploymentTest.php @@ -13,91 +13,107 @@ class DeploymentTest extends TestCase { use RefreshDatabase; - /** - * @test - */ - public function it_links_and_confirms_deployment() + public function test_it_links_and_confirms_deployment() { + // Collect $deployment = self::getDeployment(); + + // Act $deployment->createDirectory(); $deployment->link(); + + // Assert $this->assertTrue($deployment->isDeployed()); } - /** - * @test - */ - public function it_sets_deployment_directory() + public function test_it_sets_deployment_directory() { + // Collect $deployment = self::getDeployment(); + + // Act $deployment->setDirectory('abc123'); + + // Assert $this->assertTrue($deployment->getDirectory() === 'abc123'); } - /** - * @test - */ - public function it_names_deployment_folder_using_config_directory_naming_git() + public function test_it_names_deployment_folder_using_config_directory_naming_git() { + // Collect $gitHash = Exec::getGitHash(); $deployment = self::getDeployment(); + + // Act $deployment->createDirectory(); + + // Assert $this->assertTrue($deployment->getDirectoryName() === $gitHash); } - /** - * @test - */ - public function it_names_deployment_folder_using_config_directory_naming_rand() + public function test_it_names_deployment_folder_using_config_directory_naming_rand() { + // Collect $this->app['config']->set('atomic-deployments.directory-naming', 'rand'); $gitHash = Exec::getGitHash(); $deployment = self::getDeployment(); + + // Act $deployment->createDirectory(); + + // Assert $this->assertNotEmpty(trim($deployment->getDirectoryName())); $this->assertTrue($deployment->getDirectoryName() !== $gitHash); } - /** - * @test - */ - public function it_names_deployment_folder_using_config_directory_naming_datetime() + public function test_it_names_deployment_folder_using_config_directory_naming_datetime() { + // Collect $this->app['config']->set('atomic-deployments.directory-naming', 'datetime'); $shouldFind = Carbon::now()->format('Y-m-d_H-i'); $deployment = self::getDeployment(); + + // Act $deployment->createDirectory(); + + // Assert $this->assertNotEmpty(trim($deployment->getDirectoryName())); $this->assertStringContainsString($shouldFind, $deployment->getDirectoryName()); } - /** - * @test - */ - public function it_sets_deployment_path() + public function test_it_sets_deployment_path() { + // Collect $deployment = self::getDeployment(); + + // Act $deployment->setPath(); + + // Assert $this->assertNotEmpty(trim($deployment->getPath())); } - /** - * @test - */ - public function it_creates_a_directory() + public function test_it_creates_a_directory() { + // Collect $deployment = self::getDeployment(); + + // Act $deployment->createDirectory(); + + // Assert $this->assertTrue($this->fileSystem->exists($deployment->getPath())); } - /** - * @test - */ - public function it_updates_model_status() + public function test_it_updates_model_status() { + // Collect $deployment = self::getDeployment(); + + // Act $deployment->updateStatus(DeploymentStatus::SUCCESS); - $this->assertTrue((int) AtomicDeployment::first()->deployment_status === DeploymentStatus::SUCCESS); + + // Assert + $this->assertTrue(AtomicDeployment::first()->deployment_status === DeploymentStatus::SUCCESS); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index c24bce1..b373ec2 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -11,84 +11,69 @@ abstract class TestCase extends BaseTestCase { - const tmpFolder = __DIR__.'/tmp/'; - public $buildPath; - public $deploymentLink; - public $deploymentsPath; + const string TMP_FOLDER = __DIR__.'/tmp/'; + + public string $buildPath; + + public string $deploymentLink; + + public string $deploymentsPath; public ?Filesystem $fileSystem = null; public $mockConsoleOutput = false; - /** - * Setup the test environment. - */ protected function setUp(): void { parent::setUp(); Artisan::call('migrate', ['--database' => 'sqlite']); - $this->fileSystem = new Filesystem(); - $this->fileSystem->deleteDirectory(self::tmpFolder); - $this->fileSystem->makeDirectory(self::tmpFolder); + $this->fileSystem = new Filesystem; + $this->fileSystem->deleteDirectory(self::TMP_FOLDER); + $this->fileSystem->makeDirectory(self::TMP_FOLDER); $config = $this->app->config->get('atomic-deployments'); - $this->buildPath = static::tmpFolder.$config['build-path']; - $this->deploymentLink = static::tmpFolder.$config['deployment-link']; - $this->deploymentsPath = static::tmpFolder.$config['deployments-path']; + $this->buildPath = self::TMP_FOLDER.$config['build-path']; + $this->deploymentLink = self::TMP_FOLDER.$config['deployment-link']; + $this->deploymentsPath = self::TMP_FOLDER.$config['deployments-path']; $this->app['config']->set('atomic-deployments.build-path', $this->buildPath); $this->app['config']->set('atomic-deployments.deployment-link', $this->deploymentLink); $this->app['config']->set('atomic-deployments.deployments-path', $this->deploymentsPath); - $this->app['config']->set('atomic-deployments.migrate', [ - 'migration/*', - ]); + $this->app['config']->set('atomic-deployments.migrate', ['migration/*']); + $this->fileSystem->ensureDirectoryExists($this->buildPath.'/build-contents-folder'); $this->fileSystem->ensureDirectoryExists($this->deploymentsPath); Event::fake(); } - public function tearDown(): void + protected function tearDown(): void { parent::tearDown(); - $this->fileSystem->deleteDirectory(self::tmpFolder); + $this->fileSystem->deleteDirectory(self::TMP_FOLDER); } - /** - * Define environment setup. - * - * @param \Illuminate\Foundation\Application $app - * - * @return void - */ - protected function getEnvironmentSetUp($app) + protected function getEnvironmentSetUp($app): void { - // Setup default database to use sqlite :memory: $app['config']->set('database.default', 'sqlite'); $app['config']->set('database.connections.sqlite', [ - 'driver' => 'sqlite', + 'driver' => 'sqlite', 'database' => ':memory:', - 'prefix' => '', + 'prefix' => '', ]); } - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [\JTMcC\AtomicDeployments\AtomicDeploymentsServiceProvider::class]; } - /** - * @param string|array $searchStrings - */ - protected function seeInConsoleOutput($searchStrings) + protected function seeInConsoleOutput(string|array $searchStrings): void { - if (!is_array($searchStrings)) { - $searchStrings = [$searchStrings]; - } - + $searchStrings = (array) $searchStrings; $output = Artisan::output(); foreach ($searchStrings as $searchString) { @@ -96,15 +81,9 @@ protected function seeInConsoleOutput($searchStrings) } } - /** - * @param string|array $searchStrings - */ - protected function dontSeeInConsoleOutput($searchStrings) + protected function dontSeeInConsoleOutput(string|array $searchStrings): void { - if (!is_array($searchStrings)) { - $searchStrings = [$searchStrings]; - } - + $searchStrings = (array) $searchStrings; $output = Artisan::output(); foreach ($searchStrings as $searchString) { @@ -112,21 +91,18 @@ protected function dontSeeInConsoleOutput($searchStrings) } } - public static function getAtomicDeployment($hash = '') + public static function getAtomicDeployment(string $hash = ''): AtomicDeploymentService { $atomicDeployment = AtomicDeploymentService::create(); - if (!empty($hash)) { + if (! empty($hash)) { $atomicDeployment->getDeployment()->setDirectory($hash); } return $atomicDeployment; } - /** - * @return Deployment - */ - public static function getDeployment() + public static function getDeployment(): Deployment { return app(Deployment::class); } diff --git a/tests/Unit/Enum/DeploymentStatusTest.php b/tests/Unit/Enum/DeploymentStatusTest.php deleted file mode 100644 index 660a986..0000000 --- a/tests/Unit/Enum/DeploymentStatusTest.php +++ /dev/null @@ -1,19 +0,0 @@ - 0, - 'RUNNING' => 1, - 'SUCCESS' => 2, - ]; - - const model = DeploymentStatus::class; -} diff --git a/tests/Unit/Enum/EnumTestTrait.php b/tests/Unit/Enum/EnumTestTrait.php deleted file mode 100644 index 9d134a7..0000000 --- a/tests/Unit/Enum/EnumTestTrait.php +++ /dev/null @@ -1,29 +0,0 @@ -assertTrue(empty(array_diff_assoc(static::expected, $enum->getConstants()))); - } - - /** - * @test - */ - public function can_get_property_from_value() - { - $class = self::model; - $enum = new $class(); - $props = $enum->getConstants(); - $category = array_keys($props)[0]; - $value = array_values($props)[0]; - $this->assertTrue($enum->getNameFromValue($value) === $category); - } -} diff --git a/tests/Unit/Helpers/FileHelperTest.php b/tests/Unit/Helpers/FileHelperTest.php index fa37043..1338236 100644 --- a/tests/Unit/Helpers/FileHelperTest.php +++ b/tests/Unit/Helpers/FileHelperTest.php @@ -9,43 +9,45 @@ class FileHelperTest extends TestCase { - /** - * @test - */ - public function it_throws_invalid_path_exception_on_invalid_path() + public function test_it_throws_invalid_path_exception_on_invalid_path() { + // Act $this->expectException(InvalidPathException::class); + + // Act FileHelper::confirmPathsExist('not_a_real_path'); } - /** - * @test - */ - public function it_updates_symbolic_links_to_new_path() + public function test_it_updates_symbolic_links_to_new_path() { - - //create test build & deployment scenario - $oldSite = self::tmpFolder.'build/site'; + // Collect + $oldSite = self::TMP_FOLDER.'build/site'; $oldContent = $oldSite.'/content'; $oldLink = $oldSite.'/link'; - $newSite = self::tmpFolder.'deployments/site'; + $newSite = self::TMP_FOLDER.'deployments/site'; $newContent = $newSite.'/content'; $newLink = $newSite.'/link'; $this->fileSystem->ensureDirectoryExists($oldContent); $this->fileSystem->ensureDirectoryExists($newSite); - //link to old content + // Act Exec::ln($oldLink, $oldContent); + + // Assert $this->assertTrue(Exec::readlink($oldLink) === $oldContent); - //copy old content to deployment folder and confirm link still points to build folder + // Act Exec::rsync($oldSite.'/', $newSite.'/'); + + // Assert $this->assertTrue(Exec::readlink($newLink) === $oldContent); - //convert links to new deployment path and confirm + // Act FileHelper::recursivelyUpdateSymlinks($oldSite, $newSite); + + // Assert $this->assertTrue(Exec::readlink($newLink) === $newContent); } } diff --git a/tests/Unit/Services/ExecServiceTest.php b/tests/Unit/Services/ExecServiceTest.php index e110c68..c9f325f 100644 --- a/tests/Unit/Services/ExecServiceTest.php +++ b/tests/Unit/Services/ExecServiceTest.php @@ -7,25 +7,27 @@ class ExecServiceTest extends TestCase { - /** - * @test - */ - public function it_can_create_and_read_symbolic_link() + public function test_it_can_create_and_read_symbolic_link() { + // Act Exec::ln($this->deploymentLink, $this->deploymentsPath); + + // Assert $this->assertTrue(Exec::readlink($this->deploymentLink) === $this->deploymentsPath); } - /** - * @test - */ - public function it_can_remote_sync_folders() + public function test_it_can_remote_sync_folders() { + // Collect $from = $this->buildPath.'/to-move'; $to = $this->deploymentsPath; $confirm = $this->deploymentsPath.'/to-move'; $this->fileSystem->makeDirectory($from); + + // Act Exec::rsync($from, $to); + + // Assert $this->assertTrue($this->fileSystem->isDirectory($confirm)); } }