recordedCommands[] = $commands; } } function makeRailpackDeploymentJob(array $applicationAttributes = [], array $savedOutputs = []): array { $job = new TestableRailpackDeploymentJob; $reflection = new ReflectionClass(ApplicationDeploymentJob::class); $application = new Application($applicationAttributes); foreach ([ 'application' => $application, 'workdir' => '/artifacts/test-app', 'deployment_uuid' => 'deployment-uuid', 'saved_outputs' => new Collection($savedOutputs), 'env_railpack_args' => "--env 'RAILPACK_NODE_VERSION=22'", 'force_rebuild' => false, 'addHosts' => '', 'secrets_hash_key' => 'testing-app-key', ] as $property => $value) { $reflectionProperty = $reflection->getProperty($property); $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($job, $value); } return [$job, $reflection]; } function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $method, array $arguments = []): mixed { $reflectionMethod = $reflection->getMethod($method); $reflectionMethod->setAccessible(true); return $reflectionMethod->invokeArgs($job, $arguments); } it('deep merges repository railpack config with coolify overrides', function () { $repositoryConfigJson = json_encode([ '$schema' => 'https://schema.railpack.com', 'packages' => [ 'node' => '20', ], 'steps' => [ 'build' => [ 'inputs' => [['step' => 'install']], 'commands' => ['npm run build'], ], ], 'deploy' => [ 'variables' => [ 'NODE_ENV' => 'production', ], 'startCommand' => 'node index.js', ], ], JSON_THROW_ON_ERROR); [$job, $reflection] = makeRailpackDeploymentJob( [ 'install_command' => 'npm ci', 'build_command' => 'npm run build:prod', 'start_command' => 'node server.js', ], [ 'railpack_config_exists' => 'exists', 'railpack_repository_config' => $repositoryConfigJson, ], ); $repositoryConfig = invokeRailpackMethod( $job, $reflection, 'decode_railpack_config', [$repositoryConfigJson, 'repository railpack.json'], ); $overrides = [ 'deploy' => [ 'variables' => [ 'APP_ENV' => 'production', ], ], 'packages' => [ 'python' => '3.13', ], ]; $generatedConfig = invokeRailpackMethod($job, $reflection, 'merge_railpack_config', [$repositoryConfig, $overrides]); expect($generatedConfig)->toMatchArray([ '$schema' => 'https://schema.railpack.com', 'packages' => [ 'node' => '20', 'python' => '3.13', ], 'steps' => [ 'build' => [ 'inputs' => [['step' => 'install']], 'commands' => ['npm run build'], ], ], 'deploy' => [ 'variables' => [ 'NODE_ENV' => 'production', 'APP_ENV' => 'production', ], 'startCommand' => 'node index.js', ], ]); }); it('writes a generated railpack config file when repository config exists', function () { [$job, $reflection] = makeRailpackDeploymentJob( ['build_command' => 'npm run build'], [ 'railpack_config_exists' => 'exists', 'railpack_repository_config' => json_encode([ '$schema' => 'https://schema.railpack.com', 'steps' => [ 'build' => [ 'commands' => ['npm run build'], ], ], ], JSON_THROW_ON_ERROR), ], ); $configPath = invokeRailpackMethod($job, $reflection, 'generate_railpack_config_file'); expect($configPath)->toBe('.coolify/railpack.generated.json'); expect($job->recordedCommands)->toHaveCount(3); }); it('does not generate a railpack config file for command overrides alone', function () { [$job, $reflection] = makeRailpackDeploymentJob([ 'install_command' => 'npm ci', 'build_command' => 'npm run build', 'start_command' => 'node server.js', ]); $configPath = invokeRailpackMethod($job, $reflection, 'generate_railpack_config_file'); expect($configPath)->toBeNull(); expect($job->recordedCommands)->toHaveCount(1); }); it('fails fast when repository railpack config is invalid json', function () { [$job, $reflection] = makeRailpackDeploymentJob( ['build_command' => 'npm run build'], [ 'railpack_config_exists' => 'exists', 'railpack_repository_config' => '{"steps":{"build":', ], ); expect(fn () => invokeRailpackMethod($job, $reflection, 'generate_railpack_config_file')) ->toThrow(DeploymentException::class, 'Invalid repository railpack.json'); }); it('builds railpack prepare command using railpack env for install and cli flags for build/start overrides', function () { [$job, $reflection] = makeRailpackDeploymentJob( [ 'install_command' => 'npm ci', 'build_command' => 'npm run build', 'start_command' => 'node server.js', ], ); $envRailpackArgsProperty = $reflection->getProperty('env_railpack_args'); $envRailpackArgsProperty->setAccessible(true); $envRailpackArgsProperty->setValue($job, "--env 'RAILPACK_NODE_VERSION=22' --env 'RAILPACK_INSTALL_CMD=npm ci'"); $command = invokeRailpackMethod( $job, $reflection, 'railpack_prepare_command', ['.coolify/railpack.generated.json'], ); expect($command)->toContain('railpack prepare'); expect($command)->toContain("--env 'RAILPACK_NODE_VERSION=22'"); expect($command)->toContain("--env 'RAILPACK_INSTALL_CMD=npm ci'"); expect($command)->toContain('--build-cmd '.escapeshellarg('npm run build')); expect($command)->toContain('--start-cmd '.escapeshellarg('node server.js')); expect($command)->toContain('--config-file '.escapeshellarg('.coolify/railpack.generated.json')); expect($command)->toContain('--plan-out /artifacts/railpack-plan.json /artifacts/test-app'); expect($command)->not->toContain("--env 'RAILPACK_BUILD_CMD="); expect($command)->not->toContain("--env 'RAILPACK_START_CMD="); expect($command)->not->toContain('RAILPACK_BUILD_CMD='); expect($command)->not->toContain('RAILPACK_START_CMD='); }); it('builds railpack docker command with matching env and secret flags for all railpack variables', function () { [$job, $reflection] = makeRailpackDeploymentJob([ 'uuid' => 'application-uuid', ]); $command = invokeRailpackMethod( $job, $reflection, 'railpack_build_command', [ 'coollabsio/coolify:test', collect([ 'RAILPACK_NODE_VERSION' => '22', 'RAILPACK_INSTALL_CMD' => 'npm ci && npm run postinstall', 'SECRET_JSON' => '{"token":"abc"}', ]), ], ); expect($command)->toContain("env RAILPACK_NODE_VERSION='22'"); expect($command)->toContain("RAILPACK_INSTALL_CMD='npm ci && npm run postinstall'"); expect($command)->toContain("SECRET_JSON='{\"token\":\"abc\"}'"); expect($command)->toContain('--secret id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'); expect($command)->toContain('--secret id=RAILPACK_INSTALL_CMD,env=RAILPACK_INSTALL_CMD'); expect($command)->toContain('--secret id=SECRET_JSON,env=SECRET_JSON'); expect($command)->toContain(' --build-arg secrets-hash='); expect($command)->toContain('--build-arg BUILDKIT_SYNTAX="${RAILPACK_FRONTEND_IMAGE}"'); });