diff --git a/.castor/docker.php b/.castor/docker.php index 47c5d1c..9ab26fb 100644 --- a/.castor/docker.php +++ b/.castor/docker.php @@ -102,6 +102,7 @@ function build( ?string $profile = null, ): void { generate_certificates(force: false); + normalize_build_contexts_permissions(); io()->title('Building infrastructure'); @@ -590,6 +591,10 @@ function push( $services = get_services(); + if (!$dryRun) { + normalize_build_contexts_permissions($services); + } + // Only services with a cache_from can push their build cache back to the registry. $cacheFroms = array_filter(array_map( static fn (array $config) => $config['build']['cache_from'][0] ?? null, @@ -641,6 +646,75 @@ function push( run([...$command, ...array_keys($cacheFroms)], context: $c); } +/** + * The build cache depends on the permissions of the files of the build context: a + * checkout made with a 002 umask (664 files) never reuses the cache pushed by the CI + * (644 files). Gives the files tracked by git their git permissions (644 or 755, and + * 755 for their directories), whatever the umask. + * + * Untracked files (certificates...) and additional build contexts (e.g. the + * application of the production images) are left untouched. + * + * @param array|null $services + */ +function normalize_build_contexts_permissions(?array $services = null): void +{ + if (variable('power_shell')) { + return; + } + + $rootDir = variable('root_dir'); + + $contexts = []; + foreach ($services ?? get_services() as $service) { + $buildContext = $service['build']['context'] ?? null; + + // Only local directories of the project (not a git URL, for instance) + if ($buildContext && str_starts_with($buildContext, $rootDir . '/') && is_dir($buildContext)) { + $contexts[$buildContext] = true; + } + } + + if (!$contexts) { + return; + } + + $process = run( + ['git', 'ls-files', '--stage', '-z', '--', ...array_keys($contexts)], + context: context()->withQuiet()->withAllowFailure()->withWorkingDirectory($rootDir), + ); + + // Not a git repository + if (!$process->isSuccessful()) { + return; + } + + $directories = []; + foreach (explode("\0", trim($process->getOutput(), "\0")) as $entry) { + // " \t" + [$metadata, $file] = explode("\t", $entry, 2) + [1 => '']; + $mode = substr($metadata, 0, 6); + $path = "{$rootDir}/{$file}"; + + // Symbolic links and submodules have no permissions of their own + if (!\in_array($mode, ['100644', '100755'], true) || !is_file($path)) { + continue; + } + + chmod($path, '100755' === $mode ? 0o755 : 0o644); + + foreach (array_keys($contexts) as $buildContext) { + for ($directory = \dirname($path); str_starts_with($directory . '/', $buildContext . '/'); $directory = \dirname($directory)) { + $directories[$directory] = true; + } + } + } + + foreach (array_keys($directories) as $directory) { + chmod($directory, 0o755); + } +} + /** * @return array, build: array{context: string, dockerfile?: string, cache_from?: list, target?: string, additional_contexts?: array}}> */ diff --git a/CHANGELOG.md b/CHANGELOG.md index 8563174..3bb0867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * Mount the project in `/var/www` instead of `/home/app` * Add git worktree support (auto-isolated project name, ports, volumes, networks) * Add support for caching image cache in a registry + * Give the build contexts files their git permissions before building, so the registry cache is reused whatever the umask * Also use the GitHub Actions cache (`type=gha`) in the CI, through `docker-compose.ci.yml` * Add production images (`php` and `nginx`): code baked in, non-root, php-fpm on a unix socket * Share php-fpm and nginx configuration between the dev `frontend` container and the production images diff --git a/README.md b/README.md index 2ac2068..03b55d3 100644 --- a/README.md +++ b/README.md @@ -1174,6 +1174,12 @@ castor docker:push > depending on your environment. It is recommended to push the cache from the CI > environment. +> [!NOTE] +> The build cache depends on the permissions of the files of the build context. +> With a `002` umask, a checkout gets `664` files instead of the `644` ones of +> the CI, and would never reuse its cache: `castor docker:build` and `castor +> docker:push` give the files tracked by git their git permissions first. + This command will generate a bake file with the images to push from the `cache_from` directive of the `docker-compose.yml` file. If you want to add more images to push, you can add the `cache_from` directive to them.