From 8b9f454c038768f67bd139c6db63252dd9c67d37 Mon Sep 17 00:00:00 2001
From: Cinzya
Date: Sun, 28 Sep 2025 20:04:39 +0200
Subject: [PATCH 01/28] fix(ui): update docker registry image helper text for
clarity
---
resources/views/livewire/project/application/general.blade.php | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php
index ba7d2edb0..0fbd80bce 100644
--- a/resources/views/livewire/project/application/general.blade.php
+++ b/resources/views/livewire/project/application/general.blade.php
@@ -187,7 +187,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
x-bind:disabled="!canUpdate" />
@else
Date: Mon, 29 Sep 2025 13:02:29 +0200
Subject: [PATCH 02/28] refactor(global-search): change event listener to
window level for global search modal
---
resources/views/livewire/global-search.blade.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
index 1595fa486..0b9b61da4 100644
--- a/resources/views/livewire/global-search.blade.php
+++ b/resources/views/livewire/global-search.blade.php
@@ -31,8 +31,8 @@
}
},
init() {
- // Listen for custom event from navbar search button
- this.$el.addEventListener('open-global-search', () => {
+ // Listen for custom event from navbar search button at window level
+ window.addEventListener('open-global-search', () => {
this.openModal();
});
From 72f5ae0dc6cff88d54ee86d093254e297a432889 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 29 Sep 2025 14:03:49 +0200
Subject: [PATCH 03/28] feat(user-deletion): implement file locking to prevent
concurrent user deletions and enhance error handling
---
.../Commands/Cloud/CloudDeleteUser.php | 210 ++++++++++--------
1 file changed, 116 insertions(+), 94 deletions(-)
diff --git a/app/Console/Commands/Cloud/CloudDeleteUser.php b/app/Console/Commands/Cloud/CloudDeleteUser.php
index 29580a95e..a2ea9b3e5 100644
--- a/app/Console/Commands/Cloud/CloudDeleteUser.php
+++ b/app/Console/Commands/Cloud/CloudDeleteUser.php
@@ -8,6 +8,7 @@
use App\Actions\User\DeleteUserTeams;
use App\Models\User;
use Illuminate\Console\Command;
+use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -54,124 +55,141 @@ public function handle()
return 1;
}
- $this->logAction("Starting user deletion process for: {$email}");
+ // Implement file lock to prevent concurrent deletions of the same user
+ $lockKey = "user_deletion_{$this->user->id}";
+ $lock = Cache::lock($lockKey, 600); // 10 minute lock
- // Phase 1: Show User Overview (outside transaction)
- if (! $this->showUserOverview()) {
- $this->info('User deletion cancelled.');
+ if (! $lock->get()) {
+ $this->error('Another deletion process is already running for this user. Please try again later.');
+ $this->logAction("Deletion blocked for user {$email}: Another process is already running");
- return 0;
+ return 1;
}
- // If not dry run, wrap everything in a transaction
- if (! $this->isDryRun) {
- try {
- DB::beginTransaction();
+ try {
+ $this->logAction("Starting user deletion process for: {$email}");
+ // Phase 1: Show User Overview (outside transaction)
+ if (! $this->showUserOverview()) {
+ $this->info('User deletion cancelled.');
+ $lock->release();
+
+ return 0;
+ }
+
+ // If not dry run, wrap everything in a transaction
+ if (! $this->isDryRun) {
+ try {
+ DB::beginTransaction();
+
+ // Phase 2: Delete Resources
+ if (! $this->skipResources) {
+ if (! $this->deleteResources()) {
+ DB::rollBack();
+ $this->error('User deletion failed at resource deletion phase. All changes rolled back.');
+
+ return 1;
+ }
+ }
+
+ // Phase 3: Delete Servers
+ if (! $this->deleteServers()) {
+ DB::rollBack();
+ $this->error('User deletion failed at server deletion phase. All changes rolled back.');
+
+ return 1;
+ }
+
+ // Phase 4: Handle Teams
+ if (! $this->handleTeams()) {
+ DB::rollBack();
+ $this->error('User deletion failed at team handling phase. All changes rolled back.');
+
+ return 1;
+ }
+
+ // Phase 5: Cancel Stripe Subscriptions
+ if (! $this->skipStripe && isCloud()) {
+ if (! $this->cancelStripeSubscriptions()) {
+ DB::rollBack();
+ $this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.');
+
+ return 1;
+ }
+ }
+
+ // Phase 6: Delete User Profile
+ if (! $this->deleteUserProfile()) {
+ DB::rollBack();
+ $this->error('User deletion failed at final phase. All changes rolled back.');
+
+ return 1;
+ }
+
+ // Commit the transaction
+ DB::commit();
+
+ $this->newLine();
+ $this->info('✅ User deletion completed successfully!');
+ $this->logAction("User deletion completed for: {$email}");
+
+ } catch (\Exception $e) {
+ DB::rollBack();
+ $this->error('An error occurred during user deletion: '.$e->getMessage());
+ $this->logAction("User deletion failed for {$email}: ".$e->getMessage());
+
+ return 1;
+ }
+ } else {
+ // Dry run mode - just run through the phases without transaction
// Phase 2: Delete Resources
if (! $this->skipResources) {
if (! $this->deleteResources()) {
- DB::rollBack();
- $this->error('User deletion failed at resource deletion phase. All changes rolled back.');
+ $this->info('User deletion would be cancelled at resource deletion phase.');
- return 1;
+ return 0;
}
}
// Phase 3: Delete Servers
if (! $this->deleteServers()) {
- DB::rollBack();
- $this->error('User deletion failed at server deletion phase. All changes rolled back.');
+ $this->info('User deletion would be cancelled at server deletion phase.');
- return 1;
+ return 0;
}
// Phase 4: Handle Teams
if (! $this->handleTeams()) {
- DB::rollBack();
- $this->error('User deletion failed at team handling phase. All changes rolled back.');
+ $this->info('User deletion would be cancelled at team handling phase.');
- return 1;
+ return 0;
}
// Phase 5: Cancel Stripe Subscriptions
if (! $this->skipStripe && isCloud()) {
if (! $this->cancelStripeSubscriptions()) {
- DB::rollBack();
- $this->error('User deletion failed at Stripe cancellation phase. All changes rolled back.');
+ $this->info('User deletion would be cancelled at Stripe cancellation phase.');
- return 1;
+ return 0;
}
}
// Phase 6: Delete User Profile
if (! $this->deleteUserProfile()) {
- DB::rollBack();
- $this->error('User deletion failed at final phase. All changes rolled back.');
+ $this->info('User deletion would be cancelled at final phase.');
- return 1;
+ return 0;
}
- // Commit the transaction
- DB::commit();
-
$this->newLine();
- $this->info('✅ User deletion completed successfully!');
- $this->logAction("User deletion completed for: {$email}");
-
- } catch (\Exception $e) {
- DB::rollBack();
- $this->error('An error occurred during user deletion: '.$e->getMessage());
- $this->logAction("User deletion failed for {$email}: ".$e->getMessage());
-
- return 1;
- }
- } else {
- // Dry run mode - just run through the phases without transaction
- // Phase 2: Delete Resources
- if (! $this->skipResources) {
- if (! $this->deleteResources()) {
- $this->info('User deletion would be cancelled at resource deletion phase.');
-
- return 0;
- }
+ $this->info('✅ DRY RUN completed successfully! No data was deleted.');
}
- // Phase 3: Delete Servers
- if (! $this->deleteServers()) {
- $this->info('User deletion would be cancelled at server deletion phase.');
-
- return 0;
- }
-
- // Phase 4: Handle Teams
- if (! $this->handleTeams()) {
- $this->info('User deletion would be cancelled at team handling phase.');
-
- return 0;
- }
-
- // Phase 5: Cancel Stripe Subscriptions
- if (! $this->skipStripe && isCloud()) {
- if (! $this->cancelStripeSubscriptions()) {
- $this->info('User deletion would be cancelled at Stripe cancellation phase.');
-
- return 0;
- }
- }
-
- // Phase 6: Delete User Profile
- if (! $this->deleteUserProfile()) {
- $this->info('User deletion would be cancelled at final phase.');
-
- return 0;
- }
-
- $this->newLine();
- $this->info('✅ DRY RUN completed successfully! No data was deleted.');
+ return 0;
+ } finally {
+ // Ensure lock is always released
+ $lock->release();
}
-
- return 0;
}
private function showUserOverview(): bool
@@ -683,24 +701,21 @@ private function deleteUserProfile(): bool
private function getSubscriptionMonthlyValue(string $planId): int
{
- // Map plan IDs to monthly values based on config
- $subscriptionConfigs = config('subscription');
+ // Try to get pricing from subscription metadata or config
+ // Since we're using dynamic pricing, return 0 for now
+ // This could be enhanced by fetching the actual price from Stripe API
- foreach ($subscriptionConfigs as $key => $value) {
- if ($value === $planId && str_contains($key, 'stripe_price_id_')) {
- // Extract price from key pattern: stripe_price_id_basic_monthly -> basic
- $planType = str($key)->after('stripe_price_id_')->before('_')->toString();
+ // Check if this is a dynamic pricing plan
+ $dynamicMonthlyPlanId = config('subscription.stripe_price_id_dynamic_monthly');
+ $dynamicYearlyPlanId = config('subscription.stripe_price_id_dynamic_yearly');
- // Map to known prices (you may need to adjust these based on your actual pricing)
- return match ($planType) {
- 'basic' => 29,
- 'pro' => 49,
- 'ultimate' => 99,
- default => 0
- };
- }
+ if ($planId === $dynamicMonthlyPlanId || $planId === $dynamicYearlyPlanId) {
+ // For dynamic pricing, we can't determine the exact amount without calling Stripe API
+ // Return 0 to indicate dynamic/usage-based pricing
+ return 0;
}
+ // For any other plans, return 0 as we don't have hardcoded prices
return 0;
}
@@ -716,6 +731,13 @@ private function logAction(string $message): void
// Also log to a dedicated user deletion log file
$logFile = storage_path('logs/user-deletions.log');
+
+ // Ensure the logs directory exists
+ $logDir = dirname($logFile);
+ if (! is_dir($logDir)) {
+ mkdir($logDir, 0755, true);
+ }
+
$timestamp = now()->format('Y-m-d H:i:s');
file_put_contents($logFile, "[{$timestamp}] {$logMessage}\n", FILE_APPEND | LOCK_EX);
}
From 33aa32d1e667676f69e141dfc16f4e589850cff4 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 29 Sep 2025 14:04:14 +0200
Subject: [PATCH 04/28] chore(versions): update coolify version to
4.0.0-beta.433 and nightly version to 4.0.0-beta.434 in configuration files
---
config/constants.php | 2 +-
other/nightly/versions.json | 4 ++--
versions.json | 4 ++--
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/config/constants.php b/config/constants.php
index ea73d426a..749d6435b 100644
--- a/config/constants.php
+++ b/config/constants.php
@@ -2,7 +2,7 @@
return [
'coolify' => [
- 'version' => '4.0.0-beta.432',
+ 'version' => '4.0.0-beta.433',
'helper_version' => '1.0.11',
'realtime_version' => '1.0.10',
'self_hosted' => env('SELF_HOSTED', true),
diff --git a/other/nightly/versions.json b/other/nightly/versions.json
index 3255c215b..b5cf3360a 100644
--- a/other/nightly/versions.json
+++ b/other/nightly/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.432"
+ "version": "4.0.0-beta.433"
},
"nightly": {
- "version": "4.0.0-beta.433"
+ "version": "4.0.0-beta.434"
},
"helper": {
"version": "1.0.11"
diff --git a/versions.json b/versions.json
index 3255c215b..b5cf3360a 100644
--- a/versions.json
+++ b/versions.json
@@ -1,10 +1,10 @@
{
"coolify": {
"v4": {
- "version": "4.0.0-beta.432"
+ "version": "4.0.0-beta.433"
},
"nightly": {
- "version": "4.0.0-beta.433"
+ "version": "4.0.0-beta.434"
},
"helper": {
"version": "1.0.11"
From 6de181222dcb3a87801a68b88836444f17a25d77 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 29 Sep 2025 14:44:39 +0200
Subject: [PATCH 05/28] fix(ui): correct HTML structure and improve clarity in
Docker cleanup options
---
.../views/livewire/server/docker-cleanup.blade.php | 12 +++++-------
1 file changed, 5 insertions(+), 7 deletions(-)
diff --git a/resources/views/livewire/server/docker-cleanup.blade.php b/resources/views/livewire/server/docker-cleanup.blade.php
index c2d33bdda..8e96bc963 100644
--- a/resources/views/livewire/server/docker-cleanup.blade.php
+++ b/resources/views/livewire/server/docker-cleanup.blade.php
@@ -52,8 +52,7 @@
Optionally delete unused volumes (if enabled in advanced options).
Optionally remove unused networks (if enabled in advanced options).
"
- instantSave id="forceDockerCleanup" label="Force Docker Cleanup"
- />
+ instantSave id="forceDockerCleanup" label="Force Docker Cleanup" />
@@ -61,7 +60,8 @@
Advanced
- These options can cause permanent data loss and functional issues. Only enable if you fully understand the consequences
+ These options can cause permanent data loss and functional issues. Only enable if you fully
+ understand the consequences.
Volumes not attached to running containers will be permanently deleted (volumes from stopped containers are affected).
Data stored in deleted volumes cannot be recovered.
- "
- />
+ " />
+ " />
From ef4527ed47c62a5024b8ac60f6db7bdeb64264a6 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 29 Sep 2025 14:44:50 +0200
Subject: [PATCH 06/28] feat(ui): enhance resource operations interface with
dynamic selection for cloning and moving resources
---
.../shared/resource-operations.blade.php | 228 ++++++++++++------
1 file changed, 159 insertions(+), 69 deletions(-)
diff --git a/resources/views/livewire/project/shared/resource-operations.blade.php b/resources/views/livewire/project/shared/resource-operations.blade.php
index be7cbd7dc..3d850f542 100644
--- a/resources/views/livewire/project/shared/resource-operations.blade.php
+++ b/resources/views/livewire/project/shared/resource-operations.blade.php
@@ -1,76 +1,166 @@
Resource Operations
You can easily make different kind of operations on this resource.
-
Clone
-
To another project / environment on a different / same server.
-
-
- @can('update', $resource)
- @foreach ($servers->sortBy('id') as $server)
-
Server: {{ $server->name }}
- @foreach ($server->destinations() as $destination)
-
-
-
-
-
Network
-
{{ $destination->name }}
-
-
-
-
- @endforeach
- @endforeach
- @else
-
- You don't have permission to clone resources. Contact your team administrator to request access.
-
- @endcan
-
-
-
Move
-
Between projects / environments.
-
-
- This resource is currently in the {{ $resource->environment->project->name }} /
- {{ $resource->environment->name }} environment.
-
-
- @can('update', $resource)
- @forelse ($projects as $project)
-
Project: {{ $project->name }}
- @foreach ($project->environments as $environment)
-
-
-
-
-
Environment
-
{{ $environment->name }}
-
-
-
-
- @endforeach
- @empty
-
No projects found to move to
- @endforelse
+
+
Clone Resource
+
Duplicate this resource to another server or network destination.
+
+ @can('update', $resource)
+
+
+
+ Select Server
+
+ Choose a server...
+
+
+
+
+
+
+
+ Select Network Destination
+
+ Choose a destination...
+
+
+
+
+
+
+
+
+
+ Clone Resource
+
+
+ All configurations will be duplicated to the selected destination. The running application won't be
+ touched.
+
+
+
+ @else
+
+ You don't have permission to clone resources. Contact your team administrator to request access.
+
+ @endcan
+
+
Move Resource
+
Transfer this resource between projects and environments.
+
+ @can('update', $resource)
+ @if ($projects->count() > 0)
+
+
+
+ Select Target Project
+
+ Choose a project...
+
+
+
+
+
+
+
+
+ Select Target
+ Environment
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Move Resource
+
+
+ All configurations will be moved to the selected environment. The running application won't be
+ touched.
+
+
+
@else
-
- You don't have permission to move resources between projects or environments. Contact your team administrator to request access.
-
- @endcan
-
+
No other projects available for moving this resource.
+
+ @endif
+ @else
+
+ You don't have permission to move resources between projects or environments. Contact your team
+ administrator to request access.
+
+ @endcan
From e9600159898fa32267497ef20a32981e7550d00d Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Mon, 29 Sep 2025 10:52:08 +0000
Subject: [PATCH 07/28] docs: update changelog
---
CHANGELOG.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++---
1 file changed, 72 insertions(+), 4 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 04b99c646..3447b223b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,10 +4,40 @@ # Changelog
## [unreleased]
+### 🚀 Features
+
+- *(application)* Implement order-based pattern matching for watch paths with negation support
+- *(github)* Enhance Docker Compose input fields for better user experience
+- *(dev-seeders)* Add PersonalAccessTokenSeeder to create development API tokens
+- *(application)* Add conditional .env file creation for Symfony apps during PHP deployment
+- *(application)* Enhance watch path parsing to support negation syntax
+- *(application)* Add normalizeWatchPaths method to improve watch path handling
+- *(validation)* Enhance ValidGitRepositoryUrl to support additional safe characters and add comprehensive unit tests for various Git repository URL formats
+- *(deployment)* Implement detection for Laravel/Symfony frameworks and configure NIXPACKS PHP environment variables accordingly
+
### 🐛 Bug Fixes
-- *(docker)* Adjust openssh-client installation in Dockerfile to avoid version bug
-- *(docker)* Streamline openssh-client installation in Dockerfile
+- *(application)* Restrict GitHub-based application settings to non-public repositories
+- *(traits)* Update saved_outputs handling in ExecuteRemoteCommand to use collection methods for better performance
+- *(application)* Enhance domain handling by replacing both dots and dashes with underscores for HTML form binding
+- *(constants)* Reduce command timeout from 7200 to 3600 seconds for improved performance
+- *(github)* Update repository URL to point to the v4.x branch for development
+- *(models)* Update sorting of scheduled database backups to order by creation date instead of name
+- *(socialite)* Add custom base URL support for GitLab provider in OAuth settings
+- *(configuration-checker)* Update message to clarify redeployment requirement for configuration changes
+- *(application)* Reduce docker stop timeout from 30 to 10 seconds for improved application shutdown efficiency
+- *(application)* Increase docker stop timeout from 10 to 30 seconds for better application shutdown handling
+- *(validation)* Update git:// URL validation to support port numbers and tilde characters in paths
+- Resolve scroll lock issue after closing quick search modal with escape key
+- Prevent quick search modal duplication from keyboard shortcuts
+
+### 🚜 Refactor
+
+- *(tests)* Simplify matchWatchPaths tests and update implementation for better clarity
+- *(deployment)* Improve environment variable handling in ApplicationDeploymentJob
+- *(deployment)* Remove commented-out code and streamline environment variable handling in ApplicationDeploymentJob
+- *(application)* Improve handling of docker compose domains by normalizing keys and ensuring valid JSON structure
+- *(forms)* Update wire:model bindings to use 'blur' instead of 'blur-sm' for input fields across multiple views
### 📚 Documentation
@@ -15,20 +45,58 @@ ### 📚 Documentation
### ⚙️ Miscellaneous Tasks
-- *(versions)* Increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files
+- *(application)* Remove debugging statement from loadComposeFile method
+- *(workflows)* Update Claude GitHub Action configuration to support new event types and improve permissions
+
+## [4.0.0-beta.431] - 2025-09-24
+
+### 📚 Documentation
+
+- Update changelog
## [4.0.0-beta.430] - 2025-09-24
+### 🚀 Features
+
+- *(add-watch-paths-for-services)* Show watch paths field for docker compose applications
+
### 🐛 Bug Fixes
- *(PreviewCompose)* Adds port to preview urls
- *(deployment-job)* Enhance build time variable analysis
+- *(docker)* Adjust openssh-client installation in Dockerfile to avoid version bug
+- *(docker)* Streamline openssh-client installation in Dockerfile
+- *(team)* Normalize email case in invite link generation
+- *(README)* Update Juxtdigital description to reflect current services
+- *(environment-variable-warning)* Enhance warning logic to check for problematic variable values
+- *(install)* Ensure proper quoting of environment file paths to prevent issues with spaces
+- *(security)* Implement authorization checks for terminal access management
+- *(ui)* Improve mobile sidebar close behavior
+
+### 🚜 Refactor
+
+- *(installer)* Improve install script
+- *(upgrade)* Improve upgrade script
+- *(installer, upgrade)* Enhance environment variable management
+- *(upgrade)* Enhance logging and quoting in upgrade scripts
+- *(upgrade)* Replace warning div with a callout component for better UI consistency
+- *(ui)* Replace warning and error divs with callout components for improved consistency and readability
+- *(ui)* Improve styling and consistency in environment variable warning and docker cleanup components
+- *(security)* Streamline update check functionality and improve UI button interactions in patches view
### 📚 Documentation
- Update changelog
- Update changelog
+### ⚙️ Miscellaneous Tasks
+
+- *(versions)* Increment coolify version numbers to 4.0.0-beta.431 and 4.0.0-beta.432 in configuration files
+- *(versions)* Update coolify version numbers to 4.0.0-beta.432 and 4.0.0-beta.433 in configuration files
+- Remove unused files
+- Adjust wording
+- *(workflow)* Update pull request trigger to pull_request_target and refine permissions for enhanced security
+
## [4.0.0-beta.429] - 2025-09-23
### 🚀 Features
@@ -39,6 +107,7 @@ ### 🚀 Features
- *(search)* Enable query logging for global search caching
- *(environment)* Add dynamic checkbox options for environment variable settings based on user permissions and variable types
- *(redaction)* Implement sensitive information redaction in logs and commands
+- Improve detection of special network modes
- *(api)* Add endpoint to update backup configuration by UUID and backup ID; modify response to include backup id
- *(databases)* Enhance backup management API with new endpoints and improved data handling
- *(github)* Add GitHub app management endpoints
@@ -119,7 +188,6 @@ ## [4.0.0-beta.427] - 2025-09-15
### 🚀 Features
-- Improve detection of special network modes
- *(command)* Add option to sync GitHub releases to BunnyCDN and refactor sync logic
- *(ui)* Display current version in settings dropdown and update UI accordingly
- *(settings)* Add option to restrict PR deployments to repository members and contributors
From 5fcc3a32348c33821ced50641cc44565968203cd Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 29 Sep 2025 15:06:49 +0200
Subject: [PATCH 08/28] fix(workflows): update CLAUDE API key reference in
GitHub Actions workflow
---
.github/workflows/claude.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index b0fc41448..bf0e6bedc 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -38,7 +38,7 @@ jobs:
id: claude
uses: anthropics/claude-code-action@v1
with:
- anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
+ anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
From 323bff5632fbc35947734de036f76d676b7512d5 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Mon, 29 Sep 2025 15:11:18 +0200
Subject: [PATCH 09/28] Update claude.yml
---
.github/workflows/claude.yml | 49 +++++++++++++++++++-----------------
1 file changed, 26 insertions(+), 23 deletions(-)
diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml
index bf0e6bedc..9daf0e90e 100644
--- a/.github/workflows/claude.yml
+++ b/.github/workflows/claude.yml
@@ -6,9 +6,7 @@ on:
pull_request_review_comment:
types: [created]
issues:
- types: [opened, assigned, labeled]
- pull_request:
- types: [labeled]
+ types: [opened, assigned]
pull_request_review:
types: [submitted]
@@ -20,12 +18,12 @@ jobs:
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
(github.event_name == 'pull_request' && github.event.action == 'labeled' && github.event.label.name == 'Claude') ||
- (github.event_name == 'issues' && github.event.action != 'labeled' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
+ (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
runs-on: ubuntu-latest
permissions:
- contents: write
- pull-requests: write
- issues: write
+ contents: read
+ pull-requests: read
+ issues: read
id-token: write
actions: read # Required for Claude to read CI results on PRs
steps:
@@ -40,23 +38,28 @@ jobs:
with:
anthropic_api_key: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
+ # This is an optional setting that allows Claude to read CI results on PRs
+ additional_permissions: |
+ actions: read
+
+ # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4.1)
+ # model: "claude-opus-4-1-20250805"
+
# Optional: Customize the trigger phrase (default: @claude)
# trigger_phrase: "/claude"
-
+
# Optional: Trigger when specific user is assigned to an issue
# assignee_trigger: "claude-bot"
-
- # Optional: Configure Claude's behavior with CLI arguments
- # claude_args: |
- # --model claude-opus-4-1-20250805
- # --max-turns 10
- # --allowedTools "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
- # --system-prompt "Follow our coding standards. Ensure all new code has tests. Use TypeScript for new files."
-
- # Optional: Advanced settings configuration
- # settings: |
- # {
- # "env": {
- # "NODE_ENV": "test"
- # }
- # }
\ No newline at end of file
+
+ # Optional: Allow Claude to run specific commands
+ # allowed_tools: "Bash(npm install),Bash(npm run build),Bash(npm run test:*),Bash(npm run lint:*)"
+
+ # Optional: Add custom instructions for Claude to customize its behavior for your project
+ # custom_instructions: |
+ # Follow our coding standards
+ # Ensure all new code has tests
+ # Use TypeScript for new files
+
+ # Optional: Custom environment variables for Claude
+ # claude_env: |
+ # NODE_ENV: test
From db2d44ca1ff1c86de05937873dd41c84d2aab31d Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 11:19:39 +0200
Subject: [PATCH 10/28] fix(api): correct OpenAPI schema annotations for array
items
- Replace OA\Schema with OA\Items for array items in DatabasesController
- Replace OA\Items with OA\Schema for array type properties in GithubController
- Update generated OpenAPI documentation files (openapi.json and openapi.yaml)
---
.../Controllers/Api/DatabasesController.php | 2 +-
app/Http/Controllers/Api/GithubController.php | 8 +-
openapi.json | 887 ++++++++++++++++++
openapi.yaml | 559 +++++++++++
4 files changed, 1451 insertions(+), 5 deletions(-)
diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php
index 0e282fccd..5871f481a 100644
--- a/app/Http/Controllers/Api/DatabasesController.php
+++ b/app/Http/Controllers/Api/DatabasesController.php
@@ -2173,7 +2173,7 @@ public function delete_execution_by_uuid(Request $request)
properties: [
'executions' => new OA\Schema(
type: 'array',
- items: new OA\Schema(
+ items: new OA\Items(
type: 'object',
properties: [
'uuid' => ['type' => 'string'],
diff --git a/app/Http/Controllers/Api/GithubController.php b/app/Http/Controllers/Api/GithubController.php
index 8c95a585f..8c8c87238 100644
--- a/app/Http/Controllers/Api/GithubController.php
+++ b/app/Http/Controllers/Api/GithubController.php
@@ -219,9 +219,9 @@ public function create_github_app(Request $request)
schema: new OA\Schema(
type: 'object',
properties: [
- 'repositories' => new OA\Items(
+ 'repositories' => new OA\Schema(
type: 'array',
- items: new OA\Schema(type: 'object')
+ items: new OA\Items(type: 'object')
),
]
)
@@ -335,9 +335,9 @@ public function load_repositories($github_app_id)
schema: new OA\Schema(
type: 'object',
properties: [
- 'branches' => new OA\Items(
+ 'branches' => new OA\Schema(
type: 'array',
- items: new OA\Schema(type: 'object')
+ items: new OA\Items(type: 'object')
),
]
)
diff --git a/openapi.json b/openapi.json
index 2b0a81c6e..901741dd0 100644
--- a/openapi.json
+++ b/openapi.json
@@ -3309,6 +3309,55 @@
]
}
},
+ "\/databases\/{uuid}\/backups": {
+ "get": {
+ "tags": [
+ "Databases"
+ ],
+ "summary": "Get",
+ "description": "Get backups details by database UUID.",
+ "operationId": "get-database-backups-by-uuid",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "description": "UUID of the database.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Get all backups for a database",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "type": "string"
+ },
+ "example": "Content is very complex. Will be implemented later."
+ }
+ }
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "404": {
+ "$ref": "#\/components\/responses\/404"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
"\/databases\/{uuid}": {
"get": {
"tags": [
@@ -3658,6 +3707,200 @@
]
}
},
+ "\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}": {
+ "delete": {
+ "tags": [
+ "Databases"
+ ],
+ "summary": "Delete backup configuration",
+ "description": "Deletes a backup configuration and all its executions.",
+ "operationId": "delete-backup-configuration-by-uuid",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "description": "UUID of the database",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "scheduled_backup_uuid",
+ "in": "path",
+ "description": "UUID of the backup configuration to delete",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "delete_s3",
+ "in": "query",
+ "description": "Whether to delete all backup files from S3",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Backup configuration deleted.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "string",
+ "example": "Backup configuration and all executions deleted."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Backup configuration not found.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "string",
+ "example": "Backup configuration not found."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ },
+ "patch": {
+ "tags": [
+ "Databases"
+ ],
+ "summary": "Update",
+ "description": "Update a specific backup configuration for a given database, identified by its UUID and the backup ID",
+ "operationId": "update-database-backup",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "description": "UUID of the database.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "scheduled_backup_uuid",
+ "in": "path",
+ "description": "UUID of the backup configuration.",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "requestBody": {
+ "description": "Database backup configuration data",
+ "required": true,
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "save_s3": {
+ "type": "boolean",
+ "description": "Whether data is saved in s3 or not"
+ },
+ "s3_storage_uuid": {
+ "type": "string",
+ "description": "S3 storage UUID"
+ },
+ "backup_now": {
+ "type": "boolean",
+ "description": "Whether to take a backup now or not"
+ },
+ "enabled": {
+ "type": "boolean",
+ "description": "Whether the backup is enabled or not"
+ },
+ "databases_to_backup": {
+ "type": "string",
+ "description": "Comma separated list of databases to backup"
+ },
+ "dump_all": {
+ "type": "boolean",
+ "description": "Whether all databases are dumped or not"
+ },
+ "frequency": {
+ "type": "string",
+ "description": "Frequency of the backup"
+ },
+ "database_backup_retention_amount_locally": {
+ "type": "integer",
+ "description": "Retention amount of the backup locally"
+ },
+ "database_backup_retention_days_locally": {
+ "type": "integer",
+ "description": "Retention days of the backup locally"
+ },
+ "database_backup_retention_max_storage_locally": {
+ "type": "integer",
+ "description": "Max storage of the backup locally"
+ },
+ "database_backup_retention_amount_s3": {
+ "type": "integer",
+ "description": "Retention amount of the backup in s3"
+ },
+ "database_backup_retention_days_s3": {
+ "type": "integer",
+ "description": "Retention days of the backup in s3"
+ },
+ "database_backup_retention_max_storage_s3": {
+ "type": "integer",
+ "description": "Max storage of the backup in S3"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Database backup configuration updated"
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "404": {
+ "$ref": "#\/components\/responses\/404"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
"\/databases\/postgresql": {
"post": {
"tags": [
@@ -4694,6 +4937,175 @@
]
}
},
+ "\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions\/{execution_uuid}": {
+ "delete": {
+ "tags": [
+ "Databases"
+ ],
+ "summary": "Delete backup execution",
+ "description": "Deletes a specific backup execution.",
+ "operationId": "delete-backup-execution-by-uuid",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "description": "UUID of the database",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "scheduled_backup_uuid",
+ "in": "path",
+ "description": "UUID of the backup configuration",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "execution_uuid",
+ "in": "path",
+ "description": "UUID of the backup execution to delete",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ },
+ {
+ "name": "delete_s3",
+ "in": "query",
+ "description": "Whether to delete the backup from S3",
+ "required": false,
+ "schema": {
+ "type": "boolean",
+ "default": false
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Backup execution deleted.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "string",
+ "example": "Backup execution deleted."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Backup execution not found.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "string",
+ "example": "Backup execution not found."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
+ "\/databases\/{uuid}\/backups\/{scheduled_backup_uuid}\/executions": {
+ "get": {
+ "tags": [
+ "Databases"
+ ],
+ "summary": "List backup executions",
+ "description": "Get all executions for a specific backup configuration.",
+ "operationId": "list-backup-executions",
+ "parameters": [
+ {
+ "name": "uuid",
+ "in": "path",
+ "description": "UUID of the database",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "scheduled_backup_uuid",
+ "in": "path",
+ "description": "UUID of the backup configuration",
+ "required": true,
+ "schema": {
+ "type": "string",
+ "format": "uuid"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "List of backup executions",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "array",
+ "items": {
+ "properties": {
+ "uuid": {
+ "type": "string"
+ },
+ "filename": {
+ "type": "string"
+ },
+ "size": {
+ "type": "integer"
+ },
+ "created_at": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "404": {
+ "description": "Backup configuration not found."
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
"\/databases\/{uuid}\/start": {
"get": {
"tags": [
@@ -5095,6 +5507,477 @@
]
}
},
+ "\/github-apps": {
+ "post": {
+ "tags": [
+ "GitHub Apps"
+ ],
+ "summary": "Create GitHub App",
+ "description": "Create a new GitHub app.",
+ "operationId": "create-github-app",
+ "requestBody": {
+ "description": "GitHub app creation payload.",
+ "required": true,
+ "content": {
+ "application\/json": {
+ "schema": {
+ "required": [
+ "name",
+ "api_url",
+ "html_url",
+ "app_id",
+ "installation_id",
+ "client_id",
+ "client_secret",
+ "private_key_uuid"
+ ],
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Name of the GitHub app."
+ },
+ "organization": {
+ "type": "string",
+ "nullable": true,
+ "description": "Organization to associate the app with."
+ },
+ "api_url": {
+ "type": "string",
+ "description": "API URL for the GitHub app (e.g., https:\/\/api.github.com)."
+ },
+ "html_url": {
+ "type": "string",
+ "description": "HTML URL for the GitHub app (e.g., https:\/\/github.com)."
+ },
+ "custom_user": {
+ "type": "string",
+ "description": "Custom user for SSH access (default: git)."
+ },
+ "custom_port": {
+ "type": "integer",
+ "description": "Custom port for SSH access (default: 22)."
+ },
+ "app_id": {
+ "type": "integer",
+ "description": "GitHub App ID from GitHub."
+ },
+ "installation_id": {
+ "type": "integer",
+ "description": "GitHub Installation ID."
+ },
+ "client_id": {
+ "type": "string",
+ "description": "GitHub OAuth App Client ID."
+ },
+ "client_secret": {
+ "type": "string",
+ "description": "GitHub OAuth App Client Secret."
+ },
+ "webhook_secret": {
+ "type": "string",
+ "description": "Webhook secret for GitHub webhooks."
+ },
+ "private_key_uuid": {
+ "type": "string",
+ "description": "UUID of an existing private key for GitHub App authentication."
+ },
+ "is_system_wide": {
+ "type": "boolean",
+ "description": "Is this app system-wide (cloud only)."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "GitHub app created successfully.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "id": {
+ "type": "integer"
+ },
+ "uuid": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "organization": {
+ "type": "string",
+ "nullable": true
+ },
+ "api_url": {
+ "type": "string"
+ },
+ "html_url": {
+ "type": "string"
+ },
+ "custom_user": {
+ "type": "string"
+ },
+ "custom_port": {
+ "type": "integer"
+ },
+ "app_id": {
+ "type": "integer"
+ },
+ "installation_id": {
+ "type": "integer"
+ },
+ "client_id": {
+ "type": "string"
+ },
+ "private_key_id": {
+ "type": "integer"
+ },
+ "is_system_wide": {
+ "type": "boolean"
+ },
+ "team_id": {
+ "type": "integer"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "422": {
+ "$ref": "#\/components\/responses\/422"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
+ "\/github-apps\/{github_app_id}\/repositories": {
+ "get": {
+ "tags": [
+ "GitHub Apps"
+ ],
+ "summary": "Load Repositories for a GitHub App",
+ "description": "Fetch repositories from GitHub for a given GitHub app.",
+ "operationId": "load-repositories",
+ "parameters": [
+ {
+ "name": "github_app_id",
+ "in": "path",
+ "description": "GitHub App ID",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Repositories loaded successfully.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "404": {
+ "$ref": "#\/components\/responses\/404"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
+ "\/github-apps\/{github_app_id}\/repositories\/{owner}\/{repo}\/branches": {
+ "get": {
+ "tags": [
+ "GitHub Apps"
+ ],
+ "summary": "Load Branches for a GitHub Repository",
+ "description": "Fetch branches from GitHub for a given repository.",
+ "operationId": "load-branches",
+ "parameters": [
+ {
+ "name": "github_app_id",
+ "in": "path",
+ "description": "GitHub App ID",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "owner",
+ "in": "path",
+ "description": "Repository owner",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "repo",
+ "in": "path",
+ "description": "Repository name",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Branches loaded successfully.",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "": {
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "400": {
+ "$ref": "#\/components\/responses\/400"
+ },
+ "401": {
+ "$ref": "#\/components\/responses\/401"
+ },
+ "404": {
+ "$ref": "#\/components\/responses\/404"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
+ "\/github-apps\/{github_app_id}": {
+ "delete": {
+ "tags": [
+ "GitHub Apps"
+ ],
+ "summary": "Delete GitHub App",
+ "description": "Delete a GitHub app if it's not being used by any applications.",
+ "operationId": "deleteGithubApp",
+ "parameters": [
+ {
+ "name": "github_app_id",
+ "in": "path",
+ "description": "GitHub App ID",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "GitHub app deleted successfully",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "GitHub app deleted successfully"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "404": {
+ "description": "GitHub app not found"
+ },
+ "409": {
+ "description": "Conflict - GitHub app is in use",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "This GitHub app is being used by 5 application(s). Please delete all applications first."
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ },
+ "patch": {
+ "tags": [
+ "GitHub Apps"
+ ],
+ "summary": "Update GitHub App",
+ "description": "Update an existing GitHub app.",
+ "operationId": "updateGithubApp",
+ "parameters": [
+ {
+ "name": "github_app_id",
+ "in": "path",
+ "description": "GitHub App ID",
+ "required": true,
+ "schema": {
+ "type": "integer"
+ }
+ }
+ ],
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "GitHub App name"
+ },
+ "organization": {
+ "type": "string",
+ "nullable": true,
+ "description": "GitHub organization"
+ },
+ "api_url": {
+ "type": "string",
+ "description": "GitHub API URL"
+ },
+ "html_url": {
+ "type": "string",
+ "description": "GitHub HTML URL"
+ },
+ "custom_user": {
+ "type": "string",
+ "description": "Custom user for SSH"
+ },
+ "custom_port": {
+ "type": "integer",
+ "description": "Custom port for SSH"
+ },
+ "app_id": {
+ "type": "integer",
+ "description": "GitHub App ID"
+ },
+ "installation_id": {
+ "type": "integer",
+ "description": "GitHub Installation ID"
+ },
+ "client_id": {
+ "type": "string",
+ "description": "GitHub Client ID"
+ },
+ "client_secret": {
+ "type": "string",
+ "description": "GitHub Client Secret"
+ },
+ "webhook_secret": {
+ "type": "string",
+ "description": "GitHub Webhook Secret"
+ },
+ "private_key_uuid": {
+ "type": "string",
+ "description": "Private key UUID"
+ },
+ "is_system_wide": {
+ "type": "boolean",
+ "description": "Is system wide (non-cloud instances only)"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "GitHub app updated successfully",
+ "content": {
+ "application\/json": {
+ "schema": {
+ "properties": {
+ "message": {
+ "type": "string",
+ "example": "GitHub app updated successfully"
+ },
+ "data": {
+ "type": "object",
+ "description": "Updated GitHub app data"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ },
+ "401": {
+ "description": "Unauthorized"
+ },
+ "404": {
+ "description": "GitHub app not found"
+ },
+ "422": {
+ "description": "Validation error"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ]
+ }
+ },
"\/version": {
"get": {
"summary": "Version",
@@ -8890,6 +9773,10 @@
"name": "Deployments",
"description": "Deployments"
},
+ {
+ "name": "GitHub Apps",
+ "description": "GitHub Apps"
+ },
{
"name": "Projects",
"description": "Projects"
diff --git a/openapi.yaml b/openapi.yaml
index 9529fcf87..3e39c5d36 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -2097,6 +2097,39 @@ paths:
security:
-
bearerAuth: []
+ '/databases/{uuid}/backups':
+ get:
+ tags:
+ - Databases
+ summary: Get
+ description: 'Get backups details by database UUID.'
+ operationId: get-database-backups-by-uuid
+ parameters:
+ -
+ name: uuid
+ in: path
+ description: 'UUID of the database.'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ '200':
+ description: 'Get all backups for a database'
+ content:
+ application/json:
+ schema:
+ type: string
+ example: 'Content is very complex. Will be implemented later.'
+ '401':
+ $ref: '#/components/responses/401'
+ '400':
+ $ref: '#/components/responses/400'
+ '404':
+ $ref: '#/components/responses/404'
+ security:
+ -
+ bearerAuth: []
'/databases/{uuid}':
get:
tags:
@@ -2347,6 +2380,139 @@ paths:
security:
-
bearerAuth: []
+ '/databases/{uuid}/backups/{scheduled_backup_uuid}':
+ delete:
+ tags:
+ - Databases
+ summary: 'Delete backup configuration'
+ description: 'Deletes a backup configuration and all its executions.'
+ operationId: delete-backup-configuration-by-uuid
+ parameters:
+ -
+ name: uuid
+ in: path
+ description: 'UUID of the database'
+ required: true
+ schema:
+ type: string
+ -
+ name: scheduled_backup_uuid
+ in: path
+ description: 'UUID of the backup configuration to delete'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ -
+ name: delete_s3
+ in: query
+ description: 'Whether to delete all backup files from S3'
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: 'Backup configuration deleted.'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: string, example: 'Backup configuration and all executions deleted.' }
+ type: object
+ '404':
+ description: 'Backup configuration not found.'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: string, example: 'Backup configuration not found.' }
+ type: object
+ security:
+ -
+ bearerAuth: []
+ patch:
+ tags:
+ - Databases
+ summary: Update
+ description: 'Update a specific backup configuration for a given database, identified by its UUID and the backup ID'
+ operationId: update-database-backup
+ parameters:
+ -
+ name: uuid
+ in: path
+ description: 'UUID of the database.'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ -
+ name: scheduled_backup_uuid
+ in: path
+ description: 'UUID of the backup configuration.'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ requestBody:
+ description: 'Database backup configuration data'
+ required: true
+ content:
+ application/json:
+ schema:
+ properties:
+ save_s3:
+ type: boolean
+ description: 'Whether data is saved in s3 or not'
+ s3_storage_uuid:
+ type: string
+ description: 'S3 storage UUID'
+ backup_now:
+ type: boolean
+ description: 'Whether to take a backup now or not'
+ enabled:
+ type: boolean
+ description: 'Whether the backup is enabled or not'
+ databases_to_backup:
+ type: string
+ description: 'Comma separated list of databases to backup'
+ dump_all:
+ type: boolean
+ description: 'Whether all databases are dumped or not'
+ frequency:
+ type: string
+ description: 'Frequency of the backup'
+ database_backup_retention_amount_locally:
+ type: integer
+ description: 'Retention amount of the backup locally'
+ database_backup_retention_days_locally:
+ type: integer
+ description: 'Retention days of the backup locally'
+ database_backup_retention_max_storage_locally:
+ type: integer
+ description: 'Max storage of the backup locally'
+ database_backup_retention_amount_s3:
+ type: integer
+ description: 'Retention amount of the backup in s3'
+ database_backup_retention_days_s3:
+ type: integer
+ description: 'Retention days of the backup in s3'
+ database_backup_retention_max_storage_s3:
+ type: integer
+ description: 'Max storage of the backup in S3'
+ type: object
+ responses:
+ '200':
+ description: 'Database backup configuration updated'
+ '401':
+ $ref: '#/components/responses/401'
+ '400':
+ $ref: '#/components/responses/400'
+ '404':
+ $ref: '#/components/responses/404'
+ security:
+ -
+ bearerAuth: []
/databases/postgresql:
post:
tags:
@@ -3094,6 +3260,102 @@ paths:
security:
-
bearerAuth: []
+ '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions/{execution_uuid}':
+ delete:
+ tags:
+ - Databases
+ summary: 'Delete backup execution'
+ description: 'Deletes a specific backup execution.'
+ operationId: delete-backup-execution-by-uuid
+ parameters:
+ -
+ name: uuid
+ in: path
+ description: 'UUID of the database'
+ required: true
+ schema:
+ type: string
+ -
+ name: scheduled_backup_uuid
+ in: path
+ description: 'UUID of the backup configuration'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ -
+ name: execution_uuid
+ in: path
+ description: 'UUID of the backup execution to delete'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ -
+ name: delete_s3
+ in: query
+ description: 'Whether to delete the backup from S3'
+ required: false
+ schema:
+ type: boolean
+ default: false
+ responses:
+ '200':
+ description: 'Backup execution deleted.'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: string, example: 'Backup execution deleted.' }
+ type: object
+ '404':
+ description: 'Backup execution not found.'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: string, example: 'Backup execution not found.' }
+ type: object
+ security:
+ -
+ bearerAuth: []
+ '/databases/{uuid}/backups/{scheduled_backup_uuid}/executions':
+ get:
+ tags:
+ - Databases
+ summary: 'List backup executions'
+ description: 'Get all executions for a specific backup configuration.'
+ operationId: list-backup-executions
+ parameters:
+ -
+ name: uuid
+ in: path
+ description: 'UUID of the database'
+ required: true
+ schema:
+ type: string
+ -
+ name: scheduled_backup_uuid
+ in: path
+ description: 'UUID of the backup configuration'
+ required: true
+ schema:
+ type: string
+ format: uuid
+ responses:
+ '200':
+ description: 'List of backup executions'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: array, items: { properties: { uuid: { type: string }, filename: { type: string }, size: { type: integer }, created_at: { type: string }, message: { type: string }, status: { type: string } }, type: object } }
+ type: object
+ '404':
+ description: 'Backup configuration not found.'
+ security:
+ -
+ bearerAuth: []
'/databases/{uuid}/start':
get:
tags:
@@ -3348,6 +3610,300 @@ paths:
security:
-
bearerAuth: []
+ /github-apps:
+ post:
+ tags:
+ - 'GitHub Apps'
+ summary: 'Create GitHub App'
+ description: 'Create a new GitHub app.'
+ operationId: create-github-app
+ requestBody:
+ description: 'GitHub app creation payload.'
+ required: true
+ content:
+ application/json:
+ schema:
+ required:
+ - name
+ - api_url
+ - html_url
+ - app_id
+ - installation_id
+ - client_id
+ - client_secret
+ - private_key_uuid
+ properties:
+ name:
+ type: string
+ description: 'Name of the GitHub app.'
+ organization:
+ type: string
+ nullable: true
+ description: 'Organization to associate the app with.'
+ api_url:
+ type: string
+ description: 'API URL for the GitHub app (e.g., https://api.github.com).'
+ html_url:
+ type: string
+ description: 'HTML URL for the GitHub app (e.g., https://github.com).'
+ custom_user:
+ type: string
+ description: 'Custom user for SSH access (default: git).'
+ custom_port:
+ type: integer
+ description: 'Custom port for SSH access (default: 22).'
+ app_id:
+ type: integer
+ description: 'GitHub App ID from GitHub.'
+ installation_id:
+ type: integer
+ description: 'GitHub Installation ID.'
+ client_id:
+ type: string
+ description: 'GitHub OAuth App Client ID.'
+ client_secret:
+ type: string
+ description: 'GitHub OAuth App Client Secret.'
+ webhook_secret:
+ type: string
+ description: 'Webhook secret for GitHub webhooks.'
+ private_key_uuid:
+ type: string
+ description: 'UUID of an existing private key for GitHub App authentication.'
+ is_system_wide:
+ type: boolean
+ description: 'Is this app system-wide (cloud only).'
+ type: object
+ responses:
+ '201':
+ description: 'GitHub app created successfully.'
+ content:
+ application/json:
+ schema:
+ properties:
+ id: { type: integer }
+ uuid: { type: string }
+ name: { type: string }
+ organization: { type: string, nullable: true }
+ api_url: { type: string }
+ html_url: { type: string }
+ custom_user: { type: string }
+ custom_port: { type: integer }
+ app_id: { type: integer }
+ installation_id: { type: integer }
+ client_id: { type: string }
+ private_key_id: { type: integer }
+ is_system_wide: { type: boolean }
+ team_id: { type: integer }
+ type: object
+ '400':
+ $ref: '#/components/responses/400'
+ '401':
+ $ref: '#/components/responses/401'
+ '422':
+ $ref: '#/components/responses/422'
+ security:
+ -
+ bearerAuth: []
+ '/github-apps/{github_app_id}/repositories':
+ get:
+ tags:
+ - 'GitHub Apps'
+ summary: 'Load Repositories for a GitHub App'
+ description: 'Fetch repositories from GitHub for a given GitHub app.'
+ operationId: load-repositories
+ parameters:
+ -
+ name: github_app_id
+ in: path
+ description: 'GitHub App ID'
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: 'Repositories loaded successfully.'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: array, items: { type: object } }
+ type: object
+ '400':
+ $ref: '#/components/responses/400'
+ '401':
+ $ref: '#/components/responses/401'
+ '404':
+ $ref: '#/components/responses/404'
+ security:
+ -
+ bearerAuth: []
+ '/github-apps/{github_app_id}/repositories/{owner}/{repo}/branches':
+ get:
+ tags:
+ - 'GitHub Apps'
+ summary: 'Load Branches for a GitHub Repository'
+ description: 'Fetch branches from GitHub for a given repository.'
+ operationId: load-branches
+ parameters:
+ -
+ name: github_app_id
+ in: path
+ description: 'GitHub App ID'
+ required: true
+ schema:
+ type: integer
+ -
+ name: owner
+ in: path
+ description: 'Repository owner'
+ required: true
+ schema:
+ type: string
+ -
+ name: repo
+ in: path
+ description: 'Repository name'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: 'Branches loaded successfully.'
+ content:
+ application/json:
+ schema:
+ properties:
+ '': { type: array, items: { type: object } }
+ type: object
+ '400':
+ $ref: '#/components/responses/400'
+ '401':
+ $ref: '#/components/responses/401'
+ '404':
+ $ref: '#/components/responses/404'
+ security:
+ -
+ bearerAuth: []
+ '/github-apps/{github_app_id}':
+ delete:
+ tags:
+ - 'GitHub Apps'
+ summary: 'Delete GitHub App'
+ description: "Delete a GitHub app if it's not being used by any applications."
+ operationId: deleteGithubApp
+ parameters:
+ -
+ name: github_app_id
+ in: path
+ description: 'GitHub App ID'
+ required: true
+ schema:
+ type: integer
+ responses:
+ '200':
+ description: 'GitHub app deleted successfully'
+ content:
+ application/json:
+ schema:
+ properties:
+ message: { type: string, example: 'GitHub app deleted successfully' }
+ type: object
+ '401':
+ description: Unauthorized
+ '404':
+ description: 'GitHub app not found'
+ '409':
+ description: 'Conflict - GitHub app is in use'
+ content:
+ application/json:
+ schema:
+ properties:
+ message: { type: string, example: 'This GitHub app is being used by 5 application(s). Please delete all applications first.' }
+ type: object
+ security:
+ -
+ bearerAuth: []
+ patch:
+ tags:
+ - 'GitHub Apps'
+ summary: 'Update GitHub App'
+ description: 'Update an existing GitHub app.'
+ operationId: updateGithubApp
+ parameters:
+ -
+ name: github_app_id
+ in: path
+ description: 'GitHub App ID'
+ required: true
+ schema:
+ type: integer
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ properties:
+ name:
+ type: string
+ description: 'GitHub App name'
+ organization:
+ type: string
+ nullable: true
+ description: 'GitHub organization'
+ api_url:
+ type: string
+ description: 'GitHub API URL'
+ html_url:
+ type: string
+ description: 'GitHub HTML URL'
+ custom_user:
+ type: string
+ description: 'Custom user for SSH'
+ custom_port:
+ type: integer
+ description: 'Custom port for SSH'
+ app_id:
+ type: integer
+ description: 'GitHub App ID'
+ installation_id:
+ type: integer
+ description: 'GitHub Installation ID'
+ client_id:
+ type: string
+ description: 'GitHub Client ID'
+ client_secret:
+ type: string
+ description: 'GitHub Client Secret'
+ webhook_secret:
+ type: string
+ description: 'GitHub Webhook Secret'
+ private_key_uuid:
+ type: string
+ description: 'Private key UUID'
+ is_system_wide:
+ type: boolean
+ description: 'Is system wide (non-cloud instances only)'
+ type: object
+ responses:
+ '200':
+ description: 'GitHub app updated successfully'
+ content:
+ application/json:
+ schema:
+ properties:
+ message: { type: string, example: 'GitHub app updated successfully' }
+ data: { type: object, description: 'Updated GitHub app data' }
+ type: object
+ '401':
+ description: Unauthorized
+ '404':
+ description: 'GitHub app not found'
+ '422':
+ description: 'Validation error'
+ security:
+ -
+ bearerAuth: []
/version:
get:
summary: Version
@@ -5781,6 +6337,9 @@ tags:
-
name: Deployments
description: Deployments
+ -
+ name: 'GitHub Apps'
+ description: 'GitHub Apps'
-
name: Projects
description: Projects
From a03c1b3b4b69e0f21a24f136fd79e1aa5a1dfcaf Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 11:43:30 +0200
Subject: [PATCH 11/28] refactor(dashboard): remove deployment loading logic
and introduce DeploymentsIndicator component for better UI management
---
app/Livewire/Dashboard.php | 33 -------
app/Livewire/DeploymentsIndicator.php | 49 ++++++++++
.../Project/Application/PreviewsCompose.php | 2 +-
app/Livewire/Project/Shared/HealthChecks.php | 6 +-
app/Livewire/Server/Advanced.php | 5 -
app/Traits/ExecuteRemoteCommand.php | 2 +-
bootstrap/helpers/socialite.php | 2 +-
database/factories/TeamFactory.php | 4 +-
resources/views/layouts/app.blade.php | 1 +
resources/views/livewire/dashboard.blade.php | 48 ----------
.../livewire/deployments-indicator.blade.php | 92 +++++++++++++++++++
11 files changed, 150 insertions(+), 94 deletions(-)
create mode 100644 app/Livewire/DeploymentsIndicator.php
create mode 100644 resources/views/livewire/deployments-indicator.blade.php
diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php
index 18dbde0d3..45781af30 100644
--- a/app/Livewire/Dashboard.php
+++ b/app/Livewire/Dashboard.php
@@ -2,13 +2,10 @@
namespace App\Livewire;
-use App\Models\Application;
-use App\Models\ApplicationDeploymentQueue;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use Illuminate\Support\Collection;
-use Illuminate\Support\Facades\Artisan;
use Livewire\Component;
class Dashboard extends Component
@@ -19,41 +16,11 @@ class Dashboard extends Component
public Collection $privateKeys;
- public array $deploymentsPerServer = [];
-
public function mount()
{
$this->privateKeys = PrivateKey::ownedByCurrentTeam()->get();
$this->servers = Server::ownedByCurrentTeam()->get();
$this->projects = Project::ownedByCurrentTeam()->get();
- $this->loadDeployments();
- }
-
- public function cleanupQueue()
- {
- try {
- $this->authorize('cleanupDeploymentQueue', Application::class);
- } catch (\Illuminate\Auth\Access\AuthorizationException $e) {
- return handleError($e, $this);
- }
-
- Artisan::queue('cleanup:deployment-queue', [
- '--team-id' => currentTeam()->id,
- ]);
- }
-
- public function loadDeployments()
- {
- $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([
- 'id',
- 'application_id',
- 'application_name',
- 'deployment_url',
- 'pull_request_id',
- 'server_name',
- 'server_id',
- 'status',
- ])->sortBy('id')->groupBy('server_name')->toArray();
}
public function navigateToProject($projectUuid)
diff --git a/app/Livewire/DeploymentsIndicator.php b/app/Livewire/DeploymentsIndicator.php
new file mode 100644
index 000000000..34529a7e7
--- /dev/null
+++ b/app/Livewire/DeploymentsIndicator.php
@@ -0,0 +1,49 @@
+get();
+
+ return ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])
+ ->whereIn('server_id', $servers->pluck('id'))
+ ->get([
+ 'id',
+ 'application_id',
+ 'application_name',
+ 'deployment_url',
+ 'pull_request_id',
+ 'server_name',
+ 'server_id',
+ 'status',
+ ])
+ ->sortBy('id');
+ }
+
+ #[Computed]
+ public function deploymentCount()
+ {
+ return $this->deployments->count();
+ }
+
+ public function toggleExpanded()
+ {
+ $this->expanded = ! $this->expanded;
+ }
+
+ public function render()
+ {
+ return view('livewire.deployments-indicator');
+ }
+}
diff --git a/app/Livewire/Project/Application/PreviewsCompose.php b/app/Livewire/Project/Application/PreviewsCompose.php
index 7641edcc5..cfb364b6d 100644
--- a/app/Livewire/Project/Application/PreviewsCompose.php
+++ b/app/Livewire/Project/Application/PreviewsCompose.php
@@ -73,7 +73,7 @@ public function generate()
$host = $url->getHost();
$schema = $url->getScheme();
$portInt = $url->getPort();
- $port = $portInt !== null ? ':' . $portInt : '';
+ $port = $portInt !== null ? ':'.$portInt : '';
$random = new Cuid2;
$preview_fqdn = str_replace('{{random}}', $random, $template);
$preview_fqdn = str_replace('{{domain}}', $host, $preview_fqdn);
diff --git a/app/Livewire/Project/Shared/HealthChecks.php b/app/Livewire/Project/Shared/HealthChecks.php
index ee11c496d..c0714fe03 100644
--- a/app/Livewire/Project/Shared/HealthChecks.php
+++ b/app/Livewire/Project/Shared/HealthChecks.php
@@ -52,13 +52,13 @@ public function toggleHealthcheck()
try {
$this->authorize('update', $this->resource);
$wasEnabled = $this->resource->health_check_enabled;
- $this->resource->health_check_enabled = !$this->resource->health_check_enabled;
+ $this->resource->health_check_enabled = ! $this->resource->health_check_enabled;
$this->resource->save();
- if ($this->resource->health_check_enabled && !$wasEnabled && $this->resource->isRunning()) {
+ if ($this->resource->health_check_enabled && ! $wasEnabled && $this->resource->isRunning()) {
$this->dispatch('info', 'Health check has been enabled. A restart is required to apply the new settings.');
} else {
- $this->dispatch('success', 'Health check ' . ($this->resource->health_check_enabled ? 'enabled' : 'disabled') . '.');
+ $this->dispatch('success', 'Health check '.($this->resource->health_check_enabled ? 'enabled' : 'disabled').'.');
}
} catch (\Throwable $e) {
return handleError($e, $this);
diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php
index bbc3bd96a..8d17bb557 100644
--- a/app/Livewire/Server/Advanced.php
+++ b/app/Livewire/Server/Advanced.php
@@ -2,10 +2,7 @@
namespace App\Livewire\Server;
-use App\Models\InstanceSettings;
use App\Models\Server;
-use Illuminate\Support\Facades\Auth;
-use Illuminate\Support\Facades\Hash;
use Livewire\Attributes\Validate;
use Livewire\Component;
@@ -39,8 +36,6 @@ public function mount(string $server_uuid)
}
}
-
-
public function syncData(bool $toModel = false)
{
if ($toModel) {
diff --git a/app/Traits/ExecuteRemoteCommand.php b/app/Traits/ExecuteRemoteCommand.php
index 8fa47f543..4aa5aae8b 100644
--- a/app/Traits/ExecuteRemoteCommand.php
+++ b/app/Traits/ExecuteRemoteCommand.php
@@ -269,4 +269,4 @@ private function addRetryLogEntry(int $attempt, int $maxRetries, int $delay, str
$this->application_deployment_queue->save();
}
-}
\ No newline at end of file
+}
diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php
index 3b20f2d89..fd3fbe74b 100644
--- a/bootstrap/helpers/socialite.php
+++ b/bootstrap/helpers/socialite.php
@@ -75,7 +75,7 @@ function get_socialite_provider(string $provider)
$config
);
- if ($provider == 'gitlab' && !empty($oauth_setting->base_url)) {
+ if ($provider == 'gitlab' && ! empty($oauth_setting->base_url)) {
$socialite->setHost($oauth_setting->base_url);
}
diff --git a/database/factories/TeamFactory.php b/database/factories/TeamFactory.php
index 0e95842b4..26748c54e 100644
--- a/database/factories/TeamFactory.php
+++ b/database/factories/TeamFactory.php
@@ -20,7 +20,7 @@ class TeamFactory extends Factory
public function definition(): array
{
return [
- 'name' => $this->faker->company() . ' Team',
+ 'name' => $this->faker->company().' Team',
'description' => $this->faker->sentence(),
'personal_team' => false,
'show_boarding' => false,
@@ -34,7 +34,7 @@ public function personal(): static
{
return $this->state(fn (array $attributes) => [
'personal_team' => true,
- 'name' => $this->faker->firstName() . "'s Team",
+ 'name' => $this->faker->firstName()."'s Team",
]);
}
}
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php
index bb6533932..409f66ad9 100644
--- a/resources/views/layouts/app.blade.php
+++ b/resources/views/layouts/app.blade.php
@@ -7,6 +7,7 @@
@auth
+
-
Deployments
- @if (count($deploymentsPerServer) > 0)
-
- @endif
- @can('cleanupDeploymentQueue', Application::class)
-
- @endcan
-
-
- @forelse ($deploymentsPerServer as $serverName => $deployments)
-
{{ $serverName }}
-
- @empty
-
No deployments running.
- @endforelse
-
-
- @endif
diff --git a/resources/views/livewire/deployments-indicator.blade.php b/resources/views/livewire/deployments-indicator.blade.php
new file mode 100644
index 000000000..0ebaab9f8
--- /dev/null
+++ b/resources/views/livewire/deployments-indicator.blade.php
@@ -0,0 +1,92 @@
+
+ @if ($this->deploymentCount > 0)
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ $this->deploymentCount }} {{ Str::plural('deployment', $this->deploymentCount) }}
+
+
+
+
+
+
+
+
+
+
+
+ @endif
+
From a9e1d4cb79052105272dce11461289b1697d5fa0 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 11:47:39 +0200
Subject: [PATCH 12/28] fix(ui): improve queued deployment status readability
in dark mode
---
resources/views/livewire/deployments-indicator.blade.php | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/resources/views/livewire/deployments-indicator.blade.php b/resources/views/livewire/deployments-indicator.blade.php
index 0ebaab9f8..ed24249e0 100644
--- a/resources/views/livewire/deployments-indicator.blade.php
+++ b/resources/views/livewire/deployments-indicator.blade.php
@@ -53,7 +53,7 @@ class="flex items-start gap-3 p-3 rounded-lg dark:bg-coolgray-200 bg-gray-50 tra
@else
-
{{ str_replace('_', ' ', $deployment->status) }}
From 8e7c869d2307777b1f45a70fbdfea3efd5950c93 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 12:22:57 +0200
Subject: [PATCH 13/28] fix(git): handle additional repository URL cases for
'tangled' and improve branch assignment logic
---
app/Livewire/Project/New/PublicGitRepository.php | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php
index 8ec818319..89814ee7f 100644
--- a/app/Livewire/Project/New/PublicGitRepository.php
+++ b/app/Livewire/Project/New/PublicGitRepository.php
@@ -176,13 +176,16 @@ public function loadBranch()
str($this->repository_url)->startsWith('http://')) &&
! str($this->repository_url)->endsWith('.git') &&
(! str($this->repository_url)->contains('github.com') ||
- ! str($this->repository_url)->contains('git.sr.ht'))
+ ! str($this->repository_url)->contains('git.sr.ht')) &&
+ ! str($this->repository_url)->contains('tangled')
) {
+
$this->repository_url = $this->repository_url.'.git';
}
if (str($this->repository_url)->contains('github.com') && str($this->repository_url)->endsWith('.git')) {
$this->repository_url = str($this->repository_url)->beforeLast('.git')->value();
}
+
} catch (\Throwable $e) {
return handleError($e, $this);
}
@@ -190,6 +193,9 @@ public function loadBranch()
$this->branchFound = false;
$this->getGitSource();
$this->getBranch();
+ if (str($this->repository_url)->contains('tangled')) {
+ $this->git_branch = 'master';
+ }
$this->selectedBranch = $this->git_branch;
} catch (\Throwable $e) {
if ($this->rate_limit_remaining == 0) {
From 9b4abe753dc51aff34615fb81902c2fecce02273 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 12:23:04 +0200
Subject: [PATCH 14/28] fix(git): enhance error handling for missing branch
information during deployment
---
app/Jobs/ApplicationDeploymentJob.php | 18 +++++++++++++++++-
1 file changed, 17 insertions(+), 1 deletion(-)
diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index 62fbe2df5..fcdb472ee 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -1677,7 +1677,23 @@ private function check_git_if_build_needed()
);
}
if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) {
- $this->commit = $this->saved_outputs->get('git_commit_sha')->before("\t");
+ $output = $this->saved_outputs->get('git_commit_sha');
+
+ if ($output->isEmpty() || ! str($output)->contains("\t")) {
+ $errorMessage = "Failed to find branch '{$local_branch}' in repository.\n\n";
+ $errorMessage .= "Please verify:\n";
+ $errorMessage .= "- The branch name is correct\n";
+ $errorMessage .= "- The branch exists in the repository\n";
+ $errorMessage .= "- You have access to the repository\n";
+
+ if ($this->pull_request_id !== 0) {
+ $errorMessage .= "- The pull request #{$this->pull_request_id} exists and is accessible\n";
+ }
+
+ throw new \RuntimeException($errorMessage);
+ }
+
+ $this->commit = $output->before("\t");
$this->application_deployment_queue->commit = $this->commit;
$this->application_deployment_queue->save();
}
From 1fe7df7e38567fc47061301a896e8018444452b1 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 12:33:40 +0200
Subject: [PATCH 15/28] fix(git): trim whitespace from repository, branch, and
commit SHA fields
- Add automatic trimming in Application model's boot method for git_repository, git_branch, and git_commit_sha fields
- Add real-time trimming in Source Livewire component via updated{Property} methods
- Refresh component state after save to ensure UI displays trimmed values
- Prevents deployment issues caused by accidental whitespace in git configuration
---
app/Livewire/Project/Application/Source.php | 18 ++++++++++++++++++
app/Models/Application.php | 9 +++++++++
2 files changed, 27 insertions(+)
diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php
index 29be68b6c..ab2517f2b 100644
--- a/app/Livewire/Project/Application/Source.php
+++ b/app/Livewire/Project/Application/Source.php
@@ -47,6 +47,21 @@ public function mount()
}
}
+ public function updatedGitRepository()
+ {
+ $this->gitRepository = trim($this->gitRepository);
+ }
+
+ public function updatedGitBranch()
+ {
+ $this->gitBranch = trim($this->gitBranch);
+ }
+
+ public function updatedGitCommitSha()
+ {
+ $this->gitCommitSha = trim($this->gitCommitSha);
+ }
+
public function syncData(bool $toModel = false)
{
if ($toModel) {
@@ -57,6 +72,9 @@ public function syncData(bool $toModel = false)
'git_commit_sha' => $this->gitCommitSha,
'private_key_id' => $this->privateKeyId,
]);
+ // Refresh to get the trimmed values from the model
+ $this->application->refresh();
+ $this->syncData(false);
} else {
$this->gitRepository = $this->application->git_repository;
$this->gitBranch = $this->application->git_branch;
diff --git a/app/Models/Application.php b/app/Models/Application.php
index 9fffdfcda..4f1796790 100644
--- a/app/Models/Application.php
+++ b/app/Models/Application.php
@@ -155,6 +155,15 @@ protected static function booted()
if ($application->isDirty('publish_directory')) {
$payload['publish_directory'] = str($application->publish_directory)->trim();
}
+ if ($application->isDirty('git_repository')) {
+ $payload['git_repository'] = str($application->git_repository)->trim();
+ }
+ if ($application->isDirty('git_branch')) {
+ $payload['git_branch'] = str($application->git_branch)->trim();
+ }
+ if ($application->isDirty('git_commit_sha')) {
+ $payload['git_commit_sha'] = str($application->git_commit_sha)->trim();
+ }
if ($application->isDirty('status')) {
$payload['last_online_at'] = now();
}
From a897e81566c41c07c545aa70fe5af7257e4be295 Mon Sep 17 00:00:00 2001
From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com>
Date: Tue, 30 Sep 2025 13:37:03 +0200
Subject: [PATCH 16/28] feat(global-search): integrate projects and
environments into global search functionality
- Added retrieval and mapping of projects and environments to the global search results.
- Enhanced search result structure to include resource counts and descriptions for projects and environments.
- Updated the UI to reflect the new search capabilities, improving user experience when searching for resources.
---
app/Livewire/GlobalSearch.php | 74 ++++++++-
app/Models/Environment.php | 2 +
app/Models/Project.php | 2 +
app/Traits/ClearsGlobalSearchCache.php | 134 ++++++++++------
.../views/livewire/global-search.blade.php | 150 ++++++++----------
5 files changed, 231 insertions(+), 131 deletions(-)
diff --git a/app/Livewire/GlobalSearch.php b/app/Livewire/GlobalSearch.php
index dacc0d4db..15de5d838 100644
--- a/app/Livewire/GlobalSearch.php
+++ b/app/Livewire/GlobalSearch.php
@@ -3,6 +3,8 @@
namespace App\Livewire;
use App\Models\Application;
+use App\Models\Environment;
+use App\Models\Project;
use App\Models\Server;
use App\Models\Service;
use App\Models\StandaloneClickhouse;
@@ -335,11 +337,81 @@ private function loadSearchableItems()
];
});
+ // Get all projects
+ $projects = Project::ownedByCurrentTeam()
+ ->withCount(['environments', 'applications', 'services'])
+ ->get()
+ ->map(function ($project) {
+ $resourceCount = $project->applications_count + $project->services_count;
+ $resourceSummary = $resourceCount > 0
+ ? "{$resourceCount} resource".($resourceCount !== 1 ? 's' : '')
+ : 'No resources';
+
+ return [
+ 'id' => $project->id,
+ 'name' => $project->name,
+ 'type' => 'project',
+ 'uuid' => $project->uuid,
+ 'description' => $project->description,
+ 'link' => $project->navigateTo(),
+ 'project' => null,
+ 'environment' => null,
+ 'resource_count' => $resourceSummary,
+ 'environment_count' => $project->environments_count,
+ 'search_text' => strtolower($project->name.' '.$project->description.' project'),
+ ];
+ });
+
+ // Get all environments
+ $environments = Environment::query()
+ ->whereHas('project', function ($query) {
+ $query->where('team_id', auth()->user()->currentTeam()->id);
+ })
+ ->with('project')
+ ->withCount(['applications', 'services'])
+ ->get()
+ ->map(function ($environment) {
+ $resourceCount = $environment->applications_count + $environment->services_count;
+ $resourceSummary = $resourceCount > 0
+ ? "{$resourceCount} resource".($resourceCount !== 1 ? 's' : '')
+ : 'No resources';
+
+ // Build description with project context
+ $descriptionParts = [];
+ if ($environment->project) {
+ $descriptionParts[] = "Project: {$environment->project->name}";
+ }
+ if ($environment->description) {
+ $descriptionParts[] = $environment->description;
+ }
+ if (empty($descriptionParts)) {
+ $descriptionParts[] = $resourceSummary;
+ }
+
+ return [
+ 'id' => $environment->id,
+ 'name' => $environment->name,
+ 'type' => 'environment',
+ 'uuid' => $environment->uuid,
+ 'description' => implode(' • ', $descriptionParts),
+ 'link' => route('project.resource.index', [
+ 'project_uuid' => $environment->project->uuid,
+ 'environment_uuid' => $environment->uuid,
+ ]),
+ 'project' => $environment->project->name ?? null,
+ 'environment' => null,
+ 'resource_count' => $resourceSummary,
+ 'search_text' => strtolower($environment->name.' '.$environment->description.' '.$environment->project->name.' environment'),
+ ];
+ });
+
// Merge all collections
$items = $items->merge($applications)
->merge($services)
->merge($databases)
- ->merge($servers);
+ ->merge($servers)
+ ->merge($projects)
+ ->merge($environments);
return $items->toArray();
});
diff --git a/app/Models/Environment.php b/app/Models/Environment.php
index 437be7d87..bfeee01c9 100644
--- a/app/Models/Environment.php
+++ b/app/Models/Environment.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use OpenApi\Attributes as OA;
@@ -19,6 +20,7 @@
)]
class Environment extends BaseModel
{
+ use ClearsGlobalSearchCache;
use HasSafeStringAttribute;
protected $guarded = [];
diff --git a/app/Models/Project.php b/app/Models/Project.php
index 1c46042e3..a9bf76803 100644
--- a/app/Models/Project.php
+++ b/app/Models/Project.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use App\Traits\ClearsGlobalSearchCache;
use App\Traits\HasSafeStringAttribute;
use OpenApi\Attributes as OA;
use Visus\Cuid2\Cuid2;
@@ -24,6 +25,7 @@
)]
class Project extends BaseModel
{
+ use ClearsGlobalSearchCache;
use HasSafeStringAttribute;
protected $guarded = [];
diff --git a/app/Traits/ClearsGlobalSearchCache.php b/app/Traits/ClearsGlobalSearchCache.php
index ae587aa87..b9af70aba 100644
--- a/app/Traits/ClearsGlobalSearchCache.php
+++ b/app/Traits/ClearsGlobalSearchCache.php
@@ -10,77 +10,119 @@ trait ClearsGlobalSearchCache
protected static function bootClearsGlobalSearchCache()
{
static::saving(function ($model) {
- // Only clear cache if searchable fields are being changed
- if ($model->hasSearchableChanges()) {
- $teamId = $model->getTeamIdForCache();
- if (filled($teamId)) {
- GlobalSearch::clearTeamCache($teamId);
+ try {
+ // Only clear cache if searchable fields are being changed
+ if ($model->hasSearchableChanges()) {
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
}
+ } catch (\Throwable $e) {
+ // Silently fail cache clearing - don't break the save operation
+ ray('Failed to clear global search cache on saving: '.$e->getMessage());
}
});
static::created(function ($model) {
- // Always clear cache when model is created
- $teamId = $model->getTeamIdForCache();
- if (filled($teamId)) {
- GlobalSearch::clearTeamCache($teamId);
+ try {
+ // Always clear cache when model is created
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ } catch (\Throwable $e) {
+ // Silently fail cache clearing - don't break the create operation
+ ray('Failed to clear global search cache on creation: '.$e->getMessage());
}
});
static::deleted(function ($model) {
- // Always clear cache when model is deleted
- $teamId = $model->getTeamIdForCache();
- if (filled($teamId)) {
- GlobalSearch::clearTeamCache($teamId);
+ try {
+ // Always clear cache when model is deleted
+ $teamId = $model->getTeamIdForCache();
+ if (filled($teamId)) {
+ GlobalSearch::clearTeamCache($teamId);
+ }
+ } catch (\Throwable $e) {
+ // Silently fail cache clearing - don't break the delete operation
+ ray('Failed to clear global search cache on deletion: '.$e->getMessage());
}
});
}
private function hasSearchableChanges(): bool
{
- // Define searchable fields based on model type
- $searchableFields = ['name', 'description'];
+ try {
+ // Define searchable fields based on model type
+ $searchableFields = ['name', 'description'];
- // Add model-specific searchable fields
- if ($this instanceof \App\Models\Application) {
- $searchableFields[] = 'fqdn';
- $searchableFields[] = 'docker_compose_domains';
- } elseif ($this instanceof \App\Models\Server) {
- $searchableFields[] = 'ip';
- } elseif ($this instanceof \App\Models\Service) {
- // Services don't have direct fqdn, but name and description are covered
- }
- // Database models only have name and description as searchable
-
- // Check if any searchable field is dirty
- foreach ($searchableFields as $field) {
- if ($this->isDirty($field)) {
- return true;
+ // Add model-specific searchable fields
+ if ($this instanceof \App\Models\Application) {
+ $searchableFields[] = 'fqdn';
+ $searchableFields[] = 'docker_compose_domains';
+ } elseif ($this instanceof \App\Models\Server) {
+ $searchableFields[] = 'ip';
+ } elseif ($this instanceof \App\Models\Service) {
+ // Services don't have direct fqdn, but name and description are covered
+ } elseif ($this instanceof \App\Models\Project || $this instanceof \App\Models\Environment) {
+ // Projects and environments only have name and description as searchable
}
- }
+ // Database models only have name and description as searchable
- return false;
+ // Check if any searchable field is dirty
+ foreach ($searchableFields as $field) {
+ // Check if attribute exists before checking if dirty
+ if (array_key_exists($field, $this->getAttributes()) && $this->isDirty($field)) {
+ return true;
+ }
+ }
+
+ return false;
+ } catch (\Throwable $e) {
+ // If checking changes fails, assume changes exist to be safe
+ ray('Failed to check searchable changes: '.$e->getMessage());
+
+ return true;
+ }
}
private function getTeamIdForCache()
{
- // For database models, team is accessed through environment.project.team
- if (method_exists($this, 'team')) {
- if ($this instanceof \App\Models\Server) {
- $team = $this->team;
- } else {
- $team = $this->team();
+ try {
+ // For Project models (has direct team_id)
+ if ($this instanceof \App\Models\Project) {
+ return $this->team_id ?? null;
}
- if (filled($team)) {
- return is_object($team) ? $team->id : null;
+
+ // For Environment models (get team_id through project)
+ if ($this instanceof \App\Models\Environment) {
+ return $this->project?->team_id;
}
- }
- // For models with direct team_id property
- if (property_exists($this, 'team_id') || isset($this->team_id)) {
- return $this->team_id;
- }
+ // For database models, team is accessed through environment.project.team
+ if (method_exists($this, 'team')) {
+ if ($this instanceof \App\Models\Server) {
+ $team = $this->team;
+ } else {
+ $team = $this->team();
+ }
+ if (filled($team)) {
+ return is_object($team) ? $team->id : null;
+ }
+ }
- return null;
+ // For models with direct team_id property
+ if (property_exists($this, 'team_id') || isset($this->team_id)) {
+ return $this->team_id ?? null;
+ }
+
+ return null;
+ } catch (\Throwable $e) {
+ // If we can't determine team ID, return null
+ ray('Failed to get team ID for cache: '.$e->getMessage());
+
+ return null;
+ }
}
}
diff --git a/resources/views/livewire/global-search.blade.php b/resources/views/livewire/global-search.blade.php
index 0b9b61da4..2addf6f64 100644
--- a/resources/views/livewire/global-search.blade.php
+++ b/resources/views/livewire/global-search.blade.php
@@ -80,41 +80,42 @@
-
+ class="fixed top-0 left-0 z-99 flex items-start justify-center w-screen h-screen pt-[20vh]">
+
-