From e1680cb97bac760d6652f8b397bdc4db974c26b7 Mon Sep 17 00:00:00 2001 From: Sylvester Damgaard Date: Mon, 7 Sep 2026 14:16:44 +0200 Subject: [PATCH] fix(setup): detect Symfony on fresh deploys - var/cache cannot be the marker it creates Framework detection required var/cache to exist before classifying an app as Symfony, but var/cache is exactly what setupSymfony() creates. A fresh deploy was therefore detected as generic, its cache directory never made, and the app failed health checks with an unwritable var/cache. Detection now corroborates bin/console with symfony.lock or a symfony/framework-bundle composer dependency. Laravel detection gets the same treatment (artisan plus composer dependency or bootstrap/app.php) so a stray artisan file no longer triggers Laravel permission handling. --- CHANGELOG.md | 12 ++++++ internal/setup/permissions.go | 30 +++++++++++-- internal/setup/permissions_test.go | 69 ++++++++++++++++++++++++++---- 3 files changed, 99 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 96aa5b8..0377c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#135]: https://github.com/cboxdk/init/issues/135 +### Fixed + +- **Symfony apps are now detected on a fresh deploy.** Framework detection + required `var/cache` to already exist — but `var/cache` is exactly what + the permission setup creates, so a fresh Symfony deploy (or a bind mount + without `var/`) was classified as generic and never got its cache + directory. Detection now corroborates `bin/console` with `symfony.lock` + or a `symfony/framework-bundle` composer dependency instead. Laravel + detection gained the same corroboration (`artisan` plus composer + dependency or `bootstrap/app.php`), so a stray file named `artisan` no + longer triggers Laravel permission handling. + ## [3.1.2] - 2026-09-04 ### Fixed diff --git a/internal/setup/permissions.go b/internal/setup/permissions.go index 139bd03..d132076 100644 --- a/internal/setup/permissions.go +++ b/internal/setup/permissions.go @@ -6,6 +6,7 @@ import ( "os/user" "path/filepath" "strconv" + "strings" ) // Framework represents a detected PHP framework @@ -42,13 +43,22 @@ func NewPermissionManager(workdir string, log *slog.Logger) *PermissionManager { // detectFramework identifies the PHP framework in the working directory func (pm *PermissionManager) detectFramework() Framework { - // Laravel: check for artisan file - if fileExists(filepath.Join(pm.workdir, "artisan")) { + // Laravel: `artisan` plus corroborating evidence (composer dependency or + // the framework bootstrap, which artisan itself requires) so a stray file + // named artisan does not pull a generic app into Laravel handling. + if fileExists(filepath.Join(pm.workdir, "artisan")) && + (composerRequires(pm.workdir, "laravel/framework") || + fileExists(filepath.Join(pm.workdir, "bootstrap", "app.php"))) { return FrameworkLaravel } - // Symfony: check for bin/console and var/cache + // Symfony: `bin/console` plus corroborating evidence. var/cache cannot be + // the marker - it is exactly what setupSymfony() creates, so requiring it + // meant a fresh deploy was never detected and its cache dir never created. + // symfony.lock covers Flex-managed apps; the composer dependency covers + // the rest. if fileExists(filepath.Join(pm.workdir, "bin", "console")) && - dirExists(filepath.Join(pm.workdir, "var", "cache")) { + (fileExists(filepath.Join(pm.workdir, "symfony.lock")) || + composerRequires(pm.workdir, "symfony/framework-bundle")) { return FrameworkSymfony } // WordPress: check for wp-config.php @@ -58,6 +68,18 @@ func (pm *PermissionManager) detectFramework() Framework { return FrameworkGeneric } +// composerRequires reports whether the app's composer.json mentions the given +// package. A plain substring match mirrors the shell entrypoints and is +// deliberate: composer.json is developer-controlled input and the result only +// steers directory and permission setup. +func composerRequires(workdir, pkg string) bool { + data, err := os.ReadFile(filepath.Join(workdir, "composer.json")) + if err != nil { + return false + } + return strings.Contains(string(data), pkg) +} + func fileExists(path string) bool { info, err := os.Stat(path) return err == nil && !info.IsDir() diff --git a/internal/setup/permissions_test.go b/internal/setup/permissions_test.go index ff512c2..0117c7d 100644 --- a/internal/setup/permissions_test.go +++ b/internal/setup/permissions_test.go @@ -19,8 +19,14 @@ func TestPermissionManager_Setup(t *testing.T) { { name: "Laravel setup", setupFunc: func(dir string) error { - // Create artisan to make it a Laravel project - return os.WriteFile(filepath.Join(dir, "artisan"), []byte("#!/usr/bin/env php"), 0644) + // Create artisan + bootstrap/app.php to make it a Laravel project + if err := os.WriteFile(filepath.Join(dir, "artisan"), []byte("#!/usr/bin/env php"), 0644); err != nil { + return err + } + if err := os.MkdirAll(filepath.Join(dir, "bootstrap"), 0755); err != nil { + return err + } + return os.WriteFile(filepath.Join(dir, "bootstrap", "app.php"), []byte("