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
This commit is contained in:
Ossama Lafhel 2025-09-13 04:28:27 +02:00
parent 96b3e269c5
commit 5247185933
2 changed files with 174 additions and 1 deletions

View file

@ -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.<br><br>So in your case, use: <span class='dark:text-warning'>docker compose -f .{{ Str::start($application->base_directory . $application->docker_compose_location, '/') }} up -d</span>"
label="Custom Start Command" />
</div>
@if ($this->application->is_github_based())
<div class="pt-4">
<x-forms.textarea
helper="Gitignore-style rules to filter Git based webhook deployments."
placeholder="services/api/**" id="application.watch_paths"
label="Watch Paths" x-bind:disabled="shouldDisable()" />
</div>
@endif
</div>
@else
<div class="flex flex-col gap-2 xl:flex-row">
@ -296,7 +304,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
@endif
</div>
@if ($this->application->is_github_based() && !$this->application->is_public_repository())
@if ($this->application->is_github_based())
<div class="pb-4">
<x-forms.textarea
helper="Gitignore-style rules to filter Git based webhook deployments."

View file

@ -0,0 +1,165 @@
<?php
namespace Tests\Unit;
use App\Models\Application;
use Illuminate\Support\Collection;
use Tests\TestCase;
class ApplicationWatchPathsTest extends TestCase
{
public function test_is_watch_paths_triggered_returns_false_when_watch_paths_is_null()
{
$application = new Application();
$application->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'])));
}
}