mirror of
https://github.com/coollabsio/coolify.git
synced 2025-12-28 05:34:50 +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:
parent
710dc3ca4b
commit
439afca642
@ -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.');
|
||||
}
|
||||
} 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
|
||||
$services = collect(data_get($composeFile, 'services', []));
|
||||
$services = $services->map(function ($service, $name) {
|
||||
|
||||
@ -1500,10 +1500,10 @@ class Application extends BaseModel
|
||||
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) {
|
||||
return applicationParser($this, $pull_request_id, $preview_id);
|
||||
return applicationParser($this, $pull_request_id, $preview_id, $commit);
|
||||
} elseif ($this->docker_compose_raw) {
|
||||
return parseDockerComposeFile(resource: $this, isNew: false, pull_request_id: $pull_request_id, preview_id: $preview_id);
|
||||
} 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');
|
||||
$compose = data_get($resource, 'docker_compose_raw');
|
||||
@ -1324,6 +1324,20 @@ function applicationParser(Application $resource, int $pull_request_id = 0, ?int
|
||||
->values();
|
||||
|
||||
$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) {
|
||||
$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');
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user