From ab1958d741117a2d22d92272646e2b04a01f3c7b Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 11 May 2026 17:31:29 +0200 Subject: [PATCH] fix(railpack): fail fast when buildx is unavailable Require Docker buildx before Railpack builds, normalize environment variable keys before validation, and align private deploy key API docs with the supported dockerfile build pack. --- .../Controllers/Api/ApplicationsController.php | 2 +- app/Jobs/ApplicationDeploymentJob.php | 18 ++++++++++++++++++ app/Models/EnvironmentVariable.php | 4 +++- openapi.json | 6 +----- openapi.yaml | 2 +- ...ApplicationDeploymentRailpackConfigTest.php | 11 +++++++++++ tests/Unit/ValidationPatternsTest.php | 8 ++++++++ 7 files changed, 43 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 43f114bcf..9919a8054 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -650,7 +650,7 @@ class ApplicationsController extends Controller 'environment_name' => ['type' => 'string', 'description' => 'The environment name. You need to provide at least one of environment_name or environment_uuid.'], 'environment_uuid' => ['type' => 'string', 'description' => 'The environment UUID. You need to provide at least one of environment_name or environment_uuid.'], 'dockerfile' => ['type' => 'string', 'description' => 'The Dockerfile content.'], - 'build_pack' => ['type' => 'string', 'enum' => ['nixpacks', 'railpack', 'static', 'dockerfile', 'dockercompose'], 'description' => 'The build pack type.'], + 'build_pack' => ['type' => 'string', 'enum' => ['dockerfile'], 'description' => 'The build pack type.'], 'ports_exposes' => ['type' => 'string', 'description' => 'The ports to expose.'], 'destination_uuid' => ['type' => 'string', 'description' => 'The destination UUID.'], 'name' => ['type' => 'string', 'description' => 'The application name.'], diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 663499cee..591159c5f 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -181,6 +181,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue private bool $dockerBuildkitSupported = false; + private bool $dockerBuildxAvailable = false; + private bool $dockerSecretsSupported = false; private bool $skip_build = false; @@ -421,6 +423,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if ($majorVersion < 18 || ($majorVersion == 18 && $minorVersion < 9)) { $this->dockerBuildkitSupported = false; + $this->dockerBuildxAvailable = false; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} on {$serverName} does not support BuildKit (requires 18.09+)."); return; @@ -434,8 +437,11 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue if (trim($buildxAvailable) === 'available') { $this->dockerBuildkitSupported = true; + $this->dockerBuildxAvailable = true; $this->application_deployment_queue->addLogEntry("Docker {$dockerVersion} with BuildKit and Buildx detected on {$serverName}."); } else { + $this->dockerBuildxAvailable = false; + // Fallback: test DOCKER_BUILDKIT=1 support via --progress flag $buildkitTest = instant_remote_process( ["DOCKER_BUILDKIT=1 docker build --help 2>&1 | grep -q '\\-\\-progress' && echo 'supported' || echo 'not-supported'"], @@ -468,6 +474,7 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue } } catch (Exception $e) { $this->dockerBuildkitSupported = false; + $this->dockerBuildxAvailable = false; $this->dockerSecretsSupported = false; $this->application_deployment_queue->addLogEntry("Could not detect BuildKit capabilities on {$serverName}: {$e->getMessage()}"); } @@ -2760,8 +2767,19 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue return $prepare_command; } + private function ensure_docker_buildx_available_for_railpack(): void + { + if ($this->dockerBuildxAvailable) { + return; + } + + throw new DeploymentException('Railpack deployments require the Docker buildx CLI plugin on the build server. Install or enable docker buildx and retry the deployment.'); + } + private function build_railpack_image(): void { + $this->ensure_docker_buildx_available_for_railpack(); + $railpackVariables = $this->generate_railpack_env_variables(); $railpackConfigPath = $this->generate_railpack_config_file(); diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index dcbdad253..bfb02a470 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -371,7 +371,9 @@ class EnvironmentVariable extends BaseModel protected function key(): Attribute { return Attribute::make( - set: fn (string $value) => ValidationPatterns::validatedEnvironmentVariableKey($value), + set: fn (string $value) => ValidationPatterns::validatedEnvironmentVariableKey( + ValidationPatterns::normalizeEnvironmentVariableKey($value) + ), ); } diff --git a/openapi.json b/openapi.json index 1e9fc4170..25aada1e1 100644 --- a/openapi.json +++ b/openapi.json @@ -1451,11 +1451,7 @@ "build_pack": { "type": "string", "enum": [ - "nixpacks", - "railpack", - "static", - "dockerfile", - "dockercompose" + "dockerfile" ], "description": "The build pack type." }, diff --git a/openapi.yaml b/openapi.yaml index 3e652fa7b..4597b06f7 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -935,7 +935,7 @@ paths: description: 'The Dockerfile content.' build_pack: type: string - enum: [nixpacks, railpack, static, dockerfile, dockercompose] + enum: [dockerfile] description: 'The build pack type.' ports_exposes: type: string diff --git a/tests/Unit/ApplicationDeploymentRailpackConfigTest.php b/tests/Unit/ApplicationDeploymentRailpackConfigTest.php index 15bee488c..e1449a59e 100644 --- a/tests/Unit/ApplicationDeploymentRailpackConfigTest.php +++ b/tests/Unit/ApplicationDeploymentRailpackConfigTest.php @@ -205,6 +205,17 @@ it('builds railpack prepare command using railpack env for install and cli flags expect($command)->not->toContain('RAILPACK_START_CMD='); }); +it('fails fast when docker buildx is unavailable for railpack builds', function () { + [$job, $reflection] = makeRailpackDeploymentJob(); + + $dockerBuildxAvailableProperty = $reflection->getProperty('dockerBuildxAvailable'); + $dockerBuildxAvailableProperty->setAccessible(true); + $dockerBuildxAvailableProperty->setValue($job, false); + + expect(fn () => invokeRailpackMethod($job, $reflection, 'ensure_docker_buildx_available_for_railpack')) + ->toThrow(DeploymentException::class, 'Railpack deployments require the Docker buildx CLI plugin'); +}); + it('builds railpack docker command with matching env and secret flags for all railpack variables', function () { [$job, $reflection] = makeRailpackDeploymentJob([ 'uuid' => 'application-uuid', diff --git a/tests/Unit/ValidationPatternsTest.php b/tests/Unit/ValidationPatternsTest.php index 092c37b25..a959b18d5 100644 --- a/tests/Unit/ValidationPatternsTest.php +++ b/tests/Unit/ValidationPatternsTest.php @@ -1,5 +1,6 @@ toBe('node.name'); }); + +it('normalizes environment variable keys before model validation', function () { + $environmentVariable = new EnvironmentVariable; + $environmentVariable->key = ' APP_ENV '; + + expect($environmentVariable->key)->toBe('APP_ENV'); +});