diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php
index f93ecf310..e9c52d2f5 100644
--- a/app/Http/Controllers/Api/ApplicationsController.php
+++ b/app/Http/Controllers/Api/ApplicationsController.php
@@ -17,6 +17,7 @@
use App\Models\Service;
use App\Rules\ValidGitBranch;
use App\Rules\ValidGitRepositoryUrl;
+use App\Services\DockerImageParser;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use OpenApi\Attributes as OA;
@@ -1512,31 +1513,32 @@ private function create_application(Request $request, $type)
if ($return instanceof \Illuminate\Http\JsonResponse) {
return $return;
}
- // Process docker image name and tag for SHA256 digests
+ // Process docker image name and tag using DockerImageParser
$dockerImageName = $request->docker_registry_image_name;
$dockerImageTag = $request->docker_registry_image_tag;
- // Strip 'sha256:' prefix if user provided it in the tag
+ // Build the full Docker image string for parsing
if ($dockerImageTag) {
- $dockerImageTag = preg_replace('/^sha256:/i', '', trim($dockerImageTag));
+ $dockerImageString = $dockerImageName.':'.$dockerImageTag;
+ } else {
+ $dockerImageString = $dockerImageName;
}
- // Remove @sha256 from image name if user added it
- if ($dockerImageName) {
- $dockerImageName = preg_replace('/@sha256$/i', '', trim($dockerImageName));
- }
+ // Parse using DockerImageParser to normalize the image reference
+ $parser = new DockerImageParser;
+ $parser->parse($dockerImageString);
- // Check if tag is a valid SHA256 hash (64 hex characters)
- $isSha256Hash = $dockerImageTag && preg_match('/^[a-f0-9]{64}$/i', $dockerImageTag);
+ // Get normalized image name and tag
+ $normalizedImageName = $parser->getFullImageNameWithoutTag();
- // Append @sha256 to image name if using digest and not already present
- if ($isSha256Hash && ! str_ends_with($dockerImageName, '@sha256')) {
- $dockerImageName .= '@sha256';
+ // Append @sha256 to image name if using digest
+ if ($parser->isImageHash() && ! str_ends_with($normalizedImageName, '@sha256')) {
+ $normalizedImageName .= '@sha256';
}
// Set processed values back to request
- $request->offsetSet('docker_registry_image_name', $dockerImageName);
- $request->offsetSet('docker_registry_image_tag', $dockerImageTag ?: 'latest');
+ $request->offsetSet('docker_registry_image_name', $normalizedImageName);
+ $request->offsetSet('docker_registry_image_tag', $parser->getTag());
$application = new Application;
removeUnnecessaryFieldsFromRequest($request);
diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php
index e105c956a..96e3dccc1 100644
--- a/app/Livewire/Project/New/DockerImage.php
+++ b/app/Livewire/Project/New/DockerImage.php
@@ -28,18 +28,60 @@ public function mount()
$this->query = request()->query();
}
+ /**
+ * Auto-parse image name when user pastes a complete Docker image reference
+ * Examples:
+ * - nginx:stable-alpine3.21-perl@sha256:4e272eef...
+ * - ghcr.io/user/app:v1.2.3
+ * - nginx@sha256:abc123...
+ */
+ public function updatedImageName(): void
+ {
+ if (empty($this->imageName)) {
+ return;
+ }
+
+ // Don't auto-parse if user has already manually filled tag or sha256 fields
+ if (! empty($this->imageTag) || ! empty($this->imageSha256)) {
+ return;
+ }
+
+ // Only auto-parse if the image name contains a tag (:) or digest (@)
+ if (! str_contains($this->imageName, ':') && ! str_contains($this->imageName, '@')) {
+ return;
+ }
+
+ try {
+ $parser = new DockerImageParser;
+ $parser->parse($this->imageName);
+
+ // Extract the base image name (without tag/digest)
+ $baseImageName = $parser->getFullImageNameWithoutTag();
+
+ // Only update if parsing resulted in different base name
+ // This prevents unnecessary updates when user types just the name
+ if ($baseImageName !== $this->imageName) {
+ if ($parser->isImageHash()) {
+ // It's a SHA256 digest (takes priority over tag)
+ $this->imageSha256 = $parser->getTag();
+ $this->imageTag = '';
+ } elseif ($parser->getTag() !== 'latest' || str_contains($this->imageName, ':')) {
+ // It's a regular tag (only set if not default 'latest' or explicitly specified)
+ $this->imageTag = $parser->getTag();
+ $this->imageSha256 = '';
+ }
+
+ // Update imageName to just the base name
+ $this->imageName = $baseImageName;
+ }
+ } catch (\Exception $e) {
+ // If parsing fails, leave the image name as-is
+ // User will see validation error on submit
+ }
+ }
+
public function submit()
{
- // Strip 'sha256:' prefix if user pasted it
- if ($this->imageSha256) {
- $this->imageSha256 = preg_replace('/^sha256:/i', '', trim($this->imageSha256));
- }
-
- // Remove @sha256 from image name if user added it
- if ($this->imageName) {
- $this->imageName = preg_replace('/@sha256$/i', '', trim($this->imageName));
- }
-
$this->validate([
'imageName' => ['required', 'string'],
'imageTag' => ['nullable', 'string', 'regex:/^[a-z0-9][a-z0-9._-]*$/i'],
@@ -56,13 +98,16 @@ public function submit()
// Build the full Docker image string
if ($this->imageSha256) {
- $dockerImage = $this->imageName.'@sha256:'.$this->imageSha256;
+ // Strip 'sha256:' prefix if user pasted it
+ $sha256Hash = preg_replace('/^sha256:/i', '', trim($this->imageSha256));
+ $dockerImage = $this->imageName.'@sha256:'.$sha256Hash;
} elseif ($this->imageTag) {
$dockerImage = $this->imageName.':'.$this->imageTag;
} else {
$dockerImage = $this->imageName.':latest';
}
+ // Parse using DockerImageParser to normalize the image reference
$parser = new DockerImageParser;
$parser->parse($dockerImage);
diff --git a/resources/views/livewire/project/new/docker-image.blade.php b/resources/views/livewire/project/new/docker-image.blade.php
index 54c175b82..df50b8e21 100644
--- a/resources/views/livewire/project/new/docker-image.blade.php
+++ b/resources/views/livewire/project/new/docker-image.blade.php
@@ -7,8 +7,8 @@