mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
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:
parent
75d10bb7bf
commit
7eec585d54
4 changed files with 231 additions and 1 deletions
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
72
tests/Feature/PinnedCommitDeploymentTest.php
Normal file
72
tests/Feature/PinnedCommitDeploymentTest.php
Normal 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);
|
||||
});
|
||||
117
tests/Unit/PinnedCommitResolutionTest.php
Normal file
117
tests/Unit/PinnedCommitResolutionTest.php
Normal 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);
|
||||
});
|
||||
Loading…
Reference in a new issue