mirror of
https://github.com/coollabsio/coolify.git
synced 2026-03-11 08:55:47 +00:00
Merge 6b87705b77 into 201998638a
This commit is contained in:
commit
3c337cfd06
13 changed files with 1102 additions and 25 deletions
55
app/Data/RepositoryDetectionResult.php
Normal file
55
app/Data/RepositoryDetectionResult.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Data;
|
||||
|
||||
use App\Enums\BuildPackTypes;
|
||||
use Spatie\LaravelData\Data;
|
||||
|
||||
class RepositoryDetectionResult extends Data
|
||||
{
|
||||
/**
|
||||
* @param array<int, string> $dockerfiles e.g. ['Dockerfile', 'apps/api/Dockerfile']
|
||||
* @param array<int, string> $dockerComposeFiles e.g. ['docker-compose.yml']
|
||||
* @param array<string, string|null> $envFiles e.g. ['.env.example' => 'KEY=val...', '.env.dist' => null]
|
||||
* @param array<string, int|null> $dockerfilePorts e.g. ['Dockerfile' => 3000, 'apps/api/Dockerfile' => 8080]
|
||||
*/
|
||||
public function __construct(
|
||||
public array $dockerfiles = [],
|
||||
public array $dockerComposeFiles = [],
|
||||
public array $envFiles = [],
|
||||
public array $dockerfilePorts = [],
|
||||
) {}
|
||||
|
||||
public static function none(): self
|
||||
{
|
||||
return new self;
|
||||
}
|
||||
|
||||
public function getSuggestedBuildPack(): BuildPackTypes
|
||||
{
|
||||
if (count($this->dockerComposeFiles) > 0) {
|
||||
return BuildPackTypes::DOCKERCOMPOSE;
|
||||
}
|
||||
|
||||
if (count($this->dockerfiles) > 0) {
|
||||
return BuildPackTypes::DOCKERFILE;
|
||||
}
|
||||
|
||||
return BuildPackTypes::NIXPACKS;
|
||||
}
|
||||
|
||||
public function hasDockerfile(): bool
|
||||
{
|
||||
return count($this->dockerfiles) > 0;
|
||||
}
|
||||
|
||||
public function hasDockerCompose(): bool
|
||||
{
|
||||
return count($this->dockerComposeFiles) > 0;
|
||||
}
|
||||
|
||||
public function hasEnvFiles(): bool
|
||||
{
|
||||
return count($this->envFiles) > 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,17 +3,24 @@
|
|||
namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\Project;
|
||||
use App\Models\StandaloneDocker;
|
||||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Services\RepositoryDetector;
|
||||
use App\Traits\HasRepositoryDetection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use Livewire\Component;
|
||||
|
||||
class GithubPrivateRepository extends Component
|
||||
{
|
||||
use HasRepositoryDetection;
|
||||
|
||||
public $current_step = 'github_apps';
|
||||
|
||||
public $github_apps;
|
||||
|
|
@ -135,6 +142,8 @@ class GithubPrivateRepository extends Component
|
|||
}
|
||||
$this->branches = sortBranchesByPriority($this->branches);
|
||||
$this->selected_branch_name = data_get($this->branches, '0.name', 'main');
|
||||
|
||||
$this->detectRepository();
|
||||
}
|
||||
|
||||
protected function loadBranchByPage()
|
||||
|
|
@ -155,6 +164,33 @@ class GithubPrivateRepository extends Component
|
|||
$this->branches = $this->branches->concat(collect($json));
|
||||
}
|
||||
|
||||
public function detectRepository(): void
|
||||
{
|
||||
$this->detectionRan = false;
|
||||
$this->envImported = false;
|
||||
|
||||
try {
|
||||
$serverId = data_get($this->query, 'server_id');
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
$repoUrl = "https://github.com/{$this->selected_repository_owner}/{$this->selected_repository_repo}";
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: $repoUrl,
|
||||
branch: $this->selected_branch_name,
|
||||
baseDirectory: $this->base_directory ?? '/',
|
||||
serverId: (int) $serverId,
|
||||
teamId: $teamId,
|
||||
);
|
||||
|
||||
$this->applyDetectionResult($detector->detect());
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('Repository detection failed in component', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$this->detectionRan = true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
try {
|
||||
|
|
@ -188,7 +224,7 @@ class GithubPrivateRepository extends Component
|
|||
$project = Project::where('uuid', $this->parameters['project_uuid'])->first();
|
||||
$environment = $project->load(['environments'])->environments->where('uuid', $this->parameters['environment_uuid'])->first();
|
||||
|
||||
$application = Application::create([
|
||||
$application_init = [
|
||||
'name' => generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name),
|
||||
'repository_project_id' => $this->selected_repository_id,
|
||||
'git_repository' => str($this->selected_repository_owner)->trim()->toString().'/'.str($this->selected_repository_repo)->trim()->toString(),
|
||||
|
|
@ -202,22 +238,51 @@ class GithubPrivateRepository extends Component
|
|||
'destination_type' => $destination_class,
|
||||
'source_id' => $this->github_app->id,
|
||||
'source_type' => $this->github_app->getMorphClass(),
|
||||
]);
|
||||
];
|
||||
|
||||
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
$application_init['health_check_enabled'] = false;
|
||||
}
|
||||
if ($this->build_pack === 'dockerfile' && $this->selectedDockerfile) {
|
||||
if (! empty($this->detectedDockerfiles) && ! in_array($this->selectedDockerfile, $this->detectedDockerfiles, true)) {
|
||||
$this->selectedDockerfile = $this->detectedDockerfiles[0];
|
||||
}
|
||||
$application_init['dockerfile_location'] = $this->selectedDockerfile;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if (! empty($this->detectedDockerComposeFiles) && $this->selectedDockerComposeFile
|
||||
&& ! in_array($this->selectedDockerComposeFile, $this->detectedDockerComposeFiles, true)) {
|
||||
$this->selectedDockerComposeFile = $this->detectedDockerComposeFiles[0];
|
||||
$this->docker_compose_location = '/'.$this->selectedDockerComposeFile;
|
||||
}
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
}
|
||||
|
||||
$application = Application::create($application_init);
|
||||
$application->settings->is_static = $this->is_static;
|
||||
$application->settings->save();
|
||||
|
||||
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
$application->health_check_enabled = false;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
$application['docker_compose_location'] = $this->docker_compose_location;
|
||||
}
|
||||
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
||||
$application->fqdn = $fqdn;
|
||||
|
||||
$application->name = generate_application_name($this->selected_repository_owner.'/'.$this->selected_repository_repo, $this->selected_branch_name, $application->uuid);
|
||||
$application->save();
|
||||
|
||||
// Import environment variables from .env.example
|
||||
if ($this->envImported && count($this->envExampleVars) > 0) {
|
||||
DB::transaction(function () use ($application): void {
|
||||
foreach ($this->envExampleVars as $key => $value) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'resourceable_type' => $application->getMorphClass(),
|
||||
'resourceable_id' => $application->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\PrivateKey;
|
||||
|
|
@ -11,12 +12,18 @@ use App\Models\StandaloneDocker;
|
|||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Services\RepositoryDetector;
|
||||
use App\Traits\HasRepositoryDetection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class GithubPrivateRepositoryDeployKey extends Component
|
||||
{
|
||||
use HasRepositoryDetection;
|
||||
|
||||
public $current_step = 'private_keys';
|
||||
|
||||
public $parameters;
|
||||
|
|
@ -135,6 +142,31 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||
$this->current_step = 'repository';
|
||||
}
|
||||
|
||||
public function detectRepository(): void
|
||||
{
|
||||
$this->detectionRan = false;
|
||||
$this->envImported = false;
|
||||
|
||||
try {
|
||||
$serverId = data_get($this->query, 'server_id');
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: $this->repository_url,
|
||||
branch: $this->branch ?? 'main',
|
||||
baseDirectory: $this->base_directory ?? '/',
|
||||
serverId: (int) $serverId,
|
||||
teamId: $teamId,
|
||||
);
|
||||
|
||||
$this->applyDetectionResult($detector->detect());
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('Repository detection failed in component', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$this->detectionRan = true;
|
||||
}
|
||||
|
||||
public function submit()
|
||||
{
|
||||
$this->validate();
|
||||
|
|
@ -188,7 +220,18 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
$application_init['health_check_enabled'] = false;
|
||||
}
|
||||
if ($this->build_pack === 'dockerfile' && $this->selectedDockerfile) {
|
||||
if (! empty($this->detectedDockerfiles) && ! in_array($this->selectedDockerfile, $this->detectedDockerfiles, true)) {
|
||||
$this->selectedDockerfile = $this->detectedDockerfiles[0];
|
||||
}
|
||||
$application_init['dockerfile_location'] = $this->selectedDockerfile;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if (! empty($this->detectedDockerComposeFiles) && $this->selectedDockerComposeFile
|
||||
&& ! in_array($this->selectedDockerComposeFile, $this->detectedDockerComposeFiles, true)) {
|
||||
$this->selectedDockerComposeFile = $this->detectedDockerComposeFiles[0];
|
||||
$this->docker_compose_location = '/'.$this->selectedDockerComposeFile;
|
||||
}
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
|
|
@ -201,6 +244,21 @@ class GithubPrivateRepositoryDeployKey extends Component
|
|||
$application->name = generate_random_name($application->uuid);
|
||||
$application->save();
|
||||
|
||||
// Import environment variables from .env.example
|
||||
if ($this->envImported && count($this->envExampleVars) > 0) {
|
||||
DB::transaction(function () use ($application): void {
|
||||
foreach ($this->envExampleVars as $key => $value) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'resourceable_type' => $application->getMorphClass(),
|
||||
'resourceable_id' => $application->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
'application_uuid' => $application->uuid,
|
||||
'environment_uuid' => $environment->uuid,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Livewire\Project\New;
|
||||
|
||||
use App\Models\Application;
|
||||
use App\Models\EnvironmentVariable;
|
||||
use App\Models\GithubApp;
|
||||
use App\Models\GitlabApp;
|
||||
use App\Models\Project;
|
||||
|
|
@ -11,12 +12,18 @@ use App\Models\StandaloneDocker;
|
|||
use App\Models\SwarmDocker;
|
||||
use App\Rules\ValidGitBranch;
|
||||
use App\Rules\ValidGitRepositoryUrl;
|
||||
use App\Services\RepositoryDetector;
|
||||
use App\Traits\HasRepositoryDetection;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Livewire\Component;
|
||||
use Spatie\Url\Url;
|
||||
|
||||
class PublicGitRepository extends Component
|
||||
{
|
||||
use HasRepositoryDetection;
|
||||
|
||||
public string $repository_url;
|
||||
|
||||
public int $port = 3000;
|
||||
|
|
@ -177,10 +184,15 @@ class PublicGitRepository extends Component
|
|||
$this->git_branch = 'master';
|
||||
}
|
||||
$this->selectedBranch = $this->git_branch;
|
||||
|
||||
if ($this->branchFound) {
|
||||
$this->detectRepository();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
if ($this->rate_limit_remaining == 0) {
|
||||
$this->selectedBranch = $this->git_branch;
|
||||
$this->branchFound = true;
|
||||
$this->detectRepository();
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -188,8 +200,13 @@ class PublicGitRepository extends Component
|
|||
try {
|
||||
$this->git_branch = 'master';
|
||||
$this->getBranch();
|
||||
if ($this->branchFound) {
|
||||
$this->detectRepository();
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
return handleError($e, $this);
|
||||
// Both main and master failed — still run detection
|
||||
// with clone fallback to the repo's default branch
|
||||
$this->detectRepository();
|
||||
}
|
||||
} else {
|
||||
return handleError($e, $this);
|
||||
|
|
@ -197,6 +214,35 @@ class PublicGitRepository extends Component
|
|||
}
|
||||
}
|
||||
|
||||
public function detectRepository(): void
|
||||
{
|
||||
$this->detectionRan = false;
|
||||
$this->envImported = false;
|
||||
|
||||
try {
|
||||
$serverId = data_get($this->query, 'server_id');
|
||||
$teamId = currentTeam()->id;
|
||||
|
||||
$repoUrl = $this->git_source === 'other'
|
||||
? $this->git_repository
|
||||
: "https://github.com/{$this->git_repository}";
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: $repoUrl,
|
||||
branch: $this->git_branch,
|
||||
baseDirectory: $this->base_directory,
|
||||
serverId: (int) $serverId,
|
||||
teamId: $teamId,
|
||||
);
|
||||
|
||||
$this->applyDetectionResult($detector->detect());
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('Repository detection failed in component', ['error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
$this->detectionRan = true;
|
||||
}
|
||||
|
||||
private function getGitSource()
|
||||
{
|
||||
$this->git_branch = 'main';
|
||||
|
|
@ -351,7 +397,18 @@ class PublicGitRepository extends Component
|
|||
if ($this->build_pack === 'dockerfile' || $this->build_pack === 'dockerimage') {
|
||||
$application_init['health_check_enabled'] = false;
|
||||
}
|
||||
if ($this->build_pack === 'dockerfile' && $this->selectedDockerfile) {
|
||||
if (! empty($this->detectedDockerfiles) && ! in_array($this->selectedDockerfile, $this->detectedDockerfiles, true)) {
|
||||
$this->selectedDockerfile = $this->detectedDockerfiles[0];
|
||||
}
|
||||
$application_init['dockerfile_location'] = $this->selectedDockerfile;
|
||||
}
|
||||
if ($this->build_pack === 'dockercompose') {
|
||||
if (! empty($this->detectedDockerComposeFiles) && $this->selectedDockerComposeFile
|
||||
&& ! in_array($this->selectedDockerComposeFile, $this->detectedDockerComposeFiles, true)) {
|
||||
$this->selectedDockerComposeFile = $this->detectedDockerComposeFiles[0];
|
||||
$this->docker_compose_location = '/'.$this->selectedDockerComposeFile;
|
||||
}
|
||||
$application_init['docker_compose_location'] = $this->docker_compose_location;
|
||||
$application_init['base_directory'] = $this->base_directory;
|
||||
}
|
||||
|
|
@ -362,11 +419,20 @@ class PublicGitRepository extends Component
|
|||
$fqdn = generateUrl(server: $destination->server, random: $application->uuid);
|
||||
$application->fqdn = $fqdn;
|
||||
$application->save();
|
||||
if ($this->checkCoolifyConfig) {
|
||||
// $config = loadConfigFromGit($this->repository_url, $this->git_branch, $this->base_directory, $this->query['server_id'], auth()->user()->currentTeam()->id);
|
||||
// if ($config) {
|
||||
// $application->setConfig($config);
|
||||
// }
|
||||
|
||||
// Import environment variables from .env.example
|
||||
if ($this->envImported && count($this->envExampleVars) > 0) {
|
||||
DB::transaction(function () use ($application): void {
|
||||
foreach ($this->envExampleVars as $key => $value) {
|
||||
EnvironmentVariable::create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'resourceable_type' => $application->getMorphClass(),
|
||||
'resourceable_id' => $application->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return redirect()->route('project.application.configuration', [
|
||||
|
|
|
|||
131
app/Services/RepositoryDetector.php
Normal file
131
app/Services/RepositoryDetector.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Data\RepositoryDetectionResult;
|
||||
use App\Models\Server;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Visus\Cuid2\Cuid2;
|
||||
|
||||
class RepositoryDetector
|
||||
{
|
||||
/**
|
||||
* Env file patterns considered safe to import (template/example files, not real secrets).
|
||||
*/
|
||||
private const ENV_FILE_PATTERN = '^\\.env\\.(example|sample|template|dist|local\\.example|development|production|staging|testing|test)$';
|
||||
|
||||
public function __construct(
|
||||
private string $repositoryUrl,
|
||||
private string $branch,
|
||||
private string $baseDirectory,
|
||||
private int $serverId,
|
||||
private int $teamId,
|
||||
) {}
|
||||
|
||||
public function detect(): RepositoryDetectionResult
|
||||
{
|
||||
$server = Server::query()
|
||||
->where('id', $this->serverId)
|
||||
->where('team_id', $this->teamId)
|
||||
->first();
|
||||
|
||||
if (! $server) {
|
||||
Log::debug('Repository detection skipped: server not found', [
|
||||
'serverId' => $this->serverId,
|
||||
'teamId' => $this->teamId,
|
||||
]);
|
||||
|
||||
return RepositoryDetectionResult::none();
|
||||
}
|
||||
|
||||
$uuid = (string) new Cuid2;
|
||||
if (strlen($uuid) < 10 || ! preg_match('/^[a-z0-9]+$/', $uuid)) {
|
||||
return RepositoryDetectionResult::none();
|
||||
}
|
||||
|
||||
$tempDir = "/tmp/coolify-detect-{$uuid}";
|
||||
$baseDir = rtrim($this->baseDirectory, '/');
|
||||
if ($baseDir === '') {
|
||||
$baseDir = '/';
|
||||
}
|
||||
$cdBase = $baseDir === '/' ? '' : $baseDir;
|
||||
|
||||
$workDir = escapeshellarg("{$tempDir}{$cdBase}");
|
||||
$envPattern = self::ENV_FILE_PATTERN;
|
||||
|
||||
$escapedTempDir = escapeshellarg($tempDir);
|
||||
|
||||
$commands = collect([
|
||||
'rm -rf -- '.$escapedTempDir,
|
||||
"trap 'rm -rf -- {$escapedTempDir}' EXIT",
|
||||
'git clone --depth 1 -b '.escapeshellarg($this->branch).' '.escapeshellarg($this->repositoryUrl).' '.escapeshellarg($tempDir).' >/dev/null 2>&1 || git clone --depth 1 '.escapeshellarg($this->repositoryUrl).' '.escapeshellarg($tempDir).' >/dev/null 2>&1',
|
||||
"cd {$workDir}",
|
||||
// Collect file lists into shell variables
|
||||
'df_list=$(git ls-files | grep -iE \'(^|/)Dockerfile(\.[a-zA-Z0-9_-]+)?$\' || true)',
|
||||
'compose_list=$(git ls-files | grep -iE \'(^|/)(docker-compose\.(yml|yaml)|compose\.(yml|yaml))$\' || true)',
|
||||
'env_list=$(git ls-files | grep -iE \''.$envPattern.'\' || true)',
|
||||
// Build env file contents as a JSON object (uses jq to safely encode file content)
|
||||
'env_json=\'{}\'',
|
||||
'for f in $env_list; do',
|
||||
' file_json=$(jq -Rs \'.\' "$f")',
|
||||
' env_json=$(echo "$env_json" | jq --arg k "$f" --argjson v "$file_json" \'. + {($k): $v}\')',
|
||||
'done',
|
||||
// Build dockerfile ports as a JSON object
|
||||
'port_json=\'{}\'',
|
||||
'for f in $df_list; do',
|
||||
' port=$(grep -m1 \'^EXPOSE\' "$f" 2>/dev/null | awk \'{print $2}\' || true)',
|
||||
' if [ -n "$port" ] && echo "$port" | grep -qE \'^[0-9]+$\'; then',
|
||||
' port_json=$(echo "$port_json" | jq --arg k "$f" --argjson v "$port" \'. + {($k): $v}\')',
|
||||
' else',
|
||||
' port_json=$(echo "$port_json" | jq --arg k "$f" \'. + {($k): null}\')',
|
||||
' fi',
|
||||
'done',
|
||||
// Output structured JSON
|
||||
'jq -n \\',
|
||||
' --argjson dockerfiles "$(echo "$df_list" | jq -R -s \'split("\\n") | map(select(. != ""))\')" \\',
|
||||
' --argjson dockerComposeFiles "$(echo "$compose_list" | jq -R -s \'split("\\n") | map(select(. != ""))\')" \\',
|
||||
' --argjson envFiles "$env_json" \\',
|
||||
' --argjson dockerfilePorts "$port_json" \\',
|
||||
' \'$ARGS.named\'',
|
||||
]);
|
||||
|
||||
try {
|
||||
$output = instant_remote_process($commands, $server, throwError: false, timeout: 60);
|
||||
|
||||
if (! $output) {
|
||||
return RepositoryDetectionResult::none();
|
||||
}
|
||||
|
||||
return $this->parseOutput($output);
|
||||
} catch (\Throwable $e) {
|
||||
Log::debug('Repository detection failed', [
|
||||
'error' => $e->getMessage(),
|
||||
'repositoryUrl' => $this->repositoryUrl,
|
||||
'branch' => $this->branch,
|
||||
]);
|
||||
|
||||
return RepositoryDetectionResult::none();
|
||||
}
|
||||
}
|
||||
|
||||
protected function parseOutput(string $output): RepositoryDetectionResult
|
||||
{
|
||||
$data = json_decode(trim($output), true);
|
||||
|
||||
if (! is_array($data)) {
|
||||
return RepositoryDetectionResult::none();
|
||||
}
|
||||
|
||||
$dockerfilePorts = [];
|
||||
foreach ($data['dockerfilePorts'] ?? [] as $file => $port) {
|
||||
$dockerfilePorts[$file] = is_numeric($port) ? (int) $port : null;
|
||||
}
|
||||
|
||||
return new RepositoryDetectionResult(
|
||||
dockerfiles: $data['dockerfiles'] ?? [],
|
||||
dockerComposeFiles: $data['dockerComposeFiles'] ?? [],
|
||||
envFiles: $data['envFiles'] ?? [],
|
||||
dockerfilePorts: $dockerfilePorts,
|
||||
);
|
||||
}
|
||||
}
|
||||
120
app/Traits/HasRepositoryDetection.php
Normal file
120
app/Traits/HasRepositoryDetection.php
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Data\RepositoryDetectionResult;
|
||||
|
||||
trait HasRepositoryDetection
|
||||
{
|
||||
public bool $detectionRan = false;
|
||||
|
||||
public array $detectedDockerfiles = [];
|
||||
|
||||
public array $detectedDockerComposeFiles = [];
|
||||
|
||||
public ?string $selectedDockerfile = null;
|
||||
|
||||
public ?string $selectedDockerComposeFile = null;
|
||||
|
||||
public ?int $detectedPort = null;
|
||||
|
||||
public array $dockerfilePorts = [];
|
||||
|
||||
public array $detectedEnvFiles = [];
|
||||
|
||||
public ?string $selectedEnvFile = null;
|
||||
|
||||
public array $parsedEnvFiles = [];
|
||||
|
||||
public array $envExampleVars = [];
|
||||
|
||||
public bool $envImported = false;
|
||||
|
||||
protected function applyDetectionResult(RepositoryDetectionResult $result): void
|
||||
{
|
||||
$this->detectedDockerfiles = $result->dockerfiles;
|
||||
$this->detectedDockerComposeFiles = $result->dockerComposeFiles;
|
||||
$this->dockerfilePorts = $result->dockerfilePorts;
|
||||
|
||||
$suggestedBuildPack = $result->getSuggestedBuildPack()->value;
|
||||
if ($suggestedBuildPack !== $this->build_pack) {
|
||||
$this->build_pack = $suggestedBuildPack;
|
||||
$this->updatedBuildPack();
|
||||
}
|
||||
|
||||
if ($result->hasDockerfile()) {
|
||||
$this->selectedDockerfile = $result->dockerfiles[0];
|
||||
$port = $result->dockerfilePorts[$this->selectedDockerfile] ?? null;
|
||||
if ($port) {
|
||||
$this->port = $port;
|
||||
$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) {
|
||||
$this->parsedEnvFiles[$filename] = $content !== null
|
||||
? parseEnvFormatToArray($content)
|
||||
: [];
|
||||
}
|
||||
$this->selectedEnvFile = $this->detectedEnvFiles[0];
|
||||
$this->envExampleVars = $this->parsedEnvFiles[$this->selectedEnvFile] ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSelectedDockerfile(): void
|
||||
{
|
||||
if ($this->selectedDockerfile && ! in_array($this->selectedDockerfile, $this->detectedDockerfiles, true)) {
|
||||
$this->selectedDockerfile = $this->detectedDockerfiles[0] ?? null;
|
||||
}
|
||||
|
||||
if ($this->selectedDockerfile && isset($this->dockerfilePorts[$this->selectedDockerfile])) {
|
||||
$port = $this->dockerfilePorts[$this->selectedDockerfile];
|
||||
if ($port) {
|
||||
$this->port = $port;
|
||||
$this->detectedPort = $port;
|
||||
} else {
|
||||
$this->detectedPort = null;
|
||||
}
|
||||
} else {
|
||||
$this->detectedPort = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function updatedSelectedDockerComposeFile(): void
|
||||
{
|
||||
if ($this->selectedDockerComposeFile && ! in_array($this->selectedDockerComposeFile, $this->detectedDockerComposeFiles, true)) {
|
||||
$this->selectedDockerComposeFile = $this->detectedDockerComposeFiles[0] ?? null;
|
||||
}
|
||||
|
||||
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] ?? [];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
<div x-data="{ envModalOpen: false }">
|
||||
<h1>Create a new Application</h1>
|
||||
<div class="pb-4">Deploy any public or private Git repositories through a Deploy Key.</div>
|
||||
<div class="flex flex-col ">
|
||||
|
|
@ -60,16 +60,76 @@
|
|||
<x-forms.input id="publish_directory" required label="Publish Directory" />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Repository Detection --}}
|
||||
<div class="pt-6 mt-4 border-t border-neutral-200 dark:border-coolgray-300">
|
||||
<h3 class="text-lg font-bold">Smart Scan</h3>
|
||||
<p class="pt-1 pb-3 text-sm dark:text-neutral-400">Scan for Dockerfiles, Docker Compose files, and environment configuration.</p>
|
||||
|
||||
<div class="flex items-center gap-3">
|
||||
<x-forms.button type="button" wire:click="detectRepository">
|
||||
<span wire:loading.remove wire:target="detectRepository">Detect Repository</span>
|
||||
<span wire:loading wire:target="detectRepository" class="inline-flex items-center gap-2">
|
||||
<x-loading /> Scanning...
|
||||
</span>
|
||||
</x-forms.button>
|
||||
</div>
|
||||
|
||||
@if ($detectionRan)
|
||||
<div wire:loading.remove wire:target="detectRepository" class="pt-3">
|
||||
<div class="flex items-center gap-3 flex-wrap text-sm">
|
||||
@if (count($detectedDockerfiles))
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
Dockerfile{{ count($detectedDockerfiles) > 1 ? 's' : '' }}
|
||||
<span class="dark:text-neutral-400">({{ count($detectedDockerfiles) }})</span>
|
||||
</span>
|
||||
@endif
|
||||
@if (count($detectedDockerComposeFiles))
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
Docker Compose
|
||||
<span class="dark:text-neutral-400">({{ count($detectedDockerComposeFiles) }})</span>
|
||||
</span>
|
||||
@endif
|
||||
@include('livewire.project.new.partials.env-detection-badges')
|
||||
@if (!count($detectedDockerfiles) && !count($detectedDockerComposeFiles) && !count($detectedEnvFiles))
|
||||
<span class="dark:text-neutral-400">No Dockerfile, Docker Compose, or env files detected.</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Configuration --}}
|
||||
<h3 class="pt-6 text-lg font-bold">Configuration</h3>
|
||||
|
||||
{{-- Dockerfile selector when multiple detected --}}
|
||||
@if ($build_pack === 'dockerfile' && count($detectedDockerfiles) > 1)
|
||||
<x-forms.select wire:model.live="selectedDockerfile" label="Dockerfile"
|
||||
helper="Multiple Dockerfiles were detected in your repository. Select which one to use.">
|
||||
@foreach ($detectedDockerfiles as $df)
|
||||
<option value="{{ $df }}">{{ $df }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
@if (count($detectedDockerComposeFiles) > 1)
|
||||
<x-forms.select wire:model.live="selectedDockerComposeFile" label="Docker Compose File"
|
||||
helper="Multiple Docker Compose files were detected. Select which one to use.">
|
||||
@foreach ($detectedDockerComposeFiles as $cf)
|
||||
<option value="{{ $cf }}">{{ $cf }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
composeLocation: '{{ $docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
// Remove trailing slashes
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Ensure leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
|
@ -109,6 +169,9 @@
|
|||
Continue
|
||||
</x-forms.button>
|
||||
</form>
|
||||
|
||||
{{-- Environment Variables Import Modal --}}
|
||||
@include('livewire.project.new.partials.env-import-modal')
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div>
|
||||
<div x-data="{ envModalOpen: false }">
|
||||
<div class="flex items-end gap-2">
|
||||
<h1>Create a new Application</h1>
|
||||
<x-modal-input buttonTitle="+ Add GitHub App" title="New GitHub App" closeOutside="false">
|
||||
|
|
@ -57,7 +57,42 @@
|
|||
<div>No repositories found. Check your GitHub App configuration.</div>
|
||||
@endif
|
||||
@if ($branches->count() > 0)
|
||||
<h2 class="text-lg font-bold">Configuration</h2>
|
||||
{{-- Repository Detection --}}
|
||||
<div class="pt-6 mt-2 border-t border-neutral-200 dark:border-coolgray-300">
|
||||
<h3 class="text-lg font-bold">Smart Scan</h3>
|
||||
<p class="pt-1 pb-3 text-sm dark:text-neutral-400">Detected configuration from your repository.</p>
|
||||
|
||||
<div wire:loading.flex wire:target="detectRepository" class="items-center gap-2 py-3 text-sm dark:text-neutral-400">
|
||||
<x-loading /> Scanning repository for Dockerfiles and configuration...
|
||||
</div>
|
||||
|
||||
@if ($detectionRan)
|
||||
<div wire:loading.remove wire:target="detectRepository">
|
||||
<div class="flex items-center gap-3 flex-wrap text-sm">
|
||||
@if (count($detectedDockerfiles))
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
Dockerfile{{ count($detectedDockerfiles) > 1 ? 's' : '' }}
|
||||
<span class="dark:text-neutral-400">({{ count($detectedDockerfiles) }})</span>
|
||||
</span>
|
||||
@endif
|
||||
@if (count($detectedDockerComposeFiles))
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
Docker Compose
|
||||
<span class="dark:text-neutral-400">({{ count($detectedDockerComposeFiles) }})</span>
|
||||
</span>
|
||||
@endif
|
||||
@include('livewire.project.new.partials.env-detection-badges')
|
||||
@if (!count($detectedDockerfiles) && !count($detectedDockerComposeFiles) && !count($detectedEnvFiles))
|
||||
<span class="dark:text-neutral-400">No Dockerfile, Docker Compose, or env files detected.</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h2 class="pt-6 text-lg font-bold">Configuration</h2>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
<form class="flex flex-col" wire:submit='submit'>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
|
|
@ -87,16 +122,33 @@
|
|||
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Dockerfile selector when multiple detected --}}
|
||||
@if ($build_pack === 'dockerfile' && count($detectedDockerfiles) > 1)
|
||||
<x-forms.select wire:model.live="selectedDockerfile" label="Dockerfile"
|
||||
helper="Multiple Dockerfiles were detected in your repository. Select which one to use.">
|
||||
@foreach ($detectedDockerfiles as $df)
|
||||
<option value="{{ $df }}">{{ $df }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
@if (count($detectedDockerComposeFiles) > 1)
|
||||
<x-forms.select wire:model.live="selectedDockerComposeFile" label="Docker Compose File"
|
||||
helper="Multiple Docker Compose files were detected. Select which one to use.">
|
||||
@foreach ($detectedDockerComposeFiles as $cf)
|
||||
<option value="{{ $cf }}">{{ $cf }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
composeLocation: '{{ $docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
// Remove trailing slashes
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Ensure leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
|
@ -143,6 +195,9 @@
|
|||
@endif
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Environment Variables Import Modal --}}
|
||||
@include('livewire.project.new.partials.env-import-modal')
|
||||
@else
|
||||
<div class="hero">
|
||||
No GitHub Application found. Please create a new GitHub Application.
|
||||
|
|
|
|||
|
|
@ -0,0 +1,20 @@
|
|||
@if (count($detectedEnvFiles) > 0 && $envImported && count($envExampleVars) > 0)
|
||||
<button type="button" @click="envModalOpen = true"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-success/30 dark:border-success/30 hover:dark:border-success transition-colors cursor-pointer">
|
||||
<span class="badge badge-success"></span>
|
||||
{{ $selectedEnvFile }}
|
||||
<span class="text-success text-xs">({{ count($envExampleVars) }} imported)</span>
|
||||
</button>
|
||||
@elseif (count($detectedEnvFiles) > 0 && count($envExampleVars) > 0)
|
||||
<button type="button" @click="envModalOpen = true"
|
||||
class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300 hover:dark:border-warning transition-colors cursor-pointer">
|
||||
<span class="badge badge-success"></span>
|
||||
{{ count($detectedEnvFiles) > 1 ? 'Env Files (' . count($detectedEnvFiles) . ')' : $detectedEnvFiles[0] }}
|
||||
<span class="dark:text-warning text-xs">(click to import)</span>
|
||||
</button>
|
||||
@elseif (count($detectedEnvFiles) > 0)
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
{{ count($detectedEnvFiles) > 1 ? 'Env Files (' . count($detectedEnvFiles) . ')' : $detectedEnvFiles[0] }}
|
||||
</span>
|
||||
@endif
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
@if (count($envExampleVars) > 0)
|
||||
<template x-teleport="body">
|
||||
<div x-show="envModalOpen" @keydown.window.escape="envModalOpen = false"
|
||||
class="fixed top-0 left-0 z-99 flex items-center justify-center w-screen h-screen p-4">
|
||||
<div x-show="envModalOpen" x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
|
||||
x-transition:leave="ease-in duration-100" x-transition:leave-start="opacity-100"
|
||||
x-transition:leave-end="opacity-0" @click="envModalOpen = false"
|
||||
class="absolute inset-0 w-full h-full bg-black/20 backdrop-blur-xs"></div>
|
||||
<div x-show="envModalOpen" x-trap.inert.noscroll="envModalOpen"
|
||||
x-transition:enter="ease-out duration-100"
|
||||
x-transition:enter-start="opacity-0 -translate-y-2 sm:scale-95"
|
||||
x-transition:enter-end="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave="ease-in duration-100"
|
||||
x-transition:leave-start="opacity-100 translate-y-0 sm:scale-100"
|
||||
x-transition:leave-end="opacity-0 -translate-y-2 sm:scale-95"
|
||||
class="relative w-full lg:w-auto lg:min-w-xl lg:max-w-2xl border rounded-sm drop-shadow-sm bg-white border-neutral-200 dark:bg-base dark:border-coolgray-300 flex flex-col">
|
||||
<div class="flex items-center justify-between py-6 px-6 shrink-0">
|
||||
<h3 class="text-lg font-bold">Import from {{ $selectedEnvFile }}</h3>
|
||||
<button @click="envModalOpen = false"
|
||||
class="absolute top-0 right-0 flex items-center justify-center w-8 h-8 mt-5 mr-5 rounded-full dark:text-white hover:bg-neutral-100 dark:hover:bg-coolgray-300 outline-0">
|
||||
<svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@if (count($detectedEnvFiles) > 1)
|
||||
<div class="px-6 pb-3">
|
||||
<x-forms.select wire:model.live="selectedEnvFile" label="Env File"
|
||||
helper="Multiple env files were detected. Select which one to import.">
|
||||
@foreach ($detectedEnvFiles as $envFile)
|
||||
<option value="{{ $envFile }}">{{ $envFile }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
</div>
|
||||
@endif
|
||||
<div class="px-6 pb-3 text-sm dark:text-neutral-400">
|
||||
Review and edit the values below. Imported variables will be added to your application when you continue.
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 px-6 pb-2 max-h-80 overflow-y-auto scrollbar">
|
||||
@foreach ($envExampleVars as $key => $value)
|
||||
<div class="flex gap-3 items-center">
|
||||
<label class="w-1/3 text-sm font-mono truncate dark:text-neutral-400" title="{{ $key }}">{{ $key }}</label>
|
||||
<input type="text" class="w-2/3 input"
|
||||
wire:model="envExampleVars.{{ $key }}" />
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 px-6 py-5">
|
||||
<x-forms.button @click="envModalOpen = false">
|
||||
Close
|
||||
</x-forms.button>
|
||||
<div class="flex items-center gap-2">
|
||||
@if ($envImported)
|
||||
<x-forms.button wire:click="clearEnvVars" @click="envModalOpen = false">
|
||||
Remove Import
|
||||
</x-forms.button>
|
||||
@endif
|
||||
<x-forms.button isHighlighted wire:click="confirmEnvImport" @click="envModalOpen = false">
|
||||
{{ $envImported ? 'Update' : 'Import' }} {{ count($envExampleVars) }} Variable{{ count($envExampleVars) > 1 ? 's' : '' }}
|
||||
</x-forms.button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@endif
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<div x-data x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
|
||||
<div x-data="{ envModalOpen: false }" x-init="$nextTick(() => { if ($refs.autofocusInput) $refs.autofocusInput.focus(); })">
|
||||
<h1>Create a new Application</h1>
|
||||
<div class="pb-8">Deploy any public Git repositories.</div>
|
||||
|
||||
|
|
@ -29,8 +29,44 @@
|
|||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Repository Detection --}}
|
||||
<div class="pt-6 mt-6 border-t border-neutral-200 dark:border-coolgray-300">
|
||||
<h3 class="text-lg font-bold">Smart Scan</h3>
|
||||
<p class="pt-1 pb-3 text-sm dark:text-neutral-400">Detected configuration from your repository.</p>
|
||||
|
||||
<div wire:loading.flex wire:target="detectRepository" class="items-center gap-2 py-3 text-sm dark:text-neutral-400">
|
||||
<x-loading /> Scanning repository for Dockerfiles and configuration...
|
||||
</div>
|
||||
|
||||
@if ($detectionRan)
|
||||
<div wire:loading.remove wire:target="detectRepository">
|
||||
<div class="flex items-center gap-3 flex-wrap text-sm">
|
||||
@if (count($detectedDockerfiles))
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
Dockerfile{{ count($detectedDockerfiles) > 1 ? 's' : '' }}
|
||||
<span class="dark:text-neutral-400">({{ count($detectedDockerfiles) }})</span>
|
||||
</span>
|
||||
@endif
|
||||
@if (count($detectedDockerComposeFiles))
|
||||
<span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-sm dark:bg-coolgray-100 border border-neutral-200 dark:border-coolgray-300">
|
||||
<span class="badge badge-success"></span>
|
||||
Docker Compose
|
||||
<span class="dark:text-neutral-400">({{ count($detectedDockerComposeFiles) }})</span>
|
||||
</span>
|
||||
@endif
|
||||
@include('livewire.project.new.partials.env-detection-badges')
|
||||
@if (!count($detectedDockerfiles) && !count($detectedDockerComposeFiles) && !count($detectedEnvFiles))
|
||||
<span class="dark:text-neutral-400">No Dockerfile, Docker Compose, or env files detected.</span>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<!-- Application Configuration Form -->
|
||||
<form class="flex flex-col gap-2 pt-4" wire:submit='submit'>
|
||||
<h3 class="pt-6 text-lg font-bold">Configuration</h3>
|
||||
<form class="flex flex-col gap-2 pt-2" wire:submit='submit'>
|
||||
<div class="flex flex-col gap-2 pb-6">
|
||||
<div class="flex gap-2">
|
||||
@if ($git_source === 'other')
|
||||
|
|
@ -51,16 +87,33 @@
|
|||
helper="If there is a build process involved (like Svelte, React, Next, etc..), please specify the output directory for the build assets." />
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Dockerfile selector when multiple detected --}}
|
||||
@if ($build_pack === 'dockerfile' && count($detectedDockerfiles) > 1)
|
||||
<x-forms.select wire:model.live="selectedDockerfile" label="Dockerfile"
|
||||
helper="Multiple Dockerfiles were detected in your repository. Select which one to use.">
|
||||
@foreach ($detectedDockerfiles as $df)
|
||||
<option value="{{ $df }}">{{ $df }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
|
||||
@if ($build_pack === 'dockercompose')
|
||||
@if (count($detectedDockerComposeFiles) > 1)
|
||||
<x-forms.select wire:model.live="selectedDockerComposeFile" label="Docker Compose File"
|
||||
helper="Multiple Docker Compose files were detected. Select which one to use.">
|
||||
@foreach ($detectedDockerComposeFiles as $cf)
|
||||
<option value="{{ $cf }}">{{ $cf }}</option>
|
||||
@endforeach
|
||||
</x-forms.select>
|
||||
@endif
|
||||
<div x-data="{
|
||||
baseDir: '{{ $base_directory }}',
|
||||
composeLocation: '{{ $docker_compose_location }}',
|
||||
normalizePath(path) {
|
||||
if (!path || path.trim() === '') return '/';
|
||||
path = path.trim();
|
||||
// Remove trailing slashes
|
||||
path = path.replace(/\/+$/, '');
|
||||
// Ensure leading slash
|
||||
if (!path.startsWith('/')) {
|
||||
path = '/' + path;
|
||||
}
|
||||
|
|
@ -102,5 +155,8 @@
|
|||
Continue
|
||||
</x-forms.button>
|
||||
</form>
|
||||
|
||||
{{-- Environment Variables Import Modal --}}
|
||||
@include('livewire.project.new.partials.env-import-modal')
|
||||
@endif
|
||||
</div>
|
||||
|
|
|
|||
104
tests/Unit/RepositoryDetectionResultTest.php
Normal file
104
tests/Unit/RepositoryDetectionResultTest.php
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
use App\Data\RepositoryDetectionResult;
|
||||
use App\Enums\BuildPackTypes;
|
||||
|
||||
test('empty result returns correct defaults', function () {
|
||||
$result = RepositoryDetectionResult::none();
|
||||
|
||||
expect($result->dockerfiles)->toBe([])
|
||||
->and($result->dockerComposeFiles)->toBe([])
|
||||
->and($result->envFiles)->toBe([])
|
||||
->and($result->dockerfilePorts)->toBe([]);
|
||||
});
|
||||
|
||||
test('getSuggestedBuildPack returns dockercompose when compose files found', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
dockerfiles: ['Dockerfile'],
|
||||
dockerComposeFiles: ['docker-compose.yml'],
|
||||
);
|
||||
|
||||
expect($result->getSuggestedBuildPack())->toBe(BuildPackTypes::DOCKERCOMPOSE);
|
||||
});
|
||||
|
||||
test('getSuggestedBuildPack returns dockerfile when only dockerfiles found', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
dockerfiles: ['Dockerfile'],
|
||||
);
|
||||
|
||||
expect($result->getSuggestedBuildPack())->toBe(BuildPackTypes::DOCKERFILE);
|
||||
});
|
||||
|
||||
test('getSuggestedBuildPack returns nixpacks when nothing found', function () {
|
||||
$result = RepositoryDetectionResult::none();
|
||||
|
||||
expect($result->getSuggestedBuildPack())->toBe(BuildPackTypes::NIXPACKS);
|
||||
});
|
||||
|
||||
test('hasDockerfile returns true when dockerfiles present', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
dockerfiles: ['Dockerfile', 'apps/api/Dockerfile'],
|
||||
);
|
||||
|
||||
expect($result->hasDockerfile())->toBeTrue();
|
||||
});
|
||||
|
||||
test('hasDockerfile returns false when no dockerfiles', function () {
|
||||
$result = RepositoryDetectionResult::none();
|
||||
|
||||
expect($result->hasDockerfile())->toBeFalse();
|
||||
});
|
||||
|
||||
test('hasDockerCompose returns true when compose files present', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
dockerComposeFiles: ['docker-compose.yml'],
|
||||
);
|
||||
|
||||
expect($result->hasDockerCompose())->toBeTrue();
|
||||
});
|
||||
|
||||
test('hasDockerCompose returns false when no compose files', function () {
|
||||
$result = RepositoryDetectionResult::none();
|
||||
|
||||
expect($result->hasDockerCompose())->toBeFalse();
|
||||
});
|
||||
|
||||
test('dockerfilePorts stores port mapping correctly', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
dockerfiles: ['Dockerfile', 'apps/api/Dockerfile'],
|
||||
dockerfilePorts: ['Dockerfile' => 3000, 'apps/api/Dockerfile' => 8080],
|
||||
);
|
||||
|
||||
expect($result->dockerfilePorts)->toBe(['Dockerfile' => 3000, 'apps/api/Dockerfile' => 8080])
|
||||
->and($result->dockerfilePorts['Dockerfile'])->toBe(3000)
|
||||
->and($result->dockerfilePorts['apps/api/Dockerfile'])->toBe(8080);
|
||||
});
|
||||
|
||||
test('hasEnvFiles returns true when env files present', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
envFiles: ['.env.example' => 'APP_KEY=secret'],
|
||||
);
|
||||
|
||||
expect($result->hasEnvFiles())->toBeTrue();
|
||||
});
|
||||
|
||||
test('hasEnvFiles returns false when no env files', function () {
|
||||
$result = RepositoryDetectionResult::none();
|
||||
|
||||
expect($result->hasEnvFiles())->toBeFalse();
|
||||
});
|
||||
|
||||
test('envFiles stores multiple files with content', function () {
|
||||
$result = new RepositoryDetectionResult(
|
||||
envFiles: [
|
||||
'.env.example' => 'APP_KEY=secret',
|
||||
'.env.sample' => 'DB_HOST=localhost',
|
||||
'.env.dist' => null,
|
||||
],
|
||||
);
|
||||
|
||||
expect($result->envFiles)->toHaveCount(3)
|
||||
->and($result->envFiles['.env.example'])->toBe('APP_KEY=secret')
|
||||
->and($result->envFiles['.env.sample'])->toBe('DB_HOST=localhost')
|
||||
->and($result->envFiles['.env.dist'])->toBeNull();
|
||||
});
|
||||
217
tests/Unit/RepositoryDetectorTest.php
Normal file
217
tests/Unit/RepositoryDetectorTest.php
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\BuildPackTypes;
|
||||
use App\Services\RepositoryDetector;
|
||||
|
||||
test('parseOutput handles complete detection output', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => ['Dockerfile', 'apps/api/Dockerfile'],
|
||||
'dockerComposeFiles' => ['docker-compose.yml'],
|
||||
'envFiles' => ['.env.example' => "APP_NAME=MyApp\nAPP_ENV=production\nDB_HOST=localhost\nDB_PORT=5432"],
|
||||
'dockerfilePorts' => ['Dockerfile' => 3000, 'apps/api/Dockerfile' => 8080],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->dockerfiles)->toBe(['Dockerfile', 'apps/api/Dockerfile'])
|
||||
->and($result->dockerComposeFiles)->toBe(['docker-compose.yml'])
|
||||
->and($result->envFiles)->toHaveKey('.env.example')
|
||||
->and($result->envFiles['.env.example'])->toContain('APP_NAME=MyApp')
|
||||
->and($result->dockerfilePorts)->toBe(['Dockerfile' => 3000, 'apps/api/Dockerfile' => 8080])
|
||||
->and($result->getSuggestedBuildPack())->toBe(BuildPackTypes::DOCKERCOMPOSE);
|
||||
});
|
||||
|
||||
test('parseOutput handles empty repository', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => [],
|
||||
'dockerComposeFiles' => [],
|
||||
'envFiles' => (object) [],
|
||||
'dockerfilePorts' => (object) [],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->dockerfiles)->toBe([])
|
||||
->and($result->dockerComposeFiles)->toBe([])
|
||||
->and($result->envFiles)->toBe([])
|
||||
->and($result->dockerfilePorts)->toBe([])
|
||||
->and($result->getSuggestedBuildPack())->toBe(BuildPackTypes::NIXPACKS);
|
||||
});
|
||||
|
||||
test('parseOutput handles dockerfile without EXPOSE', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => ['Dockerfile'],
|
||||
'dockerComposeFiles' => [],
|
||||
'envFiles' => (object) [],
|
||||
'dockerfilePorts' => ['Dockerfile' => null],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->dockerfiles)->toBe(['Dockerfile'])
|
||||
->and($result->dockerfilePorts)->toBe(['Dockerfile' => null])
|
||||
->and($result->getSuggestedBuildPack())->toBe(BuildPackTypes::DOCKERFILE);
|
||||
});
|
||||
|
||||
test('parseOutput handles only env files without dockerfiles', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => [],
|
||||
'dockerComposeFiles' => [],
|
||||
'envFiles' => ['.env.example' => "SECRET_KEY=changeme\nDATABASE_URL=postgres://localhost/db"],
|
||||
'dockerfilePorts' => (object) [],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->dockerfiles)->toBe([])
|
||||
->and($result->envFiles)->toHaveKey('.env.example')
|
||||
->and($result->envFiles['.env.example'])->toContain('SECRET_KEY=changeme')
|
||||
->and($result->envFiles['.env.example'])->toContain('DATABASE_URL=postgres://localhost/db')
|
||||
->and($result->getSuggestedBuildPack())->toBe(BuildPackTypes::NIXPACKS);
|
||||
});
|
||||
|
||||
test('parseOutput handles multiple compose files', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => [],
|
||||
'dockerComposeFiles' => ['docker-compose.yml', 'compose.yaml'],
|
||||
'envFiles' => (object) [],
|
||||
'dockerfilePorts' => (object) [],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->dockerComposeFiles)->toBe(['docker-compose.yml', 'compose.yaml'])
|
||||
->and($result->getSuggestedBuildPack())->toBe(BuildPackTypes::DOCKERCOMPOSE);
|
||||
});
|
||||
|
||||
test('parseOutput handles multiple env files', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => [],
|
||||
'dockerComposeFiles' => [],
|
||||
'envFiles' => [
|
||||
'.env.example' => "APP_KEY=base64:abc\nAPP_ENV=local",
|
||||
'.env.sample' => "DB_HOST=127.0.0.1\nDB_PORT=5432",
|
||||
'.env.dist' => 'REDIS_HOST=localhost',
|
||||
],
|
||||
'dockerfilePorts' => (object) [],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->envFiles)->toHaveCount(3)
|
||||
->and($result->envFiles['.env.example'])->toContain('APP_KEY=base64:abc')
|
||||
->and($result->envFiles['.env.sample'])->toContain('DB_HOST=127.0.0.1')
|
||||
->and($result->envFiles['.env.dist'])->toContain('REDIS_HOST=localhost')
|
||||
->and($result->hasEnvFiles())->toBeTrue();
|
||||
});
|
||||
|
||||
test('parseOutput handles string port values from JSON', function () {
|
||||
$output = json_encode([
|
||||
'dockerfiles' => ['Dockerfile'],
|
||||
'dockerComposeFiles' => [],
|
||||
'envFiles' => (object) [],
|
||||
'dockerfilePorts' => ['Dockerfile' => '3000'],
|
||||
]);
|
||||
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, $output);
|
||||
|
||||
expect($result->dockerfilePorts)->toBe(['Dockerfile' => 3000])
|
||||
->and($result->dockerfilePorts['Dockerfile'])->toBeInt();
|
||||
});
|
||||
|
||||
test('parseOutput returns none for invalid JSON', function () {
|
||||
$detector = new RepositoryDetector(
|
||||
repositoryUrl: 'https://github.com/test/repo',
|
||||
branch: 'main',
|
||||
baseDirectory: '/',
|
||||
serverId: 1,
|
||||
teamId: 1,
|
||||
);
|
||||
|
||||
$reflection = new ReflectionClass($detector);
|
||||
$method = $reflection->getMethod('parseOutput');
|
||||
|
||||
$result = $method->invoke($detector, 'not valid json');
|
||||
|
||||
expect($result->dockerfiles)->toBe([])
|
||||
->and($result->dockerComposeFiles)->toBe([])
|
||||
->and($result->envFiles)->toBe([])
|
||||
->and($result->dockerfilePorts)->toBe([]);
|
||||
});
|
||||
Loading…
Reference in a new issue