Skip to content

refactor: remove CoolifyResource model, derive app UUID from API#49

Merged
StuMason merged 5 commits intomainfrom
remove-coolify-resource-model
Jan 19, 2026
Merged

refactor: remove CoolifyResource model, derive app UUID from API#49
StuMason merged 5 commits intomainfrom
remove-coolify-resource-model

Conversation

@StuMason
Copy link
Owner

Summary

  • BREAKING CHANGE: Removes database storage for Coolify resource info
  • Application UUIDs are now derived from the Coolify API by matching the current git repository
  • Only COOLIFY_PROJECT_UUID is stored in .env, everything else is fetched from API

Changes

  • Remove CoolifyResource model and migration
  • Add Coolify::getApplicationUuid() that fetches apps from API and matches by git repo
  • Add Coolify::getCurrentGitRepository() helper
  • Update ProvisionCommand to write COOLIFY_PROJECT_UUID to .env instead of database
  • Update all commands (DeployCommand, LogsCommand, StatusCommand, RestartCommand, RollbackCommand) to use Coolify::getApplicationUuid()
  • Update DashboardStatsController to derive app info from API
  • Simplify EnvironmentController (multi-environment support removed)
  • Update all tests for new architecture

Why

The previous approach of storing Coolify details in a local database was flawed because:

  1. Running migrate:fresh --seed would wipe the Coolify configuration
  2. The data was redundant - it can all be derived from the Coolify API
  3. Simpler architecture: just store the project UUID in .env and fetch everything else

Test plan

  • All 230 tests pass
  • Lint passes

The webhook secret is already stored on Coolify via the API and displayed
for the user to copy to GitHub. Storing it in the local database was
pointless and a potential security concern.

- Remove webhook_secret from CoolifyResource model
- Remove migration that added webhook_secret column
- Secret is still generated, set on Coolify, and displayed in output

Stu Mason + AI <me@stumason.dev>
BREAKING CHANGE: Removes database storage for Coolify resource info.
Application UUIDs are now derived from the Coolify API by matching
the current git repository.

Changes:
- Remove CoolifyResource model and migration
- Add Coolify::getApplicationUuid() that fetches apps from API
- Add Coolify::getCurrentGitRepository() helper
- Update ProvisionCommand to write COOLIFY_PROJECT_UUID to .env
- Update all commands to use Coolify::getApplicationUuid()
- Update DashboardStatsController to derive from API
- Simplify EnvironmentController (multi-env support removed)
- Update all tests for new architecture
@claude
Copy link

claude bot commented Jan 19, 2026

Code Review

This is a significant architectural refactor that removes the database-backed storage model in favor of deriving application UUIDs from the Coolify API. Overall, this is a well-executed change with good test coverage. Here's my detailed feedback:


Strengths

  1. Clear Motivation: The rationale is sound - removing database storage eliminates the migrate:fresh problem and reduces redundant data
  2. Comprehensive Test Updates: All 230 tests updated and passing
  3. Backward Compatibility Handled: Migration files deleted appropriately since this is a breaking change
  4. Good Documentation: Config file has clear docblock explaining the new project_uuid approach
  5. Consistent Implementation: All commands and controllers updated consistently

🔍 Code Quality Issues

Critical: Security - Command Injection Risk

Location: src/Coolify.php:144

$result = Process::run('git remote get-url origin 2>/dev/null');

Issue: While this appears safe (no user input), the code runs in the application's working directory which could be influenced by deployment configuration.

Recommendation: Add validation to ensure we're in a git repository:

public static function getCurrentGitRepository(): ?string
{
    // Validate we're in a git repo first
    if (\!is_dir(base_path('.git'))) {
        return null;
    }
    
    $result = Process::run('git remote get-url origin 2>/dev/null');
    // ... rest of code
}

High: Performance - N+1 API Calls

Location: src/Coolify.php:121

$applications = static::applications()->all();

foreach ($applications as $app) {
    // Match repository
}

Issue: getApplicationUuid() is called on every command execution, and it fetches all applications from the Coolify API. For users with many applications, this could be slow.

Impact:

  • Dashboard loads will fetch all applications
  • Every artisan command that derives UUID will make this API call
  • Cache TTL of 30s means frequent re-fetching

Recommendation:

  1. Consider increasing default cache TTL for this specific operation
  2. Add performance logging/monitoring for this lookup
  3. Consider a local cache file as fallback (optional)

Medium: .env File Manipulation Issues

Location: src/Console/ProvisionCommand.php:1562-1584

protected function updateEnvFile(string , string ): void
{
    // ...
    if (preg_match("/^{}=.*/m", $content)) {
        $content = preg_replace("/^{}=.*/m", "{}={}", $content);
    }
    // ...
}

Issues:

  1. No escaping: If $key contains regex special characters, this will fail
  2. No value escaping: Values with spaces/quotes aren't properly quoted
  3. Commented lines: Will match commented env vars like # COOLIFY_PROJECT_UUID=old
  4. No atomic writes: File corruption possible if process dies mid-write

Recommendation: Use Laravel's built-in DotenvEditor or add proper escaping:

protected function updateEnvFile(string $key, string $value): void
{
    $envPath = base_path('.env');
    $key = preg_quote($key, '/');
    
    if (\!File::exists($envPath)) {
        File::put($envPath, "{$key}={$value}\n");
        return;
    }
    
    $content = File::get($envPath);
    
    // Only match uncommented lines
    if (preg_match("/^{$key}=.*/m", $content)) {
        $content = preg_replace("/^{$key}=.*/m", "{$key}={$value}", $content);
    } else {
        $content = rtrim($content, "\n") . "\n{$key}={$value}\n";
    }
    
    // Atomic write
    $tempPath = $envPath . '.tmp';
    File::put($tempPath, $content);
    File::move($tempPath, $envPath);
}

Medium: Git Repository Normalization

Location: src/Coolify.php:126-128

$normalizedAppRepo = preg_replace('/\.git$/', '', $appRepo);
$normalizedAppRepo = preg_replace('#^git@github\.com:#', '', $normalizedAppRepo);
$normalizedAppRepo = preg_replace('#^https?://github\.com/#', '', $normalizedAppRepo);

Issues:

  1. GitHub-only: Hardcoded to GitHub - won't work with GitLab, Bitbucket, self-hosted Git
  2. Case sensitivity: Uses strcasecmp() at line 130 but GitHub usernames are case-insensitive while repo names are not
  3. No SSH port handling: ssh://git@github.com:22/owner/repo format not handled

Recommendation:

protected static function normalizeGitUrl(string $url): string
{
    // Remove .git suffix
    $url = preg_replace('/\.git$/', '', $url);
    
    // Extract owner/repo from various formats
    // git@host:owner/repo or git@host:port/owner/repo
    if (preg_match('#^git@[^:]+:(\d+/)?(.+)$#', $url, $matches)) {
        return $matches[2];
    }
    
    // https://host/owner/repo or http://host/owner/repo
    if (preg_match('#^https?://[^/]+/(.+)$#', $url, $matches)) {
        return $matches[1];
    }
    
    // ssh://git@host/owner/repo
    if (preg_match('#^ssh://[^/]+/(.+)$#', $url, $matches)) {
        return $matches[1];
    }
    
    return $url;
}

Low: Missing Error Context

Location: src/Http/Controllers/DashboardStatsController.php:537-550

} catch (CoolifyApiException) {
    // Application not found
}

Issue: Silent failures make debugging difficult. Users won't know if the app wasn't found vs API is down vs authentication failed.

Recommendation: Log errors at minimum:

} catch (CoolifyApiException $e) {
    Log::warning('Failed to fetch application from Coolify', [
        'app_uuid' => $appUuid,
        'error' => $e->getMessage(),
    ]);
}

Low: Unintended File in PR

Location: .claude/voice.json

{"voice": "nova", "speed": 1.1, "enabled": true}

Issue: This appears to be a personal configuration file that shouldn't be in version control.

Recommendation:

  1. Add .claude/ to .gitignore
  2. Remove from this PR

🔒 Security Concerns

Cache Poisoning Risk

Location: src/Coolify.php:111-136

The cache key is only based on project_uuid:

$cacheKey = "coolify.app_uuid.{$projectUuid}";

Issue: If git repository changes (e.g., rebasing, switching repos), the cached UUID remains for up to 30 seconds, potentially deploying to wrong application.

Recommendation: Include git remote URL in cache key:

$gitRepo = static::getCurrentGitRepository();
$cacheKey = "coolify.app_uuid.{$projectUuid}." . md5($gitRepo ?? 'none');

🎯 Test Coverage Concerns

  1. Missing Tests: No tests for the new getCurrentGitRepository() method
  2. No Integration Tests: All tests mock HTTP - no tests verify the git repository matching logic actually works
  3. Edge Cases: No tests for:
    • Missing project UUID in config
    • Git repository not matching any Coolify app
    • Multiple apps with same repository (edge case)

Recommendation: Add unit tests:

it('extracts GitHub repository from git remote', function () {
    // Mock Process facade to return git@github.com:StuMason/laravel-coolify.git
    $result = Coolify::getCurrentGitRepository();
    expect($result)->toBe('StuMason/laravel-coolify');
});

it('returns null when not in git repository', function () {
    // Mock Process failure
    $result = Coolify::getCurrentGitRepository();
    expect($result)->toBeNull();
});

📊 Performance Considerations

Operation Before After Impact
Deploy command 1 DB query 1 API call (all apps) ⚠️ Slower for large projects
Dashboard load 3-4 DB queries 3-4 API calls ⚠️ Slightly slower
Cache hit rate N/A Critical Must be high for good performance

Recommendations:

  1. Monitor API call frequency in production
  2. Consider increasing cache TTL to 5 minutes for app UUID lookups
  3. Add metrics/logging for cache hit/miss rates

🔄 Breaking Change Handling

This PR correctly identifies this as a BREAKING CHANGE. However:

  1. Migration Path: No upgrade documentation for existing users
  2. Database Cleanup: Old tables remain in user databases

Recommendation: Add to PR description:

## Upgrade Guide

1. Note your existing `COOLIFY_PROJECT_UUID` from database or Coolify dashboard
2. Add to `.env`: `COOLIFY_PROJECT_UUID=your-uuid-here`
3. Run: `composer update`
4. Optional cleanup: Drop `coolify_resources` table

Minor Code Style

  1. Inconsistent error messages: Some commands say "Run coolify:provision first", others don't
  2. Magic number: Cache TTL default of 30 seconds in line 112 - should reference config default
  3. Unused imports: SecurityKeyRepository removed from DashboardStatsController but import might remain elsewhere

📝 Recommendations Summary

Must Fix:

  • ✅ Fix .env file manipulation to handle edge cases properly
  • ✅ Remove .claude/voice.json from PR
  • ✅ Add validation for git directory existence

Should Fix:

  • 🔶 Add error logging instead of silent catch blocks
  • 🔶 Improve git URL normalization for non-GitHub hosts
  • 🔶 Include git remote in cache key to prevent stale cache issues
  • 🔶 Add unit tests for getCurrentGitRepository()

Consider:

  • 💭 Monitor performance impact of fetching all applications
  • 💭 Document upgrade path for existing users
  • 💭 Increase cache TTL for UUID lookups

Verdict

This is a well-designed refactor that simplifies the architecture significantly. The core idea is sound and the implementation is mostly solid. However, there are several edge cases in .env manipulation and git URL parsing that need attention before merging.

Recommendation: Request changes for the critical issues (especially .env handling and the personal config file), then merge after fixes.

Great work on maintaining test coverage through this large refactor! 🎉

- Remove all references to coolify_resources database table
- Document COOLIFY_PROJECT_UUID being set automatically by provision
- Explain automatic application lookup by git repository matching
- Update config reference with new options
- Add null validation in deploy(), status(), logs() methods
- Fix database category overwrites (multiple DBs of same type)
- Add docblock noting GitHub-only support for git repository detection
- Remove .claude/ from git and add to .gitignore
- Add .git directory validation before running git commands
- Include git repository in cache key to prevent stale cache
- Improve .env file manipulation with proper regex escaping and atomic writes
- Fix dashboard test to properly mock git repository lookup
@StuMason
Copy link
Owner Author

@claude please re-review

@claude
Copy link

claude bot commented Jan 19, 2026

Claude finished @StuMason's task in 1m 47s —— View job


Re-Review Complete ✅

  • Fetch latest changes from PR branch
  • Review updated code for previously identified issues
  • Check for any new changes since last review
  • Provide updated review feedback

Summary

Great work addressing the feedback from my previous review! I've verified that all 230 tests pass and lint is clean. Here's the status of the previously identified issues:


Issues Resolved

1. .claude/voice.json file removed

The personal configuration file has been removed from the PR and .claude/ added to .gitignore. ✅

2. Git directory validation added

getCurrentGitRepository() now validates the .git directory exists before running git commands:

if (! is_dir(base_path('.git'))) {
    return null;
}

3. Cache key includes git repository

The cache key now includes the git remote URL hash to prevent stale cache issues:

$cacheKey = "coolify.app_uuid.{$projectUuid}.".md5($gitRepo);

4. .env file manipulation improved

The updateEnvFile() method now properly:

  • Escapes regex special characters with preg_quote()
  • Uses atomic writes via temp file
  • Only matches uncommented lines
$escapedKey = preg_quote($key, '/');
// ...
$tempPath = $envPath.'.tmp';
File::put($tempPath, $content);
File::move($tempPath, $envPath);


🔶 Remaining Observations (Non-blocking)

1. GitHub-only repository matching

The git URL normalization still only supports GitHub URLs (lines 127-129, 162 in Coolify.php). This is documented in the docblock:

* Note: Currently only supports GitHub repositories. GitLab, Bitbucket,
* and other providers will return null and require using --uuid flag.

This is acceptable as a known limitation with a documented workaround.

2. Silent API error handling

Some catch blocks still silently swallow errors (e.g., DashboardStatsController.php:65-67, 143-145, 161-162). Consider adding logging in a follow-up PR for better debugging.

3. Performance consideration

getApplicationUuid() fetches all applications on each call. With the 30-second cache TTL this should be manageable, but for users with many Coolify applications, consider monitoring API performance.


🧪 Verification

  • ✅ All 230 tests pass
  • ✅ PHPStan static analysis passes (0 errors)

Verdict

LGTM! 🎉

The critical issues from the previous review have been addressed. The code is clean, well-tested, and the architectural simplification makes sense. This PR is ready to merge.

@StuMason StuMason merged commit 949779c into main Jan 19, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant