2026-04-09 16:45:42 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
use App\Exceptions\DeploymentException;
|
|
|
|
|
use App\Jobs\ApplicationDeploymentJob;
|
|
|
|
|
use App\Models\Application;
|
2026-07-11 19:35:10 +00:00
|
|
|
use App\Models\ApplicationDeploymentQueue;
|
2026-06-29 08:19:22 +00:00
|
|
|
use App\Models\ApplicationSetting;
|
|
|
|
|
use App\Models\EnvironmentVariable;
|
2026-07-11 19:35:10 +00:00
|
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
2026-04-09 16:45:42 +00:00
|
|
|
use Illuminate\Support\Collection;
|
2026-04-30 14:27:08 +00:00
|
|
|
use Tests\TestCase;
|
|
|
|
|
|
2026-07-11 19:35:10 +00:00
|
|
|
uses(TestCase::class, RefreshDatabase::class);
|
2026-04-09 16:45:42 +00:00
|
|
|
|
|
|
|
|
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'",
|
2026-04-28 12:37:31 +00:00
|
|
|
'force_rebuild' => false,
|
|
|
|
|
'addHosts' => '',
|
|
|
|
|
'secrets_hash_key' => 'testing-app-key',
|
2026-04-09 16:45:42 +00:00
|
|
|
] 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');
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-09 17:01:52 +00:00
|
|
|
it('builds railpack prepare command using railpack env for install and cli flags for build/start overrides', function () {
|
2026-04-09 16:45:42 +00:00
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob(
|
|
|
|
|
[
|
|
|
|
|
'install_command' => 'npm ci',
|
|
|
|
|
'build_command' => 'npm run build',
|
|
|
|
|
'start_command' => 'node server.js',
|
|
|
|
|
],
|
|
|
|
|
);
|
2026-04-28 12:37:31 +00:00
|
|
|
$envRailpackArgsProperty = $reflection->getProperty('env_railpack_args');
|
|
|
|
|
$envRailpackArgsProperty->setAccessible(true);
|
|
|
|
|
$envRailpackArgsProperty->setValue($job, "--env 'RAILPACK_NODE_VERSION=22' --env 'RAILPACK_INSTALL_CMD=npm ci'");
|
2026-04-09 16:45:42 +00:00
|
|
|
|
|
|
|
|
$command = invokeRailpackMethod(
|
|
|
|
|
$job,
|
|
|
|
|
$reflection,
|
|
|
|
|
'railpack_prepare_command',
|
|
|
|
|
['.coolify/railpack.generated.json'],
|
|
|
|
|
);
|
|
|
|
|
|
2026-04-09 17:01:52 +00:00
|
|
|
expect($command)->toContain('railpack prepare');
|
|
|
|
|
expect($command)->toContain("--env 'RAILPACK_NODE_VERSION=22'");
|
2026-04-28 12:37:31 +00:00
|
|
|
expect($command)->toContain("--env 'RAILPACK_INSTALL_CMD=npm ci'");
|
2026-04-09 17:01:52 +00:00
|
|
|
expect($command)->toContain('--build-cmd '.escapeshellarg('npm run build'));
|
|
|
|
|
expect($command)->toContain('--start-cmd '.escapeshellarg('node server.js'));
|
2026-04-09 16:45:42 +00:00
|
|
|
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=");
|
2026-04-09 17:01:52 +00:00
|
|
|
expect($command)->not->toContain('RAILPACK_BUILD_CMD=');
|
|
|
|
|
expect($command)->not->toContain('RAILPACK_START_CMD=');
|
2026-04-09 16:45:42 +00:00
|
|
|
});
|
2026-04-28 12:37:31 +00:00
|
|
|
|
2026-05-11 15:31:29 +00:00
|
|
|
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');
|
|
|
|
|
});
|
|
|
|
|
|
2026-07-02 16:53:00 +00:00
|
|
|
it('checks buildx inside the helper container before railpack builds', function () {
|
|
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob([], [
|
|
|
|
|
'railpack_helper_buildx_available' => 'available',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
invokeRailpackMethod($job, $reflection, 'ensure_helper_docker_buildx_available_for_railpack');
|
|
|
|
|
|
|
|
|
|
expect($job->recordedCommands[0][0][0])
|
|
|
|
|
->toContain('DOCKER_CONFIG=/root/.docker docker buildx version');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('fails clearly when buildx is missing inside the helper container', function () {
|
|
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob([], [
|
|
|
|
|
'railpack_helper_buildx_available' => 'not-available',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
expect(fn () => invokeRailpackMethod($job, $reflection, 'ensure_helper_docker_buildx_available_for_railpack'))
|
|
|
|
|
->toThrow(DeploymentException::class, 'helper container');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('pins docker config while running railpack buildx commands', function () {
|
|
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob([
|
|
|
|
|
'uuid' => 'application-uuid',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$command = invokeRailpackMethod(
|
|
|
|
|
$job,
|
|
|
|
|
$reflection,
|
|
|
|
|
'railpack_build_command',
|
|
|
|
|
[
|
|
|
|
|
'coollabsio/coolify:test',
|
|
|
|
|
collect([]),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($command)
|
|
|
|
|
->toContain('DOCKER_CONFIG=/root/.docker docker buildx create --name coolify-railpack')
|
|
|
|
|
->toContain('DOCKER_CONFIG=/root/.docker docker buildx build --builder coolify-railpack');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('filters reserved docker client variables from railpack build secrets', function () {
|
|
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob([
|
|
|
|
|
'uuid' => 'application-uuid',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$command = invokeRailpackMethod(
|
|
|
|
|
$job,
|
|
|
|
|
$reflection,
|
|
|
|
|
'railpack_build_command',
|
|
|
|
|
[
|
|
|
|
|
'coollabsio/coolify:test',
|
|
|
|
|
collect([
|
|
|
|
|
'DOCKER_CONFIG' => '/tmp/no-buildx',
|
|
|
|
|
'DOCKER_HOST' => 'tcp://invalid:2375',
|
|
|
|
|
'RAILPACK_NODE_VERSION' => '22',
|
|
|
|
|
'APP_ENV' => 'production',
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect($command)
|
|
|
|
|
->toContain("--secret 'id=RAILPACK_NODE_VERSION,env=RAILPACK_NODE_VERSION'")
|
|
|
|
|
->toContain("--secret 'id=APP_ENV,env=APP_ENV'")
|
|
|
|
|
->not->toContain('id=DOCKER_CONFIG')
|
|
|
|
|
->not->toContain('id=DOCKER_HOST')
|
|
|
|
|
->not->toContain("env 'DOCKER_CONFIG=")
|
|
|
|
|
->not->toContain("env 'DOCKER_HOST=");
|
|
|
|
|
});
|
|
|
|
|
|
2026-04-28 12:37:31 +00:00
|
|
|
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',
|
2026-05-11 11:29:21 +00:00
|
|
|
'RAILPACK_DEPLOY_APT_PACKAGES' => 'curl wget',
|
2026-04-28 12:37:31 +00:00
|
|
|
'SECRET_JSON' => '{"token":"abc"}',
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
2026-06-24 20:11:46 +00:00
|
|
|
// Build-time variables are interpolated by sourcing the build-time .env file before
|
|
|
|
|
// the build, so user/Coolify variables must NOT be forwarded inline as literals.
|
|
|
|
|
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
|
2026-05-11 13:43:09 +00:00
|
|
|
expect($command)->toContain("env 'RAILPACK_NODE_VERSION=22'");
|
|
|
|
|
expect($command)->toContain("'RAILPACK_INSTALL_CMD=npm ci && npm run postinstall'");
|
|
|
|
|
expect($command)->toContain("'RAILPACK_DEPLOY_APT_PACKAGES=curl wget'");
|
2026-06-24 20:11:46 +00:00
|
|
|
// SECRET_JSON is not a buildpack control variable, so it is provided via the sourced
|
|
|
|
|
// build-time .env file (which supports $VAR interpolation) rather than inline `env`.
|
|
|
|
|
expect($command)->not->toContain("'SECRET_JSON={\"token\":\"abc\"}'");
|
2026-05-11 13:43:09 +00:00
|
|
|
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=RAILPACK_DEPLOY_APT_PACKAGES,env=RAILPACK_DEPLOY_APT_PACKAGES'");
|
|
|
|
|
expect($command)->toContain("--secret 'id=SECRET_JSON,env=SECRET_JSON'");
|
2026-04-28 12:37:31 +00:00
|
|
|
expect($command)->toContain(' --build-arg secrets-hash=');
|
2026-04-30 14:27:08 +00:00
|
|
|
expect($command)->toContain('--build-arg BUILDKIT_SYNTAX="ghcr.io/railwayapp/railpack-frontend:v'.config('constants.coolify.railpack_version').'"');
|
2026-04-28 12:37:31 +00:00
|
|
|
});
|
2026-06-24 20:11:46 +00:00
|
|
|
|
|
|
|
|
it('interpolates build-time variable references for railpack by sourcing the build-time env file', function () {
|
|
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob([
|
|
|
|
|
'uuid' => 'application-uuid',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
// Mirrors the issue: BETTER_AUTH_URL=$COOLIFY_URL must be interpolated at build time.
|
|
|
|
|
$command = invokeRailpackMethod(
|
|
|
|
|
$job,
|
|
|
|
|
$reflection,
|
|
|
|
|
'railpack_build_command',
|
|
|
|
|
[
|
|
|
|
|
'coollabsio/coolify:test',
|
|
|
|
|
collect([
|
|
|
|
|
'BETTER_AUTH_URL' => '$COOLIFY_URL',
|
|
|
|
|
'COOLIFY_URL' => 'https://sapere-10.bobman.dev',
|
|
|
|
|
]),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// The literal `$COOLIFY_URL` must NOT be forwarded inline; it is resolved by the shell
|
|
|
|
|
// after sourcing the build-time .env file, then read through the build secret.
|
|
|
|
|
expect($command)->toContain('set -a && source /artifacts/build-time.env && set +a');
|
|
|
|
|
expect($command)->not->toContain("'BETTER_AUTH_URL=\$COOLIFY_URL'");
|
|
|
|
|
expect($command)->not->toContain("env 'BETTER_AUTH_URL");
|
|
|
|
|
expect($command)->toContain("--secret 'id=BETTER_AUTH_URL,env=BETTER_AUTH_URL'");
|
|
|
|
|
expect($command)->toContain("--secret 'id=COOLIFY_URL,env=COOLIFY_URL'");
|
|
|
|
|
});
|
2026-06-29 08:19:22 +00:00
|
|
|
|
|
|
|
|
it('creates an empty build-time env file for railpack when there are no generated build-time variables', function () {
|
|
|
|
|
[$job, $reflection] = makeRailpackDeploymentJob([
|
|
|
|
|
'build_pack' => 'railpack',
|
|
|
|
|
'compose_parsing_version' => '3',
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
$applicationProperty = $reflection->getProperty('application');
|
|
|
|
|
$applicationProperty->setAccessible(true);
|
|
|
|
|
$application = $applicationProperty->getValue($job);
|
|
|
|
|
$application->setRelation('settings', new ApplicationSetting([
|
|
|
|
|
'include_source_commit_in_build' => false,
|
|
|
|
|
'is_env_sorting_enabled' => false,
|
|
|
|
|
]));
|
|
|
|
|
$application->setRelation('environment_variables', collect([
|
|
|
|
|
new EnvironmentVariable(['key' => 'COOLIFY_FQDN']),
|
|
|
|
|
new EnvironmentVariable(['key' => 'COOLIFY_URL']),
|
|
|
|
|
new EnvironmentVariable(['key' => 'COOLIFY_BRANCH']),
|
|
|
|
|
new EnvironmentVariable(['key' => 'COOLIFY_RESOURCE_UUID']),
|
|
|
|
|
]));
|
|
|
|
|
|
|
|
|
|
foreach ([
|
2026-07-11 19:35:10 +00:00
|
|
|
'application_deployment_queue' => new class extends ApplicationDeploymentQueue
|
2026-06-29 08:19:22 +00:00
|
|
|
{
|
2026-07-11 19:35:10 +00:00
|
|
|
public function addLogEntry(string $message, string $type = 'stdout', bool $hidden = false): void {}
|
2026-06-29 08:19:22 +00:00
|
|
|
},
|
|
|
|
|
'build_pack' => 'railpack',
|
|
|
|
|
'pull_request_id' => 0,
|
|
|
|
|
] as $property => $value) {
|
|
|
|
|
$reflectionProperty = $reflection->getProperty($property);
|
|
|
|
|
$reflectionProperty->setAccessible(true);
|
|
|
|
|
$reflectionProperty->setValue($job, $value);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
invokeRailpackMethod($job, $reflection, 'save_buildtime_environment_variables');
|
|
|
|
|
|
|
|
|
|
expect(collect($job->recordedCommands)->flatten()->implode(' '))->toContain('touch /artifacts/build-time.env');
|
|
|
|
|
});
|