feat(railpack): add buildpack control var filtering and dev seeder
Extract NIXPACKS_/RAILPACK_ prefix filtering into a reusable `scopeWithoutBuildpackControlVariables` query scope on EnvironmentVariable. Apply scope consistently to runtime vars, runtime preview vars, and buildtime var generation in ApplicationDeploymentJob. Refactor `generate_railpack_env_variables` to return a Collection. Add `RAILPACK_FRONTEND_IMAGE` constant and bake it into the coolify-helper Dockerfile as a build arg. Add DevelopmentRailpackExamplesSeeder (dev/local env only) for seeding example Railpack apps, wired into DatabaseSeeder. Add tests: - ApplicationDeploymentControlVarFilteringTest: verifies control vars are excluded from runtime and buildtime envs - DevelopmentRailpackExamplesSeederTest: verifies seeder behavior - ApplicationDeploymentRailpackEnvParityTest: parity checks for env handling across build/runtime paths
This commit is contained in:
parent
5cef7cc092
commit
b3339d1034
12 changed files with 1177 additions and 77 deletions
|
|
@ -53,6 +53,8 @@ class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
|||
|
||||
private const RAILPACK_GENERATED_CONFIG_PATH = '.coolify/railpack.generated.json';
|
||||
|
||||
private const RAILPACK_FRONTEND_IMAGE_ENV = '${RAILPACK_FRONTEND_IMAGE}';
|
||||
|
||||
public $tries = 1;
|
||||
|
||||
public $timeout = 3600;
|
||||
|
|
@ -1259,11 +1261,11 @@ private function generate_runtime_environment_variables()
|
|||
$envs = collect([]);
|
||||
$sort = $this->application->settings->is_env_sorting_enabled;
|
||||
if ($sort) {
|
||||
$sorted_environment_variables = $this->application->environment_variables->sortBy('key');
|
||||
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('key');
|
||||
$sorted_environment_variables = $this->application->runtime_environment_variables->sortBy('key');
|
||||
$sorted_environment_variables_preview = $this->application->runtime_environment_variables_preview->sortBy('key');
|
||||
} else {
|
||||
$sorted_environment_variables = $this->application->environment_variables->sortBy('id');
|
||||
$sorted_environment_variables_preview = $this->application->environment_variables_preview->sortBy('id');
|
||||
$sorted_environment_variables = $this->application->runtime_environment_variables->sortBy('id');
|
||||
$sorted_environment_variables_preview = $this->application->runtime_environment_variables_preview->sortBy('id');
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$sorted_environment_variables = $sorted_environment_variables->filter(function ($env) {
|
||||
|
|
@ -1634,6 +1636,7 @@ private function generate_buildtime_environment_variables()
|
|||
// 4. Add user-defined build-time variables LAST (highest priority - can override everything)
|
||||
if ($this->pull_request_id === 0) {
|
||||
$sorted_environment_variables = $this->application->environment_variables()
|
||||
->withoutBuildpackControlVariables()
|
||||
->where('is_buildtime', true) // ONLY build-time variables
|
||||
->orderBy($this->application->settings->is_env_sorting_enabled ? 'key' : 'id')
|
||||
->get();
|
||||
|
|
@ -1686,6 +1689,7 @@ private function generate_buildtime_environment_variables()
|
|||
}
|
||||
} else {
|
||||
$sorted_environment_variables = $this->application->environment_variables_preview()
|
||||
->withoutBuildpackControlVariables()
|
||||
->where('is_buildtime', true) // ONLY build-time variables
|
||||
->orderBy($this->application->settings->is_env_sorting_enabled ? 'key' : 'id')
|
||||
->get();
|
||||
|
|
@ -2468,28 +2472,114 @@ private function generate_nixpacks_env_variables()
|
|||
$this->env_nixpacks_args = $this->env_nixpacks_args->implode(' ');
|
||||
}
|
||||
|
||||
private function generate_railpack_env_variables(): void
|
||||
private function generate_railpack_env_variables(): Collection
|
||||
{
|
||||
$variables = $this->railpack_build_variables();
|
||||
|
||||
$this->env_railpack_args = $variables
|
||||
->map(function ($value, $key) {
|
||||
return '--env '.escapeShellValue("{$key}={$value}");
|
||||
})
|
||||
->implode(' ');
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
private function railpack_environment_variables_collection(): Collection
|
||||
{
|
||||
$this->env_railpack_args = collect([]);
|
||||
if ($this->pull_request_id === 0) {
|
||||
foreach ($this->application->railpack_environment_variables as $env) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_railpack_args->push("--env {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($this->application->railpack_environment_variables_preview as $env) {
|
||||
if (! is_null($env->real_value) && $env->real_value !== '') {
|
||||
$this->env_railpack_args->push("--env {$env->key}={$env->real_value}");
|
||||
}
|
||||
}
|
||||
return $this->application->railpack_environment_variables;
|
||||
}
|
||||
|
||||
// Note: COOLIFY_* vars are NOT passed to railpack prepare because railpack treats
|
||||
// all --env vars as secrets that must be provided during docker buildx build.
|
||||
// COOLIFY_* vars are informational and available at runtime via .env file.
|
||||
return $this->application->railpack_environment_variables_preview;
|
||||
}
|
||||
|
||||
$this->env_railpack_args = $this->env_railpack_args->implode(' ');
|
||||
private function normalize_resolved_build_variable_value(EnvironmentVariable $environmentVariable): ?string
|
||||
{
|
||||
$resolvedValue = $environmentVariable->getResolvedValueWithServer($this->mainServer);
|
||||
if (is_null($resolvedValue) || $resolvedValue === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($environmentVariable->is_literal || $environmentVariable->is_multiline) {
|
||||
return trim($resolvedValue, "'");
|
||||
}
|
||||
|
||||
return $resolvedValue;
|
||||
}
|
||||
|
||||
private function railpack_build_variables(): Collection
|
||||
{
|
||||
$variables = $this->railpack_environment_variables_collection()
|
||||
->mapWithKeys(function (EnvironmentVariable $environmentVariable) {
|
||||
$value = $this->normalize_resolved_build_variable_value($environmentVariable);
|
||||
if (is_null($value) || $value === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [$environmentVariable->key => $value];
|
||||
});
|
||||
|
||||
if ($this->application->install_command) {
|
||||
$variables->put('RAILPACK_INSTALL_CMD', $this->application->install_command);
|
||||
}
|
||||
|
||||
return $variables;
|
||||
}
|
||||
|
||||
private function railpack_build_environment_prefix(Collection $variables): string
|
||||
{
|
||||
if ($variables->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return 'env '.$variables
|
||||
->map(function ($value, $key) {
|
||||
return "{$key}=".escapeShellValue($value);
|
||||
})
|
||||
->implode(' ').' ';
|
||||
}
|
||||
|
||||
private function railpack_build_secret_flags(Collection $variables): string
|
||||
{
|
||||
if ($variables->isEmpty()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ' '.$variables
|
||||
->map(function ($value, $key) {
|
||||
return "--secret id={$key},env={$key}";
|
||||
})
|
||||
->implode(' ');
|
||||
}
|
||||
|
||||
private function railpack_build_command(string $imageName, Collection $variables): string
|
||||
{
|
||||
$cacheArgs = '';
|
||||
if ($this->force_rebuild) {
|
||||
$cacheArgs = '--no-cache';
|
||||
} else {
|
||||
$cacheArgs = "--build-arg cache-key='{$this->application->uuid}'";
|
||||
}
|
||||
|
||||
if ($variables->isNotEmpty()) {
|
||||
$cacheArgs .= ' --build-arg secrets-hash='.$this->generate_secrets_hash($variables);
|
||||
}
|
||||
|
||||
$environmentPrefix = $this->railpack_build_environment_prefix($variables);
|
||||
$secretFlags = $this->railpack_build_secret_flags($variables);
|
||||
|
||||
return 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
|
||||
." && {$environmentPrefix}docker buildx build --builder coolify-railpack"
|
||||
." {$this->addHosts} --network host"
|
||||
.' --build-arg BUILDKIT_SYNTAX="'.self::RAILPACK_FRONTEND_IMAGE_ENV.'"'
|
||||
." {$cacheArgs}"
|
||||
."{$secretFlags}"
|
||||
.' -f /artifacts/railpack-plan.json'
|
||||
.' --progress plain'
|
||||
.' --load'
|
||||
." -t {$imageName}"
|
||||
." {$this->workdir}";
|
||||
}
|
||||
|
||||
private function decode_railpack_config(string $config, string $source): array
|
||||
|
|
@ -2609,10 +2699,6 @@ private function railpack_prepare_command(?string $configFilePath = null): strin
|
|||
{
|
||||
$prepare_command = 'railpack prepare';
|
||||
|
||||
if ($this->application->install_command) {
|
||||
$prepare_command .= ' --env '.escapeShellValue("RAILPACK_INSTALL_CMD={$this->application->install_command}");
|
||||
}
|
||||
|
||||
if ($this->application->build_command) {
|
||||
$prepare_command .= ' --build-cmd '.escapeShellValue($this->application->build_command);
|
||||
}
|
||||
|
|
@ -2636,7 +2722,7 @@ private function railpack_prepare_command(?string $configFilePath = null): strin
|
|||
|
||||
private function build_railpack_image(): void
|
||||
{
|
||||
$this->generate_railpack_env_variables();
|
||||
$railpackVariables = $this->generate_railpack_env_variables();
|
||||
$railpackConfigPath = $this->generate_railpack_config_file();
|
||||
|
||||
// Step 1: Generate build plan with railpack prepare
|
||||
|
|
@ -2660,34 +2746,7 @@ private function build_railpack_image(): void
|
|||
$this->pull_latest_image($this->application->static_image);
|
||||
}
|
||||
|
||||
$cache_args = '';
|
||||
if ($this->force_rebuild) {
|
||||
$cache_args = '--no-cache';
|
||||
} else {
|
||||
$cache_args = "--build-arg cache-key='{$this->application->uuid}'";
|
||||
}
|
||||
|
||||
$installCommandEnv = '';
|
||||
$installCommandSecret = '';
|
||||
if ($this->application->install_command) {
|
||||
$installCommandEnv = 'env RAILPACK_INSTALL_CMD='.escapeShellValue($this->application->install_command).' ';
|
||||
$installCommandSecret = ' --secret id=RAILPACK_INSTALL_CMD,env=RAILPACK_INSTALL_CMD';
|
||||
$cache_args .= ' --build-arg secrets-hash='.$this->generate_secrets_hash(collect([
|
||||
'RAILPACK_INSTALL_CMD' => $this->application->install_command,
|
||||
]));
|
||||
}
|
||||
|
||||
$build_command = 'docker buildx create --name coolify-railpack --driver docker-container 2>/dev/null || true'
|
||||
." && {$installCommandEnv}docker buildx build --builder coolify-railpack"
|
||||
." {$this->addHosts} --network host"
|
||||
." --build-arg BUILDKIT_SYNTAX='ghcr.io/railwayapp/railpack-frontend'"
|
||||
." {$cache_args}"
|
||||
."{$installCommandSecret}"
|
||||
.' -f /artifacts/railpack-plan.json'
|
||||
.' --progress plain'
|
||||
.' --load'
|
||||
." -t {$image_name}"
|
||||
." {$this->workdir}";
|
||||
$build_command = $this->railpack_build_command($image_name, $railpackVariables);
|
||||
|
||||
$base64_build_command = base64_encode($build_command);
|
||||
$this->execute_remote_command(
|
||||
|
|
@ -2859,7 +2918,7 @@ private function generate_env_variables()
|
|||
// For build process, include only environment variables where is_buildtime = true
|
||||
if ($this->pull_request_id === 0) {
|
||||
$envs = $this->application->environment_variables()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->withoutBuildpackControlVariables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
|
|
@ -2871,7 +2930,7 @@ private function generate_env_variables()
|
|||
}
|
||||
} else {
|
||||
$envs = $this->application->environment_variables_preview()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->withoutBuildpackControlVariables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
|
||||
|
|
@ -3951,7 +4010,7 @@ private function add_build_env_variables_to_dockerfile()
|
|||
if ($this->pull_request_id === 0) {
|
||||
// Only add environment variables that are available during build
|
||||
$envs = $this->application->environment_variables()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->withoutBuildpackControlVariables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
foreach ($envs as $env) {
|
||||
|
|
@ -3973,7 +4032,7 @@ private function add_build_env_variables_to_dockerfile()
|
|||
} else {
|
||||
// Only add preview environment variables that are available during build
|
||||
$envs = $this->application->environment_variables_preview()
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->withoutBuildpackControlVariables()
|
||||
->where('is_buildtime', true)
|
||||
->get();
|
||||
foreach ($envs as $env) {
|
||||
|
|
|
|||
|
|
@ -960,8 +960,7 @@ public function runtime_environment_variables()
|
|||
{
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
||||
->where('is_preview', false)
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('key', 'not like', 'RAILPACK_%');
|
||||
->withoutBuildpackControlVariables();
|
||||
}
|
||||
|
||||
public function nixpacks_environment_variables()
|
||||
|
|
@ -996,8 +995,7 @@ public function runtime_environment_variables_preview()
|
|||
{
|
||||
return $this->morphMany(EnvironmentVariable::class, 'resourceable')
|
||||
->where('is_preview', true)
|
||||
->where('key', 'not like', 'NIXPACKS_%')
|
||||
->where('key', 'not like', 'RAILPACK_%');
|
||||
->withoutBuildpackControlVariables();
|
||||
}
|
||||
|
||||
public function nixpacks_environment_variables_preview()
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use OpenApi\Attributes as OA;
|
||||
|
||||
|
|
@ -32,6 +33,8 @@
|
|||
)]
|
||||
class EnvironmentVariable extends BaseModel
|
||||
{
|
||||
public const BUILDPACK_CONTROL_VARIABLE_PREFIXES = ['NIXPACKS_', 'RAILPACK_'];
|
||||
|
||||
protected $attributes = [
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
|
|
@ -78,7 +81,7 @@ class EnvironmentVariable extends BaseModel
|
|||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (EnvironmentVariable $environment_variable) {
|
||||
static::created(function (ModelsEnvironmentVariable $environment_variable) {
|
||||
if ($environment_variable->resourceable_type === Application::class && ! $environment_variable->is_preview) {
|
||||
$found = ModelsEnvironmentVariable::where('key', $environment_variable->key)
|
||||
->where('resourceable_type', Application::class)
|
||||
|
|
@ -109,7 +112,7 @@ protected static function booted()
|
|||
]);
|
||||
});
|
||||
|
||||
static::saving(function (EnvironmentVariable $environmentVariable) {
|
||||
static::saving(function (ModelsEnvironmentVariable $environmentVariable) {
|
||||
$environmentVariable->updateIsShared();
|
||||
});
|
||||
}
|
||||
|
|
@ -119,6 +122,30 @@ public function service()
|
|||
return $this->belongsTo(Service::class);
|
||||
}
|
||||
|
||||
public function scopeWithoutBuildpackControlVariables(Builder $query): Builder
|
||||
{
|
||||
foreach (self::BUILDPACK_CONTROL_VARIABLE_PREFIXES as $prefix) {
|
||||
$query->where('key', 'not like', "{$prefix}%");
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
public static function isBuildpackControlKey(?string $key): bool
|
||||
{
|
||||
if (blank($key)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (self::BUILDPACK_CONTROL_VARIABLE_PREFIXES as $prefix) {
|
||||
if (str($key)->startsWith($prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function value(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
|
|
|
|||
|
|
@ -31,5 +31,11 @@ public function run(): void
|
|||
CaSslCertSeeder::class,
|
||||
PersonalAccessTokenSeeder::class,
|
||||
]);
|
||||
|
||||
if (in_array(config('app.env'), ['local', 'development', 'dev'], true)) {
|
||||
$this->call([
|
||||
DevelopmentRailpackExamplesSeeder::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
441
database/seeders/DevelopmentRailpackExamplesSeeder.php
Normal file
441
database/seeders/DevelopmentRailpackExamplesSeeder.php
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Enums\ProxyStatus;
|
||||
use App\Enums\ProxyTypes;
|
||||
use App\Models\Application;
|
||||
use App\Models\Environment;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Database\Seeder;
|
||||
use RuntimeException;
|
||||
|
||||
class DevelopmentRailpackExamplesSeeder extends Seeder
|
||||
{
|
||||
public const PROJECT_UUID = 'railpack-examples';
|
||||
|
||||
public const ENVIRONMENT_UUID = 'railpack-examples-production';
|
||||
|
||||
public const GIT_REPOSITORY = 'coollabsio/coolify-examples';
|
||||
|
||||
public const GIT_BRANCH = 'next';
|
||||
|
||||
public const REPOSITORY_PROJECT_ID = 603035348;
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
if (! $this->isDevelopmentEnvironment()) {
|
||||
$this->command?->warn('Skipping DevelopmentRailpackExamplesSeeder outside development mode.');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->ensureDevelopmentPrerequisitesExist();
|
||||
$destination = StandaloneDocker::query()->find(0);
|
||||
|
||||
if (! $destination) {
|
||||
throw new RuntimeException('StandaloneDocker with id=0 is required before running DevelopmentRailpackExamplesSeeder.');
|
||||
}
|
||||
|
||||
$environment = $this->prepareEnvironment();
|
||||
|
||||
foreach (self::examples() as $example) {
|
||||
$this->upsertApplication($environment, $destination, $example);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function examples(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'uuid' => 'railpack-simple-webserver',
|
||||
'name' => 'Railpack Simple Webserver Example',
|
||||
'base_directory' => '/node/simple-webserver',
|
||||
'ports_exposes' => '3000',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-expressjs',
|
||||
'name' => 'Railpack Express.js Example',
|
||||
'base_directory' => '/node/expressjs',
|
||||
'ports_exposes' => '3000',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-fastify',
|
||||
'name' => 'Railpack Fastify Example',
|
||||
'base_directory' => '/node/fastify',
|
||||
'ports_exposes' => '3000',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-nestjs',
|
||||
'name' => 'Railpack NestJS Example',
|
||||
'base_directory' => '/node/nestjs',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start:prod',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-adonisjs',
|
||||
'name' => 'Railpack AdonisJS Example',
|
||||
'base_directory' => '/node/adonisjs',
|
||||
'ports_exposes' => '3333',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-hono',
|
||||
'name' => 'Railpack Hono Example',
|
||||
'base_directory' => '/node/hono',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-koa',
|
||||
'name' => 'Railpack Koa Example',
|
||||
'base_directory' => '/node/koa',
|
||||
'ports_exposes' => '3000',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-nextjs-ssr',
|
||||
'name' => 'Railpack Next.js SSR Example',
|
||||
'base_directory' => '/node/nextjs/ssr',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-nuxtjs-ssr',
|
||||
'name' => 'Railpack NuxtJS SSR Example',
|
||||
'base_directory' => '/node/nuxtjs/ssr',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run preview -- --host 0.0.0.0 --port 3000',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-astro-ssr',
|
||||
'name' => 'Railpack Astro SSR Example',
|
||||
'base_directory' => '/node/astro/ssr',
|
||||
'ports_exposes' => '4321',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-sveltekit-ssr',
|
||||
'name' => 'Railpack SvelteKit SSR Example',
|
||||
'base_directory' => '/node/sveltekit/ssr',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-tanstack-start-ssr',
|
||||
'name' => 'Railpack TanStack Start SSR Example',
|
||||
'base_directory' => '/node/tanstack-start/ssr',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-angular-ssr',
|
||||
'name' => 'Railpack Angular SSR Example',
|
||||
'base_directory' => '/node/angular/ssr',
|
||||
'ports_exposes' => '4000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-vue-ssr',
|
||||
'name' => 'Railpack Vue SSR Example',
|
||||
'base_directory' => '/node/vue/ssr',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run start',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-qwik-ssr',
|
||||
'name' => 'Railpack Qwik SSR Example',
|
||||
'base_directory' => '/node/qwik/ssr',
|
||||
'ports_exposes' => '3000',
|
||||
'build_command' => 'npm run build',
|
||||
'start_command' => 'npm run serve',
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-react-static',
|
||||
'name' => 'Railpack React Static Example',
|
||||
'base_directory' => '/node/react',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/dist',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-vite-static',
|
||||
'name' => 'Railpack Vite Static Example',
|
||||
'base_directory' => '/node/vite',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/dist',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-eleventy-static',
|
||||
'name' => 'Railpack Eleventy Static Example',
|
||||
'base_directory' => '/node/eleventy',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/_site',
|
||||
'is_static' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-gatsby-static',
|
||||
'name' => 'Railpack Gatsby Static Example',
|
||||
'base_directory' => '/node/gatsby',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/public',
|
||||
'is_static' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-nextjs-static',
|
||||
'name' => 'Railpack Next.js Static Example',
|
||||
'base_directory' => '/node/nextjs/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/out',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-nuxtjs-static',
|
||||
'name' => 'Railpack NuxtJS Static Example',
|
||||
'base_directory' => '/node/nuxtjs/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/.output/public',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-astro-static',
|
||||
'name' => 'Railpack Astro Static Example',
|
||||
'base_directory' => '/node/astro/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/dist',
|
||||
'is_static' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-sveltekit-static',
|
||||
'name' => 'Railpack SvelteKit Static Example',
|
||||
'base_directory' => '/node/sveltekit/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/build',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-tanstack-start-static',
|
||||
'name' => 'Railpack TanStack Start Static Example',
|
||||
'base_directory' => '/node/tanstack-start/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/.output/public',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-angular-static',
|
||||
'name' => 'Railpack Angular Static Example',
|
||||
'base_directory' => '/node/angular/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/dist/static/browser',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-vue-static',
|
||||
'name' => 'Railpack Vue Static Example',
|
||||
'base_directory' => '/node/vue/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/dist',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
[
|
||||
'uuid' => 'railpack-qwik-static',
|
||||
'name' => 'Railpack Qwik Static Example',
|
||||
'base_directory' => '/node/qwik/static',
|
||||
'ports_exposes' => '80',
|
||||
'build_command' => 'npm run build',
|
||||
'publish_directory' => '/dist',
|
||||
'is_static' => true,
|
||||
'is_spa' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function ensureDevelopmentPrerequisitesExist(): void
|
||||
{
|
||||
Team::query()->firstOrCreate(
|
||||
['id' => 0],
|
||||
[
|
||||
'name' => 'Root Team',
|
||||
'description' => 'The root team',
|
||||
'personal_team' => true,
|
||||
],
|
||||
);
|
||||
|
||||
PrivateKey::query()->firstOrCreate(
|
||||
['id' => 1],
|
||||
[
|
||||
'uuid' => 'ssh',
|
||||
'team_id' => 0,
|
||||
'name' => 'Testing Host Key',
|
||||
'description' => 'This is a test docker container',
|
||||
'private_key' => <<<'KEY'
|
||||
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||
QyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevAAAAJi/QySHv0Mk
|
||||
hwAAAAtzc2gtZWQyNTUxOQAAACBbhpqHhqv6aI67Mj9abM3DVbmcfYhZAhC7ca4d9UCevA
|
||||
AAAECBQw4jg1WRT2IGHMncCiZhURCts2s24HoDS0thHnnRKVuGmoeGq/pojrsyP1pszcNV
|
||||
uZx9iFkCELtxrh31QJ68AAAAEXNhaWxANzZmZjY2ZDJlMmRkAQIDBA==
|
||||
-----END OPENSSH PRIVATE KEY-----
|
||||
KEY,
|
||||
],
|
||||
);
|
||||
|
||||
Server::query()->firstOrCreate(
|
||||
['id' => 0],
|
||||
[
|
||||
'uuid' => 'localhost',
|
||||
'name' => 'localhost',
|
||||
'description' => 'This is a test docker container in development mode',
|
||||
'ip' => 'coolify-testing-host',
|
||||
'team_id' => 0,
|
||||
'private_key_id' => 1,
|
||||
'proxy' => [
|
||||
'type' => ProxyTypes::TRAEFIK->value,
|
||||
'status' => ProxyStatus::EXITED->value,
|
||||
],
|
||||
],
|
||||
);
|
||||
|
||||
StandaloneDocker::query()->firstOrCreate(
|
||||
['id' => 0],
|
||||
[
|
||||
'uuid' => 'docker',
|
||||
'name' => 'Standalone Docker 1',
|
||||
'network' => 'coolify',
|
||||
'server_id' => 0,
|
||||
],
|
||||
);
|
||||
|
||||
$this->ensurePublicGithubSourceExists();
|
||||
}
|
||||
|
||||
private function ensurePublicGithubSourceExists(): void
|
||||
{
|
||||
GithubApp::query()->firstOrCreate(
|
||||
['id' => 0],
|
||||
[
|
||||
'uuid' => 'github-public',
|
||||
'name' => 'Public GitHub',
|
||||
'api_url' => 'https://api.github.com',
|
||||
'html_url' => 'https://github.com',
|
||||
'is_public' => true,
|
||||
'team_id' => 0,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function isDevelopmentEnvironment(): bool
|
||||
{
|
||||
return in_array(config('app.env'), ['local', 'development', 'dev'], true);
|
||||
}
|
||||
|
||||
private function prepareEnvironment(): Environment
|
||||
{
|
||||
$project = Project::query()->firstOrNew(['uuid' => self::PROJECT_UUID]);
|
||||
$project->fill([
|
||||
'name' => 'Railpack Examples',
|
||||
'description' => 'Development-only Railpack examples from coollabsio/coolify-examples@next.',
|
||||
'team_id' => 0,
|
||||
]);
|
||||
$project->save();
|
||||
|
||||
$environment = $project->environments()->first();
|
||||
|
||||
if (! $environment) {
|
||||
$environment = $project->environments()->create([
|
||||
'name' => 'production',
|
||||
'uuid' => self::ENVIRONMENT_UUID,
|
||||
]);
|
||||
} else {
|
||||
$environment->update([
|
||||
'name' => 'production',
|
||||
'uuid' => self::ENVIRONMENT_UUID,
|
||||
]);
|
||||
}
|
||||
|
||||
return $environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $example
|
||||
*/
|
||||
private function upsertApplication(Environment $environment, StandaloneDocker $destination, array $example): void
|
||||
{
|
||||
$application = Application::withTrashed()->firstOrNew(['uuid' => $example['uuid']]);
|
||||
$application->fill([
|
||||
'name' => $example['name'],
|
||||
'description' => $example['name'],
|
||||
'fqdn' => "http://{$example['uuid']}.127.0.0.1.sslip.io",
|
||||
'repository_project_id' => self::REPOSITORY_PROJECT_ID,
|
||||
'git_repository' => self::GIT_REPOSITORY,
|
||||
'git_branch' => self::GIT_BRANCH,
|
||||
'build_pack' => 'railpack',
|
||||
'ports_exposes' => $example['ports_exposes'],
|
||||
'base_directory' => $example['base_directory'],
|
||||
'publish_directory' => $example['publish_directory'] ?? null,
|
||||
'static_image' => 'nginx:alpine',
|
||||
'install_command' => $example['install_command'] ?? null,
|
||||
'build_command' => $example['build_command'] ?? null,
|
||||
'start_command' => $example['start_command'] ?? null,
|
||||
'environment_id' => $environment->id,
|
||||
'destination_id' => $destination->id,
|
||||
'destination_type' => StandaloneDocker::class,
|
||||
'source_id' => 0,
|
||||
'source_type' => GithubApp::class,
|
||||
]);
|
||||
$application->save();
|
||||
|
||||
if ($application->trashed()) {
|
||||
$application->restore();
|
||||
}
|
||||
|
||||
$application->settings()->updateOrCreate(
|
||||
['application_id' => $application->id],
|
||||
[
|
||||
'is_static' => $example['is_static'] ?? false,
|
||||
'is_spa' => $example['is_spa'] ?? false,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,8 @@ ARG MISE_VERSION
|
|||
|
||||
USER root
|
||||
WORKDIR /artifacts
|
||||
ENV RAILPACK_VERSION=${RAILPACK_VERSION}
|
||||
ENV RAILPACK_FRONTEND_IMAGE=ghcr.io/railwayapp/railpack-frontend:v${RAILPACK_VERSION}
|
||||
RUN apk upgrade --no-cache && \
|
||||
apk add --no-cache bash curl git git-lfs openssh-client tar tini
|
||||
RUN mkdir -p ~/.docker/cli-plugins
|
||||
|
|
|
|||
12
openapi.json
12
openapi.json
|
|
@ -4386,8 +4386,8 @@
|
|||
"description": "Number of days to retain backups locally"
|
||||
},
|
||||
"database_backup_retention_max_storage_locally": {
|
||||
"type": "integer",
|
||||
"description": "Max storage (MB) for local backups"
|
||||
"type": "number",
|
||||
"description": "Max storage (GB) for local backups"
|
||||
},
|
||||
"database_backup_retention_amount_s3": {
|
||||
"type": "integer",
|
||||
|
|
@ -4398,8 +4398,8 @@
|
|||
"description": "Number of days to retain backups in S3"
|
||||
},
|
||||
"database_backup_retention_max_storage_s3": {
|
||||
"type": "integer",
|
||||
"description": "Max storage (MB) for S3 backups"
|
||||
"type": "number",
|
||||
"description": "Max storage (GB) for S3 backups"
|
||||
},
|
||||
"timeout": {
|
||||
"type": "integer",
|
||||
|
|
@ -4956,7 +4956,7 @@
|
|||
"description": "Retention days of the backup locally"
|
||||
},
|
||||
"database_backup_retention_max_storage_locally": {
|
||||
"type": "integer",
|
||||
"type": "number",
|
||||
"description": "Max storage of the backup locally"
|
||||
},
|
||||
"database_backup_retention_amount_s3": {
|
||||
|
|
@ -4968,7 +4968,7 @@
|
|||
"description": "Retention days of the backup in s3"
|
||||
},
|
||||
"database_backup_retention_max_storage_s3": {
|
||||
"type": "integer",
|
||||
"type": "number",
|
||||
"description": "Max storage of the backup in S3"
|
||||
},
|
||||
"timeout": {
|
||||
|
|
|
|||
12
openapi.yaml
12
openapi.yaml
|
|
@ -2765,8 +2765,8 @@ paths:
|
|||
type: integer
|
||||
description: 'Number of days to retain backups locally'
|
||||
database_backup_retention_max_storage_locally:
|
||||
type: integer
|
||||
description: 'Max storage (MB) for local backups'
|
||||
type: number
|
||||
description: 'Max storage (GB) for local backups'
|
||||
database_backup_retention_amount_s3:
|
||||
type: integer
|
||||
description: 'Number of backups to retain in S3'
|
||||
|
|
@ -2774,8 +2774,8 @@ paths:
|
|||
type: integer
|
||||
description: 'Number of days to retain backups in S3'
|
||||
database_backup_retention_max_storage_s3:
|
||||
type: integer
|
||||
description: 'Max storage (MB) for S3 backups'
|
||||
type: number
|
||||
description: 'Max storage (GB) for S3 backups'
|
||||
timeout:
|
||||
type: integer
|
||||
description: 'Backup job timeout in seconds (min: 60, max: 36000)'
|
||||
|
|
@ -3160,7 +3160,7 @@ paths:
|
|||
type: integer
|
||||
description: 'Retention days of the backup locally'
|
||||
database_backup_retention_max_storage_locally:
|
||||
type: integer
|
||||
type: number
|
||||
description: 'Max storage of the backup locally'
|
||||
database_backup_retention_amount_s3:
|
||||
type: integer
|
||||
|
|
@ -3169,7 +3169,7 @@ paths:
|
|||
type: integer
|
||||
description: 'Retention days of the backup in s3'
|
||||
database_backup_retention_max_storage_s3:
|
||||
type: integer
|
||||
type: number
|
||||
description: 'Max storage of the backup in S3'
|
||||
timeout:
|
||||
type: integer
|
||||
|
|
|
|||
287
tests/Feature/ApplicationDeploymentControlVarFilteringTest.php
Normal file
287
tests/Feature/ApplicationDeploymentControlVarFilteringTest.php
Normal file
|
|
@ -0,0 +1,287 @@
|
|||
<?php
|
||||
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\ApplicationDeploymentQueue;
|
||||
use App\Models\ApplicationPreview;
|
||||
use App\Models\Environment;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
class TestableControlVarFilteringDeploymentJob extends ApplicationDeploymentJob
|
||||
{
|
||||
public array $recordedCommands = [];
|
||||
|
||||
public ?string $writtenDockerfile = null;
|
||||
|
||||
public function __construct() {}
|
||||
|
||||
public function execute_remote_command(...$commands)
|
||||
{
|
||||
$this->recordedCommands[] = $commands;
|
||||
|
||||
foreach ($commands as $command) {
|
||||
$commandString = is_array($command) ? ($command['command'] ?? $command[0] ?? null) : $command;
|
||||
|
||||
if (! is_string($commandString)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/echo .*?([A-Za-z0-9+\\/=]{16,}).*?\\| base64 -d \\| tee \\/artifacts\\/test-app\\/Dockerfile > \\/dev\\/null/', $commandString, $matches) === 1) {
|
||||
$this->writtenDockerfile = base64_decode($matches[1]) ?: null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function makeDeploymentControlVarFixture(array $applicationAttributes = []): array
|
||||
{
|
||||
$team = Team::create([
|
||||
'name' => 'Control Var Team',
|
||||
'description' => 'Team for deployment control var tests.',
|
||||
'personal_team' => false,
|
||||
'show_boarding' => false,
|
||||
]);
|
||||
$project = Project::create([
|
||||
'name' => 'Control Var Project',
|
||||
'team_id' => $team->id,
|
||||
]);
|
||||
$environment = Environment::where('project_id', $project->id)->firstOrFail();
|
||||
$server = Server::factory()->create([
|
||||
'team_id' => $team->id,
|
||||
]);
|
||||
|
||||
$application = Application::factory()->create([
|
||||
'environment_id' => $environment->id,
|
||||
'build_pack' => 'dockerfile',
|
||||
...$applicationAttributes,
|
||||
]);
|
||||
|
||||
$application->settings()->update([
|
||||
'inject_build_args_to_dockerfile' => true,
|
||||
'include_source_commit_in_build' => false,
|
||||
'is_env_sorting_enabled' => false,
|
||||
]);
|
||||
|
||||
return [$application->fresh(), $server];
|
||||
}
|
||||
|
||||
function createApplicationEnvironmentVariable(Application $application, array $attributes): EnvironmentVariable
|
||||
{
|
||||
return EnvironmentVariable::create([
|
||||
'resourceable_type' => Application::class,
|
||||
'resourceable_id' => $application->id,
|
||||
'is_preview' => false,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => true,
|
||||
'is_multiline' => false,
|
||||
'is_literal' => false,
|
||||
...$attributes,
|
||||
]);
|
||||
}
|
||||
|
||||
function makeControlVarFilteringJob(Application $application, Server $server, array $overrides = []): array
|
||||
{
|
||||
$job = new TestableControlVarFilteringDeploymentJob;
|
||||
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
||||
|
||||
$queue = Mockery::mock(ApplicationDeploymentQueue::class);
|
||||
$queue->shouldReceive('addLogEntry')->andReturnNull();
|
||||
|
||||
$properties = [
|
||||
'application' => $application->fresh(),
|
||||
'application_deployment_queue' => $queue,
|
||||
'build_pack' => $application->build_pack,
|
||||
'mainServer' => $server,
|
||||
'pull_request_id' => 0,
|
||||
'commit' => 'HEAD',
|
||||
'workdir' => '/artifacts/test-app',
|
||||
'deployment_uuid' => 'deployment-uuid',
|
||||
'dockerfile_location' => '/Dockerfile',
|
||||
'container_name' => 'control-var-app',
|
||||
'coolify_variables' => null,
|
||||
'dockerSecretsSupported' => false,
|
||||
];
|
||||
|
||||
$mergedProperties = array_merge($properties, $overrides);
|
||||
$mergedProperties['saved_outputs'] = new Collection($overrides['saved_outputs'] ?? []);
|
||||
|
||||
if (($mergedProperties['pull_request_id'] ?? 0) !== 0 && ! array_key_exists('preview', $mergedProperties)) {
|
||||
$mergedProperties['preview'] = ApplicationPreview::create([
|
||||
'application_id' => $application->id,
|
||||
'pull_request_id' => $mergedProperties['pull_request_id'],
|
||||
'pull_request_html_url' => 'https://example.com/pr/'.$mergedProperties['pull_request_id'],
|
||||
'fqdn' => 'https://preview.example.com',
|
||||
]);
|
||||
}
|
||||
|
||||
foreach ($mergedProperties as $property => $value) {
|
||||
$reflectionProperty = $reflection->getProperty($property);
|
||||
$reflectionProperty->setAccessible(true);
|
||||
$reflectionProperty->setValue($job, $value);
|
||||
}
|
||||
|
||||
return [$job, $reflection];
|
||||
}
|
||||
|
||||
function invokeDeploymentJobMethod(object $job, ReflectionClass $reflection, string $method): mixed
|
||||
{
|
||||
$reflectionMethod = $reflection->getMethod($method);
|
||||
$reflectionMethod->setAccessible(true);
|
||||
|
||||
return $reflectionMethod->invoke($job);
|
||||
}
|
||||
|
||||
function readDeploymentJobProperty(object $job, ReflectionClass $reflection, string $property): mixed
|
||||
{
|
||||
$reflectionProperty = $reflection->getProperty($property);
|
||||
$reflectionProperty->setAccessible(true);
|
||||
|
||||
return $reflectionProperty->getValue($job);
|
||||
}
|
||||
|
||||
it('filters buildpack control vars from generic build args', function () {
|
||||
[$application, $server] = makeDeploymentControlVarFixture();
|
||||
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'APP_ENV',
|
||||
'value' => 'production',
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'NIXPACKS_NODE_VERSION',
|
||||
'value' => '22',
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'RAILPACK_NODE_VERSION',
|
||||
'value' => '20',
|
||||
]);
|
||||
|
||||
[$job, $reflection] = makeControlVarFilteringJob($application, $server);
|
||||
|
||||
invokeDeploymentJobMethod($job, $reflection, 'generate_env_variables');
|
||||
|
||||
/** @var Collection $envArgs */
|
||||
$envArgs = readDeploymentJobProperty($job, $reflection, 'env_args');
|
||||
|
||||
expect($envArgs->get('APP_ENV'))->toBe('production');
|
||||
expect($envArgs->has('NIXPACKS_NODE_VERSION'))->toBeFalse();
|
||||
expect($envArgs->has('RAILPACK_NODE_VERSION'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('filters buildpack control vars from preview build-time env files', function () {
|
||||
[$application, $server] = makeDeploymentControlVarFixture();
|
||||
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'APP_ENV',
|
||||
'value' => 'production',
|
||||
'is_preview' => true,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'NIXPACKS_NODE_VERSION',
|
||||
'value' => '22',
|
||||
'is_preview' => true,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'RAILPACK_NODE_VERSION',
|
||||
'value' => '20',
|
||||
'is_preview' => true,
|
||||
]);
|
||||
|
||||
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
|
||||
'pull_request_id' => 42,
|
||||
]);
|
||||
|
||||
/** @var Collection $buildtimeEnvs */
|
||||
$buildtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_buildtime_environment_variables');
|
||||
|
||||
expect($buildtimeEnvs->contains(fn (string $env) => str($env)->startsWith('APP_ENV=')))->toBeTrue();
|
||||
expect($buildtimeEnvs->contains(fn (string $env) => str($env)->startsWith('NIXPACKS_NODE_VERSION=')))->toBeFalse();
|
||||
expect($buildtimeEnvs->contains(fn (string $env) => str($env)->startsWith('RAILPACK_NODE_VERSION=')))->toBeFalse();
|
||||
});
|
||||
|
||||
it('filters buildpack control vars from preview runtime env fallback', function () {
|
||||
[$application, $server] = makeDeploymentControlVarFixture();
|
||||
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'APP_NAME',
|
||||
'value' => 'coolify',
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'NIXPACKS_NODE_VERSION',
|
||||
'value' => '22',
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'RAILPACK_NODE_VERSION',
|
||||
'value' => '20',
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'PREVIEW_FLAG',
|
||||
'value' => 'enabled',
|
||||
'is_preview' => true,
|
||||
'is_runtime' => true,
|
||||
'is_buildtime' => false,
|
||||
]);
|
||||
|
||||
$application->environment_variables_preview()
|
||||
->whereIn('key', ['APP_NAME', 'NIXPACKS_NODE_VERSION', 'RAILPACK_NODE_VERSION'])
|
||||
->delete();
|
||||
|
||||
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
|
||||
'pull_request_id' => 99,
|
||||
]);
|
||||
|
||||
/** @var Collection $runtimeEnvs */
|
||||
$runtimeEnvs = invokeDeploymentJobMethod($job, $reflection, 'generate_runtime_environment_variables');
|
||||
|
||||
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('APP_NAME=')))->toBeTrue();
|
||||
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('PREVIEW_FLAG=')))->toBeTrue();
|
||||
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('NIXPACKS_NODE_VERSION=')))->toBeFalse();
|
||||
expect($runtimeEnvs->contains(fn (string $env) => str($env)->startsWith('RAILPACK_NODE_VERSION=')))->toBeFalse();
|
||||
});
|
||||
|
||||
it('filters buildpack control vars from dockerfile arg injection', function () {
|
||||
[$application, $server] = makeDeploymentControlVarFixture();
|
||||
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'APP_ENV',
|
||||
'value' => 'production',
|
||||
'is_runtime' => false,
|
||||
'is_buildtime' => true,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'NIXPACKS_NODE_VERSION',
|
||||
'value' => '22',
|
||||
'is_runtime' => false,
|
||||
'is_buildtime' => true,
|
||||
]);
|
||||
createApplicationEnvironmentVariable($application, [
|
||||
'key' => 'RAILPACK_NODE_VERSION',
|
||||
'value' => '20',
|
||||
'is_runtime' => false,
|
||||
'is_buildtime' => true,
|
||||
]);
|
||||
|
||||
[$job, $reflection] = makeControlVarFilteringJob($application, $server, [
|
||||
'saved_outputs' => [
|
||||
'dockerfile' => "FROM php:8.4-cli\nRUN php -v",
|
||||
],
|
||||
]);
|
||||
|
||||
invokeDeploymentJobMethod($job, $reflection, 'add_build_env_variables_to_dockerfile');
|
||||
|
||||
expect($job->writtenDockerfile)->toContain('ARG APP_ENV=production');
|
||||
expect($job->writtenDockerfile)->not->toContain('ARG NIXPACKS_NODE_VERSION=');
|
||||
expect($job->writtenDockerfile)->not->toContain('ARG RAILPACK_NODE_VERSION=');
|
||||
});
|
||||
121
tests/Feature/DevelopmentRailpackExamplesSeederTest.php
Normal file
121
tests/Feature/DevelopmentRailpackExamplesSeederTest.php
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\PrivateKey;
|
||||
use App\Models\Project;
|
||||
use App\Models\Server;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\Team;
|
||||
use Database\Seeders\DevelopmentRailpackExamplesSeeder;
|
||||
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);
|
||||
|
||||
function seedRailpackExamplePrerequisites(): void
|
||||
{
|
||||
test()->seed([
|
||||
UserSeeder::class,
|
||||
TeamSeeder::class,
|
||||
PrivateKeySeeder::class,
|
||||
ServerSeeder::class,
|
||||
ProjectSeeder::class,
|
||||
StandaloneDockerSeeder::class,
|
||||
GithubAppSeeder::class,
|
||||
]);
|
||||
}
|
||||
|
||||
it('can seed the railpack examples directly on a clean development database', function () {
|
||||
config()->set('app.env', 'local');
|
||||
|
||||
$this->seed(DevelopmentRailpackExamplesSeeder::class);
|
||||
|
||||
expect(Team::query()->find(0))->not->toBeNull();
|
||||
expect(PrivateKey::query()->find(1))->not->toBeNull();
|
||||
expect(Server::query()->find(0))->not->toBeNull();
|
||||
expect(StandaloneDocker::query()->find(0))->not->toBeNull();
|
||||
expect(GithubApp::query()->find(0))->not->toBeNull();
|
||||
expect(Project::query()->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)->exists())->toBeTrue();
|
||||
expect(Application::query()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples()));
|
||||
});
|
||||
|
||||
it('seeds the railpack examples in development mode', function () {
|
||||
config()->set('app.env', 'local');
|
||||
|
||||
seedRailpackExamplePrerequisites();
|
||||
$this->seed(DevelopmentRailpackExamplesSeeder::class);
|
||||
|
||||
$project = Project::query()
|
||||
->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)
|
||||
->first();
|
||||
|
||||
expect($project)
|
||||
->not->toBeNull()
|
||||
->and($project->name)->toBe('Railpack Examples')
|
||||
->and($project->environments)->toHaveCount(1)
|
||||
->and($project->environments->first()->uuid)->toBe(DevelopmentRailpackExamplesSeeder::ENVIRONMENT_UUID);
|
||||
|
||||
$applications = $project->applications()->with('settings')->orderBy('uuid')->get();
|
||||
|
||||
expect($applications)->toHaveCount(count(DevelopmentRailpackExamplesSeeder::examples()));
|
||||
expect($applications->every(fn (Application $application) => $application->build_pack === 'railpack'))->toBeTrue();
|
||||
expect($applications->every(fn (Application $application) => $application->git_repository === DevelopmentRailpackExamplesSeeder::GIT_REPOSITORY))->toBeTrue();
|
||||
expect($applications->every(fn (Application $application) => $application->git_branch === DevelopmentRailpackExamplesSeeder::GIT_BRANCH))->toBeTrue();
|
||||
|
||||
$nestjs = $applications->firstWhere('uuid', 'railpack-nestjs');
|
||||
$angularStatic = $applications->firstWhere('uuid', 'railpack-angular-static');
|
||||
$eleventyStatic = $applications->firstWhere('uuid', 'railpack-eleventy-static');
|
||||
|
||||
expect($nestjs)
|
||||
->not->toBeNull()
|
||||
->and($nestjs->base_directory)->toBe('/node/nestjs')
|
||||
->and($nestjs->ports_exposes)->toBe('3000')
|
||||
->and($nestjs->build_command)->toBe('npm run build')
|
||||
->and($nestjs->start_command)->toBe('npm run start:prod')
|
||||
->and($nestjs->settings->is_static)->toBeFalse();
|
||||
|
||||
expect($angularStatic)
|
||||
->not->toBeNull()
|
||||
->and($angularStatic->publish_directory)->toBe('/dist/static/browser')
|
||||
->and($angularStatic->ports_exposes)->toBe('80')
|
||||
->and($angularStatic->settings->is_static)->toBeTrue()
|
||||
->and($angularStatic->settings->is_spa)->toBeTrue();
|
||||
|
||||
expect($eleventyStatic)
|
||||
->not->toBeNull()
|
||||
->and($eleventyStatic->publish_directory)->toBe('/_site')
|
||||
->and($eleventyStatic->settings->is_static)->toBeTrue()
|
||||
->and($eleventyStatic->settings->is_spa)->toBeFalse();
|
||||
});
|
||||
|
||||
it('skips the railpack examples outside development mode', function () {
|
||||
config()->set('app.env', 'testing');
|
||||
|
||||
seedRailpackExamplePrerequisites();
|
||||
$this->seed(DevelopmentRailpackExamplesSeeder::class);
|
||||
|
||||
expect(Project::query()->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)->exists())->toBeFalse();
|
||||
expect(Application::query()->where('uuid', 'railpack-nextjs-ssr')->exists())->toBeFalse();
|
||||
});
|
||||
|
||||
it('is idempotent when run multiple times', function () {
|
||||
config()->set('app.env', 'local');
|
||||
|
||||
seedRailpackExamplePrerequisites();
|
||||
$this->seed(DevelopmentRailpackExamplesSeeder::class);
|
||||
$this->seed(DevelopmentRailpackExamplesSeeder::class);
|
||||
|
||||
$project = Project::query()
|
||||
->where('uuid', DevelopmentRailpackExamplesSeeder::PROJECT_UUID)
|
||||
->first();
|
||||
|
||||
expect($project)->not->toBeNull();
|
||||
expect($project->applications()->count())->toBe(count(DevelopmentRailpackExamplesSeeder::examples()));
|
||||
});
|
||||
|
|
@ -30,6 +30,9 @@ function makeRailpackDeploymentJob(array $applicationAttributes = [], array $sav
|
|||
'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);
|
||||
|
|
@ -175,6 +178,9 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
|
|||
'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,
|
||||
|
|
@ -184,8 +190,8 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
|
|||
);
|
||||
|
||||
expect($command)->toContain('railpack prepare');
|
||||
expect($command)->toContain('--env '.escapeshellarg('RAILPACK_INSTALL_CMD=npm ci'));
|
||||
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'));
|
||||
|
|
@ -195,3 +201,32 @@ function invokeRailpackMethod(object $job, ReflectionClass $reflection, string $
|
|||
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}"');
|
||||
});
|
||||
|
|
|
|||
124
tests/Unit/ApplicationDeploymentRailpackEnvParityTest.php
Normal file
124
tests/Unit/ApplicationDeploymentRailpackEnvParityTest.php
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<?php
|
||||
|
||||
use App\Jobs\ApplicationDeploymentJob;
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\Server;
|
||||
|
||||
it('generates escaped railpack env args from resolved values and includes install command', function () {
|
||||
$application = Mockery::mock(Application::class);
|
||||
$application->shouldReceive('getAttribute')->with('install_command')->andReturn('npm ci && npm run postinstall');
|
||||
|
||||
$nodeVersion = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$nodeVersion->forceFill([
|
||||
'key' => 'RAILPACK_NODE_VERSION',
|
||||
'is_literal' => false,
|
||||
'is_multiline' => false,
|
||||
]);
|
||||
$nodeVersion->shouldReceive('getResolvedValueWithServer')->once()->with(Mockery::type(Server::class))->andReturn('22');
|
||||
|
||||
$literalValue = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$literalValue->forceFill([
|
||||
'key' => 'RAILPACK_CUSTOM_FLAG',
|
||||
'is_literal' => true,
|
||||
'is_multiline' => false,
|
||||
]);
|
||||
$literalValue->shouldReceive('getResolvedValueWithServer')->once()->with(Mockery::type(Server::class))->andReturn("'hello world'");
|
||||
|
||||
$jsonValue = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$jsonValue->forceFill([
|
||||
'key' => 'RAILPACK_JSON',
|
||||
'is_literal' => false,
|
||||
'is_multiline' => false,
|
||||
]);
|
||||
$jsonValue->shouldReceive('getResolvedValueWithServer')->once()->with(Mockery::type(Server::class))->andReturn('{"token":"abc"}');
|
||||
|
||||
$nullValue = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$nullValue->forceFill([
|
||||
'key' => 'RAILPACK_NULL',
|
||||
'is_literal' => false,
|
||||
'is_multiline' => false,
|
||||
]);
|
||||
$nullValue->shouldReceive('getResolvedValueWithServer')->once()->with(Mockery::type(Server::class))->andReturn(null);
|
||||
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('railpack_environment_variables')
|
||||
->andReturn(collect([$nodeVersion, $literalValue, $jsonValue, $nullValue]));
|
||||
|
||||
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
|
||||
$job->shouldAllowMockingProtectedMethods();
|
||||
|
||||
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
||||
$applicationProperty = $reflection->getProperty('application');
|
||||
$applicationProperty->setAccessible(true);
|
||||
$applicationProperty->setValue($job, $application);
|
||||
|
||||
$pullRequestProperty = $reflection->getProperty('pull_request_id');
|
||||
$pullRequestProperty->setAccessible(true);
|
||||
$pullRequestProperty->setValue($job, 0);
|
||||
|
||||
$mainServerProperty = $reflection->getProperty('mainServer');
|
||||
$mainServerProperty->setAccessible(true);
|
||||
$mainServerProperty->setValue($job, Mockery::mock(Server::class));
|
||||
|
||||
$method = $reflection->getMethod('generate_railpack_env_variables');
|
||||
$method->setAccessible(true);
|
||||
$variables = $method->invoke($job);
|
||||
|
||||
$envArgsProperty = $reflection->getProperty('env_railpack_args');
|
||||
$envArgsProperty->setAccessible(true);
|
||||
$envArgs = $envArgsProperty->getValue($job);
|
||||
|
||||
expect($variables->all())->toBe([
|
||||
'RAILPACK_NODE_VERSION' => '22',
|
||||
'RAILPACK_CUSTOM_FLAG' => 'hello world',
|
||||
'RAILPACK_JSON' => '{"token":"abc"}',
|
||||
'RAILPACK_INSTALL_CMD' => 'npm ci && npm run postinstall',
|
||||
]);
|
||||
expect($envArgs)->toContain("--env 'RAILPACK_NODE_VERSION=22'");
|
||||
expect($envArgs)->toContain("--env 'RAILPACK_CUSTOM_FLAG=hello world'");
|
||||
expect($envArgs)->toContain("--env 'RAILPACK_JSON={\"token\":\"abc\"}'");
|
||||
expect($envArgs)->toContain("--env 'RAILPACK_INSTALL_CMD=npm ci && npm run postinstall'");
|
||||
expect($envArgs)->not->toContain('RAILPACK_NULL');
|
||||
});
|
||||
|
||||
it('uses preview railpack environment variables for preview deployments', function () {
|
||||
$application = Mockery::mock(Application::class);
|
||||
$application->shouldReceive('getAttribute')->with('install_command')->andReturn(null);
|
||||
|
||||
$previewValue = Mockery::mock(EnvironmentVariable::class)->makePartial();
|
||||
$previewValue->forceFill([
|
||||
'key' => 'RAILPACK_PREVIEW_ONLY',
|
||||
'is_literal' => false,
|
||||
'is_multiline' => false,
|
||||
]);
|
||||
$previewValue->shouldReceive('getResolvedValueWithServer')->once()->with(Mockery::type(Server::class))->andReturn('preview-value');
|
||||
|
||||
$application->shouldReceive('getAttribute')
|
||||
->with('railpack_environment_variables_preview')
|
||||
->andReturn(collect([$previewValue]));
|
||||
|
||||
$job = Mockery::mock(ApplicationDeploymentJob::class)->makePartial();
|
||||
$job->shouldAllowMockingProtectedMethods();
|
||||
|
||||
$reflection = new ReflectionClass(ApplicationDeploymentJob::class);
|
||||
$applicationProperty = $reflection->getProperty('application');
|
||||
$applicationProperty->setAccessible(true);
|
||||
$applicationProperty->setValue($job, $application);
|
||||
|
||||
$pullRequestProperty = $reflection->getProperty('pull_request_id');
|
||||
$pullRequestProperty->setAccessible(true);
|
||||
$pullRequestProperty->setValue($job, 42);
|
||||
|
||||
$mainServerProperty = $reflection->getProperty('mainServer');
|
||||
$mainServerProperty->setAccessible(true);
|
||||
$mainServerProperty->setValue($job, Mockery::mock(Server::class));
|
||||
|
||||
$method = $reflection->getMethod('generate_railpack_env_variables');
|
||||
$method->setAccessible(true);
|
||||
$variables = $method->invoke($job);
|
||||
|
||||
expect($variables->all())->toBe([
|
||||
'RAILPACK_PREVIEW_ONLY' => 'preview-value',
|
||||
]);
|
||||
});
|
||||
Loading…
Reference in a new issue