From 5247185933e4138e71c941599af3f3937d4bf5c0 Mon Sep 17 00:00:00 2001 From: Ossama Lafhel Date: Sat, 13 Sep 2025 04:28:27 +0200 Subject: [PATCH] feat(add-watch-paths-for-services): show watch paths field for docker compose applications - Fix UI template to display Watch Paths for all GitHub-based applications - Remove condition that limited Watch Paths to private repositories only - Add comprehensive unit tests for isWatchPathsTriggered() method - Test various pattern matching scenarios (wildcards, globs, etc.) - Watch Paths now works for Docker Compose apps with both public and private repos --- .../project/application/general.blade.php | 10 +- tests/Unit/ApplicationWatchPathsTest.php | 165 ++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 tests/Unit/ApplicationWatchPathsTest.php diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 315385593..3ffe074fa 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -265,6 +265,14 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" helper="If you use this, you need to specify paths relatively and should use the same compose file in the custom command, otherwise the automatically configured labels / etc won't work.

So in your case, use: docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d" label="Custom Start Command" /> + @if ($this->application->is_github_based()) +
+ +
+ @endif @else
@@ -296,7 +304,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @endif
- @if ($this->application->is_github_based() && !$this->application->is_public_repository()) + @if ($this->application->is_github_based())
watch_paths = null; + + $modified_files = collect(['docker-compose.yml', 'README.md']); + + $this->assertFalse($application->isWatchPathsTriggered($modified_files)); + } + + public function test_is_watch_paths_triggered_with_exact_match() + { + $application = new Application(); + $application->watch_paths = "docker-compose.yml\nDockerfile"; + + // Exact match should return true + $this->assertTrue($application->isWatchPathsTriggered(collect(['docker-compose.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['Dockerfile']))); + + // Non-matching file should return false + $this->assertFalse($application->isWatchPathsTriggered(collect(['README.md']))); + } + + public function test_is_watch_paths_triggered_with_wildcard_patterns() + { + $application = new Application(); + $application->watch_paths = "*.yml\nsrc/**/*.php\nconfig/*"; + + // Wildcard matches + $this->assertTrue($application->isWatchPathsTriggered(collect(['docker-compose.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['production.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['src/Controllers/UserController.php']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['src/Models/User.php']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['config/app.php']))); + + // Non-matching files + $this->assertFalse($application->isWatchPathsTriggered(collect(['README.md']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['src/index.js']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['configurations/deep/file.php']))); + } + + public function test_is_watch_paths_triggered_with_multiple_files() + { + $application = new Application(); + $application->watch_paths = "docker-compose.yml\n*.env"; + + // At least one file matches + $modified_files = collect(['README.md', 'docker-compose.yml', 'package.json']); + $this->assertTrue($application->isWatchPathsTriggered($modified_files)); + + // No files match + $modified_files = collect(['README.md', 'package.json', 'src/index.js']); + $this->assertFalse($application->isWatchPathsTriggered($modified_files)); + } + + public function test_is_watch_paths_triggered_with_complex_patterns() + { + $application = new Application(); + // fnmatch doesn't support {a,b} syntax, so we need to use separate patterns + $application->watch_paths = "**/*.js\n**/*.jsx\n**/*.ts\n**/*.tsx"; + + // JavaScript/TypeScript files should match + $this->assertTrue($application->isWatchPathsTriggered(collect(['src/index.js']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['components/Button.jsx']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['types/user.ts']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['pages/Home.tsx']))); + + // Deeply nested files should match + $this->assertTrue($application->isWatchPathsTriggered(collect(['src/components/ui/Button.tsx']))); + + // Non-matching files + $this->assertFalse($application->isWatchPathsTriggered(collect(['README.md']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['package.json']))); + } + + public function test_is_watch_paths_triggered_with_question_mark_pattern() + { + $application = new Application(); + $application->watch_paths = "test?.txt\nfile-?.yml"; + + // Single character wildcard matches + $this->assertTrue($application->isWatchPathsTriggered(collect(['test1.txt']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['testA.txt']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['file-1.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['file-B.yml']))); + + // Non-matching files + $this->assertFalse($application->isWatchPathsTriggered(collect(['test.txt']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['test12.txt']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['file.yml']))); + } + + public function test_is_watch_paths_triggered_with_character_set_pattern() + { + $application = new Application(); + $application->watch_paths = "[abc]test.txt\nfile[0-9].yml"; + + // Character set matches + $this->assertTrue($application->isWatchPathsTriggered(collect(['atest.txt']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['btest.txt']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['ctest.txt']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['file1.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['file9.yml']))); + + // Non-matching files + $this->assertFalse($application->isWatchPathsTriggered(collect(['dtest.txt']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['test.txt']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['fileA.yml']))); + } + + public function test_is_watch_paths_triggered_with_empty_watch_paths() + { + $application = new Application(); + $application->watch_paths = ''; + + $this->assertFalse($application->isWatchPathsTriggered(collect(['any-file.txt']))); + } + + public function test_is_watch_paths_triggered_with_whitespace_only_patterns() + { + $application = new Application(); + $application->watch_paths = "\n \n\t\n"; + + $this->assertFalse($application->isWatchPathsTriggered(collect(['any-file.txt']))); + } + + public function test_is_watch_paths_triggered_for_dockercompose_typical_patterns() + { + $application = new Application(); + $application->watch_paths = "docker-compose*.yml\n.env*\nDockerfile*\nservices/**"; + + // Docker Compose related files + $this->assertTrue($application->isWatchPathsTriggered(collect(['docker-compose.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['docker-compose.prod.yml']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['docker-compose-dev.yml']))); + + // Environment files + $this->assertTrue($application->isWatchPathsTriggered(collect(['.env']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['.env.local']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['.env.production']))); + + // Dockerfile variations + $this->assertTrue($application->isWatchPathsTriggered(collect(['Dockerfile']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['Dockerfile.prod']))); + + // Service files + $this->assertTrue($application->isWatchPathsTriggered(collect(['services/api/app.js']))); + $this->assertTrue($application->isWatchPathsTriggered(collect(['services/web/index.html']))); + + // Non-matching files (e.g., documentation, configs outside services) + $this->assertFalse($application->isWatchPathsTriggered(collect(['README.md']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['package.json']))); + $this->assertFalse($application->isWatchPathsTriggered(collect(['config/nginx.conf']))); + } +} \ No newline at end of file