authorize('create', Application::class); try { $application = $this->applicationForDetection(); } catch (\Throwable $e) { handleError($e, $this); return; } Validator::make([ 'branch' => $application->git_branch, 'base_directory' => $this->base_directory ?? '/', ], [ 'branch' => ['required', new ValidGitBranch], 'base_directory' => ['required', 'string', 'regex:~^/?(?:[a-zA-Z0-9_.-]+/)*[a-zA-Z0-9_.-]*$~', 'not_regex:~(^|/)\.\.?(/|$)~'], ])->validate(); $destination = find_resource_destination_for_current_team(data_get($this->query, 'destination')); if (! $destination) { $this->dispatch('error', 'Destination not found.'); return; } $this->authorize('update', $destination->server); $this->detectionRan = false; $this->detectionFailed = false; $this->applyDetectionResult(RepositoryDetectionResult::none()); try { $detector = new RepositoryDetector($application, $this->base_directory ?? '/', $destination->server); $this->applyDetectionResult($detector->detect()); } catch (\Throwable $e) { // The exception can contain the clone command with a Git token, so do not log the message. Log::debug('Repository detection failed', ['exception' => $e::class]); $this->detectionFailed = true; $this->dispatch('error', 'Smart Scan could not read the repository. Check the repository access and the branch.'); } $this->detectionRan = true; } /** * Builds an application that is not saved, only to generate the clone command for the scan. */ protected function unsavedApplicationForDetection(string $repository, string $branch, GithubApp|GitlabApp|GiteaApp|null $source = null, ?PrivateKey $privateKey = null): Application { $application = (new Application)->forceFill([ 'git_repository' => $repository, 'git_branch' => $branch, 'git_commit_sha' => 'HEAD', 'private_key_id' => $privateKey?->id, ]); $application->setRelation('settings', (new ApplicationSetting)->forceFill([ 'is_git_shallow_clone_enabled' => true, 'is_git_submodules_enabled' => false, ])); $application->setRelation('source', $source); $application->setRelation('private_key', $privateKey); return $application; } protected function importDetectedEnvironmentVariables(Application $application): void { if (! $this->envImported || empty($this->envExampleVars)) { return; } Validator::make(['variables' => $this->envExampleVars], [ 'variables' => ['array'], 'variables.*' => ['nullable', 'string'], ])->validate(); $application->getConnection()->transaction(function () use ($application): void { foreach ($this->envExampleVars as $key => $value) { if (! preg_match('/^[A-Z_][A-Z0-9_]*$/i', (string) $key)) { continue; } EnvironmentVariable::create([ 'key' => $key, 'value' => $value, 'resourceable_type' => $application->getMorphClass(), 'resourceable_id' => $application->id, 'is_preview' => false, ]); } }); } protected function applyDetectionResult(RepositoryDetectionResult $result): void { $this->detectedDockerfiles = $result->dockerfiles; $this->detectedDockerComposeFiles = $result->dockerComposeFiles; $this->dockerfilePorts = $result->dockerfilePorts; $this->selectedDockerfile = null; $this->selectedDockerComposeFile = null; $this->detectedPort = null; $this->detectedEnvFiles = []; $this->parsedEnvFiles = []; $this->selectedEnvFile = null; $this->envExampleVars = []; $this->envImported = false; $this->suggestedBuildPack = $result->getSuggestedBuildPack()?->value; if ($this->suggestedBuildPack && $this->suggestedBuildPack !== $this->build_pack) { $this->build_pack = $this->suggestedBuildPack; $this->updatedBuildPack(); } if ($result->hasDockerfile()) { $this->selectedDockerfile = $result->dockerfiles[0]; $port = $result->dockerfilePorts[$this->selectedDockerfile] ?? null; $this->port = $port ?? 3000; $this->detectedPort = $port; } if ($result->hasDockerCompose()) { $this->selectedDockerComposeFile = $result->dockerComposeFiles[0]; $this->docker_compose_location = '/'.$this->selectedDockerComposeFile; } if ($result->hasEnvFiles()) { $this->detectedEnvFiles = array_keys($result->envFiles); $this->parsedEnvFiles = []; foreach ($result->envFiles as $filename => $content) { $parsed = $content !== null ? parseEnvFormatToArray($content) : []; // parseEnvFormatToArray returns ['KEY' => ['value' => ..., 'comment' => ...]]. // Flatten to a plain ['KEY' => 'value'] map for the import form. $this->parsedEnvFiles[$filename] = collect($parsed) ->map(fn ($item) => is_array($item) ? (string) ($item['value'] ?? '') : (string) $item) ->all(); } $this->selectedEnvFile = $this->detectedEnvFiles[0]; $this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile] ?? []; } } /** * Repository path without a leading slash, so a typed path matches the detected paths. */ protected function normalizeRepositoryFilePath(?string $path): ?string { $path = ltrim(trim((string) $path), '/'); return $path === '' ? null : $path; } /** * Dockerfile location for the new application: a detected file or a path the user typed. */ protected function selectedDockerfileLocation(): ?string { $path = $this->normalizeRepositoryFilePath($this->selectedDockerfile); if ($path === null) { return null; } $location = '/'.$path; Validator::make( ['selectedDockerfile' => $location], ['selectedDockerfile' => ValidationPatterns::filePathRules()], ValidationPatterns::filePathMessages('selectedDockerfile', 'Dockerfile'), )->validate(); return $location; } public function updatedSelectedDockerfile(): void { $this->selectedDockerfile = $this->normalizeRepositoryFilePath($this->selectedDockerfile); $this->resetErrorBag('selectedDockerfile'); $this->selectedDockerfileLocation(); $port = $this->dockerfilePorts[$this->selectedDockerfile ?? ''] ?? null; $this->port = $port ?? 3000; $this->detectedPort = $port; } public function updatedSelectedDockerComposeFile(): void { $this->selectedDockerComposeFile = $this->normalizeRepositoryFilePath($this->selectedDockerComposeFile); if ($this->selectedDockerComposeFile) { $this->docker_compose_location = '/'.$this->selectedDockerComposeFile; } } public function updatedSelectedEnvFile(): void { if ($this->selectedEnvFile && isset($this->parsedEnvFiles[$this->selectedEnvFile])) { $this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile]; $this->envImported = false; } } public function confirmEnvImport(): void { $this->envImported = true; } public function clearEnvVars(): void { $this->envImported = false; $this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile] ?? []; } }