Skip to content

Commit

Permalink
Merge pull request #4 from J-T-McC/feature/deployment-naming
Browse files Browse the repository at this point in the history
Feature/deployment naming
  • Loading branch information
J-T-McC authored Jan 24, 2021
2 parents 0b0f893 + a8ac1ba commit 92e0279
Show file tree
Hide file tree
Showing 9 changed files with 291 additions and 16 deletions.
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ return [

/**
* Symbolic link to the current deployed build
* This path should be used for schedules and setting your web root.
* This path will be used for schedules and setting your web root.
*/
'deployment-link' => env('ATM_DEPLOYMENT_LINK'),

Expand Down Expand Up @@ -85,6 +85,16 @@ return [
*/
'deployment-class' => \JTMcC\AtomicDeployments\Services\Deployment::class,

/**
* Logic used when creating a deployment directory
*
* Default => git - uses hash for current HEAD
* Options: [ git, rand ]
*
* If your build does not use git, use rand.
*/
'directory-naming' => 'git'

];
```

Expand Down
22 changes: 16 additions & 6 deletions config/atomic-deployments.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

/**
* Symbolic link to the current deployed build
* This path should be used for schedules and setting your web root.
* This path will be used for schedules and setting your web root.
*/
'deployment-link' => env('ATM_DEPLOYMENT_LINK'),

Expand All @@ -28,13 +28,14 @@
'build-limit' => 10,

/**
* Migrate files|folders from the outgoing production build to your new release using a relative path and pattern.
* Logic used when creating a deployment directory.
*
* @see https://www.php.net/manual/en/function.glob.php
* Default => git - uses hash for current HEAD
* Options: [ git, rand ]
*
* If your build does not use git, use rand.
*/
'migrate' => [
// 'storage/framework/sessions/*',
],
'directory-naming' => 'git',

/**
* Deployment class used.
Expand All @@ -44,4 +45,13 @@
*/
'deployment-class' => \JTMcC\AtomicDeployments\Services\Deployment::class,

/**
* Migrate files|folders from the outgoing production build to your new release using a relative path and pattern.
*
* @see https://www.php.net/manual/en/function.glob.php
*/
'migrate' => [
// 'storage/framework/sessions/*',
],

];
27 changes: 27 additions & 0 deletions src/Helpers/FileHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
namespace JTMcC\AtomicDeployments\Helpers;

use Illuminate\Support\Facades\File;
use JTMcC\AtomicDeployments\Exceptions\ExecuteFailedException;
use JTMcC\AtomicDeployments\Exceptions\InvalidPathException;
use JTMcC\AtomicDeployments\Services\Exec;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;

class FileHelper
{
Expand All @@ -24,4 +28,27 @@ public static function confirmPathsExist(string ...$paths): bool

return true;
}

/**
* Recursively update symbolic links with new endpoint.
*
* @param $from
* @param $to
*
* @throws ExecuteFailedException
*/
public static function recursivelyUpdateSymlinks($from, $to)
{
$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);
}
}
}
}
}
32 changes: 27 additions & 5 deletions src/Services/AtomicDeploymentService.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use JTMcC\AtomicDeployments\Events\DeploymentFailed;
use JTMcC\AtomicDeployments\Events\DeploymentSuccessful;
use JTMcC\AtomicDeployments\Exceptions\ExecuteFailedException;
use JTMcC\AtomicDeployments\Helpers\FileHelper;
use JTMcC\AtomicDeployments\Interfaces\DeploymentInterface;
use JTMcC\AtomicDeployments\Models\AtomicDeployment as Model;
use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus;
Expand Down Expand Up @@ -89,6 +90,7 @@ public function deploy(?Closure $success = null, ?Closure $failed = null): void
$this->createDeploymentDirectory();
$this->copyDeploymentContents();
$this->copyMigrationContents();
$this->updateSymlinks();
$this->linkDeployment();
$this->confirmSymbolicLink();

Expand Down Expand Up @@ -123,7 +125,7 @@ public function updateDeploymentStatus(int $status): void

public function linkDeployment(): void
{
Output::info("Creating web root symbolic link: {$this->deployment->getDeploymentLink()} -> {$this->deployment->getDeploymentPath()}");
Output::info("Creating symbolic link: {$this->deployment->getDeploymentLink()} -> {$this->deployment->getDeploymentPath()}");
if ($this->isDryRun()) {
Output::warn('Dry run - Skipping symbolic link deployment');

Expand Down Expand Up @@ -237,6 +239,27 @@ public function copyMigrationContents(): void
}
}

/**
* @throws ExecuteFailedException
*/
public function updateSymlinks()
{
Output::info('Correcting old symlinks that still reference the build directory');

if ($this->isDryRun()) {
Output::warn('Dry run - skipping symlink corrections');

return;
}

FileHelper::recursivelyUpdateSymlinks(
$this->getDeployment()->getBuildPath(),
$this->getDeployment()->getDeploymentPath()
);

Output::info('Finished correcting symlinks');
}

public function rollback(): void
{
Output::warn('Atomic deployment rollback has been requested');
Expand All @@ -249,22 +272,21 @@ public function rollback(): void
$this->initialDeploymentPath &&
$this->initialDeploymentPath !== $currentPath
) {
Output::emergency('Atomic deployment rollback has been requested');
Output::emergency("Attempting to link web root to {$this->initialDeploymentPath}");
Output::emergency("Attempting to link deployment at {$this->initialDeploymentPath}");

try {
//attempt to revert link to our original path
Exec::ln($this->deployment->getDeploymentLink(), $this->initialDeploymentPath);
if ($this->deployment->getCurrentDeploymentPath() === $this->initialDeploymentPath) {
Output::info('Successfully rolled back symbolic web root');
Output::info('Successfully rolled back symbolic link');

return;
}
} catch (ExecuteFailedException $e) {
Output::throwable($e);
}

Output::emergency('Failed to roll back symbolic web root');
Output::emergency('Failed to roll back symbolic link');

return;
}
Expand Down
28 changes: 25 additions & 3 deletions src/Services/Deployment.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace JTMcC\AtomicDeployments\Services;

use Illuminate\Support\Facades\File;
use Illuminate\Support\Str;
use JTMcC\AtomicDeployments\Exceptions\ExecuteFailedException;
use JTMcC\AtomicDeployments\Exceptions\InvalidPathException;
use JTMcC\AtomicDeployments\Helpers\FileHelper;
Expand All @@ -18,6 +19,7 @@ class Deployment implements DeploymentInterface
protected string $buildPath;
protected string $deploymentLink;
protected string $deploymentsPath;
protected string $directoryNaming;

protected string $deploymentPath = '';
protected string $deploymentDirectory = '';
Expand All @@ -32,6 +34,7 @@ public function __construct(AtomicDeployment $model)
$this->deploymentLink = config('atomic-deployments.deployment-link');
$this->deploymentsPath = config('atomic-deployments.deployments-path');
$this->buildPath = config('atomic-deployments.build-path');
$this->directoryNaming = config('atomic-deployments.directory-naming');
$this->model = $model;

if ($this->model->deployment_path) {
Expand Down Expand Up @@ -67,6 +70,11 @@ public function setDeploymentDirectory(string $name = ''): void
$this->setDeploymentPath();
}

public function getDeploymentDirectory(): string
{
return $this->deploymentDirectory;
}

/**
* Get the current symlinked deployment path.
*
Expand All @@ -84,6 +92,22 @@ public function getCurrentDeploymentPath(): string
return $result;
}

/**
* @throws ExecuteFailedException
*
* @return string
*/
public function getDirectoryName()
{
switch ($this->directoryNaming) {
case 'rand':
return Str::random(5).time();
case 'git':
default:
return Exec::getGitHash();
}
}

/**
* Sets full path for deployment.
*
Expand All @@ -93,9 +117,7 @@ public function getCurrentDeploymentPath(): string
public function setDeploymentPath(): void
{
if (empty(trim($this->deploymentDirectory))) {
//default directory name to current HEAD hash
//TODO add configurable option for alternatives
$this->setDeploymentDirectory(Exec::getGitHash());
$this->setDeploymentDirectory($this->getDirectoryName());
}

if (strpos($this->deploymentsPath, $this->buildPath) !== false) {
Expand Down
54 changes: 54 additions & 0 deletions tests/Integration/Services/AtomicDeploymentServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Tests\Integration\Services;

use Illuminate\Foundation\Testing\RefreshDatabase;
use JTMcC\AtomicDeployments\Events\DeploymentFailed;
use JTMcC\AtomicDeployments\Events\DeploymentSuccessful;
use JTMcC\AtomicDeployments\Exceptions\InvalidPathException;
use JTMcC\AtomicDeployments\Models\AtomicDeployment;
use JTMcC\AtomicDeployments\Models\Enums\DeploymentStatus;
Expand Down Expand Up @@ -90,4 +92,56 @@ public function it_doesnt_allow_deployments_folder_to_be_subdirectory_of_build_f
$atomicDeployment = self::getAtomicDeployment();
$atomicDeployment->createDeploymentDirectory();
}

/**
* @test
*/
public function it_rolls_back_symbolic_link_to_deployment_detected_on_boot()
{
$atomicDeployment1 = self::getAtomicDeployment();
$atomicDeployment1->createDeploymentDirectory();
$atomicDeployment1->linkDeployment();
$this->assertTrue($atomicDeployment1->getDeployment()->isDeployed());

$atomicDeployment2 = self::getAtomicDeployment('abc123');
$atomicDeployment2->createDeploymentDirectory();
$atomicDeployment2->linkDeployment();

$this->assertTrue($atomicDeployment2->getDeployment()->isDeployed());
$this->assertFalse($atomicDeployment1->getDeployment()->isDeployed());

$atomicDeployment2->rollback();

$this->assertTrue($atomicDeployment1->getDeployment()->isDeployed());
}

/**
* @test
*/
public function it_calls_closure_on_success()
{
$this->expectsEvents(DeploymentSuccessful::class);
$success = false;
self::getAtomicDeployment()->deploy(function () use (&$success) {
$success = true;
});
$this->assertTrue($success);
}

/**
* @test
*/
public function it_calls_closure_on_failure()
{
$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();
$atomicDeployment->deploy(fn () => '', function () use (&$failed) {
$failed = true;
});
$this->assertTrue($failed);
}
}
Loading

0 comments on commit 92e0279

Please sign in to comment.