mirror of
https://github.com/tiennm99/coolify.git
synced 2026-04-17 17:21:04 +00:00
Inject commit-based image tags for Docker Compose build services
For Docker Compose applications with build directives, inject commit-based
image tags (uuid_servicename:commit) to enable rollback functionality.
Previously these services always used 'latest' tags, making rollback impossible.
- Only injects tags for services with build: but no explicit image:
- Uses pr-{id} tags for pull request deployments
- Respects user-defined image: fields (preserves user intent)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -620,7 +620,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||||||
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
|
$this->application_deployment_queue->addLogEntry('Build secrets are configured. Ensure your docker-compose file includes build.secrets configuration for services that need them.');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'));
|
$composeFile = $this->application->parse(pull_request_id: $this->pull_request_id, preview_id: data_get($this->preview, 'id'), commit: $this->commit);
|
||||||
// Always add .env file to services
|
// Always add .env file to services
|
||||||
$services = collect(data_get($composeFile, 'services', []));
|
$services = collect(data_get($composeFile, 'services', []));
|
||||||
$services = $services->map(function ($service, $name) {
|
$services = $services->map(function ($service, $name) {
|
||||||
|
|||||||
@@ -1500,10 +1500,10 @@ class Application extends BaseModel
|
|||||||
instant_remote_process($commands, $this->destination->server, false);
|
instant_remote_process($commands, $this->destination->server, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function parse(int $pull_request_id = 0, ?int $preview_id = null)
|
public function parse(int $pull_request_id = 0, ?int $preview_id = null, ?string $commit = null)
|
||||||
{
|
{
|
||||||
if ((int) $this->compose_parsing_version >= 3) {
|
if ((int) $this->compose_parsing_version >= 3) {
|
||||||
return applicationParser($this, $pull_request_id, $preview_id);
|
return applicationParser($this, $pull_request_id, $preview_id, $commit);
|
||||||
} elseif ($this->docker_compose_raw) {
|
} elseif ($this->docker_compose_raw) {
|
||||||
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -358,7 +358,7 @@ function parseDockerVolumeString(string $volumeString): array
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null): Collection
|
function applicationParser(Application $resource, int $pull_request_id = 0, ?int $preview_id = null, ?string $commit = null): Collection
|
||||||
{
|
{
|
||||||
$uuid = data_get($resource, 'uuid');
|
$uuid = data_get($resource, 'uuid');
|
||||||
$compose = data_get($resource, 'docker_compose_raw');
|
$compose = data_get($resource, 'docker_compose_raw');
|
||||||
@@ -1324,6 +1324,20 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
|||||||
->values();
|
->values();
|
||||||
|
|
||||||
$payload['env_file'] = $envFiles;
|
$payload['env_file'] = $envFiles;
|
||||||
|
|
||||||
|
// Inject commit-based image tag for services with build directive (for rollback support)
|
||||||
|
// Only inject if service has build but no explicit image defined
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
if ($hasBuild && ! $hasImage && $commit) {
|
||||||
|
$imageTag = str($commit)->substr(0, 128)->value();
|
||||||
|
if ($isPullRequest) {
|
||||||
|
$imageTag = "pr-{$pullRequestId}";
|
||||||
|
}
|
||||||
|
$imageRepo = "{$uuid}_{$serviceName}";
|
||||||
|
$payload['image'] = "{$imageRepo}:{$imageTag}";
|
||||||
|
}
|
||||||
|
|
||||||
if ($isPullRequest) {
|
if ($isPullRequest) {
|
||||||
$serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId);
|
$serviceName = addPreviewDeploymentSuffix($serviceName, $pullRequestId);
|
||||||
}
|
}
|
||||||
|
|||||||
171
tests/Unit/Parsers/ApplicationParserImageTagTest.php
Normal file
171
tests/Unit/Parsers/ApplicationParserImageTagTest.php
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tests for Docker Compose image tag injection in applicationParser.
|
||||||
|
*
|
||||||
|
* These tests verify the logic for injecting commit-based image tags
|
||||||
|
* into Docker Compose services with build directives.
|
||||||
|
*/
|
||||||
|
it('injects image tag for services with build but no image directive', function () {
|
||||||
|
// Test the condition: hasBuild && !hasImage && commit
|
||||||
|
$service = [
|
||||||
|
'build' => './app',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
$commit = 'abc123def456';
|
||||||
|
$uuid = 'app-uuid';
|
||||||
|
$serviceName = 'web';
|
||||||
|
|
||||||
|
expect($hasBuild)->toBeTrue();
|
||||||
|
expect($hasImage)->toBeFalse();
|
||||||
|
|
||||||
|
// Simulate the image injection logic
|
||||||
|
if ($hasBuild && ! $hasImage && $commit) {
|
||||||
|
$imageTag = str($commit)->substr(0, 128)->value();
|
||||||
|
$imageRepo = "{$uuid}_{$serviceName}";
|
||||||
|
$service['image'] = "{$imageRepo}:{$imageTag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($service['image'])->toBe('app-uuid_web:abc123def456');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inject image tag when service has explicit image directive', function () {
|
||||||
|
// User has specified their own image - we respect it
|
||||||
|
$service = [
|
||||||
|
'build' => './app',
|
||||||
|
'image' => 'myregistry/myapp:latest',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
$commit = 'abc123def456';
|
||||||
|
|
||||||
|
expect($hasBuild)->toBeTrue();
|
||||||
|
expect($hasImage)->toBeTrue();
|
||||||
|
|
||||||
|
// The condition should NOT trigger
|
||||||
|
$shouldInject = $hasBuild && ! $hasImage && $commit;
|
||||||
|
expect($shouldInject)->toBeFalse();
|
||||||
|
|
||||||
|
// Image should remain unchanged
|
||||||
|
expect($service['image'])->toBe('myregistry/myapp:latest');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inject image tag when there is no commit', function () {
|
||||||
|
$service = [
|
||||||
|
'build' => './app',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
$commit = null;
|
||||||
|
|
||||||
|
expect($hasBuild)->toBeTrue();
|
||||||
|
expect($hasImage)->toBeFalse();
|
||||||
|
|
||||||
|
// The condition should NOT trigger (no commit)
|
||||||
|
$shouldInject = $hasBuild && ! $hasImage && $commit;
|
||||||
|
expect($shouldInject)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not inject image tag for services without build directive', function () {
|
||||||
|
// Service that pulls a pre-built image
|
||||||
|
$service = [
|
||||||
|
'image' => 'nginx:alpine',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
$commit = 'abc123def456';
|
||||||
|
|
||||||
|
expect($hasBuild)->toBeFalse();
|
||||||
|
expect($hasImage)->toBeTrue();
|
||||||
|
|
||||||
|
// The condition should NOT trigger (no build)
|
||||||
|
$shouldInject = $hasBuild && ! $hasImage && $commit;
|
||||||
|
expect($shouldInject)->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('uses pr-{id} tag for pull request deployments', function () {
|
||||||
|
$service = [
|
||||||
|
'build' => './app',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
$commit = 'abc123def456';
|
||||||
|
$uuid = 'app-uuid';
|
||||||
|
$serviceName = 'web';
|
||||||
|
$isPullRequest = true;
|
||||||
|
$pullRequestId = 42;
|
||||||
|
|
||||||
|
// Simulate the PR image injection logic
|
||||||
|
if ($hasBuild && ! $hasImage && $commit) {
|
||||||
|
$imageTag = str($commit)->substr(0, 128)->value();
|
||||||
|
if ($isPullRequest) {
|
||||||
|
$imageTag = "pr-{$pullRequestId}";
|
||||||
|
}
|
||||||
|
$imageRepo = "{$uuid}_{$serviceName}";
|
||||||
|
$service['image'] = "{$imageRepo}:{$imageTag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
expect($service['image'])->toBe('app-uuid_web:pr-42');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('truncates commit SHA to 128 characters', function () {
|
||||||
|
$service = [
|
||||||
|
'build' => './app',
|
||||||
|
];
|
||||||
|
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
// Create a very long commit string
|
||||||
|
$commit = str_repeat('a', 200);
|
||||||
|
$uuid = 'app-uuid';
|
||||||
|
$serviceName = 'web';
|
||||||
|
|
||||||
|
if ($hasBuild && ! $hasImage && $commit) {
|
||||||
|
$imageTag = str($commit)->substr(0, 128)->value();
|
||||||
|
$imageRepo = "{$uuid}_{$serviceName}";
|
||||||
|
$service['image'] = "{$imageRepo}:{$imageTag}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tag should be exactly 128 characters
|
||||||
|
$parts = explode(':', $service['image']);
|
||||||
|
expect(strlen($parts[1]))->toBe(128);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple services with build directives', function () {
|
||||||
|
$services = [
|
||||||
|
'web' => ['build' => './web'],
|
||||||
|
'worker' => ['build' => './worker'],
|
||||||
|
'api' => ['build' => './api', 'image' => 'custom:tag'], // Has explicit image
|
||||||
|
'redis' => ['image' => 'redis:alpine'], // No build
|
||||||
|
];
|
||||||
|
|
||||||
|
$commit = 'abc123';
|
||||||
|
$uuid = 'app-uuid';
|
||||||
|
|
||||||
|
foreach ($services as $serviceName => $service) {
|
||||||
|
$hasBuild = data_get($service, 'build') !== null;
|
||||||
|
$hasImage = data_get($service, 'image') !== null;
|
||||||
|
|
||||||
|
if ($hasBuild && ! $hasImage && $commit) {
|
||||||
|
$imageTag = str($commit)->substr(0, 128)->value();
|
||||||
|
$imageRepo = "{$uuid}_{$serviceName}";
|
||||||
|
$services[$serviceName]['image'] = "{$imageRepo}:{$imageTag}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// web and worker should get injected images
|
||||||
|
expect($services['web']['image'])->toBe('app-uuid_web:abc123');
|
||||||
|
expect($services['worker']['image'])->toBe('app-uuid_worker:abc123');
|
||||||
|
|
||||||
|
// api keeps its custom image (has explicit image)
|
||||||
|
expect($services['api']['image'])->toBe('custom:tag');
|
||||||
|
|
||||||
|
// redis keeps its image (no build directive)
|
||||||
|
expect($services['redis']['image'])->toBe('redis:alpine');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user