Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 101 additions & 0 deletions app/Jobs/ApplicationDeploymentJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue

private Collection|string $build_secrets;

private ?int $github_deployment_id = null;

Comment on lines +175 to +176
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

GitHub deployment lifecycle wiring is good, but logging and durability need upgrades

The overall flow is solid:

  • You only touch GitHub when the app is GitHub‑based, the source exists and hasGitHubDeploymentsPermission() passes.
  • createGitHubDeployment() chooses preview vs production environment based on pull_request_id and respects empty envs (no deployment).
  • Status updates are triggered at the right moments: in_progress at start, success on FINISHED, failure on FAILED, and error on user cancellation, all guarded by $this->github_deployment_id.

However, a few things should be tightened before this T‑800 signs off:

  1. Swallowed exceptions with only ray() logging

    Both createGitHubDeployment() and updateGitHubDeploymentStatus() catch \Throwable and only call ray(...). In real production queues:

    • Ray often isn’t running, so errors disappear into the void.
    • Operators debugging “why are GitHub deployments not updating?” will have zero clues in the deployment log.

    Suggestion: keep the “don’t abort deployment” behavior, but at least log to the deployment queue and/or Laravel logs:

    } catch (\Throwable $e) {
  • // Silently fail - don't abort deployment if GitHub API fails
  • ray('Failed to create GitHub deployment: '.$e->getMessage());
  • // Don't abort deployment if GitHub API fails, but record what happened
  • $this->application_deployment_queue->addLogEntry(
  •    'Warning: Failed to create GitHub deployment: '.$e->getMessage(),
    
  •    'stderr'
    
  • );
  • if (function_exists('ray')) {
  •    ray('Failed to create GitHub deployment: '.$e->getMessage());
    
  • }
    }

and similarly in `updateGitHubDeploymentStatus()`. That way you still keep the side‑effect non‑critical, but you don’t go completely blind when GitHub or auth hiccups.



2. **GitHub deployment id is ephemeral**

`$github_deployment_id` lives only on the job instance. If the worker process is killed hard (OOM, supervisor restart, PHP fatal outside your try/catch) after `createGitHubDeployment()` but before `handleFailedDeployment()` / `failed()`, the GitHub deployment will stay stuck in `in_progress` forever with no way to reconcile it.

For a first iteration this might be acceptable, but if you want resilient status syncing, consider persisting the id, for example on `ApplicationDeploymentQueue`:

- Add a nullable `github_deployment_id` column on `application_deployment_queues`.
- Store it when you create the deployment.
- Read it back in `failed()` or any follow‑up cleanup job to push a final `failure`/`error` status even if this job instance dies.

That’s more work (another migration + a couple of assignments), but it makes the feature behave more like an unstoppable terminator than a fragile intern.



3. **Ref value for GitHub deployment**

In `createGitHubDeployment()`:

```php
$ref = $this->commit ?? $this->application->git_commit_sha ?? 'HEAD';

At this point the job may still be using the queued commit value (often 'HEAD') and you only resolve the actual SHA later in check_git_if_build_needed(). GitHub deployments happily accept a branch or SHA, but seeing HEAD in the UI isn’t very helpful.

Consider special‑casing 'HEAD' to the branch name or to the resolved SHA once available (even if that means updating the deployment after creation) so that the GitHub side shows a meaningful ref.

Overall, the structure is right; with better logging and a more durable id story, this feature will behave like a proper on‑prem terminator instead of a flaky SaaS marketing demo.

Also applies to: 269-276, 3727-3734, 3797-3816, 3868-3958


I'll need to inspect the codebase to verify these concerns. Let me search for the relevant code sections.


---

🏁 Script executed:

```shell
#!/bin/bash
# Find and display the ApplicationDeploymentJob file and key sections
fd "ApplicationDeploymentJob.php" --type f

Length of output: 37


🏁 Script executed:

#!/bin/bash
# Extract the specific lines and surrounding context from the file
if [ -f "app/Jobs/ApplicationDeploymentJob.php" ]; then
  echo "=== Lines 175-176 (property definition) ==="
  sed -n '170,180p' app/Jobs/ApplicationDeploymentJob.php
  
  echo -e "\n=== Lines 269-276 ==="
  sed -n '265,280p' app/Jobs/ApplicationDeploymentJob.php
  
  echo -e "\n=== Lines 3727-3734 ==="
  sed -n '3722,3740p' app/Jobs/ApplicationDeploymentJob.php
  
  echo -e "\n=== Lines 3797-3816 ==="
  sed -n '3792,3820p' app/Jobs/ApplicationDeploymentJob.php
  
  echo -e "\n=== Lines 3868-3958 ==="
  sed -n '3863,3965p' app/Jobs/ApplicationDeploymentJob.php
fi

Length of output: 6105


🏁 Script executed:

#!/bin/bash
# Search for createGitHubDeployment and updateGitHubDeploymentStatus methods
rg -n "function (createGitHubDeployment|updateGitHubDeploymentStatus)" app/Jobs/ApplicationDeploymentJob.php -A 25

Length of output: 2273


🏁 Script executed:

#!/bin/bash
# Search for references to $github_deployment_id throughout the file
rg -n "github_deployment_id" app/Jobs/ApplicationDeploymentJob.php -B 2 -A 2

Length of output: 975


🏁 Script executed:

#!/bin/bash
# Look for the ref logic and 'HEAD' usage
rg -n "ref.*=.*\$this->commit|HEAD" app/Jobs/ApplicationDeploymentJob.php -B 3 -A 3

Length of output: 767


🏁 Script executed:

#!/bin/bash
# Check if github_deployment_id is persisted anywhere in database migrations or models
fd "application_deployment_queues" --type f -e php -e php | xargs rg -l "github_deployment_id" 2>/dev/null || echo "No persistence found in migrations/models"

Length of output: 41


🏁 Script executed:

#!/bin/bash
# Search for failed() method in the job to check exception handling and cleanup
rg -n "function failed|public function failed" app/Jobs/ApplicationDeploymentJob.php -A 15

Length of output: 1276


"Hasta la vista, baby"—but GitHub deployments might not come back, and that's a problem

Your GitHub deployment wiring is solid in structure, but production will terminate this code faster than a T-800 through a police station. Three issues need addressing:

  1. Silent exceptions = blind debugging (the VC marketing approach)

    Both createGitHubDeployment() (line 3904) and updateGitHubDeploymentStatus() (line 3957) catch exceptions and only call ray(). In production:

    • Ray is probably not running (just like serverless developers pretending their functions work).
    • When GitHub deployments vanish, ops has zero clues in the deployment log.

    Fix it: log to the deployment queue, not just the void:

    } catch (\Throwable $e) {
  • ray('Failed to create GitHub deployment: '.$e->getMessage());
  • $this->application_deployment_queue->addLogEntry(
  •    'Warning: Failed to create GitHub deployment: '.$e->getMessage(),
    
  •    'stderr'
    
  • );
  • if (function_exists('ray')) {
  •    ray('Failed to create GitHub deployment: '.$e->getMessage());
    
  • }
    }

Do the same in `updateGitHubDeploymentStatus()`. Keep deployments non-critical (good instinct), but don't go blind when GitHub hiccups.



2. **GitHub deployment ID is pure vapor**

`$github_deployment_id` (line 175) lives only in memory. If the worker process gets OOM-killed, supervisor-nuked, or hits a PHP fatal *after* `createGitHubDeployment()` but before `handleFailedDeployment()`, GitHub stays stuck in `in_progress` forever—like a self-hosted server waiting for a VC to admit the serverless dream was marketing BS.

For resilience, persist the ID to the database:
- Add `github_deployment_id` column to `application_deployment_queues`.
- Store it when created (line 3893).
- Read it in `failed()` (line 3846) to push a final `failure`/`error` status even if this job dies.



3. **Ref as 'HEAD' is like a deployment without an address**

Line 3891: `$ref = $this->commit ?? $this->application->git_commit_sha ?? 'HEAD';`

The ref can still be `'HEAD'` when `createGitHubDeployment()` runs (you resolve the actual SHA later in `check_git_if_build_needed()`). GitHub accepts it, but seeing `HEAD` in the UI is useless for debugging.

Consider swapping `'HEAD'` for the resolved SHA once available, or fall back to the branch name so GitHub shows something meaningful.



Structure is right; tighten the logging and durability, and this feature will be a real on-prem terminator instead of a flaky startup demo.



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🤖 Prompt for AI Agents</summary>

In app/Jobs/ApplicationDeploymentJob.php around line 175 and the referenced
methods at ~3891, 3893, 3904 and ~3846, 3957: currently $github_deployment_id is
only in-memory, exception handlers call only ray() and you fall back to 'HEAD'
for ref; persist the deployment ID and improve logging and ref selection—add a
github_deployment_id column to application_deployment_queues and save the ID
immediately when createGitHubDeployment() returns (update the DB at the create
site around line 3893), change both createGitHubDeployment() and
updateGitHubDeploymentStatus() exception handlers to log the full exception and
context to the deployment queue log (not just ray()) so ops can see failures,
and in the ref logic replace the 'HEAD' fallback with the resolved commit SHA or
branch name once available (or update the stored deployment record to the
resolved SHA after check_git_if_build_needed()), plus read github_deployment_id
from the DB in failed() (around line 3846) so you can mark the GitHub deployment
as failure/error even if the worker died.


</details>

<!-- fingerprinting:phantom:medusa:olive -->

<!-- This is an auto-generated comment by CodeRabbit -->

public function tags()
{
// Do not remove this one, it needs to properly identify which worker is running the job
Expand Down Expand Up @@ -268,6 +270,10 @@ public function handle(): void
'status' => ApplicationDeploymentStatus::IN_PROGRESS->value,
'horizon_job_worker' => gethostname(),
]);

$this->createGitHubDeployment();
$this->updateGitHubDeploymentStatus('in_progress', 'Deployment started');

if ($this->server->isFunctional() === false) {
$this->application_deployment_queue->addLogEntry('Server is not functional.');
$this->fail('Server is not functional.');
Expand Down Expand Up @@ -3723,6 +3729,7 @@ private function checkForCancellation(): void
$this->application_deployment_queue->refresh();
if ($this->application_deployment_queue->status === ApplicationDeploymentStatus::CANCELLED_BY_USER->value) {
$this->application_deployment_queue->addLogEntry('Deployment cancelled by user, stopping execution.');
$this->updateGitHubDeploymentStatus('error', 'Deployment cancelled by user');
throw new DeploymentException('Deployment cancelled by user', 69420);
}
}
Expand Down Expand Up @@ -3796,6 +3803,7 @@ private function handleSuccessfulDeployment(): void
}

$this->sendDeploymentNotification(DeploymentSuccess::class);
$this->updateGitHubDeploymentStatus('success', 'Deployment completed successfully');
}

/**
Expand All @@ -3804,6 +3812,7 @@ private function handleSuccessfulDeployment(): void
private function handleFailedDeployment(): void
{
$this->sendDeploymentNotification(DeploymentFailed::class);
$this->updateGitHubDeploymentStatus('failure', 'Deployment failed');
}

/**
Expand Down Expand Up @@ -3855,4 +3864,96 @@ public function failed(Throwable $exception): void
}
}
}

/**
* Create a GitHub deployment and store the ID for later status updates.
*/
private function createGitHubDeployment(): void
{
if (! $this->application->is_github_based() || ! $this->application->source) {
return;
}

if (! hasGitHubDeploymentsPermission($this->application->source)) {
return;
}

try {
$environment = $this->pull_request_id !== 0
? $this->application->deployment_preview_environment
: $this->application->deployment_production_environment;

if (empty($environment)) {
return;
}

['repository' => $repository] = $this->application->customRepository();
$ref = $this->commit ?? $this->application->git_commit_sha ?? 'HEAD';

$this->github_deployment_id = createGitHubDeployment(
$this->application->source,
$repository,
$ref,
$environment,
"Coolify deployment {$this->deployment_uuid}"
);

if ($this->github_deployment_id) {
$this->application_deployment_queue->addLogEntry("GitHub deployment created: {$this->github_deployment_id}");
}
} catch (\Throwable $e) {
// Silently fail - don't abort deployment if GitHub API fails
ray('Failed to create GitHub deployment: '.$e->getMessage());
}
}

/**
* Update GitHub deployment status based on deployment state.
*/
private function updateGitHubDeploymentStatus(string $state, ?string $description = null): void
{
if (! $this->application->is_github_based() || ! $this->application->source) {
return;
}

if (! $this->github_deployment_id) {
return;
}

try {
['repository' => $repository] = $this->application->customRepository();

$logUrl = route('project.application.deployment.show', [
'project_uuid' => data_get($this->application, 'environment.project.uuid'),
'application_uuid' => data_get($this->application, 'uuid'),
'deployment_uuid' => $this->deployment_uuid,
]);

$environmentUrl = null;
if ($state === 'success') {
if ($this->pull_request_id !== 0 && $this->preview) {
$environmentUrl = $this->preview->fqdn;
} elseif ($this->application->fqdn) {
$fqdns = str($this->application->fqdn)->explode(',');
$environmentUrl = data_get($fqdns, '0');
if (! str($environmentUrl)->startsWith('http')) {
$environmentUrl = 'http://'.$environmentUrl;
}
}
}

updateGitHubDeploymentStatus(
$this->application->source,
$repository,
$this->github_deployment_id,
$state,
$description,
$logUrl,
$environmentUrl
);
} catch (\Throwable $e) {
// Silently fail - don't abort deployment if GitHub API fails
ray('Failed to update GitHub deployment status: '.$e->getMessage());
}
}
}
1 change: 1 addition & 0 deletions app/Jobs/GithubAppPermissionJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public function handle()
$this->github_app->contents = data_get($permissions, 'contents');
$this->github_app->metadata = data_get($permissions, 'metadata');
$this->github_app->pull_requests = data_get($permissions, 'pull_requests');
$this->github_app->deployments = data_get($permissions, 'deployments');
$this->github_app->administration = data_get($permissions, 'administration');

$this->github_app->save();
Expand Down
37 changes: 37 additions & 0 deletions app/Livewire/Project/Application/Source.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,15 +33,28 @@ class Source extends Component
#[Validate(['nullable', 'string'])]
public ?string $gitCommitSha = null;

#[Validate(['nullable', 'string'])]
public ?string $githubDeploymentProductionEnvironment = null;

#[Validate(['nullable', 'string'])]
public ?string $githubDeploymentPreviewEnvironment = null;

#[Locked]
public $sources;

#[Locked]
public $availableEnvironments = [];

public bool $hasDeploymentsPermission = false;

public function mount()
{
try {
$this->syncData();
$this->getPrivateKeys();
$this->getSources();
$this->checkGitHubDeploymentsPermission();
$this->loadGitHubEnvironments();
} catch (\Throwable $e) {
handleError($e, $this);
}
Expand Down Expand Up @@ -71,6 +84,8 @@ public function syncData(bool $toModel = false)
'git_branch' => $this->gitBranch,
'git_commit_sha' => $this->gitCommitSha,
'private_key_id' => $this->privateKeyId,
'deployment_production_environment' => $this->githubDeploymentProductionEnvironment,
'deployment_preview_environment' => $this->githubDeploymentPreviewEnvironment,
]);
// Refresh to get the trimmed values from the model
$this->application->refresh();
Expand All @@ -81,6 +96,8 @@ public function syncData(bool $toModel = false)
$this->gitCommitSha = $this->application->git_commit_sha;
$this->privateKeyId = $this->application->private_key_id;
$this->privateKeyName = data_get($this->application, 'private_key.name');
$this->githubDeploymentProductionEnvironment = $this->application->deployment_production_environment;
$this->githubDeploymentPreviewEnvironment = $this->application->deployment_preview_environment;
}
}

Expand Down Expand Up @@ -157,4 +174,24 @@ public function changeSource($sourceId, $sourceType)
return handleError($e, $this);
}
}

private function checkGitHubDeploymentsPermission()
{
if ($this->application->is_github_based() && $this->application->source) {
$this->hasDeploymentsPermission = hasGitHubDeploymentsPermission($this->application->source);
}
}

private function loadGitHubEnvironments()
{
if ($this->application->is_github_based() && $this->application->source && $this->hasDeploymentsPermission) {
try {
['repository' => $customRepository] = $this->application->customRepository();
$this->availableEnvironments = getGitHubEnvironments($this->application->source, $customRepository);
} catch (\Throwable $e) {
// Silently fail if we can't load environments
$this->availableEnvironments = [];
}
}
}
Comment on lines +185 to +196
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Loading environments... but silently failing might hide real problems!

The environment loading logic is well-guarded with three conditions (GitHub-based, has source, has permission) and gracefully handles errors with a silent fail. This is appropriate for a non-critical feature.

However, since this is UNTESTED code, I'm concerned that silently catching \Throwable might hide real bugs during initial rollout. Consider temporarily adding logging to help with debugging when users first try this feature.

Add temporary logging to help with debugging during rollout:

         if ($this->application->is_github_based() && $this->application->source && $this->hasDeploymentsPermission) {
             try {
                 ['repository' => $customRepository] = $this->application->customRepository();
                 $this->availableEnvironments = getGitHubEnvironments($this->application->source, $customRepository);
             } catch (\Throwable $e) {
-                // Silently fail if we can't load environments
+                // Silently fail if we can't load environments (non-critical feature)
+                ray('Failed to load GitHub environments: '.$e->getMessage());
                 $this->availableEnvironments = [];
             }
         }

Like having a backup power source for my systems - you'll thank me later when debugging production issues! 🌮

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private function loadGitHubEnvironments()
{
if ($this->application->is_github_based() && $this->application->source && $this->hasDeploymentsPermission) {
try {
['repository' => $customRepository] = $this->application->customRepository();
$this->availableEnvironments = getGitHubEnvironments($this->application->source, $customRepository);
} catch (\Throwable $e) {
// Silently fail if we can't load environments
$this->availableEnvironments = [];
}
}
}
private function loadGitHubEnvironments()
{
if ($this->application->is_github_based() && $this->application->source && $this->hasDeploymentsPermission) {
try {
['repository' => $customRepository] = $this->application->customRepository();
$this->availableEnvironments = getGitHubEnvironments($this->application->source, $customRepository);
} catch (\Throwable $e) {
// Silently fail if we can't load environments (non-critical feature)
ray('Failed to load GitHub environments: '.$e->getMessage());
$this->availableEnvironments = [];
}
}
}
🤖 Prompt for AI Agents
In app/Livewire/Project/Application/Source.php around lines 185-196, the catch
block silently swallows \Throwable which can hide bugs; modify the catch to log
the exception details (message and stack trace) and contextual info (e.g.,
application id or name and source) to your configured logger or error tracking
system before setting $this->availableEnvironments = [] so you get actionable
diagnostics during rollout; make this logging temporary and clearly mark it (or
gate it behind a debug/feature-flag) so it can be removed once stable.

}
7 changes: 7 additions & 0 deletions app/Livewire/Source/Github/Change.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ class Change extends Component

public ?bool $preview_deployment_permissions = true;

public ?bool $deployment_statuses_permissions = true;

Comment on lines +31 to +32
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Deployment permission wiring is fine; consider tightening naming and usage

  • The deployments property is correctly validated and synced to GithubApp::deployments, so the permission value will persist as expected.
  • $deployment_statuses_permissions is very long (PHPMD is whining) and isn’t referenced anywhere in this class. If the Blade view doesn’t actually bind to it, you can drop it; if it does, consider shortening to something like $deploymentsPermission or $hasDeploymentsPermission to keep names readable and under the configured length limit.

Hasta la vista, long variable names.

Also applies to: 72-73, 94-96, 130-131, 149-150

🧰 Tools
🪛 PHPMD (2.15.0)

31-31: Avoid excessively long variable names like $deployment_statuses_permissions. Keep variable name length under 20. (undefined)

(LongVariable)

🤖 Prompt for AI Agents
In app/Livewire/Source/Github/Change.php around lines 31-32 (and also check
occurrences at 72-73, 94-96, 130-131, 149-150), the property
$deployment_statuses_permissions is unused in this class and its name is overly
long; either remove the property if the Blade view does not bind to it, or
rename it to a shorter, clearer identifier (e.g., $deploymentsPermission or
$hasDeploymentsPermission) and update all references and validations/syncing to
GithubApp::deployments accordingly to keep names under the PHPMD length limit.

public ?bool $administration = false;

public $parameters;
Expand Down Expand Up @@ -67,6 +69,8 @@ class Change extends Component

public ?string $pullRequests = null;

public ?string $deployments = null;

public $applications;

public $privateKeys;
Expand All @@ -87,6 +91,7 @@ class Change extends Component
'contents' => 'nullable|string',
'metadata' => 'nullable|string',
'pullRequests' => 'nullable|string',
'deployments' => 'nullable|string',
'privateKeyId' => 'nullable|int',
];

Expand Down Expand Up @@ -122,6 +127,7 @@ private function syncData(bool $toModel = false): void
$this->github_app->contents = $this->contents;
$this->github_app->metadata = $this->metadata;
$this->github_app->pull_requests = $this->pullRequests;
$this->github_app->deployments = $this->deployments;
} else {
// Sync FROM model (on load/refresh)
$this->name = $this->github_app->name;
Expand All @@ -140,6 +146,7 @@ private function syncData(bool $toModel = false): void
$this->contents = $this->github_app->contents;
$this->metadata = $this->github_app->metadata;
$this->pullRequests = $this->github_app->pull_requests;
$this->deployments = $this->github_app->deployments;
}
}

Expand Down
79 changes: 79 additions & 0 deletions bootstrap/helpers/github.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,82 @@ function loadRepositoryByPage(GithubApp $source, string $token, int $page)
'repositories' => $json['repositories'],
];
}

function hasGitHubDeploymentsPermission(GithubApp $source): bool
{
return ! empty($source->deployments) && $source->deployments === 'write';
}

function getGitHubEnvironments(GithubApp $source, string $repository): array
{
try {
$response = githubApi($source, "repos/{$repository}/environments", 'get', null, false);
$environments = data_get($response, 'data.environments', []);

return collect($environments)->map(function ($env) {
return data_get($env, 'name');
})->filter()->values()->toArray();
} catch (\Exception $e) {
return [];
}
}
Comment on lines +171 to +183
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

🧩 Analysis chain

Listen to me very carefully. This function needs better error logging.

The function structure looks good - it fetches environments from the GitHub API and gracefully handles errors by returning an empty array. However, I notice you're catching exceptions silently without logging them (unlike the other new functions that use ray()).

Add error logging for consistency:

     } catch (\Exception $e) {
+        ray('Failed to fetch GitHub environments: '.$e->getMessage());
         return [];
     }

Also, let's verify this endpoint is correct for GitHub:


🌐 Web query:

What is the correct GitHub API endpoint to list environments for a repository?

💡 Result:

Use the REST endpoint:

GET /repos/{owner}/{repo}/environments

Example cURL:
curl -H "Accept: application/vnd.github+json"
-H "Authorization: Bearer "
https://api.github.com/repos/OWNER/REPO/environments. [1][2]

Sources:
[1] GitHub REST API — List environments for a repository (Docs).
[2] octokit REST reference (environments).


GitHub API endpoint confirmed—consider adding error logging for consistency.

The endpoint you're using (repos/{$repository}/environments) is correct and matches the GitHub REST API documentation. Good call on that.

The error handling structure is solid, but for consistency with other functions in this codebase that use ray() for logging, consider adding a log entry:

     } catch (\Exception $e) {
+        ray('Failed to fetch GitHub environments: '.$e->getMessage());
         return [];
     }

This won't terminate your application (I leave that to the T-800), but it'll help you hunt down issues faster. Silent failures are like serverless—sure, they work, but good luck debugging when VC money runs out and you actually need to maintain it. Give me a self-hosted server with visible logs any day over that mystery meat. 🌮

🤖 Prompt for AI Agents
In bootstrap/helpers/github.php around lines 171-183, the catch block currently
swallows exceptions silently; add a ray() logging call inside the catch to
record the exception and context (e.g., repository and exception message/stack)
before returning the empty array so failures are visible in logs; ensure ray()
is invoked safely (no throw) and includes enough context to trace the failing
API call.


function createGitHubDeployment(GithubApp $source, string $repository, string $ref, string $environment, ?string $description = null): ?int
{
try {
$data = [
'ref' => $ref,
'environment' => $environment,
'auto_merge' => false,
'required_contexts' => [],
];

if ($description) {
$data['description'] = $description;
}

$response = githubApi($source, "repos/{$repository}/deployments", 'post', $data, false);

return data_get($response, 'data.id');
} catch (\Exception $e) {
ray('Failed to create GitHub deployment: '.$e->getMessage());

return null;
}
}
Comment on lines +185 to +207
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

I have detailed files on GitHub deployments... and this needs careful review!

This function creates GitHub deployments, but I've detected a potential issue that could be dangerous in an untested PR:

Line 192 sets required_contexts: [] which bypasses all CI/CD checks and required status checks. This means deployments will be created even if tests are failing, builds are broken, or other required checks haven't passed. In a production environment, this could lead to deploying broken code!

Consider making required_contexts configurable or at least document this behavior:

 function createGitHubDeployment(GithubApp $source, string $repository, string $ref, string $environment, ?string $description = null): ?int
 {
     try {
         $data = [
             'ref' => $ref,
             'environment' => $environment,
             'auto_merge' => false,
-            'required_contexts' => [],
+            'required_contexts' => [], // BYPASSES ALL STATUS CHECKS - deployments will be created regardless of CI/CD status
         ];

Let's verify what GitHub's API expects here:


🌐 Web query:

GitHub API deployments required_contexts parameter behavior and best practices

💡 Result:

Summary — behavior

  • If you omit required_contexts when creating a deployment, GitHub will require “all unique” commit status contexts for that commit to be success before creating the deployment. [1][2]
  • If you pass an empty array ([]), commit-status checking is bypassed (no contexts are required) and the deployment can be created regardless of status checks. [1][2]
  • If you pass an array of context names, only those named contexts are required to be in success (you may also list contexts that have not yet been submitted). [1][2]
  • Required-status / protected-branch settings are separate: branch protection can require status checks or successful deployments before merging; required_contexts on the deployment API controls which commit-status contexts must be green for that deployment call. [4][6]

Sources

  • Deployments API parameter docs (explains omitted = all unique, empty = bypass, and subset behavior). [1][2][5][8]
  • Branch protection / required status checks and require-deployments docs (how branch protection interacts with status checks and deployments). [4][7]

Practical best practices

  • Explicitly list the contexts you care about instead of relying on the default “all unique” behavior — that avoids surprises when new CI jobs are added. [1][2]
  • Use an empty array only when you intentionally want to bypass CI checks (and understand branch-protection may still block merges). [1][2][4]
  • Keep branch-protection required status contexts and the contexts you pass to deployments aligned (use the API to read the branch’s required contexts if you need programmatic parity). [4]
  • Prefer named, stable status contexts in your CI jobs (avoid autogenerated context names that change), so required_contexts remain valid over time. [1][2]
  • After creating a deployment, use deployment_status events / API to report progress and final state (in case other systems rely on successful deployments for merging or promotions). [8][7]
  • Ensure the token/app used has the necessary repo scopes to read status contexts and create deployments. [4][8]

If you want, I can:

  • show a minimal example curl/JS payload for create-deployment with required_contexts set; or
  • fetch the exact REST endpoint snippets for your GitHub API version.

🏁 Script executed:

#!/bin/bash
# Check for calls to createGitHubDeployment and surrounding context
rg -n 'createGitHubDeployment' -B3 -A3 --type=php

Length of output: 2143


🏁 Script executed:

#!/bin/bash
# Search for any documentation or comments about GitHub deployments/status checks
rg -n -i 'required.*context|status.*check|bypass.*ci' --type=php --type=md

Length of output: 2953


🏁 Script executed:

#!/bin/bash
# Look for any configuration or environment variables that might control this
rg -n -i 'required_contexts|deployment.*config' --type=php

Length of output: 6763


Add documentation or make CI/CD bypass configurable—currently all status checks are unconditionally skipped.

When passing an empty array to required_contexts, commit-status checking is bypassed and deployments can be created regardless of test failures or broken builds. The code at line 192 does this unconditionally with no configuration option, documentation, or guard rails.

The danger: Coolify will happily deploy broken code to your self-hosted infrastructure (which is awesome, by the way—servers > serverless hype) without caring if CI failed.

Choose one:

  • Document why this bypass exists and that it's intentional
  • Make required_contexts configurable (empty = bypass, null/omitted = require all, array = explicit list)
  • Explicitly list the contexts you care about instead of relying on the default "all unique" behavior
  • At minimum: add a security warning comment explaining the bypass

I'd also recommend a gluten-free taco while you're fixing this—you'll need the fuel for careful review. 🌮


function updateGitHubDeploymentStatus(
GithubApp $source,
string $repository,
int $deploymentId,
string $state,
?string $description = null,
?string $logUrl = null,
?string $environmentUrl = null
): bool {
try {
$data = [
'state' => $state,
];

if ($description) {
$data['description'] = $description;
}

if ($logUrl) {
$data['log_url'] = $logUrl;
}

if ($environmentUrl) {
$data['environment_url'] = $environmentUrl;
}

githubApi($source, "repos/{$repository}/deployments/{$deploymentId}/statuses", 'post', $data, false);

return true;
} catch (\Exception $e) {
ray('Failed to update GitHub deployment status: '.$e->getMessage());

return false;
}
}
Comment on lines +209 to +243
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Your state values... give them to me... but only if they're valid!

This function updates GitHub deployment status but there's a critical flaw: it doesn't validate the $state parameter before sending it to the GitHub API. GitHub only accepts specific state values: error, failure, inactive, in_progress, queued, pending, or success.

If an invalid state is passed (due to a typo or logic error elsewhere), the API call will fail silently and return false, but the caller won't know WHY it failed. This is especially dangerous in untested code!

Add state validation:

 function updateGitHubDeploymentStatus(
     GithubApp $source,
     string $repository,
     int $deploymentId,
     string $state,
     ?string $description = null,
     ?string $logUrl = null,
     ?string $environmentUrl = null
 ): bool {
     try {
+        $validStates = ['error', 'failure', 'inactive', 'in_progress', 'queued', 'pending', 'success'];
+        if (!in_array($state, $validStates, true)) {
+            ray('Invalid GitHub deployment state: '.$state);
+            return false;
+        }
+
         $data = [
             'state' => $state,
         ];

Verify the valid states with GitHub:


🌐 Web query:

GitHub API deployment status valid state values

💡 Result:

Valid values for a deployment status "state" are:

  • error
  • failure
  • inactive
  • in_progress
  • queued
  • pending
  • success

Source: GitHub REST API docs. [1][2]

References
[1] REST API endpoints for deployment statuses — GitHub Docs.
[2] REST API endpoints for deployments — GitHub Docs.


State validation is missing—add it before the API terminates your sanity.

This function bypasses state validation entirely. GitHub's REST API only accepts specific deployment status states: error, failure, inactive, in_progress, queued, pending, or success. Without validation, passing an invalid state (typo, logic error, etc.) will cause the API call to fail silently—the caller gets false but no reason why. It's like serverless marketing: promises everything, delivers nothing. Self-hosted validation, on the other hand? That's what we need. Crispy. Reliable. Like a properly made gluten-free taco.

 function updateGitHubDeploymentStatus(
     GithubApp $source,
     string $repository,
     int $deploymentId,
     string $state,
     ?string $description = null,
     ?string $logUrl = null,
     ?string $environmentUrl = null
 ): bool {
     try {
+        $validStates = ['error', 'failure', 'inactive', 'in_progress', 'queued', 'pending', 'success'];
+        if (!in_array($state, $validStates, true)) {
+            ray('Invalid GitHub deployment state: '.$state);
+            return false;
+        }
+
         $data = [
             'state' => $state,
         ];
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function updateGitHubDeploymentStatus(
GithubApp $source,
string $repository,
int $deploymentId,
string $state,
?string $description = null,
?string $logUrl = null,
?string $environmentUrl = null
): bool {
try {
$data = [
'state' => $state,
];
if ($description) {
$data['description'] = $description;
}
if ($logUrl) {
$data['log_url'] = $logUrl;
}
if ($environmentUrl) {
$data['environment_url'] = $environmentUrl;
}
githubApi($source, "repos/{$repository}/deployments/{$deploymentId}/statuses", 'post', $data, false);
return true;
} catch (\Exception $e) {
ray('Failed to update GitHub deployment status: '.$e->getMessage());
return false;
}
}
function updateGitHubDeploymentStatus(
GithubApp $source,
string $repository,
int $deploymentId,
string $state,
?string $description = null,
?string $logUrl = null,
?string $environmentUrl = null
): bool {
try {
$validStates = ['error', 'failure', 'inactive', 'in_progress', 'queued', 'pending', 'success'];
if (!in_array($state, $validStates, true)) {
ray('Invalid GitHub deployment state: '.$state);
return false;
}
$data = [
'state' => $state,
];
if ($description) {
$data['description'] = $description;
}
if ($logUrl) {
$data['log_url'] = $logUrl;
}
if ($environmentUrl) {
$data['environment_url'] = $environmentUrl;
}
githubApi($source, "repos/{$repository}/deployments/{$deploymentId}/statuses", 'post', $data, false);
return true;
} catch (\Exception $e) {
ray('Failed to update GitHub deployment status: '.$e->getMessage());
return false;
}
}
🤖 Prompt for AI Agents
In bootstrap/helpers/github.php around lines 209-243, add validation for the
deployment state before calling the GitHub API: define the allowed states array
['error','failure','inactive','in_progress','queued','pending','success'],
normalize/check the incoming $state against that list, and if it’s not valid log
a clear message (including the invalid value and allowed values) and return
false (or throw a clear exception) instead of calling githubApi; only proceed to
build $data and call githubApi when $state passes validation.

Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->string('deployment_production_environment')->nullable();
$table->string('deployment_preview_environment')->nullable();
});
Schema::table('github_apps', function (Blueprint $table) {
$table->string('deployments')->nullable();
});
}

/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('applications', function (Blueprint $table) {
$table->dropColumn('deployment_production_environment');
$table->dropColumn('deployment_preview_environment');
});
Schema::table('github_apps', function (Blueprint $table) {
$table->dropColumn('deployments');
});
}
};
Loading