diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 545735cf6..337562bb0 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -2327,7 +2327,7 @@ private function check_git_if_build_needed() ], ); } - if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback) { + if ($this->saved_outputs->get('git_commit_sha') && ! $this->rollback && $this->shouldResolveBranchHeadCommit()) { // 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) @@ -2359,6 +2359,13 @@ private function check_git_if_build_needed() } } + private function shouldResolveBranchHeadCommit(): bool + { + $commit = trim($this->commit); + + return $commit === '' || $commit === 'HEAD'; + } + private function clone_repository() { $importCommands = $this->generate_git_import_commands(); diff --git a/tests/Unit/ApplicationDeploymentJobCommitResolutionTest.php b/tests/Unit/ApplicationDeploymentJobCommitResolutionTest.php new file mode 100644 index 000000000..f11d6ac16 --- /dev/null +++ b/tests/Unit/ApplicationDeploymentJobCommitResolutionTest.php @@ -0,0 +1,121 @@ +setValue($job, $value); +} + +function getDeploymentJobProperty(object $job, string $property): mixed +{ + $reflectionProperty = new ReflectionProperty(ApplicationDeploymentJob::class, $property); + + return $reflectionProperty->getValue($job); +} + +function invokeCheckGitIfBuildNeeded(object $job): void +{ + $method = new ReflectionMethod(ApplicationDeploymentJob::class, 'check_git_if_build_needed'); + $method->invoke($job); +} + +function makeDeploymentJobForCommitCheck(string $pinnedSha, string $branchHeadSha): object +{ + $job = new class extends ApplicationDeploymentJob + { + public function __construct() {} + + public function execute_remote_command(...$commands) {} + }; + + $application = new class extends Application + { + public function generateGitImportCommands(string $deployment_uuid, int $pull_request_id = 0, ?string $git_type = null, bool $exec_in_docker = true, bool $only_checkout = false, ?string $custom_base_dir = null, ?string $commit = null) + { + return [ + 'commands' => collect([]), + 'branch' => 'main', + 'fullRepoUrl' => 'https://github.com/coollabsio/coolify.git', + ]; + } + }; + $application->forceFill([ + 'uuid' => 'application-uuid', + 'git_branch' => 'main', + ]); + $application->setRelation('settings', (object) [ + 'include_source_commit_in_build' => false, + 'use_build_secrets' => false, + ]); + $application->setRelation('private_key', null); + + $deploymentQueue = new class extends ApplicationDeploymentQueue + { + public bool $saved = false; + + public function save(array $options = []): bool + { + $this->saved = true; + + return true; + } + }; + $deploymentQueue->commit = $pinnedSha; + + setDeploymentJobProperty($job, 'application', $application); + setDeploymentJobProperty($job, 'application_deployment_queue', $deploymentQueue); + setDeploymentJobProperty($job, 'deployment_uuid', 'deployment-uuid'); + setDeploymentJobProperty($job, 'pull_request_id', 0); + setDeploymentJobProperty($job, 'commit', $pinnedSha); + setDeploymentJobProperty($job, 'rollback', false); + setDeploymentJobProperty($job, 'git_type', 'github'); + setDeploymentJobProperty($job, 'saved_outputs', collect([ + 'git_commit_sha' => str("{$branchHeadSha}\trefs/heads/main"), + ])); + + return $job; +} + +function shouldResolveBranchHeadForCommit(?string $commit): bool +{ + $job = (new ReflectionClass(ApplicationDeploymentJob::class))->newInstanceWithoutConstructor(); + + $commitProperty = new ReflectionProperty($job, 'commit'); + $commitProperty->setValue($job, $commit ?? ''); + + $method = new ReflectionMethod($job, 'shouldResolveBranchHeadCommit'); + + return $method->invoke($job); +} + +describe('ApplicationDeploymentJob commit resolution', function () { + test('resolves branch head for HEAD deployments', function () { + expect(shouldResolveBranchHeadForCommit('HEAD'))->toBeTrue(); + }); + + test('resolves branch head for blank deployments', function () { + expect(shouldResolveBranchHeadForCommit(''))->toBeTrue(); + }); + + test('keeps pinned deployment commits instead of replacing them with branch head', function () { + expect(shouldResolveBranchHeadForCommit('abc123def456abc123def456abc123def456abc1'))->toBeFalse(); + }); + + test('check git does not overwrite pinned deployment commit with branch head', function () { + $pinnedSha = 'abc123def456abc123def456abc123def456abc1'; + $branchHeadSha = '111222333444555666777888999000aaabbbccc1'; + $job = makeDeploymentJobForCommitCheck($pinnedSha, $branchHeadSha); + + invokeCheckGitIfBuildNeeded($job); + + $deploymentQueue = getDeploymentJobProperty($job, 'application_deployment_queue'); + + expect(getDeploymentJobProperty($job, 'commit'))->toBe($pinnedSha) + ->and($deploymentQueue->commit)->toBe($pinnedSha) + ->and($deploymentQueue->saved)->toBeFalse(); + }); +});