From 3db0700c3426da38bd805548aef96262d2659f63 Mon Sep 17 00:00:00 2001 From: Adrien Peyre Date: Tue, 2 Jun 2026 15:40:38 +0200 Subject: [PATCH] Add shared_dirs_populate option to populate shared dirs from release --- docs/recipe/deploy/shared.md | 16 +++- recipe/deploy/shared.php | 8 ++ tests/spec/SharedDirsPopulateTest.php | 112 ++++++++++++++++++++++++++ 3 files changed, 134 insertions(+), 2 deletions(-) create mode 100644 tests/spec/SharedDirsPopulateTest.php diff --git a/docs/recipe/deploy/shared.md b/docs/recipe/deploy/shared.md index 90b2639d0..b12ec2e13 100644 --- a/docs/recipe/deploy/shared.md +++ b/docs/recipe/deploy/shared.md @@ -23,8 +23,20 @@ set('shared_dirs', ['storage']); +### shared_dirs_populate +[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L18) + +When true, populate existing shared dirs with any new files/dirs present in the release. +Useful when new subdirectories are added to the repo over time and should be propagated to shared. +Existing files in shared are never overwritten. + +```php title="Default value" +true +``` + + ### shared_files -[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L20) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L25) List of files what will be shared between releases. Each release will have symlink to those files stored in [deploy_path](/docs/recipe/common.md#deploy_path)/shared dir. @@ -38,7 +50,7 @@ set('shared_files', ['.env']); ## Tasks ### deploy\:shared {#deploy-shared} -[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L23) +[Source](https://github.com/deployphp/deployer/blob/master/recipe/deploy/shared.php#L28) Creates symlinks for shared files and dirs. diff --git a/recipe/deploy/shared.php b/recipe/deploy/shared.php index a2066fe60..6b7ed81e4 100644 --- a/recipe/deploy/shared.php +++ b/recipe/deploy/shared.php @@ -12,6 +12,11 @@ // ``` set('shared_dirs', []); +// When true, populate existing shared dirs with any new files/dirs present in the release. +// Useful when new subdirectories are added to the repo over time and should be propagated to shared. +// Existing files in shared are never overwritten. +set('shared_dirs_populate', true); + // List of files what will be shared between releases. // Each release will have symlink to those files stored in {{deploy_path}}/shared dir. // ```php @@ -46,6 +51,9 @@ if (test("[ -d $(echo {{release_path}}/$dir) ]")) { run("cp -r$copyVerbosity {{release_path}}/$dir $sharedPath/" . dirname($dir)); } + } elseif (get('shared_dirs_populate') && test("[ -d $(echo {{release_path}}/$dir) ]")) { + // Populate shared dir with new files/dirs from release without overwriting existing content. + run("cp -r$copyVerbosity --no-dereference --no-clobber {{release_path}}/$dir/. $sharedPath/$dir/"); } // Remove from source. diff --git a/tests/spec/SharedDirsPopulateTest.php b/tests/spec/SharedDirsPopulateTest.php new file mode 100644 index 000000000..865973a7e --- /dev/null +++ b/tests/spec/SharedDirsPopulateTest.php @@ -0,0 +1,112 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Deployer; + +use spec\SpecTest; + +use const __TEMP_DIR__; + +class SharedDirsPopulateTest extends SpecTest +{ + public function testPopulateCopiesNewReleaseFilesWithoutOverwriting() + { + $repo = $this->createRepo('populate_repo', ['data/a.txt' => 'first']); + $recipe = $this->writeRecipe('prod1', $repo, true); + $deployPath = __TEMP_DIR__ . '/prod1'; + + $this->depFile($recipe, 'deploy'); + self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); + self::assertFileExists($deployPath . '/shared/data/a.txt'); + self::assertSame('first', file_get_contents($deployPath . '/shared/data/a.txt')); + + // Simulate a new file being added to the repository over time. + $this->addFileAndCommit($repo, 'data/b.txt', 'second'); + + $this->depFile($recipe, 'deploy'); + self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); + + self::assertFileExists($deployPath . '/shared/data/b.txt'); + self::assertSame('second', file_get_contents($deployPath . '/shared/data/b.txt')); + self::assertFileExists($deployPath . '/current/data/b.txt'); + + // Existing shared file must not be overwritten by the release content. + self::assertSame('first', file_get_contents($deployPath . '/shared/data/a.txt')); + } + + public function testPopulateDisabledByDefaultDoesNotSyncNewFiles() + { + $repo = $this->createRepo('no_populate_repo', ['data/a.txt' => 'first']); + $recipe = $this->writeRecipe('prod2', $repo, false); + $deployPath = __TEMP_DIR__ . '/prod2'; + + $this->depFile($recipe, 'deploy'); + self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); + self::assertFileExists($deployPath . '/shared/data/a.txt'); + + $this->addFileAndCommit($repo, 'data/b.txt', 'second'); + + $this->depFile($recipe, 'deploy'); + self::assertEquals(0, $this->tester->getStatusCode(), $this->tester->getDisplay()); + + self::assertFileDoesNotExist($deployPath . '/shared/data/b.txt'); + } + + private function createRepo(string $name, array $files): string + { + $dir = __TEMP_DIR__ . '/' . $name; + mkdir($dir, 0777, true); + + foreach ($files as $path => $content) { + $full = $dir . '/' . $path; + if (!is_dir(dirname($full))) { + mkdir(dirname($full), 0777, true); + } + file_put_contents($full, $content); + } + + exec("cd $dir && git init 2>&1"); + exec("cd $dir && git config user.name 'Deployer Test' && git config user.email 'test@example.com'"); + exec("cd $dir && git add . && git commit -m 'init' 2>&1"); + + return $dir; + } + + private function addFileAndCommit(string $repo, string $path, string $content): void + { + $full = $repo . '/' . $path; + if (!is_dir(dirname($full))) { + mkdir(dirname($full), 0777, true); + } + file_put_contents($full, $content); + + exec("cd $repo && git add . && git commit -m 'add file' 2>&1"); + } + + private function writeRecipe(string $hostname, string $repository, bool $populate): string + { + $populateValue = $populate ? 'true' : 'false'; + $recipe = <<