diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php
index dbb91aa24..971c1d806 100644
--- a/app/Jobs/ApplicationDeploymentJob.php
+++ b/app/Jobs/ApplicationDeploymentJob.php
@@ -459,7 +459,7 @@ private function decide_what_to_do()
private function post_deployment()
{
GetContainersStatus::dispatch($this->server);
- $this->next(ApplicationDeploymentStatus::FINISHED->value);
+ $this->completeDeployment();
if ($this->pull_request_id !== 0) {
if ($this->application->is_github_based()) {
ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::FINISHED);
@@ -1008,7 +1008,7 @@ private function just_restart()
$this->generate_image_names();
$this->check_image_locally_or_remotely();
$this->should_skip_build();
- $this->next(ApplicationDeploymentStatus::FINISHED->value);
+ $this->completeDeployment();
}
private function should_skip_build()
@@ -3023,9 +3023,7 @@ private function stop_running_container(bool $force = false)
$this->application_deployment_queue->addLogEntry('----------------------------------------');
}
$this->application_deployment_queue->addLogEntry('New container is not healthy, rolling back to the old container.');
- $this->application_deployment_queue->update([
- 'status' => ApplicationDeploymentStatus::FAILED->value,
- ]);
+ $this->failDeployment();
$this->graceful_shutdown_container($this->container_name);
}
}
@@ -3659,42 +3657,116 @@ private function checkForCancellation(): void
}
}
- private function next(string $status)
+ /**
+ * Transition deployment to a new status with proper validation and side effects.
+ * This is the single source of truth for status transitions.
+ */
+ private function transitionToStatus(ApplicationDeploymentStatus $status): void
{
- // Refresh to get latest status
- $this->application_deployment_queue->refresh();
-
- // Never allow changing status from FAILED or CANCELLED_BY_USER to anything else
- if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
- $this->application->environment->project->team?->notify(new DeploymentFailed($this->application, $this->deployment_uuid, $this->preview));
-
+ if ($this->isInTerminalState()) {
return;
}
+
+ $this->updateDeploymentStatus($status);
+ $this->handleStatusTransition($status);
+ queue_next_deployment($this->application);
+ }
+
+ /**
+ * Check if deployment is in a terminal state (FAILED or CANCELLED).
+ * Terminal states cannot be changed.
+ */
+ private function isInTerminalState(): bool
+ {
+ $this->application_deployment_queue->refresh();
+
+ if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::FAILED->value) {
+ return true;
+ }
+
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
- // Job was cancelled, stop execution
$this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.');
throw new \RuntimeException('Deployment cancelled by user', 69420);
}
+ return false;
+ }
+
+ /**
+ * Update the deployment status in the database.
+ */
+ private function updateDeploymentStatus(ApplicationDeploymentStatus $status): void
+ {
$this->application_deployment_queue->update([
- 'status' => $status,
+ 'status' => $status->value,
]);
+ }
- queue_next_deployment($this->application);
+ /**
+ * Execute status-specific side effects (events, notifications, additional deployments).
+ */
+ private function handleStatusTransition(ApplicationDeploymentStatus $status): void
+ {
+ match ($status) {
+ ApplicationDeploymentStatus::FINISHED => $this->handleSuccessfulDeployment(),
+ ApplicationDeploymentStatus::FAILED => $this->handleFailedDeployment(),
+ default => null,
+ };
+ }
- if ($status === ApplicationDeploymentStatus::FINISHED->value) {
- event(new ApplicationConfigurationChanged($this->application->team()->id));
+ /**
+ * Handle side effects when deployment succeeds.
+ */
+ private function handleSuccessfulDeployment(): void
+ {
+ event(new ApplicationConfigurationChanged($this->application->team()->id));
- if (! $this->only_this_server) {
- $this->deploy_to_additional_destinations();
- }
- $this->application->environment->project->team?->notify(new DeploymentSuccess($this->application, $this->deployment_uuid, $this->preview));
+ if (! $this->only_this_server) {
+ $this->deploy_to_additional_destinations();
}
+
+ $this->sendDeploymentNotification(DeploymentSuccess::class);
+ }
+
+ /**
+ * Handle side effects when deployment fails.
+ */
+ private function handleFailedDeployment(): void
+ {
+ $this->sendDeploymentNotification(DeploymentFailed::class);
+ }
+
+ /**
+ * Send deployment status notification to the team.
+ */
+ private function sendDeploymentNotification(string $notificationClass): void
+ {
+ $this->application->environment->project->team?->notify(
+ new $notificationClass($this->application, $this->deployment_uuid, $this->preview)
+ );
+ }
+
+ /**
+ * Complete deployment successfully.
+ * Sends success notification and triggers additional deployments if needed.
+ */
+ private function completeDeployment(): void
+ {
+ $this->transitionToStatus(ApplicationDeploymentStatus::FINISHED);
+ }
+
+ /**
+ * Fail the deployment.
+ * Sends failure notification and queues next deployment.
+ */
+ private function failDeployment(): void
+ {
+ $this->transitionToStatus(ApplicationDeploymentStatus::FAILED);
}
public function failed(Throwable $exception): void
{
- $this->next(ApplicationDeploymentStatus::FAILED->value);
+ $this->failDeployment();
$this->application_deployment_queue->addLogEntry('Oops something is not okay, are you okay? 😢', 'stderr');
if (str($exception->getMessage())->isNotEmpty()) {
$this->application_deployment_queue->addLogEntry($exception->getMessage(), 'stderr');
diff --git a/app/Livewire/Settings/Advanced.php b/app/Livewire/Settings/Advanced.php
index 832123d5a..be38ae1d8 100644
--- a/app/Livewire/Settings/Advanced.php
+++ b/app/Livewire/Settings/Advanced.php
@@ -85,19 +85,8 @@ public function submit()
// Handle allowed IPs with subnet support and 0.0.0.0 special case
$this->allowed_ips = str($this->allowed_ips)->replaceEnd(',', '')->trim();
- // Check if user entered 0.0.0.0 or left field empty (both allow access from anywhere)
- $allowsFromAnywhere = false;
- if (empty($this->allowed_ips)) {
- $allowsFromAnywhere = true;
- } elseif ($this->allowed_ips === '0.0.0.0' || str_contains($this->allowed_ips, '0.0.0.0')) {
- $allowsFromAnywhere = true;
- }
-
- // Check if it's 0.0.0.0 (allow all) or empty
- if ($this->allowed_ips === '0.0.0.0' || empty($this->allowed_ips)) {
- // Keep as is - empty means no restriction, 0.0.0.0 means allow all
- } else {
- // Validate and clean up the entries
+ // Only validate and clean up if we have IPs and it's not 0.0.0.0 (allow all)
+ if (! empty($this->allowed_ips) && ! in_array('0.0.0.0', array_map('trim', explode(',', $this->allowed_ips)))) {
$invalidEntries = [];
$validEntries = str($this->allowed_ips)->trim()->explode(',')->map(function ($entry) use (&$invalidEntries) {
$entry = str($entry)->trim()->toString();
@@ -133,7 +122,6 @@ public function submit()
return;
}
- // Also check if we have no valid entries after filtering
if ($validEntries->isEmpty()) {
$this->dispatch('error', 'No valid IP addresses or subnets provided');
@@ -144,14 +132,6 @@ public function submit()
}
$this->instantSave();
-
- // Show security warning if allowing access from anywhere
- if ($allowsFromAnywhere) {
- $message = empty($this->allowed_ips)
- ? 'Empty IP allowlist allows API access from anywhere.
This is not recommended for production environments!'
- : 'Using 0.0.0.0 allows API access from anywhere.
This is not recommended for production environments!';
- $this->dispatch('warning', $message);
- }
} catch (\Exception $e) {
return handleError($e, $this);
}
diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php
index 351407dac..4bd0b798a 100644
--- a/app/Livewire/Source/Github/Change.php
+++ b/app/Livewire/Source/Github/Change.php
@@ -47,19 +47,19 @@ class Change extends Component
public int $customPort;
- public int $appId;
+ public ?int $appId = null;
- public int $installationId;
+ public ?int $installationId = null;
- public string $clientId;
+ public ?string $clientId = null;
- public string $clientSecret;
+ public ?string $clientSecret = null;
- public string $webhookSecret;
+ public ?string $webhookSecret = null;
public bool $isSystemWide;
- public int $privateKeyId;
+ public ?int $privateKeyId = null;
public ?string $contents = null;
@@ -78,16 +78,16 @@ class Change extends Component
'htmlUrl' => 'required|string',
'customUser' => 'required|string',
'customPort' => 'required|int',
- 'appId' => 'required|int',
- 'installationId' => 'required|int',
- 'clientId' => 'required|string',
- 'clientSecret' => 'required|string',
- 'webhookSecret' => 'required|string',
+ 'appId' => 'nullable|int',
+ 'installationId' => 'nullable|int',
+ 'clientId' => 'nullable|string',
+ 'clientSecret' => 'nullable|string',
+ 'webhookSecret' => 'nullable|string',
'isSystemWide' => 'required|bool',
'contents' => 'nullable|string',
'metadata' => 'nullable|string',
'pullRequests' => 'nullable|string',
- 'privateKeyId' => 'required|int',
+ 'privateKeyId' => 'nullable|int',
];
public function boot()
@@ -148,47 +148,48 @@ public function checkPermissions()
try {
$this->authorize('view', $this->github_app);
+ // Validate required fields before attempting to fetch permissions
+ $missingFields = [];
+
+ if (! $this->github_app->app_id) {
+ $missingFields[] = 'App ID';
+ }
+
+ if (! $this->github_app->private_key_id) {
+ $missingFields[] = 'Private Key';
+ }
+
+ if (! empty($missingFields)) {
+ $fieldsList = implode(', ', $missingFields);
+ $this->dispatch('error', "Cannot fetch permissions. Please set the following required fields first: {$fieldsList}");
+
+ return;
+ }
+
+ // Verify the private key exists and is accessible
+ if (! $this->github_app->privateKey) {
+ $this->dispatch('error', 'Private Key not found. Please select a valid private key.');
+
+ return;
+ }
+
GithubAppPermissionJob::dispatchSync($this->github_app);
$this->github_app->refresh()->makeVisible('client_secret')->makeVisible('webhook_secret');
$this->dispatch('success', 'Github App permissions updated.');
} catch (\Throwable $e) {
+ // Provide better error message for unsupported key formats
+ $errorMessage = $e->getMessage();
+ if (str_contains($errorMessage, 'DECODER routines::unsupported') ||
+ str_contains($errorMessage, 'parse your key')) {
+ $this->dispatch('error', 'The selected private key format is not supported for GitHub Apps.
Please use an RSA private key in PEM format (BEGIN RSA PRIVATE KEY).
OpenSSH format keys (BEGIN OPENSSH PRIVATE KEY) are not supported.');
+
+ return;
+ }
+
return handleError($e, $this);
}
}
- // public function check()
- // {
-
- // Need administration:read:write permission
- // https://docs.github.com/en/rest/actions/self-hosted-runners?apiVersion=2022-11-28#list-self-hosted-runners-for-a-repository
-
- // $github_access_token = generateGithubInstallationToken($this->github_app);
- // $repositories = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/installation/repositories?per_page=100");
- // $runners_by_repository = collect([]);
- // $repositories = $repositories->json()['repositories'];
- // foreach ($repositories as $repository) {
- // $runners_downloads = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/downloads");
- // $runners = Http::withToken($github_access_token)->get("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners");
- // $token = Http::withHeaders([
- // 'Authorization' => "Bearer $github_access_token",
- // 'Accept' => 'application/vnd.github+json'
- // ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/registration-token");
- // $token = $token->json();
- // $remove_token = Http::withHeaders([
- // 'Authorization' => "Bearer $github_access_token",
- // 'Accept' => 'application/vnd.github+json'
- // ])->withBody(null)->post("{$this->github_app->api_url}/repos/{$repository['full_name']}/actions/runners/remove-token");
- // $remove_token = $remove_token->json();
- // $runners_by_repository->put($repository['full_name'], [
- // 'token' => $token,
- // 'remove_token' => $remove_token,
- // 'runners' => $runners->json(),
- // 'runners_downloads' => $runners_downloads->json()
- // ]);
- // }
-
- // }
-
public function mount()
{
try {
@@ -340,10 +341,13 @@ public function createGithubAppManually()
$this->authorize('update', $this->github_app);
$this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret');
- $this->github_app->app_id = '1234567890';
- $this->github_app->installation_id = '1234567890';
+ $this->github_app->app_id = 1234567890;
+ $this->github_app->installation_id = 1234567890;
$this->github_app->save();
- $this->dispatch('success', 'Github App updated.');
+
+ // Redirect to avoid Livewire morphing issues when view structure changes
+ return redirect()->route('source.github.show', ['github_app_uuid' => $this->github_app->uuid])
+ ->with('success', 'Github App updated. You can now configure the details.');
}
public function instantSave()
diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php
index f5d851b64..2f1482c89 100644
--- a/app/Livewire/Source/Github/Create.php
+++ b/app/Livewire/Source/Github/Create.php
@@ -50,11 +50,9 @@ public function createGitHubApp()
'html_url' => $this->html_url,
'custom_user' => $this->custom_user,
'custom_port' => $this->custom_port,
+ 'is_system_wide' => $this->is_system_wide,
'team_id' => currentTeam()->id,
];
- if (isCloud()) {
- $payload['is_system_wide'] = $this->is_system_wide;
- }
$github_app = GithubApp::create($payload);
if (session('from')) {
session(['from' => session('from') + ['source_id' => $github_app->id]]);
diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php
index 5550df81f..0d643306c 100644
--- a/app/Models/GithubApp.php
+++ b/app/Models/GithubApp.php
@@ -12,6 +12,7 @@ class GithubApp extends BaseModel
protected $casts = [
'is_public' => 'boolean',
+ 'is_system_wide' => 'boolean',
'type' => 'string',
];
diff --git a/resources/views/livewire/settings/advanced.blade.php b/resources/views/livewire/settings/advanced.blade.php
index 65d7181c6..c47c2cfef 100644
--- a/resources/views/livewire/settings/advanced.blade.php
+++ b/resources/views/livewire/settings/advanced.blade.php
@@ -1,77 +1,93 @@