feat(environment-variables): implement environment variable analysis for build-time issues
- Added EnvironmentVariableAnalyzer trait to analyze and warn about problematic environment variables during the build process. - Integrated analysis into ApplicationDeploymentJob and Livewire components to provide feedback on potential build issues. - Introduced a new Blade component for displaying warnings related to environment variables in the UI.
This commit is contained in:
@@ -18,6 +18,7 @@ use App\Models\StandaloneDocker;
|
|||||||
use App\Models\SwarmDocker;
|
use App\Models\SwarmDocker;
|
||||||
use App\Notifications\Application\DeploymentFailed;
|
use App\Notifications\Application\DeploymentFailed;
|
||||||
use App\Notifications\Application\DeploymentSuccess;
|
use App\Notifications\Application\DeploymentSuccess;
|
||||||
|
use App\Traits\EnvironmentVariableAnalyzer;
|
||||||
use App\Traits\ExecuteRemoteCommand;
|
use App\Traits\ExecuteRemoteCommand;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Exception;
|
use Exception;
|
||||||
@@ -39,7 +40,7 @@ use Yosymfony\Toml\Toml;
|
|||||||
|
|
||||||
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
class ApplicationDeploymentJob implements ShouldBeEncrypted, ShouldQueue
|
||||||
{
|
{
|
||||||
use Dispatchable, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
|
use Dispatchable, EnvironmentVariableAnalyzer, ExecuteRemoteCommand, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
public $tries = 1;
|
public $tries = 1;
|
||||||
|
|
||||||
@@ -2710,6 +2711,30 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
$this->application_deployment_queue->addLogEntry('New container started.');
|
$this->application_deployment_queue->addLogEntry('New container started.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function analyzeBuildTimeVariables($variables)
|
||||||
|
{
|
||||||
|
$variablesArray = $variables->toArray();
|
||||||
|
$warnings = self::analyzeBuildVariables($variablesArray);
|
||||||
|
|
||||||
|
if (empty($warnings)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$this->application_deployment_queue->addLogEntry('----------------------------------------');
|
||||||
|
foreach ($warnings as $warning) {
|
||||||
|
$messages = self::formatBuildWarning($warning);
|
||||||
|
foreach ($messages as $message) {
|
||||||
|
$this->application_deployment_queue->addLogEntry($message, type: 'warning');
|
||||||
|
}
|
||||||
|
$this->application_deployment_queue->addLogEntry('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add general advice
|
||||||
|
$this->application_deployment_queue->addLogEntry('💡 Tips to resolve build issues:', type: 'info');
|
||||||
|
$this->application_deployment_queue->addLogEntry(' 1. Set these variables as "Runtime only" in the environment variables settings', type: 'info');
|
||||||
|
$this->application_deployment_queue->addLogEntry(' 2. Use different values for build-time (e.g., NODE_ENV=development for build)', type: 'info');
|
||||||
|
$this->application_deployment_queue->addLogEntry(' 3. Consider using multi-stage Docker builds to separate build and runtime environments', type: 'info');
|
||||||
|
}
|
||||||
|
|
||||||
private function generate_build_env_variables()
|
private function generate_build_env_variables()
|
||||||
{
|
{
|
||||||
if ($this->application->build_pack === 'nixpacks') {
|
if ($this->application->build_pack === 'nixpacks') {
|
||||||
@@ -2719,6 +2744,11 @@ COPY ./nginx.conf /etc/nginx/conf.d/default.conf");
|
|||||||
$variables = collect([])->merge($this->env_args);
|
$variables = collect([])->merge($this->env_args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Analyze build variables for potential issues
|
||||||
|
if ($variables->isNotEmpty()) {
|
||||||
|
$this->analyzeBuildTimeVariables($variables);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
if ($this->dockerBuildkitSupported && $this->application->settings->use_build_secrets) {
|
||||||
$this->generate_build_secrets($variables);
|
$this->generate_build_secrets($variables);
|
||||||
$this->build_args = '';
|
$this->build_args = '';
|
||||||
|
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
||||||
|
|
||||||
|
use App\Traits\EnvironmentVariableAnalyzer;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Add extends Component
|
class Add extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests;
|
use AuthorizesRequests, EnvironmentVariableAnalyzer;
|
||||||
|
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
||||||
@@ -27,6 +28,8 @@ class Add extends Component
|
|||||||
|
|
||||||
public bool $is_buildtime = true;
|
public bool $is_buildtime = true;
|
||||||
|
|
||||||
|
public array $problematicVariables = [];
|
||||||
|
|
||||||
protected $listeners = ['clearAddEnv' => 'clear'];
|
protected $listeners = ['clearAddEnv' => 'clear'];
|
||||||
|
|
||||||
protected $rules = [
|
protected $rules = [
|
||||||
@@ -50,6 +53,7 @@ class Add extends Component
|
|||||||
public function mount()
|
public function mount()
|
||||||
{
|
{
|
||||||
$this->parameters = get_route_parameters();
|
$this->parameters = get_route_parameters();
|
||||||
|
$this->problematicVariables = self::getProblematicVariablesForFrontend();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function submit()
|
public function submit()
|
||||||
|
@@ -4,13 +4,14 @@ namespace App\Livewire\Project\Shared\EnvironmentVariable;
|
|||||||
|
|
||||||
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
use App\Models\EnvironmentVariable as ModelsEnvironmentVariable;
|
||||||
use App\Models\SharedEnvironmentVariable;
|
use App\Models\SharedEnvironmentVariable;
|
||||||
|
use App\Traits\EnvironmentVariableAnalyzer;
|
||||||
use App\Traits\EnvironmentVariableProtection;
|
use App\Traits\EnvironmentVariableProtection;
|
||||||
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
|
|
||||||
class Show extends Component
|
class Show extends Component
|
||||||
{
|
{
|
||||||
use AuthorizesRequests, EnvironmentVariableProtection;
|
use AuthorizesRequests, EnvironmentVariableAnalyzer, EnvironmentVariableProtection;
|
||||||
|
|
||||||
public $parameters;
|
public $parameters;
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ class Show extends Component
|
|||||||
|
|
||||||
public bool $is_redis_credential = false;
|
public bool $is_redis_credential = false;
|
||||||
|
|
||||||
|
public array $problematicVariables = [];
|
||||||
|
|
||||||
protected $listeners = [
|
protected $listeners = [
|
||||||
'refreshEnvs' => 'refresh',
|
'refreshEnvs' => 'refresh',
|
||||||
'refresh',
|
'refresh',
|
||||||
@@ -77,6 +80,7 @@ class Show extends Component
|
|||||||
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
|
if ($this->type === 'standalone-redis' && ($this->env->key === 'REDIS_PASSWORD' || $this->env->key === 'REDIS_USERNAME')) {
|
||||||
$this->is_redis_credential = true;
|
$this->is_redis_credential = true;
|
||||||
}
|
}
|
||||||
|
$this->problematicVariables = self::getProblematicVariablesForFrontend();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getResourceProperty()
|
public function getResourceProperty()
|
||||||
|
221
app/Traits/EnvironmentVariableAnalyzer.php
Normal file
221
app/Traits/EnvironmentVariableAnalyzer.php
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Traits;
|
||||||
|
|
||||||
|
trait EnvironmentVariableAnalyzer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* List of environment variables that commonly cause build issues when set to production values.
|
||||||
|
* Each entry contains the variable pattern and associated metadata.
|
||||||
|
*/
|
||||||
|
protected static function getProblematicBuildVariables(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'NODE_ENV' => [
|
||||||
|
'problematic_values' => ['production', 'prod'],
|
||||||
|
'affects' => 'Node.js/npm/yarn',
|
||||||
|
'issue' => 'Skips devDependencies installation which are often required for building (webpack, typescript, etc.)',
|
||||||
|
'recommendation' => 'Uncheck "Available at Buildtime" or use "development" during build',
|
||||||
|
],
|
||||||
|
'NPM_CONFIG_PRODUCTION' => [
|
||||||
|
'problematic_values' => ['true', '1', 'yes'],
|
||||||
|
'affects' => 'npm',
|
||||||
|
'issue' => 'Forces npm to skip devDependencies',
|
||||||
|
'recommendation' => 'Remove from build-time variables or set to false',
|
||||||
|
],
|
||||||
|
'YARN_PRODUCTION' => [
|
||||||
|
'problematic_values' => ['true', '1', 'yes'],
|
||||||
|
'affects' => 'Yarn',
|
||||||
|
'issue' => 'Forces yarn to skip devDependencies',
|
||||||
|
'recommendation' => 'Remove from build-time variables or set to false',
|
||||||
|
],
|
||||||
|
'COMPOSER_NO_DEV' => [
|
||||||
|
'problematic_values' => ['1', 'true', 'yes'],
|
||||||
|
'affects' => 'PHP/Composer',
|
||||||
|
'issue' => 'Skips require-dev packages which may include build tools',
|
||||||
|
'recommendation' => 'Set as "Runtime only" or remove from build-time variables',
|
||||||
|
],
|
||||||
|
'MIX_ENV' => [
|
||||||
|
'problematic_values' => ['prod', 'production'],
|
||||||
|
'affects' => 'Elixir/Phoenix',
|
||||||
|
'issue' => 'Production mode may skip development dependencies needed for compilation',
|
||||||
|
'recommendation' => 'Use "dev" for build or set as "Runtime only"',
|
||||||
|
],
|
||||||
|
'RAILS_ENV' => [
|
||||||
|
'problematic_values' => ['production'],
|
||||||
|
'affects' => 'Ruby on Rails',
|
||||||
|
'issue' => 'May affect asset precompilation and dependency handling',
|
||||||
|
'recommendation' => 'Consider using "development" for build phase',
|
||||||
|
],
|
||||||
|
'RACK_ENV' => [
|
||||||
|
'problematic_values' => ['production'],
|
||||||
|
'affects' => 'Ruby/Rack',
|
||||||
|
'issue' => 'May affect dependency handling and build behavior',
|
||||||
|
'recommendation' => 'Consider using "development" for build phase',
|
||||||
|
],
|
||||||
|
'BUNDLE_WITHOUT' => [
|
||||||
|
'problematic_values' => ['development', 'test', 'development:test'],
|
||||||
|
'affects' => 'Ruby/Bundler',
|
||||||
|
'issue' => 'Excludes gem groups that may contain build dependencies',
|
||||||
|
'recommendation' => 'Remove from build-time variables or adjust groups',
|
||||||
|
],
|
||||||
|
'FLASK_ENV' => [
|
||||||
|
'problematic_values' => ['production'],
|
||||||
|
'affects' => 'Python/Flask',
|
||||||
|
'issue' => 'May affect debug mode and development tools availability',
|
||||||
|
'recommendation' => 'Usually safe, but consider "development" for complex builds',
|
||||||
|
],
|
||||||
|
'DJANGO_SETTINGS_MODULE' => [
|
||||||
|
'problematic_values' => [], // Check if contains 'production' or 'prod'
|
||||||
|
'affects' => 'Python/Django',
|
||||||
|
'issue' => 'Production settings may disable debug tools needed during build',
|
||||||
|
'recommendation' => 'Use development settings for build phase',
|
||||||
|
'check_function' => 'checkDjangoSettings',
|
||||||
|
],
|
||||||
|
'APP_ENV' => [
|
||||||
|
'problematic_values' => ['production', 'prod'],
|
||||||
|
'affects' => 'Laravel/Symfony',
|
||||||
|
'issue' => 'May affect dependency installation and build optimizations',
|
||||||
|
'recommendation' => 'Consider using "local" or "development" for build',
|
||||||
|
],
|
||||||
|
'ASPNETCORE_ENVIRONMENT' => [
|
||||||
|
'problematic_values' => ['Production'],
|
||||||
|
'affects' => '.NET/ASP.NET Core',
|
||||||
|
'issue' => 'May affect build-time configurations and optimizations',
|
||||||
|
'recommendation' => 'Usually safe, but verify build requirements',
|
||||||
|
],
|
||||||
|
'CI' => [
|
||||||
|
'problematic_values' => ['true', '1', 'yes'],
|
||||||
|
'affects' => 'Various tools',
|
||||||
|
'issue' => 'Changes behavior in many tools (disables interactivity, changes caching)',
|
||||||
|
'recommendation' => 'Usually beneficial for builds, but be aware of behavior changes',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze an environment variable for potential build issues.
|
||||||
|
* Always returns a warning if the key is in our list, regardless of value.
|
||||||
|
*/
|
||||||
|
public static function analyzeBuildVariable(string $key, string $value): ?array
|
||||||
|
{
|
||||||
|
$problematicVars = self::getProblematicBuildVariables();
|
||||||
|
|
||||||
|
// Direct key match
|
||||||
|
if (isset($problematicVars[$key])) {
|
||||||
|
$config = $problematicVars[$key];
|
||||||
|
|
||||||
|
// Check if it has a custom check function
|
||||||
|
if (isset($config['check_function'])) {
|
||||||
|
$method = $config['check_function'];
|
||||||
|
if (method_exists(self::class, $method)) {
|
||||||
|
return self::$method($key, $value, $config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always return warning for known problematic variables
|
||||||
|
return [
|
||||||
|
'variable' => $key,
|
||||||
|
'value' => $value,
|
||||||
|
'affects' => $config['affects'],
|
||||||
|
'issue' => $config['issue'],
|
||||||
|
'recommendation' => $config['recommendation'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Analyze multiple environment variables for potential build issues.
|
||||||
|
*/
|
||||||
|
public static function analyzeBuildVariables(array $variables): array
|
||||||
|
{
|
||||||
|
$warnings = [];
|
||||||
|
|
||||||
|
foreach ($variables as $key => $value) {
|
||||||
|
$warning = self::analyzeBuildVariable($key, $value);
|
||||||
|
if ($warning) {
|
||||||
|
$warnings[] = $warning;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom check for Django settings module.
|
||||||
|
*/
|
||||||
|
protected static function checkDjangoSettings(string $key, string $value, array $config): ?array
|
||||||
|
{
|
||||||
|
// Always return warning for DJANGO_SETTINGS_MODULE when it's set as build-time
|
||||||
|
return [
|
||||||
|
'variable' => $key,
|
||||||
|
'value' => $value,
|
||||||
|
'affects' => $config['affects'],
|
||||||
|
'issue' => $config['issue'],
|
||||||
|
'recommendation' => $config['recommendation'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a formatted warning message for deployment logs.
|
||||||
|
*/
|
||||||
|
public static function formatBuildWarning(array $warning): array
|
||||||
|
{
|
||||||
|
$messages = [
|
||||||
|
"⚠️ Build-time environment variable warning: {$warning['variable']}={$warning['value']}",
|
||||||
|
" Affects: {$warning['affects']}",
|
||||||
|
" Issue: {$warning['issue']}",
|
||||||
|
" Recommendation: {$warning['recommendation']}",
|
||||||
|
];
|
||||||
|
|
||||||
|
return $messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a variable should show a warning in the UI.
|
||||||
|
*/
|
||||||
|
public static function shouldShowBuildWarning(string $key): bool
|
||||||
|
{
|
||||||
|
return isset(self::getProblematicBuildVariables()[$key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get UI warning message for a specific variable.
|
||||||
|
*/
|
||||||
|
public static function getUIWarningMessage(string $key): ?string
|
||||||
|
{
|
||||||
|
$problematicVars = self::getProblematicBuildVariables();
|
||||||
|
|
||||||
|
if (! isset($problematicVars[$key])) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$config = $problematicVars[$key];
|
||||||
|
$problematicValuesStr = implode(', ', $config['problematic_values']);
|
||||||
|
|
||||||
|
return "Setting {$key} to {$problematicValuesStr} as a build-time variable may cause issues. {$config['issue']} Consider: {$config['recommendation']}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get problematic variables configuration for frontend use.
|
||||||
|
*/
|
||||||
|
public static function getProblematicVariablesForFrontend(): array
|
||||||
|
{
|
||||||
|
$vars = self::getProblematicBuildVariables();
|
||||||
|
$result = [];
|
||||||
|
|
||||||
|
foreach ($vars as $key => $config) {
|
||||||
|
// Skip the check_function as it's PHP-specific
|
||||||
|
$result[$key] = [
|
||||||
|
'problematic_values' => $config['problematic_values'],
|
||||||
|
'affects' => $config['affects'],
|
||||||
|
'issue' => $config['issue'],
|
||||||
|
'recommendation' => $config['recommendation'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,32 @@
|
|||||||
|
@props(['problematicVariables' => []])
|
||||||
|
|
||||||
|
<template x-data="{
|
||||||
|
problematicVars: @js($problematicVariables),
|
||||||
|
get showWarning() {
|
||||||
|
const currentKey = $wire.key;
|
||||||
|
const isBuildtime = $wire.is_buildtime;
|
||||||
|
|
||||||
|
if (!isBuildtime || !currentKey) return false;
|
||||||
|
if (!this.problematicVars.hasOwnProperty(currentKey)) return false;
|
||||||
|
|
||||||
|
// Always show warning for known problematic variables when set as buildtime
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
get warningMessage() {
|
||||||
|
if (!this.showWarning) return null;
|
||||||
|
const config = this.problematicVars[$wire.key];
|
||||||
|
if (!config) return null;
|
||||||
|
return config.issue;
|
||||||
|
},
|
||||||
|
get recommendation() {
|
||||||
|
if (!this.showWarning) return null;
|
||||||
|
const config = this.problematicVars[$wire.key];
|
||||||
|
if (!config) return null;
|
||||||
|
return `Recommendation: ${config.recommendation}`;
|
||||||
|
}
|
||||||
|
}" x-if="showWarning">
|
||||||
|
<div class="p-3 rounded-lg bg-yellow-50 dark:bg-yellow-900/20 border border-yellow-200 dark:border-yellow-800">
|
||||||
|
<div class="text-sm text-yellow-700 dark:text-yellow-300" x-text="warningMessage"></div>
|
||||||
|
<div class="text-sm text-yellow-700 dark:text-yellow-300" x-text="recommendation"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@@ -3,11 +3,15 @@
|
|||||||
<x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required />
|
<x-forms.textarea x-show="$wire.is_multiline === true" x-cloak id="value" label="Value" required />
|
||||||
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
|
<x-forms.input x-show="$wire.is_multiline === false" x-cloak placeholder="production" id="value"
|
||||||
x-bind:label="$wire.is_multiline === false && 'Value'" required />
|
x-bind:label="$wire.is_multiline === false && 'Value'" required />
|
||||||
|
|
||||||
@if (!$shared || $isNixpacks)
|
@if (!$shared || $isNixpacks)
|
||||||
<x-forms.checkbox id="is_buildtime"
|
<x-forms.checkbox id="is_buildtime"
|
||||||
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
helper="Make this variable available during Docker build process. Useful for build secrets and dependencies."
|
||||||
label="Available at Buildtime" />
|
label="Available at Buildtime" />
|
||||||
<x-forms.checkbox id="is_runtime"
|
|
||||||
|
<x-environment-variable-warning :problematic-variables="$problematicVariables" />
|
||||||
|
|
||||||
|
<x-forms.checkbox id="is_runtime"
|
||||||
helper="Make this variable available in the running container at runtime."
|
helper="Make this variable available in the running container at runtime."
|
||||||
label="Available at Runtime" />
|
label="Available at Runtime" />
|
||||||
<x-forms.checkbox id="is_literal"
|
<x-forms.checkbox id="is_literal"
|
||||||
|
@@ -188,6 +188,7 @@
|
|||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
</div>
|
</div>
|
||||||
|
<x-environment-variable-warning :problematic-variables="$problematicVariables" />
|
||||||
<div class="flex w-full justify-end gap-2">
|
<div class="flex w-full justify-end gap-2">
|
||||||
@if ($isDisabled)
|
@if ($isDisabled)
|
||||||
<x-forms.button disabled type="submit">Update</x-forms.button>
|
<x-forms.button disabled type="submit">Update</x-forms.button>
|
||||||
|
Reference in New Issue
Block a user