feat(railpack): add config merging, beta badge, and nodejs seeder example
- Implement railpack.json + generated config deep merging logic in ApplicationDeploymentJob with JSON validation and assoc array checks - Label Railpack as "Beta" in all build pack selectors and show a visible beta badge when railpack is selected in new-app forms - Add railpack-nodejs Fastify example to ApplicationSeeder - Add ApplicationSeederTest and ApplicationDeploymentRailpackConfigTest covering config merge behavior and seeder correctness
This commit is contained in:
parent
b1740cdc79
commit
d7e1b7ec37
10 changed files with 482 additions and 19 deletions
|
|
@ -33,6 +33,7 @@
|
|||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Sleep;
|
||||
use Illuminate\Support\Str;
|
||||
use JsonException;
|
||||
use Spatie\Url\Url;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Throwable;
|
||||
|
|
@ -48,6 +49,10 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private const NIXPACKS_PLAN_PATH = '/artifacts/thegameplan.json';
|
||||
|
||||
private const RAILPACK_REPOSITORY_CONFIG_PATH = 'railpack.json';
|
||||
|
||||
private const RAILPACK_GENERATED_CONFIG_PATH = '.coolify/railpack.generated.json';
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 3600;
|
||||
|
|
@ -2487,28 +2492,170 @@ private function generate_railpack_env_variables(): void
|
|||
$this->env_railpack_args = $this->env_railpack_args->implode(' ');
|
||||
}
|
||||
|
||||
private function build_railpack_image(): void
|
||||
private function decode_railpack_config(string $config, string $source): array
|
||||
{
|
||||
$this->generate_railpack_env_variables();
|
||||
try {
|
||||
$decoded = json_decode($config, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $exception) {
|
||||
throw new DeploymentException("Invalid {$source}: {$exception->getMessage()}", $exception->getCode(), $exception);
|
||||
}
|
||||
|
||||
// Step 1: Generate build plan with railpack prepare
|
||||
if (! is_array($decoded)) {
|
||||
throw new DeploymentException("Invalid {$source}: expected a JSON object.");
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private function is_assoc_array(array $value): bool
|
||||
{
|
||||
if ($value === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return array_keys($value) !== range(0, count($value) - 1);
|
||||
}
|
||||
|
||||
private function merge_railpack_config(array $base, array $overrides): array
|
||||
{
|
||||
foreach ($overrides as $key => $value) {
|
||||
if (
|
||||
array_key_exists($key, $base)
|
||||
&& is_array($base[$key])
|
||||
&& is_array($value)
|
||||
&& $this->is_assoc_array($base[$key])
|
||||
&& $this->is_assoc_array($value)
|
||||
) {
|
||||
$base[$key] = $this->merge_railpack_config($base[$key], $value);
|
||||
} else {
|
||||
$base[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
private function railpack_config_overrides(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private function railpack_prepare_environment_variables(): Collection
|
||||
{
|
||||
$variables = collect([]);
|
||||
|
||||
if ($this->application->install_command) {
|
||||
$variables->put('RAILPACK_INSTALL_CMD', $this->application->install_command);
|
||||
}
|
||||
|
||||
if ($this->application->build_command) {
|
||||
$variables->put('RAILPACK_BUILD_CMD', $this->application->build_command);
|
||||
}
|
||||
|
||||
if ($this->application->start_command) {
|
||||
$variables->put('RAILPACK_START_CMD', $this->application->start_command);
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
private function generated_railpack_config_relative_path(): string
|
||||
{
|
||||
return self::RAILPACK_GENERATED_CONFIG_PATH;
|
||||
}
|
||||
|
||||
private function generated_railpack_config_absolute_path(): string
|
||||
{
|
||||
return "{$this->workdir}/".self::RAILPACK_GENERATED_CONFIG_PATH;
|
||||
}
|
||||
|
||||
private function generate_railpack_config_file(): ?string
|
||||
{
|
||||
$repositoryConfig = [];
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "test -f {$this->workdir}/".self::RAILPACK_REPOSITORY_CONFIG_PATH." && echo 'exists' || echo 'missing'"),
|
||||
'hidden' => true,
|
||||
'save' => 'railpack_config_exists',
|
||||
]);
|
||||
|
||||
if (str($this->saved_outputs->get('railpack_config_exists'))->trim()->toString() === 'exists') {
|
||||
$this->execute_remote_command([
|
||||
executeInDocker($this->deployment_uuid, "cat {$this->workdir}/".self::RAILPACK_REPOSITORY_CONFIG_PATH),
|
||||
'hidden' => true,
|
||||
'save' => 'railpack_repository_config',
|
||||
]);
|
||||
|
||||
$repositoryConfig = $this->decode_railpack_config(
|
||||
$this->saved_outputs->get('railpack_repository_config', ''),
|
||||
'repository railpack.json'
|
||||
);
|
||||
}
|
||||
|
||||
$overrides = $this->railpack_config_overrides();
|
||||
if ($repositoryConfig === [] && $overrides === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$mergedConfig = $this->merge_railpack_config($repositoryConfig, $overrides);
|
||||
if (! array_key_exists('$schema', $mergedConfig)) {
|
||||
$mergedConfig['$schema'] = 'https://schema.railpack.com';
|
||||
}
|
||||
|
||||
try {
|
||||
$encodedConfig = json_encode($mergedConfig, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
||||
} catch (JsonException $exception) {
|
||||
throw new DeploymentException("Failed to encode generated Railpack config: {$exception->getMessage()}", $exception->getCode(), $exception);
|
||||
}
|
||||
|
||||
$configPath = $this->generated_railpack_config_absolute_path();
|
||||
$encodedConfig = base64_encode($encodedConfig);
|
||||
|
||||
$this->execute_remote_command(
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "mkdir -p {$this->workdir}/.coolify"),
|
||||
'hidden' => true,
|
||||
],
|
||||
[
|
||||
executeInDocker($this->deployment_uuid, "echo '{$encodedConfig}' | base64 -d | tee {$configPath} > /dev/null"),
|
||||
'hidden' => true,
|
||||
]
|
||||
);
|
||||
|
||||
return $this->generated_railpack_config_relative_path();
|
||||
}
|
||||
|
||||
private function railpack_prepare_command(?string $configFilePath = null): string
|
||||
{
|
||||
$prepare_command = 'railpack prepare';
|
||||
$prepareEnvironmentVariables = $this->railpack_prepare_environment_variables()
|
||||
->map(fn ($value, $key) => "{$key}=".escapeShellValue($value))
|
||||
->implode(' ');
|
||||
|
||||
if ($prepareEnvironmentVariables !== '') {
|
||||
$prepare_command = "{$prepareEnvironmentVariables} {$prepare_command}";
|
||||
}
|
||||
|
||||
if ($this->env_railpack_args) {
|
||||
$prepare_command .= " {$this->env_railpack_args}";
|
||||
}
|
||||
if ($this->application->build_command) {
|
||||
$prepare_command .= ' --env '.escapeShellValue("RAILPACK_BUILD_CMD={$this->application->build_command}");
|
||||
}
|
||||
if ($this->application->start_command) {
|
||||
$prepare_command .= ' --env '.escapeShellValue("RAILPACK_START_CMD={$this->application->start_command}");
|
||||
}
|
||||
if ($this->application->install_command) {
|
||||
$prepare_command .= ' --env '.escapeShellValue("RAILPACK_INSTALL_CMD={$this->application->install_command}");
|
||||
|
||||
if ($configFilePath) {
|
||||
$prepare_command .= ' --config-file '.escapeShellValue($configFilePath);
|
||||
}
|
||||
|
||||
$prepare_command .= " --plan-out /artifacts/railpack-plan.json {$this->workdir}";
|
||||
|
||||
return $prepare_command;
|
||||
}
|
||||
|
||||
private function build_railpack_image(): void
|
||||
{
|
||||
$this->generate_railpack_env_variables();
|
||||
$railpackConfigPath = $this->generate_railpack_config_file();
|
||||
|
||||
// Step 1: Generate build plan with railpack prepare
|
||||
$prepare_command = $this->railpack_prepare_command($railpackConfigPath);
|
||||
|
||||
$this->application_deployment_queue->addLogEntry('Generating Railpack build plan.');
|
||||
$this->execute_remote_command(
|
||||
[executeInDocker($this->deployment_uuid, $prepare_command), 'hidden' => true],
|
||||
|
|
|
|||
|
|
@ -47,6 +47,22 @@ public function run(): void
|
|||
'source_id' => 1,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
Application::create([
|
||||
'uuid' => 'railpack-nodejs',
|
||||
'name' => 'Railpack NodeJS Fastify Example',
|
||||
'fqdn' => 'http://railpack-nodejs.127.0.0.1.sslip.io',
|
||||
'repository_project_id' => 603035348,
|
||||
'git_repository' => 'coollabsio/coolify-examples',
|
||||
'git_branch' => 'v4.x',
|
||||
'base_directory' => '/nodejs',
|
||||
'build_pack' => 'railpack',
|
||||
'ports_exposes' => '3000',
|
||||
'environment_id' => 1,
|
||||
'destination_id' => 0,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'source_id' => 1,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
Application::create([
|
||||
'uuid' => 'dockerfile',
|
||||
'name' => 'Dockerfile Example',
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
<x-forms.select x-bind:disabled="shouldDisable()" wire:model.live="buildPack" label="Build Pack"
|
||||
required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
|
@ -236,11 +236,15 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry"
|
|||
<x-forms.input helper="If you modify this, you probably need to have a {{ $buildPack === 'railpack' ? 'railpack.json' : 'nixpacks.toml' }}"
|
||||
id="startCommand" label="Start Command" x-bind:disabled="!canUpdate" />
|
||||
</div>
|
||||
<div class="pt-1 text-xs">{{ $buildPack === 'railpack' ? 'Railpack' : 'Nixpacks' }} will detect the required configuration
|
||||
automatically.
|
||||
@if ($buildPack === 'nixpacks')
|
||||
<div class="pt-1 text-xs">
|
||||
|
||||
<span class="font-medium">Nixpacks</span>
|
||||
will detect the required configuration automatically.
|
||||
<a class="underline" href="https://coolify.io/docs/applications/">Framework
|
||||
Specific Docs</a>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
|
|||
<x-forms.input id="branch" required label="Branch" />
|
||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
|
@ -61,6 +61,14 @@ class="loading loading-xs dark:text-warning loading-spinner"></span>
|
|||
<x-forms.input id="publish_directory" required label="Publish Directory" />
|
||||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'railpack')
|
||||
<div>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@
|
|||
</x-forms.select>
|
||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
|
@ -93,6 +93,14 @@
|
|||
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'railpack')
|
||||
<div>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@
|
|||
@endif
|
||||
<x-forms.select wire:model.live="build_pack" label="Build Pack" required>
|
||||
<option value="nixpacks">Nixpacks</option>
|
||||
<option value="railpack">Railpack</option>
|
||||
<option value="railpack">Railpack (Beta)</option>
|
||||
<option value="static">Static</option>
|
||||
<option value="dockerfile">Dockerfile</option>
|
||||
<option value="dockercompose">Docker Compose</option>
|
||||
|
|
@ -52,6 +52,14 @@
|
|||
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
|
||||
@endif
|
||||
</div>
|
||||
@if ($build_pack === 'railpack')
|
||||
<div>
|
||||
<span
|
||||
class="px-2 py-1 text-xs font-bold uppercase tracking-wide bg-coollabs/10 dark:bg-warning/20 text-coollabs dark:text-warning rounded">
|
||||
Beta
|
||||
</span>
|
||||
</div>
|
||||
@endif
|
||||
@if ($build_pack === 'dockercompose')
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
|
|
|
|||
|
|
@ -63,6 +63,24 @@
|
|||
->assertSuccessful()
|
||||
->assertSeeInOrder([
|
||||
'<option value="nixpacks">Nixpacks</option>',
|
||||
'<option value="railpack">Railpack</option>',
|
||||
'<option value="railpack">Railpack (Beta)</option>',
|
||||
], false);
|
||||
});
|
||||
|
||||
test('existing application shows railpack beta badge in build helper copy', function () {
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $this->environment->id,
|
||||
'destination_id' => $this->destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'build_pack' => 'railpack',
|
||||
'static_image' => 'nginx:alpine',
|
||||
'base_directory' => '/',
|
||||
'is_http_basic_auth_enabled' => false,
|
||||
'redirect' => 'no',
|
||||
]);
|
||||
|
||||
Livewire::test(General::class, ['application' => $application])
|
||||
->assertSuccessful()
|
||||
->assertSee('Railpack')
|
||||
->assertSee('Beta');
|
||||
});
|
||||
|
|
|
|||
51
tests/Feature/ApplicationSeederTest.php
Normal file
51
tests/Feature/ApplicationSeederTest.php
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use Database\Seeders\ApplicationSeeder;
|
||||
use Database\Seeders\GithubAppSeeder;
|
||||
use Database\Seeders\PrivateKeySeeder;
|
||||
use Database\Seeders\ProjectSeeder;
|
||||
use Database\Seeders\ServerSeeder;
|
||||
use Database\Seeders\StandaloneDockerSeeder;
|
||||
use Database\Seeders\TeamSeeder;
|
||||
use Database\Seeders\UserSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('seeds a railpack nodejs fastify example alongside the existing nixpacks example', function () {
|
||||
$this->seed([
|
||||
UserSeeder::class,
|
||||
TeamSeeder::class,
|
||||
PrivateKeySeeder::class,
|
||||
ServerSeeder::class,
|
||||
ProjectSeeder::class,
|
||||
StandaloneDockerSeeder::class,
|
||||
GithubAppSeeder::class,
|
||||
ApplicationSeeder::class,
|
||||
]);
|
||||
|
||||
$nixpacksExample = Application::where('uuid', 'nodejs')->first();
|
||||
$railpackExample = Application::where('uuid', 'railpack-nodejs')->first();
|
||||
|
||||
expect($nixpacksExample)
|
||||
->not->toBeNull()
|
||||
->and($nixpacksExample->name)->toBe('NodeJS Fastify Example')
|
||||
->and($nixpacksExample->build_pack)->toBe('nixpacks')
|
||||
->and($nixpacksExample->base_directory)->toBe('/nodejs')
|
||||
->and($nixpacksExample->ports_exposes)->toBe('3000');
|
||||
|
||||
expect($railpackExample)
|
||||
->not->toBeNull()
|
||||
->and($railpackExample->name)->toBe('Railpack NodeJS Fastify Example')
|
||||
->and($railpackExample->fqdn)->toBe('http://railpack-nodejs.127.0.0.1.sslip.io')
|
||||
->and($railpackExample->repository_project_id)->toBe(603035348)
|
||||
->and($railpackExample->git_repository)->toBe('coollabsio/coolify-examples')
|
||||
->and($railpackExample->git_branch)->toBe('v4.x')
|
||||
->and($railpackExample->base_directory)->toBe('/nodejs')
|
||||
->and($railpackExample->build_pack)->toBe('railpack')
|
||||
->and($railpackExample->ports_exposes)->toBe('3000')
|
||||
->and($railpackExample->environment_id)->toBe(1)
|
||||
->and($railpackExample->destination_id)->toBe(0)
|
||||
->and($railpackExample->source_id)->toBe(1);
|
||||
});
|
||||
|
|
@ -38,6 +38,14 @@
|
|||
test('public repository flow keeps railpack available after branch lookup', function () {
|
||||
Livewire::test(PublicGitRepository::class, ['type' => 'public'])
|
||||
->set('branchFound', true)
|
||||
->assertSeeInOrder(['Nixpacks', 'Railpack']);
|
||||
->assertSeeInOrder(['Nixpacks', 'Railpack (Beta)'])
|
||||
->assertSee('Beta');
|
||||
});
|
||||
|
||||
test('deploy key repository flow shows railpack beta label in build pack selector', function () {
|
||||
Livewire::test(GithubPrivateRepositoryDeployKey::class, ['type' => 'private-deploy-key'])
|
||||
->set('current_step', 'repository')
|
||||
->assertSee('Railpack (Beta)')
|
||||
->assertSee('Beta');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
195
tests/Unit/ApplicationDeploymentRailpackConfigTest.php
Normal file
195
tests/Unit/ApplicationDeploymentRailpackConfigTest.php
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<?php
|
||||
|
||||
use App\Exceptions\DeploymentException;
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class TestableRailpackDeploymentJob extends ApplicationDeploymentJob
|
||||
{
|
||||
public array $recordedCommands = [];
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function execute_remote_command(...$commands)
|
||||
{
|
||||
$this->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'",
|
||||
] 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 process env vars for command overrides', function () {
|
||||
[$job, $reflection] = makeRailpackDeploymentJob(
|
||||
[
|
||||
'install_command' => 'npm ci',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'node server.js',
|
||||
],
|
||||
);
|
||||
|
||||
$command = invokeRailpackMethod(
|
||||
$job,
|
||||
$reflection,
|
||||
'railpack_prepare_command',
|
||||
['.coolify/railpack.generated.json'],
|
||||
);
|
||||
|
||||
expect($command)->toContain("railpack prepare --env 'RAILPACK_NODE_VERSION=22'");
|
||||
expect($command)->toContain('RAILPACK_INSTALL_CMD='.escapeshellarg('npm ci'));
|
||||
expect($command)->toContain('RAILPACK_BUILD_CMD='.escapeshellarg('npm run build'));
|
||||
expect($command)->toContain('RAILPACK_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("--env 'RAILPACK_INSTALL_CMD=");
|
||||
});
|
||||
Loading…
Reference in a new issue