From 239e28630f9490661af7957f2c8be1cec0b3e3a9 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 6 Nov 2025 08:43:43 +0100 Subject: [PATCH 1/3] style: update background colors to use gray-50 for consistency in auth views --- resources/css/app.css | 2 +- resources/views/auth/login.blade.php | 4 +- resources/views/auth/register.blade.php | 19 +- resources/views/auth/reset-password.blade.php | 20 +- .../views/auth/two-factor-challenge.blade.php | 2 +- resources/views/layouts/base.blade.php | 420 +++++++++--------- 6 files changed, 238 insertions(+), 229 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index fa1e61cb27..70759e5421 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -82,7 +82,7 @@ */ html, body { - @apply w-full min-h-full bg-neutral-50 dark:bg-base dark:text-neutral-400; + @apply w-full min-h-full bg-gray-50 dark:bg-base dark:text-neutral-400; } body { diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index f85dc268ea..ede49117ae 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -61,7 +61,7 @@ class="text-sm dark:text-neutral-400 hover:text-coollabs dark:hover:text-warning
- + Don't have an account?
@@ -82,7 +82,7 @@ class="block w-full text-center py-3 px-4 rounded-lg border border-neutral-300 d
- or + or continue with
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php index 3db9437269..cdfa52a98f 100644 --- a/resources/views/auth/register.blade.php +++ b/resources/views/auth/register.blade.php @@ -33,7 +33,8 @@ function getOldOrLocal($key, $localValue)

Root User Setup

-

This user will be the root user with full admin access.

+

This user will be the root user with full + admin access.

@@ -58,13 +59,16 @@ function getOldOrLocal($key, $localValue) -
+

- Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. + Your password should be min 8 characters long and contain at least one uppercase letter, + one lowercase letter, one number, and one symbol.

- + Create Account @@ -74,17 +78,18 @@ function getOldOrLocal($key, $localValue)
- + Already have an account?
- + {{ __('auth.already_registered') }} - + \ No newline at end of file diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php index a4a07ebd63..3e0c237b40 100644 --- a/resources/views/auth/reset-password.blade.php +++ b/resources/views/auth/reset-password.blade.php @@ -47,16 +47,19 @@ label="{{ __('input.email') }}" /> - + -
+

- Your password should be min 8 characters long and contain at least one uppercase letter, one lowercase letter, one number, and one symbol. + Your password should be min 8 characters long and contain at least one uppercase letter, + one lowercase letter, one number, and one symbol.

- + {{ __('auth.reset_password') }} @@ -66,17 +69,18 @@
- + Remember your password?
- + Back to Login - + \ No newline at end of file diff --git a/resources/views/auth/two-factor-challenge.blade.php b/resources/views/auth/two-factor-challenge.blade.php index d4531cbe87..05dbcc90cd 100644 --- a/resources/views/auth/two-factor-challenge.blade.php +++ b/resources/views/auth/two-factor-challenge.blade.php @@ -120,7 +120,7 @@ class="mt-2 text-sm dark:text-neutral-400 hover:text-black dark:hover:text-white
- + Need help?
diff --git a/resources/views/layouts/base.blade.php b/resources/views/layouts/base.blade.php index c577f72487..7bb366cd43 100644 --- a/resources/views/layouts/base.blade.php +++ b/resources/views/layouts/base.blade.php @@ -2,7 +2,7 @@ - + }, timeout); + return; + } else { + window.location.reload(); + } + }) + window.Livewire.on('info', (message) => { + if (typeof message === 'string') { + window.toast('Info', { + type: 'info', + description: message, + }) + return; + } + if (message.length == 1) { + window.toast('Info', { + type: 'info', + description: message[0], + }) + } else if (message.length == 2) { + window.toast(message[0], { + type: 'info', + description: message[1], + }) + } + }) + window.Livewire.on('error', (message) => { + if (typeof message === 'string') { + window.toast('Error', { + type: 'danger', + description: message, + }) + return; + } + if (message.length == 1) { + window.toast('Error', { + type: 'danger', + description: message[0], + }) + } else if (message.length == 2) { + window.toast(message[0], { + type: 'danger', + description: message[1], + }) + } + }) + window.Livewire.on('warning', (message) => { + if (typeof message === 'string') { + window.toast('Warning', { + type: 'warning', + description: message, + }) + return; + } + if (message.length == 1) { + window.toast('Warning', { + type: 'warning', + description: message[0], + }) + } else if (message.length == 2) { + window.toast(message[0], { + type: 'warning', + description: message[1], + }) + } + }) + window.Livewire.on('success', (message) => { + if (typeof message === 'string') { + window.toast('Success', { + type: 'success', + description: message, + }) + return; + } + if (message.length == 1) { + window.toast('Success', { + type: 'success', + description: message[0], + }) + } else if (message.length == 2) { + window.toast(message[0], { + type: 'success', + description: message[1], + }) + } + }) + }); + + @show - + \ No newline at end of file From dbf7957795b60020663759ac0ba9172375a3a58f Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 6 Nov 2025 08:54:35 +0100 Subject: [PATCH 2/3] fix: inserting ARG statements in Dockerfile after FROM instructions --- app/Jobs/ApplicationDeploymentJob.php | 55 +++++++++++++++++++++------ 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index a240a759a6..1dfcaaafce 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -3226,6 +3226,20 @@ private function generate_secrets_hash($variables) return hash_hmac('sha256', $secrets_string, $this->secrets_hash_key); } + protected function findFromInstructionLines($dockerfile): array + { + $fromLines = []; + foreach ($dockerfile as $index => $line) { + $trimmedLine = trim($line); + // Check if line starts with FROM (case-insensitive) + if (preg_match('/^FROM\s+/i', $trimmedLine)) { + $fromLines[] = $index; + } + } + + return $fromLines; + } + private function add_build_env_variables_to_dockerfile() { if ($this->dockerBuildkitSupported) { @@ -3238,6 +3252,18 @@ private function add_build_env_variables_to_dockerfile() 'ignore_errors' => true, ]); $dockerfile = collect(str($this->saved_outputs->get('dockerfile'))->trim()->explode("\n")); + + // Find all FROM instruction positions + $fromLines = $this->findFromInstructionLines($dockerfile); + + // If no FROM instructions found, skip ARG insertion + if (empty($fromLines)) { + return; + } + + // Collect all ARG statements to insert + $argsToInsert = collect(); + if ($this->pull_request_id === 0) { // Only add environment variables that are available during build $envs = $this->application->environment_variables() @@ -3246,9 +3272,9 @@ private function add_build_env_variables_to_dockerfile() ->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + $argsToInsert->push("ARG {$env->key}"); } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + $argsToInsert->push("ARG {$env->key}={$env->real_value}"); } } // Add Coolify variables as ARGs @@ -3258,9 +3284,7 @@ private function add_build_env_variables_to_dockerfile() ->map(function ($var) { return "ARG {$var}"; }); - foreach ($coolify_vars as $arg) { - $dockerfile->splice(1, 0, [$arg]); - } + $argsToInsert = $argsToInsert->merge($coolify_vars); } } else { // Only add preview environment variables that are available during build @@ -3270,9 +3294,9 @@ private function add_build_env_variables_to_dockerfile() ->get(); foreach ($envs as $env) { if (data_get($env, 'is_multiline') === true) { - $dockerfile->splice(1, 0, ["ARG {$env->key}"]); + $argsToInsert->push("ARG {$env->key}"); } else { - $dockerfile->splice(1, 0, ["ARG {$env->key}={$env->real_value}"]); + $argsToInsert->push("ARG {$env->key}={$env->real_value}"); } } // Add Coolify variables as ARGs @@ -3282,15 +3306,24 @@ private function add_build_env_variables_to_dockerfile() ->map(function ($var) { return "ARG {$var}"; }); - foreach ($coolify_vars as $arg) { - $dockerfile->splice(1, 0, [$arg]); - } + $argsToInsert = $argsToInsert->merge($coolify_vars); } } + // Add secrets hash if we have environment variables if ($envs->isNotEmpty()) { $secrets_hash = $this->generate_secrets_hash($envs); - $dockerfile->splice(1, 0, ["ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"]); + $argsToInsert->push("ARG COOLIFY_BUILD_SECRETS_HASH={$secrets_hash}"); + } + + // Insert ARGs after each FROM instruction (in reverse order to maintain correct line numbers) + if ($argsToInsert->isNotEmpty()) { + foreach (array_reverse($fromLines) as $fromLineIndex) { + // Insert all ARGs after this FROM instruction + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } } $dockerfile_base64 = base64_encode($dockerfile->implode("\n")); From 4968e9fa2bf622df15da5292534f9709a5d64338 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Thu, 6 Nov 2025 08:54:40 +0100 Subject: [PATCH 3/3] test: add unit tests for Dockerfile ARG insertion logic --- tests/Unit/DockerfileArgInsertionTest.php | 218 ++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 tests/Unit/DockerfileArgInsertionTest.php diff --git a/tests/Unit/DockerfileArgInsertionTest.php b/tests/Unit/DockerfileArgInsertionTest.php new file mode 100644 index 0000000000..593f09145b --- /dev/null +++ b/tests/Unit/DockerfileArgInsertionTest.php @@ -0,0 +1,218 @@ +makePartial(); + + $dockerfile = collect([ + 'FROM node:16', + 'WORKDIR /app', + 'COPY . .', + ]); + + $result = $job->findFromInstructionLines($dockerfile); + + expect($result)->toBe([0]); +}); + +it('finds FROM instructions with comments before', function () { + $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); + + $dockerfile = collect([ + '# Build stage', + '# Another comment', + 'FROM node:16', + 'WORKDIR /app', + ]); + + $result = $job->findFromInstructionLines($dockerfile); + + expect($result)->toBe([2]); +}); + +it('finds multiple FROM instructions in multi-stage dockerfile', function () { + $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); + + $dockerfile = collect([ + 'FROM node:16 AS builder', + 'WORKDIR /app', + 'RUN npm install', + '', + 'FROM nginx:alpine', + 'COPY --from=builder /app/dist /usr/share/nginx/html', + ]); + + $result = $job->findFromInstructionLines($dockerfile); + + expect($result)->toBe([0, 4]); +}); + +it('handles FROM with different cases', function () { + $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); + + $dockerfile = collect([ + 'from node:16', + 'From nginx:alpine', + 'FROM alpine:latest', + ]); + + $result = $job->findFromInstructionLines($dockerfile); + + expect($result)->toBe([0, 1, 2]); +}); + +it('returns empty array when no FROM instructions found', function () { + $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); + + $dockerfile = collect([ + '# Just comments', + 'WORKDIR /app', + 'RUN npm install', + ]); + + $result = $job->findFromInstructionLines($dockerfile); + + expect($result)->toBe([]); +}); + +it('inserts ARGs after FROM in simple dockerfile', function () { + $dockerfile = collect([ + 'FROM node:16', + 'WORKDIR /app', + 'COPY . .', + ]); + + $fromLines = [0]; + $argsToInsert = collect(['ARG MY_VAR=value', 'ARG ANOTHER_VAR']); + + foreach (array_reverse($fromLines) as $fromLineIndex) { + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } + + expect($dockerfile[0])->toBe('FROM node:16'); + expect($dockerfile[1])->toBe('ARG MY_VAR=value'); + expect($dockerfile[2])->toBe('ARG ANOTHER_VAR'); + expect($dockerfile[3])->toBe('WORKDIR /app'); +}); + +it('inserts ARGs after each FROM in multi-stage dockerfile', function () { + $dockerfile = collect([ + 'FROM node:16 AS builder', + 'WORKDIR /app', + '', + 'FROM nginx:alpine', + 'COPY --from=builder /app/dist /usr/share/nginx/html', + ]); + + $fromLines = [0, 3]; + $argsToInsert = collect(['ARG MY_VAR=value']); + + foreach (array_reverse($fromLines) as $fromLineIndex) { + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } + + // First stage + expect($dockerfile[0])->toBe('FROM node:16 AS builder'); + expect($dockerfile[1])->toBe('ARG MY_VAR=value'); + expect($dockerfile[2])->toBe('WORKDIR /app'); + + // Second stage (index shifted by +1 due to inserted ARG) + expect($dockerfile[4])->toBe('FROM nginx:alpine'); + expect($dockerfile[5])->toBe('ARG MY_VAR=value'); +}); + +it('inserts ARGs after FROM when comments precede FROM', function () { + $dockerfile = collect([ + '# Build stage comment', + 'FROM node:16', + 'WORKDIR /app', + ]); + + $fromLines = [1]; + $argsToInsert = collect(['ARG MY_VAR=value']); + + foreach (array_reverse($fromLines) as $fromLineIndex) { + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } + + expect($dockerfile[0])->toBe('# Build stage comment'); + expect($dockerfile[1])->toBe('FROM node:16'); + expect($dockerfile[2])->toBe('ARG MY_VAR=value'); + expect($dockerfile[3])->toBe('WORKDIR /app'); +}); + +it('handles real-world nuxt multi-stage dockerfile with comments', function () { + $job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial(); + + $dockerfile = collect([ + '# Build Stage 1', + '', + 'FROM node:22-alpine AS build', + 'WORKDIR /app', + '', + 'RUN corepack enable', + '', + '# Copy package.json and your lockfile, here we add pnpm-lock.yaml for illustration', + 'COPY package.json pnpm-lock.yaml .npmrc ./', + '', + '# Install dependencies', + 'RUN pnpm i', + '', + '# Copy the entire project', + 'COPY . ./', + '', + '# Build the project', + 'RUN pnpm run build', + '', + '# Build Stage 2', + '', + 'FROM node:22-alpine', + 'WORKDIR /app', + '', + '# Only `.output` folder is needed from the build stage', + 'COPY --from=build /app/.output/ ./', + '', + '# Change the port and host', + 'ENV PORT=80', + 'ENV HOST=0.0.0.0', + '', + 'EXPOSE 80', + '', + 'CMD ["node", "/app/server/index.mjs"]', + ]); + + // Find FROM instructions + $fromLines = $job->findFromInstructionLines($dockerfile); + + expect($fromLines)->toBe([2, 21]); + + // Simulate ARG insertion + $argsToInsert = collect(['ARG BUILD_VAR=production']); + + foreach (array_reverse($fromLines) as $fromLineIndex) { + foreach ($argsToInsert->reverse() as $arg) { + $dockerfile->splice($fromLineIndex + 1, 0, [$arg]); + } + } + + // Verify first stage + expect($dockerfile[2])->toBe('FROM node:22-alpine AS build'); + expect($dockerfile[3])->toBe('ARG BUILD_VAR=production'); + expect($dockerfile[4])->toBe('WORKDIR /app'); + + // Verify second stage (index shifted by +1 due to first ARG insertion) + expect($dockerfile[22])->toBe('FROM node:22-alpine'); + expect($dockerfile[23])->toBe('ARG BUILD_VAR=production'); + expect($dockerfile[24])->toBe('WORKDIR /app'); +});