diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
index f250a860b..fdc65ebdb 100644
--- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php
+++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php
@@ -6,6 +6,7 @@ use App\Models\EnvironmentVariable;
use App\Traits\EnvironmentVariableProtection;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Livewire\Component;
+use Visus\Cuid2\Cuid2;
class All extends Component
{
@@ -398,4 +399,171 @@ class All extends Component
unset($this->environmentVariablesPreview);
$this->getDevView();
}
+
+ public function loadFromEnvExample()
+ {
+ try {
+ $this->authorize('manageEnvironment', $this->resource);
+
+ if ($this->resourceClass !== 'App\Models\Application') {
+ throw new \RuntimeException('This feature is only available for applications.');
+ }
+
+ if (! $this->canLoadEnvExample) {
+ throw new \RuntimeException('This feature requires a git repository source.');
+ }
+
+ $envExampleContent = $this->fetchEnvExampleFromGit();
+
+ if ($envExampleContent === null) {
+ $this->dispatch('error', 'No .env.example file found in the repository.');
+
+ return;
+ }
+
+ $variables = parseEnvFormatToArray($envExampleContent);
+
+ if (empty($variables)) {
+ $this->dispatch('warning', 'The .env.example file is empty or contains no valid variables.');
+
+ return;
+ }
+
+ $addedCount = 0;
+ $skippedCount = 0;
+ $maxOrder = $this->resource->environment_variables()->max('order') ?? 0;
+
+ foreach ($variables as $key => $value) {
+ if (str($key)->startsWith('SERVICE_FQDN') ||
+ str($key)->startsWith('SERVICE_URL') ||
+ str($key)->startsWith('SERVICE_NAME')) {
+ $skippedCount++;
+
+ continue;
+ }
+
+ $exists = $this->resource->environment_variables()->where('key', $key)->exists();
+ if ($exists) {
+ $skippedCount++;
+
+ continue;
+ }
+
+ $environment = new EnvironmentVariable;
+ $environment->key = $key;
+ $environment->value = $value;
+ $environment->is_multiline = false;
+ $environment->is_preview = false;
+ $environment->is_runtime = true;
+ $environment->is_buildtime = true;
+ $environment->resourceable_id = $this->resource->id;
+ $environment->resourceable_type = $this->resource->getMorphClass();
+ $environment->order = ++$maxOrder;
+ $environment->save();
+
+ $addedCount++;
+ }
+
+ $this->refreshEnvs();
+
+ if ($addedCount > 0) {
+ $message = "Added {$addedCount} environment variable(s)";
+ if ($skippedCount > 0) {
+ $message .= ", skipped {$skippedCount} existing";
+ }
+ $this->dispatch('success', $message.'.');
+ } else {
+ $this->dispatch('warning', 'All variables from .env.example already exist.');
+ }
+ } catch (\Throwable $e) {
+ return handleError($e, $this);
+ }
+ }
+
+ private function fetchEnvExampleFromGit(): ?string
+ {
+ $uuid = (string) new Cuid2;
+
+ ['commands' => $cloneCommand] = $this->resource->generateGitImportCommands(
+ deployment_uuid: $uuid,
+ only_checkout: true,
+ exec_in_docker: false,
+ custom_base_dir: '.'
+ );
+
+ $workdir = rtrim($this->resource->base_directory ?? '', '/');
+ $envFile = '.env.example';
+ $fileList = collect([".$workdir/$envFile"]);
+
+ $server = $this->resource->destination->server;
+
+ $getGitVersion = instant_remote_process(['git --version'], $server, false);
+ $gitVersion = str($getGitVersion)->explode(' ')->last();
+
+ if (version_compare($gitVersion, '2.35.1', '<')) {
+ $fileList = $fileList->map(function ($file) {
+ $parts = explode('/', trim($file, '.'));
+ $paths = collect();
+ $currentPath = '';
+ foreach ($parts as $part) {
+ $currentPath .= ($currentPath ? '/' : '').$part;
+ if (str($currentPath)->isNotEmpty()) {
+ $paths->push($currentPath);
+ }
+ }
+
+ return $paths;
+ })->flatten()->unique()->values();
+
+ $commands = collect([
+ "rm -rf /tmp/{$uuid}",
+ "mkdir -p /tmp/{$uuid}",
+ "cd /tmp/{$uuid}",
+ $cloneCommand,
+ 'git sparse-checkout init',
+ "git sparse-checkout set {$fileList->implode(' ')}",
+ 'git read-tree -mu HEAD',
+ "cat .$workdir/$envFile",
+ ]);
+ } else {
+ $commands = collect([
+ "rm -rf /tmp/{$uuid}",
+ "mkdir -p /tmp/{$uuid}",
+ "cd /tmp/{$uuid}",
+ $cloneCommand,
+ 'git sparse-checkout init --cone',
+ "git sparse-checkout set {$fileList->implode(' ')}",
+ 'git read-tree -mu HEAD',
+ "cat .$workdir/$envFile",
+ ]);
+ }
+
+ try {
+ $content = instant_remote_process($commands, $server);
+
+ return $content;
+ } catch (\Exception $e) {
+ if (str($e->getMessage())->contains('No such file')) {
+ return null;
+ }
+ throw $e;
+ } finally {
+ instant_remote_process(["rm -rf /tmp/{$uuid}"], $server, false);
+ }
+ }
+
+ public function getCanLoadEnvExampleProperty(): bool
+ {
+ if ($this->resourceClass !== 'App\Models\Application') {
+ return false;
+ }
+
+ if (filled(data_get($this->resource, 'dockerfile'))) {
+ return false;
+ }
+
+ $deploymentType = $this->resource->deploymentType();
+
+ return in_array($deploymentType, ['source', 'deploy_key']);
+ }
}
diff --git a/resources/views/livewire/project/shared/environment-variable/all.blade.php b/resources/views/livewire/project/shared/environment-variable/all.blade.php
index a962b2cec..49806459d 100644
--- a/resources/views/livewire/project/shared/environment-variable/all.blade.php
+++ b/resources/views/livewire/project/shared/environment-variable/all.blade.php
@@ -8,6 +8,12 @@
+ @if ($this->canLoadEnvExample)
+
+ Load from .env.example
+
+ @endif
{{ $view === 'normal' ? 'Developer view' : 'Normal view' }}
@endcan
diff --git a/tests/Unit/Livewire/EnvironmentVariableLoadFromExampleTest.php b/tests/Unit/Livewire/EnvironmentVariableLoadFromExampleTest.php
new file mode 100644
index 000000000..3527154d4
--- /dev/null
+++ b/tests/Unit/Livewire/EnvironmentVariableLoadFromExampleTest.php
@@ -0,0 +1,100 @@
+resourceClass = 'App\Models\Service';
+
+ expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
+});
+
+it('canLoadEnvExample returns false for StandalonePostgresql resources', function () {
+ $component = new All;
+ $component->resourceClass = 'App\Models\StandalonePostgresql';
+
+ expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
+});
+
+it('canLoadEnvExample returns false for dockerfile applications', function () {
+ $mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
+ $mockApplication->shouldReceive('getAttribute')
+ ->with('dockerfile')
+ ->andReturn('FROM alpine');
+
+ $component = new All;
+ $component->resourceClass = 'App\Models\Application';
+ $component->resource = $mockApplication;
+
+ expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
+});
+
+it('canLoadEnvExample returns true for source deployment type', function () {
+ $mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
+ $mockApplication->shouldReceive('getAttribute')
+ ->with('dockerfile')
+ ->andReturn(null);
+ $mockApplication->shouldReceive('deploymentType')
+ ->andReturn('source');
+
+ $component = new All;
+ $component->resourceClass = 'App\Models\Application';
+ $component->resource = $mockApplication;
+
+ expect($component->getCanLoadEnvExampleProperty())->toBeTrue();
+});
+
+it('canLoadEnvExample returns true for deploy_key deployment type', function () {
+ $mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
+ $mockApplication->shouldReceive('getAttribute')
+ ->with('dockerfile')
+ ->andReturn(null);
+ $mockApplication->shouldReceive('deploymentType')
+ ->andReturn('deploy_key');
+
+ $component = new All;
+ $component->resourceClass = 'App\Models\Application';
+ $component->resource = $mockApplication;
+
+ expect($component->getCanLoadEnvExampleProperty())->toBeTrue();
+});
+
+it('canLoadEnvExample returns false for other deployment types', function () {
+ $mockApplication = Mockery::mock('App\Models\Application')->shouldIgnoreMissing();
+ $mockApplication->shouldReceive('getAttribute')
+ ->with('dockerfile')
+ ->andReturn(null);
+ $mockApplication->shouldReceive('deploymentType')
+ ->andReturn('dockerimage');
+
+ $component = new All;
+ $component->resourceClass = 'App\Models\Application';
+ $component->resource = $mockApplication;
+
+ expect($component->getCanLoadEnvExampleProperty())->toBeFalse();
+});
+
+it('loadFromEnvExample method exists on All component', function () {
+ $component = new All;
+
+ expect(method_exists($component, 'loadFromEnvExample'))->toBeTrue();
+});
+
+it('parseEnvFormatToArray correctly parses env file format', function () {
+ $content = <<<'ENV'
+# Comment line
+DATABASE_URL=postgres://localhost:5432
+APP_NAME="My App"
+EMPTY_VALUE=
+QUOTED='single quoted'
+ENV;
+
+ $result = parseEnvFormatToArray($content);
+
+ expect($result)->toBeArray()
+ ->and($result['DATABASE_URL'])->toBe('postgres://localhost:5432')
+ ->and($result['APP_NAME'])->toBe('My App')
+ ->and($result['EMPTY_VALUE'])->toBe('')
+ ->and($result['QUOTED'])->toBe('single quoted')
+ ->and($result)->not->toHaveKey('# Comment line');
+});