diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 4f9481794..815d6c318 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -1154,12 +1154,15 @@ private function generate_image_names() $this->production_image_name = "{$this->dockerImage}:{$this->dockerImageTag}"; } } elseif ($this->pull_request_id !== 0) { + $previewImageTag = $this->previewImageTag(); + $previewBuildImageTag = $this->previewImageTag(build: true); + if ($this->application->docker_registry_image_name) { - $this->build_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}-build"; - $this->production_image_name = "{$this->application->docker_registry_image_name}:pr-{$this->pull_request_id}"; + $this->build_image_name = "{$this->application->docker_registry_image_name}:{$previewBuildImageTag}"; + $this->production_image_name = "{$this->application->docker_registry_image_name}:{$previewImageTag}"; } else { - $this->build_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}-build"; - $this->production_image_name = "{$this->application->uuid}:pr-{$this->pull_request_id}"; + $this->build_image_name = "{$this->application->uuid}:{$previewBuildImageTag}"; + $this->production_image_name = "{$this->application->uuid}:{$previewImageTag}"; } } else { $this->dockerImageTag = str($this->commit)->substr(0, 128); @@ -1176,6 +1179,27 @@ private function generate_image_names() } } + private function previewImageTag(bool $build = false): string + { + $prefix = "pr-{$this->pull_request_id}-"; + $suffix = $build ? '-build' : ''; + $maxCommitLength = max(1, 128 - strlen($prefix) - strlen($suffix)); + $commitSource = ($this->commit === 'HEAD' || blank($this->commit)) + ? $this->deployment_uuid + : $this->commit; + + $commit = Str::of($commitSource) + ->replaceMatches('/[^A-Za-z0-9_.-]/', '-') + ->substr(0, $maxCommitLength) + ->toString(); + + if ($commit === '') { + $commit = 'HEAD'; + } + + return "{$prefix}{$commit}{$suffix}"; + } + private function just_restart() { $this->application_deployment_queue->addLogEntry("Restarting {$this->customRepository}:{$this->application->git_branch} on {$this->server->name}."); diff --git a/templates/service-templates-latest.json b/templates/service-templates-latest.json index b57d9d29c..4b21a8798 100644 --- a/templates/service-templates-latest.json +++ b/templates/service-templates-latest.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX1VSTF9MSVRFUVVFRU5fODAwMAogICAgdm9sdW1lczoKICAgICAgLSAnbGl0ZXF1ZWVuLWRhdGE6L2hvbWUvbGl0ZXF1ZWVuL2RhdGEnCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGFiYXNlcwogICAgICAgIHRhcmdldDogL3NydgogICAgICAgIGlzX2RpcmVjdG9yeTogdHJ1ZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc6PiAvZGV2L3RjcC8xMjcuMC4wLjEvODAwMCcgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/templates/service-templates.json b/templates/service-templates.json index ea9fd145a..8dd787f2e 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -1634,6 +1634,20 @@ "minversion": "0.0.0", "port": "2368" }, + "gitea-runner": { + "documentation": "https://github.com/go-gitea/gitea?utm_source=coolify.io", + "slogan": "Gitea Actions runner for docker", + "compose": "c2VydmljZXM6CiAgcnVubmVyOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vZ2l0ZWEvcnVubmVyOjEuMC4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0dJVEVBX0lOU1RBTkNFX1VSTD0ke0dJVEVBX0lOU1RBTkNFX1VSTH0nCiAgICAgIC0gJ0dJVEVBX1JVTk5FUl9SRUdJU1RSQVRJT05fVE9LRU49JHtHSVRFQV9SVU5ORVJfUkVHSVNUUkFUSU9OX1RPS0VOfScKICAgICAgLSAnR0lURUFfUlVOTkVSX05BTUU9JHtHSVRFQV9SVU5ORVJfTkFNRTotZ2l0ZWEtcnVubmVyfScKICAgICAgLSAnR0lURUFfUlVOTkVSX0xBQkVMUz0ke0dJVEVBX1JVTk5FUl9MQUJFTFM6LXVidW50dS1sYXRlc3Q6ZG9ja2VyOi8vbm9kZToyMn0nCiAgICAgIC0gJ0dJVEVBX1RPS0VOPSR7R0lURUFfVE9LRU59JwogICAgd29ya2luZ19kaXI6IC9kYXRhCiAgICB2b2x1bWVzOgogICAgICAtICdydW5uZXItZGF0YTovZGF0YScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gInBzIGF1eCB8IGdyZXAgJ1tSXXVubmVyJyA+IC9kZXYvbnVsbCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "gitea", + "actions", + "runner", + "docker" + ], + "category": "devtools", + "logo": "svgs/gitea.svg", + "minversion": "0.0.0" + }, "gitea-with-mariadb": { "documentation": "https://docs.gitea.com?utm_source=coolify.io", "slogan": "Gitea is a self-hosted, lightweight Git service, offering version control, collaboration, and code hosting.", @@ -2587,22 +2601,6 @@ "minversion": "0.0.0", "port": "4000" }, - "litequeen": { - "documentation": "https://litequeen.com/?utm_source=coolify.io", - "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", - "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTElURVFVRUVOXzgwMDAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpdGVxdWVlbi1kYXRhOi9ob21lL2xpdGVxdWVlbi9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZXMKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc19kaXJlY3Rvcnk6IHRydWUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMK", - "tags": [ - "sqlite", - "sqlite-database-management", - "self-hosted", - "vps", - "database" - ], - "category": "database", - "logo": "svgs/litequeen.svg", - "minversion": "0.0.0", - "port": "8000" - }, "lobe-chat": { "documentation": "https://github.com/lobehub/lobe-chat?tab=readme-ov-file#b-deploying-with-docker?utm_source=coolify.io", "slogan": "An open-source, modern-design AI chat framework.", diff --git a/tests/Feature/ApplicationPreviewImageNameTest.php b/tests/Feature/ApplicationPreviewImageNameTest.php new file mode 100644 index 000000000..a8d3c0e9d --- /dev/null +++ b/tests/Feature/ApplicationPreviewImageNameTest.php @@ -0,0 +1,126 @@ +newInstanceWithoutConstructor(); + + $application = new Application; + $application->uuid = 'preview-app'; + $application->build_pack = 'dockerfile'; + $application->dockerfile = null; + $application->docker_registry_image_name = $registryImageName; + + foreach ([ + 'application' => $application, + 'pull_request_id' => $pullRequestId, + 'commit' => $commit, + 'deployment_uuid' => $deploymentUuid, + ] as $property => $value) { + $reflectionProperty = $reflection->getProperty($property); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($job, $value); + } + + return $job; +} + +function generatePreviewImageNames(object $job): array +{ + $reflection = new ReflectionClass(ApplicationDeploymentJob::class); + $method = $reflection->getMethod('generate_image_names'); + $method->setAccessible(true); + $method->invoke($job); + + $buildImageName = $reflection->getProperty('build_image_name'); + $buildImageName->setAccessible(true); + + $productionImageName = $reflection->getProperty('production_image_name'); + $productionImageName->setAccessible(true); + + return [ + 'build' => $buildImageName->getValue($job), + 'production' => $productionImageName->getValue($job), + ]; +} + +it('includes the pull request id and commit in preview image names', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: '111222333444555666777888999000aaabbbccc1', + pullRequestId: 123, + )); + + expect($names['production'])->toBe('preview-app:pr-123-111222333444555666777888999000aaabbbccc1') + ->and($names['build'])->toBe('preview-app:pr-123-111222333444555666777888999000aaabbbccc1-build'); +}); + +it('generates different preview image names for different commits on the same pull request', function () { + $firstCommitNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + pullRequestId: 123, + )); + $secondCommitNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb', + pullRequestId: 123, + )); + + expect($firstCommitNames['production'])->not->toBe($secondCommitNames['production']) + ->and($firstCommitNames['build'])->not->toBe($secondCommitNames['build']); +}); + +it('uses the deployment uuid for preview image names when commit is HEAD', function () { + $firstDeploymentNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'HEAD', + pullRequestId: 123, + deploymentUuid: 'deployment-one', + )); + $secondDeploymentNames = generatePreviewImageNames(makePreviewImageNameJob( + commit: 'HEAD', + pullRequestId: 123, + deploymentUuid: 'deployment-two', + )); + + expect($firstDeploymentNames['production'])->toBe('preview-app:pr-123-deployment-one') + ->and($firstDeploymentNames['build'])->toBe('preview-app:pr-123-deployment-one-build') + ->and($secondDeploymentNames['production'])->toBe('preview-app:pr-123-deployment-two') + ->and($secondDeploymentNames['build'])->toBe('preview-app:pr-123-deployment-two-build'); +}); + +it('uses the configured registry image name for commit-specific preview tags', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: '111222333444555666777888999000aaabbbccc1', + pullRequestId: 123, + registryImageName: 'registry.example.com/team/app', + )); + + expect($names['production'])->toBe('registry.example.com/team/app:pr-123-111222333444555666777888999000aaabbbccc1') + ->and($names['build'])->toBe('registry.example.com/team/app:pr-123-111222333444555666777888999000aaabbbccc1-build'); +}); + +it('sanitizes and truncates preview image tags to docker tag limits', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: str_repeat('feature/add dockerfile changes/', 10), + pullRequestId: 123, + )); + + $productionTag = str($names['production'])->after(':')->toString(); + $buildTag = str($names['build'])->after(':')->toString(); + + expect(strlen($productionTag))->toBeLessThanOrEqual(128) + ->and(strlen($buildTag))->toBeLessThanOrEqual(128) + ->and($productionTag)->toMatch('/^pr-123-[A-Za-z0-9_.-]+$/') + ->and($buildTag)->toMatch('/^pr-123-[A-Za-z0-9_.-]+-build$/'); +}); + +it('keeps non-preview dockerfile image names commit based', function () { + $names = generatePreviewImageNames(makePreviewImageNameJob( + commit: '111222333444555666777888999000aaabbbccc1', + pullRequestId: 0, + )); + + expect($names['production'])->toBe('preview-app:111222333444555666777888999000aaabbbccc1') + ->and($names['build'])->toBe('preview-app:111222333444555666777888999000aaabbbccc1-build'); +});