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 @@ public function create_private_deploy_key_application(Request $request) '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 @@ private function detectBuildKitCapabilities(): void 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 @@ private function detectBuildKitCapabilities(): void 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 @@ private function detectBuildKitCapabilities(): void } } 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 @@ private function railpack_prepare_command(?string $configFilePath = null): strin 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 @@ private function set_environment_variables(?string $environment_variable = null) 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 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $ 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'); +});