Fix pinned commit propagation so deployments record/display the configured SHA and stop being overwritten by ls‑remote

Queueing a deployment now honors git_commit_sha when set (non‑PR, non‑rollback). The deployment job keeps the pinned SHA as the resolved commit and avoids overwriting it with branch head. A hidden log entry records the actual checked‑out HEAD for verification. Added tests to ensure pinned commits are persisted and unpinned commits still resolve normally.
This commit is contained in:
isbool 2026-01-09 16:26:16 +02:00
parent 75d10bb7bf
commit 7eec585d54
4 changed files with 231 additions and 1 deletions

View file

@ -2062,6 +2062,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
$this->coolify_variables .= "COOLIFY_RESOURCE_UUID={$this->application->uuid} ";
}
private function resolvePinnedCommit(): ?string
{
if ($this->pull_request_id !== 0 || $this->rollback) {
return null;
}
$pinnedCommit = str(data_get($this->application, 'git_commit_sha'))->trim()->value();
if ($pinnedCommit === '' || $pinnedCommit === 'HEAD') {
return null;
}
return $pinnedCommit;
}
private function check_git_if_build_needed()
{
if (is_object($this->source) && $this->source->getMorphClass() === \App\Models\GithubApp::class && $this->source->is_public === false) {
@ -2075,6 +2089,14 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
}
}
}
$pinnedCommit = $this->resolvePinnedCommit();
if ($pinnedCommit) {
$this->commit = $pinnedCommit;
if ($this->application_deployment_queue->commit !== $pinnedCommit) {
$this->application_deployment_queue->commit = $pinnedCommit;
$this->application_deployment_queue->save();
}
}
$this->generate_git_import_commands();
$local_branch = $this->branch;
if ($this->pull_request_id !== 0) {
@ -2121,7 +2143,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
],
);
}
if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) {
if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback && ! $pinnedCommit) {
// Extract commit SHA from git ls-remote output, handling multi-line output (e.g., redirect warnings)
// Expected format: "commit_sha\trefs/heads/branch" possibly preceded by warning lines
// Note: Git warnings can be on the same line as the result (no newline)
@ -2168,6 +2190,20 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
]
);
$this->create_workdir();
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git rev-parse HEAD"),
'hidden' => true,
'save' => 'checked_out_commit',
'ignore_errors' => true,
]
);
if ($this->saved_outputs->get('checked_out_commit')) {
$checkedOutCommit = str($this->saved_outputs->get('checked_out_commit'))->value();
if ($checkedOutCommit !== '') {
$this->application_deployment_queue->addLogEntry("Checked out commit: {$checkedOutCommit}", hidden: true);
}
}
$this->execute_remote_command(
[
executeInDocker($this->deployment_uuid, "cd {$this->workdir} && git log -1 {$this->commit} --pretty=%B"),

View file

@ -28,6 +28,11 @@ function queue_application_deployment(Application $application, string $deployme
$destination_id = $destination->id;
}
$pinnedCommit = str($application->git_commit_sha ?? '')->trim()->value();
if ($pull_request_id === 0 && ! $rollback && $pinnedCommit !== '' && $pinnedCommit !== 'HEAD') {
$commit = $pinnedCommit;
}
// Check if the deployment queue is full for this server
$serverForQueueCheck = $server ?? Server::find($server_id);
$queue_limit = $serverForQueueCheck->settings->deployment_queue_limit ?? 25;

View file

@ -0,0 +1,72 @@
<?php
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\Environment;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
uses(RefreshDatabase::class);
beforeEach(function () {
Bus::fake();
$this->team = Team::factory()->create();
$this->project = Project::create([
'name' => 'Pinned Commit Project',
'team_id' => $this->team->id,
]);
$this->environment = Environment::create([
'name' => 'production',
'project_id' => $this->project->id,
]);
$this->server = Server::factory()->create(['team_id' => $this->team->id]);
$this->destination = $this->server->standaloneDockers()->first();
});
test('pinned commit overrides queued commit', function () {
$pinnedCommit = str_repeat('a', 40);
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_type' => StandaloneDocker::class,
'destination_id' => $this->destination->id,
'git_commit_sha' => $pinnedCommit,
]);
$deploymentUuid = 'pinned-commit-deployment';
queue_application_deployment(
application: $application,
deployment_uuid: $deploymentUuid,
commit: str_repeat('b', 40),
is_webhook: true
);
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
expect($deployment)->not->toBeNull();
expect($deployment->commit)->toBe($pinnedCommit);
});
test('un-pinned commit keeps the requested commit', function () {
$requestedCommit = str_repeat('c', 40);
$application = Application::factory()->create([
'environment_id' => $this->environment->id,
'destination_type' => StandaloneDocker::class,
'destination_id' => $this->destination->id,
'git_commit_sha' => 'HEAD',
]);
$deploymentUuid = 'head-commit-deployment';
queue_application_deployment(
application: $application,
deployment_uuid: $deploymentUuid,
commit: $requestedCommit
);
$deployment = ApplicationDeploymentQueue::where('deployment_uuid', $deploymentUuid)->first();
expect($deployment)->not->toBeNull();
expect($deployment->commit)->toBe($requestedCommit);
});

View file

@ -0,0 +1,117 @@
<?php
use App\Jobs\ApplicationDeploymentJob;
use App\Models\Application;
use App\Models\ApplicationDeploymentQueue;
use App\Models\ApplicationSetting;
function setJobProperty(object $job, string $property, mixed $value): void
{
$reflection = new ReflectionClass($job);
$prop = $reflection->getProperty($property);
$prop->setAccessible(true);
$prop->setValue($job, $value);
}
function getJobProperty(object $job, string $property): mixed
{
$reflection = new ReflectionClass($job);
$prop = $reflection->getProperty($property);
$prop->setAccessible(true);
return $prop->getValue($job);
}
it('keeps pinned commit when resolving git commit sha', function () {
$pinnedCommit = str_repeat('a', 40);
$remoteCommit = str_repeat('b', 40);
$lsRemoteOutput = $remoteCommit."\trefs/heads/main";
$settings = new ApplicationSetting([
'include_source_commit_in_build' => false,
'use_build_secrets' => false,
'is_git_submodules_enabled' => false,
'is_git_lfs_enabled' => false,
'is_git_shallow_clone_enabled' => false,
]);
$application = Application::factory()->make([
'git_repository' => 'https://example.com/acme/repo.git',
'git_branch' => 'main',
'git_commit_sha' => $pinnedCommit,
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
]);
$application->setRelation('settings', $settings);
$queue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial();
$queue->commit = $pinnedCommit;
$queue->shouldReceive('save')->never();
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldReceive('execute_remote_command')->andReturnNull();
setJobProperty($job, 'application', $application);
setJobProperty($job, 'application_deployment_queue', $queue);
setJobProperty($job, 'deployment_uuid', 'deployment-uuid');
setJobProperty($job, 'pull_request_id', 0);
setJobProperty($job, 'commit', 'HEAD');
setJobProperty($job, 'rollback', false);
setJobProperty($job, 'git_type', null);
setJobProperty($job, 'saved_outputs', collect([
'git_commit_sha' => str($lsRemoteOutput),
]));
setJobProperty($job, 'source', 'other');
$method = new ReflectionMethod(ApplicationDeploymentJob::class, 'check_git_if_build_needed');
$method->setAccessible(true);
$method->invoke($job);
expect(getJobProperty($job, 'commit'))->toBe($pinnedCommit);
});
it('updates commit from ls-remote when no pinned commit is set', function () {
$remoteCommit = str_repeat('c', 40);
$lsRemoteOutput = $remoteCommit."\trefs/heads/main";
$settings = new ApplicationSetting([
'include_source_commit_in_build' => false,
'use_build_secrets' => false,
'is_git_submodules_enabled' => false,
'is_git_lfs_enabled' => false,
'is_git_shallow_clone_enabled' => false,
]);
$application = Application::factory()->make([
'git_repository' => 'https://example.com/acme/repo.git',
'git_branch' => 'main',
'git_commit_sha' => 'HEAD',
'build_pack' => 'nixpacks',
'ports_exposes' => '3000',
]);
$application->setRelation('settings', $settings);
$queue = Mockery::mock(ApplicationDeploymentQueue::class)->makePartial();
$queue->commit = 'HEAD';
$queue->shouldReceive('save')->once();
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
$job->shouldReceive('execute_remote_command')->andReturnNull();
setJobProperty($job, 'application', $application);
setJobProperty($job, 'application_deployment_queue', $queue);
setJobProperty($job, 'deployment_uuid', 'deployment-uuid');
setJobProperty($job, 'pull_request_id', 0);
setJobProperty($job, 'commit', 'HEAD');
setJobProperty($job, 'rollback', false);
setJobProperty($job, 'git_type', null);
setJobProperty($job, 'saved_outputs', collect([
'git_commit_sha' => str($lsRemoteOutput),
]));
setJobProperty($job, 'source', 'other');
$method = new ReflectionMethod(ApplicationDeploymentJob::class, 'check_git_if_build_needed');
$method->setAccessible(true);
$method->invoke($job);
expect(getJobProperty($job, 'commit'))->toBe($remoteCommit);
expect($queue->commit)->toBe($remoteCommit);
});