diff --git a/app/Filament/Pages/DeploymentPackage.php b/app/Filament/Pages/DeploymentPackage.php index 2ab29f0..a9ddf7b 100644 --- a/app/Filament/Pages/DeploymentPackage.php +++ b/app/Filament/Pages/DeploymentPackage.php @@ -4,6 +4,7 @@ use App\Services\Deployment\ChangeSetAnalyzer; use App\Services\Deployment\DeploymentGuard; +use App\Services\Deployment\DeploymentPackageBuilder; use App\Services\Deployment\DeploymentTargets; use App\Services\Deployment\GitRepository; use Filament\Forms\Components\Select; @@ -18,10 +19,10 @@ use RuntimeException; /** - * Deployment csomagoló - fázis 1: célkörnyezet, commitlista és diff-előnézet. + * Deployment csomagoló: célkörnyezet, commit-tartomány, diff-előnézet és csomagolás. * - * Ez az oldal (a fázis 1-ben) semmit nem ír a fájlrendszerre, csak olvassa a git - * történetet. A tényleges csomagolás a fázis 2-ben érkezik. + * Az oldal a git történetet olvassa, és a kijelölt tartományból egy időbélyeges mappát + * állít elő a config('deployment.output_path') alatt - a repón kívülre nem ír. * * A hozzáférést a DeploymentGuard dönti el (env kapcsoló + stage whitelist + * developer szerep + feature flag), és a mount()-on kívül minden akció elején @@ -50,6 +51,12 @@ class DeploymentPackage extends Page implements HasForms public ?string $gitError = null; + /** @var array a felületen kézzel kivett fájlok */ + public array $skippedPaths = []; + + /** @var array|null az utoljára elkészített csomag adatai */ + public ?array $lastPackage = null; + public static function canAccess(): bool { return app(DeploymentGuard::class)->isAllowed(auth()->user()); @@ -132,6 +139,67 @@ public function clearRange(): void { $this->fromCommit = null; $this->toCommit = null; + $this->skippedPaths = []; + } + + /** + * Egy fájl kézi ki-/visszavétele a csomagból. + */ + public function toggleFile(string $path): void + { + app(DeploymentGuard::class)->ensureAllowed(auth()->user()); + + $this->skippedPaths = in_array($path, $this->skippedPaths, true) + ? array_values(array_diff($this->skippedPaths, [$path])) + : array_merge($this->skippedPaths, [$path]); + } + + /** + * A csomag előállítása a kijelölt tartományból. + */ + public function createPackage(): void + { + app(DeploymentGuard::class)->ensureAllowed(auth()->user()); + + if (! $this->fromCommit || ! $this->toCommit) { + Notification::make() + ->title('Nincs kijelölt tartomány') + ->body('Válassz egy kezdő és egy záró commitot.') + ->danger() + ->send(); + + return; + } + + try { + $this->lastPackage = app(DeploymentPackageBuilder::class)->build( + (string) ($this->data['target'] ?? ''), + (string) ($this->data['branch'] ?? ''), + $this->fromCommit, + $this->toCommit, + $this->skippedPaths, + auth()->user()?->email, + ); + } catch (RuntimeException $exception) { + Notification::make() + ->title('A csomag nem készült el') + ->body($exception->getMessage()) + ->danger() + ->persistent() + ->send(); + + return; + } + + Notification::make() + ->title('A csomag elkészült') + ->body(sprintf( + '%d fájl a(z) %s mappában.', + $this->lastPackage['counts']['copied'], + $this->lastPackage['name'], + )) + ->success() + ->send(); } /** diff --git a/app/Services/Deployment/DeploymentGuard.php b/app/Services/Deployment/DeploymentGuard.php index 623d421..bd9e425 100644 --- a/app/Services/Deployment/DeploymentGuard.php +++ b/app/Services/Deployment/DeploymentGuard.php @@ -4,6 +4,7 @@ use App\Models\User; use Laravel\Pennant\Feature; +use RuntimeException; /** * A deployment csomagoló hozzáférés-ellenőrzése egy helyen. @@ -26,9 +27,12 @@ public function isAllowed(?User $user = null): bool } /** - * Az első nem teljesülő feltétel magyarázata, vagy null, ha minden rendben. + * Csak a környezeti feltételek (1-2. réteg), felhasználó nélkül. + * + * Ezt hívja a csomagoló service is: így akkor sem fut le, ha valaki a felületet + * megkerülve (konzolról, jobból) indítaná el egy szerveren. */ - public function denialReason(?User $user = null): ?string + public function environmentDenialReason(): ?string { if (! config('deployment.enabled')) { return 'A deployment csomagoló ki van kapcsolva (DEPLOYMENT_PACKAGE_ENABLED).'; @@ -45,6 +49,18 @@ public function denialReason(?User $user = null): ?string ); } + return null; + } + + /** + * Az első nem teljesülő feltétel magyarázata, vagy null, ha minden rendben. + */ + public function denialReason(?User $user = null): ?string + { + if ($reason = $this->environmentDenialReason()) { + return $reason; + } + if (! $user) { return 'Bejelentkezés szükséges.'; } @@ -66,4 +82,11 @@ public function ensureAllowed(?User $user = null): void abort(403, $reason); } } + + public function ensureEnvironmentAllowed(): void + { + if ($reason = $this->environmentDenialReason()) { + throw new RuntimeException($reason); + } + } } diff --git a/app/Services/Deployment/DeploymentPackageBuilder.php b/app/Services/Deployment/DeploymentPackageBuilder.php new file mode 100644 index 0000000..b49b54d --- /dev/null +++ b/app/Services/Deployment/DeploymentPackageBuilder.php @@ -0,0 +1,354 @@ + $skippedPaths a felületen kézzel kivett fájlok + * @return array{ + * name:string, path:string, target:string, branch:string, + * from:string, to:string, created_at:string, created_by:string, + * counts:array{copied:int, deleted:int, excluded:int, skipped:int, missing:int}, + * files:array, + * deleted:array, skipped:array, missing:array, + * warnings:array + * } + */ + public function build( + string $targetKey, + string $branch, + string $fromCommit, + string $toCommit, + array $skippedPaths = [], + ?string $createdBy = null, + ): array { + $this->guard->ensureEnvironmentAllowed(); + + $target = $this->targets->find($targetKey); + + if (! $target) { + throw new RuntimeException('Ismeretlen célkörnyezet.'); + } + + $from = $this->repository->resolveCommit($fromCommit); + $to = $this->repository->resolveCommit($toCommit); + + if (! $from || ! $to) { + throw new RuntimeException('Ismeretlen commit azonosító.'); + } + + $changeSet = $this->analyzer->analyze( + $this->repository->changedFiles($from, $to), + $target, + $branch, + ); + + $statuses = []; + + foreach ($changeSet['files'] as $file) { + $statuses[$file['path']] = $file['status']; + } + + $skipped = array_values(array_intersect($changeSet['copied'], $skippedPaths)); + $copy = array_values(array_diff($changeSet['copied'], $skippedPaths)); + $maxFiles = (int) config('deployment.max_files', 500); + + if (count($copy) > $maxFiles) { + throw new RuntimeException(sprintf( + 'A csomag %d fájlt tartalmazna, a limit %d. Szűkítsd a commit-tartományt.', + count($copy), + $maxFiles, + )); + } + + if ($copy === [] && $changeSet['deleted'] === []) { + throw new RuntimeException('Nincs mit csomagolni: a tartományban nincs a célgépre kerülő változás.'); + } + + $createdAt = Carbon::now(); + $path = $this->createDirectory($targetKey, $createdAt); + $written = $this->repository->archiveTo($to, $copy, $path); + + // Ha a git archive kihagyott valamit (pl. .gitattributes export-ignore), az itt + // derül ki - csendben hiányzó fájl a manuális deploynál pont a legrosszabb hiba. + $missing = array_values(array_diff($copy, $written)); + + $manifest = [ + 'name' => basename($path), + 'path' => $path, + 'target' => $targetKey, + 'branch' => $branch, + 'from' => $from, + 'to' => $to, + 'created_at' => $createdAt->toIso8601String(), + 'created_by' => $createdBy ?? 'ismeretlen', + 'counts' => [ + 'copied' => count($written), + 'deleted' => count($changeSet['deleted']), + 'excluded' => $changeSet['counts']['excluded'], + 'skipped' => count($skipped), + 'missing' => count($missing), + ], + 'files' => $this->describeFiles($path, $written, $statuses), + 'deleted' => $changeSet['deleted'], + 'skipped' => $skipped, + 'missing' => $missing, + 'warnings' => $changeSet['warnings'], + ]; + + $this->writeVersionMarker($path, $manifest, $target); + $this->writeManifest($path, $manifest, $target); + $this->writeDeleteList($path, $manifest, $target); + $this->writeInstructions($path, $manifest, $target); + + return $manifest; + } + + /** + * @param array $written + * @param array $statuses + * @return array + */ + private function describeFiles(string $path, array $written, array $statuses): array + { + sort($written); + + return array_map(function (string $relative) use ($path, $statuses): array { + $absolute = $path.DIRECTORY_SEPARATOR.str_replace('/', DIRECTORY_SEPARATOR, $relative); + + return [ + 'path' => $relative, + 'status' => $statuses[$relative] ?? '?', + 'size' => $this->files->exists($absolute) ? $this->files->size($absolute) : 0, + 'sha1' => $this->files->exists($absolute) ? (string) sha1_file($absolute) : '', + ]; + }, $written); + } + + private function createDirectory(string $targetKey, Carbon $createdAt): string + { + $base = rtrim((string) config('deployment.output_path'), '/\\'); + $name = sprintf('deploy_%s_%s', $targetKey, $createdAt->format('Ymd_His')); + $path = $base.DIRECTORY_SEPARATOR.$name; + + // Ugyanabban a másodpercben indított második csomag ne írja felül az elsőt. + $suffix = 2; + + while ($this->files->exists($path)) { + $path = $base.DIRECTORY_SEPARATOR.$name.'-'.$suffix++; + } + + $this->files->makeDirectory($path, 0775, true); + + return $path; + } + + /** + * A kirakott verzió azonosítója - ettől kezdve a "melyik commit fut a szerveren?" + * kérdés egy HTTP kéréssel megválaszolható, nem kézi fájldiffel. + * + * @param array $manifest + * @param array $target + */ + private function writeVersionMarker(string $path, array $manifest, array $target): void + { + $this->put($path.'/public/deploy-version.json', json_encode([ + 'target' => $target['key'], + 'commit' => $manifest['to'], + 'short' => substr((string) $manifest['to'], 0, 7), + 'branch' => $manifest['branch'], + 'packaged_at' => $manifest['created_at'], + 'by' => $manifest['created_by'], + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)."\n"); + } + + /** + * @param array $manifest + * @param array $target + */ + private function writeManifest(string $path, array $manifest, array $target): void + { + $this->put( + $path.'/_MANIFEST.json', + json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)."\n", + ); + + $lines = [ + 'DEPLOYMENT CSOMAG', + str_repeat('=', 60), + 'Csomag: '.$manifest['name'], + 'Célkörnyezet: '.$target['label'], + 'Cél útvonal: '.$target['remote_path'], + 'Branch: '.$manifest['branch'], + 'Tartomány: '.substr((string) $manifest['from'], 0, 7).' (kizárva) -> '.substr((string) $manifest['to'], 0, 7).' (beleértve)', + 'Készítette: '.$manifest['created_by'], + 'Készült: '.$manifest['created_at'], + '', + sprintf('MÁSOLANDÓ FÁJLOK (%d)', $manifest['counts']['copied']), + str_repeat('-', 60), + ]; + + foreach ($manifest['files'] as $file) { + // mb_str_pad, mert a sprintf %-70s bájtban számol - egy ékezetes fájlnév + // elcsúsztatná az oszlopot. + $lines[] = sprintf( + '%-2s %s %8d %s', + $file['status'], + mb_str_pad($file['path'], 70), + $file['size'], + $file['sha1'], + ); + } + + if ($manifest['deleted'] !== []) { + $lines[] = ''; + $lines[] = sprintf('TÖRLENDŐ A CÉLGÉPEN (%d) - ld. _TORLENDO.txt', $manifest['counts']['deleted']); + $lines[] = str_repeat('-', 60); + + foreach ($manifest['deleted'] as $deleted) { + $lines[] = ' '.$deleted; + } + } + + if ($manifest['skipped'] !== []) { + $lines[] = ''; + $lines[] = sprintf('KÉZZEL KIVÉVE A CSOMAGBÓL (%d)', $manifest['counts']['skipped']); + $lines[] = str_repeat('-', 60); + + foreach ($manifest['skipped'] as $skipped) { + $lines[] = ' '.$skipped; + } + } + + if ($manifest['missing'] !== []) { + $lines[] = ''; + $lines[] = sprintf('FIGYELEM - NEM KERÜLT A CSOMAGBA (%d)', $manifest['counts']['missing']); + $lines[] = str_repeat('-', 60); + + foreach ($manifest['missing'] as $missing) { + $lines[] = ' '.$missing; + } + } + + $this->put($path.'/_MANIFEST.txt', implode("\n", $lines)."\n"); + } + + /** + * @param array $manifest + * @param array $target + */ + private function writeDeleteList(string $path, array $manifest, array $target): void + { + $lines = [ + '# '.$manifest['name'].' - a célgépen TÖRLENDŐ fájlok', + '# Cél: '.$target['label'].' - '.$target['remote_path'], + '# A másolás ezeket nem intézi el, a törlés kézi lépés.', + '', + ]; + + foreach ($manifest['deleted'] as $deleted) { + $lines[] = $deleted; + } + + if ($manifest['deleted'] === []) { + $lines[] = '# (nincs törlendő fájl ebben a csomagban)'; + } + + $this->put($path.'/_TORLENDO.txt', implode("\n", $lines)."\n"); + } + + /** + * @param array $manifest + * @param array $target + */ + private function writeInstructions(string $path, array $manifest, array $target): void + { + $steps = [ + 'A csomag mappájának **tartalmát** másold fel ide: `'.$target['remote_path'].'`' + .' — mappa-összeolvasztás, NEM a teljes app mappa cseréje. A `_` kezdetű fájlokat ne másold fel.', + ]; + + if ($manifest['deleted'] !== []) { + $steps[] = sprintf('Töröld a `_TORLENDO.txt`-ben felsorolt %d fájlt a célgépen.', $manifest['counts']['deleted']); + } + + foreach ($manifest['warnings'] as $warning) { + if (str_contains($warning['title'], 'Migráció')) { + $steps[] = 'Futtasd a célgépen: `php artisan migrate`'; + } + + if (str_contains($warning['title'], 'composer')) { + $steps[] = 'Futtasd a célgépen: `composer install --no-dev` (a vendor/ nincs a csomagban)'; + } + + if (str_contains($warning['title'], 'Frontend')) { + $steps[] = 'A fordított assetek NINCSENEK a csomagban (`.gitignore`) — `npm run build` után kézzel másold fel a `public/build` tartalmát.'; + } + } + + $steps[] = 'Ürítsd a cache-t: `php artisan config:clear && php artisan view:clear && php artisan route:clear`'; + $steps[] = 'Ellenőrizd a kirakott verziót: `curl https://'.$target['domain'].'/deploy-version.json`' + .' — a commitnak `'.substr((string) $manifest['to'], 0, 7).'` kell lennie.'; + + $lines = [ + '# Teendők — '.$manifest['name'], + '', + '| | |', + '|---|---|', + '| Célkörnyezet | '.$target['label'].' |', + '| Cél útvonal | `'.$target['remote_path'].'` |', + '| Tartomány | `'.substr((string) $manifest['from'], 0, 7).'` → `'.substr((string) $manifest['to'], 0, 7).'` |', + '| Fájlok | '.$manifest['counts']['copied'].' másolandó, '.$manifest['counts']['deleted'].' törlendő |', + '', + ]; + + foreach ($steps as $index => $step) { + $lines[] = ($index + 1).'. '.$step; + } + + if ($manifest['missing'] !== []) { + $lines[] = ''; + $lines[] = '> **Figyelem:** '.$manifest['counts']['missing'].' fájl nem került a csomagba, ' + .'a listájuk a `_MANIFEST.txt` végén van. Ezeket ellenőrizd, mielőtt feltöltesz.'; + } + + $this->put($path.'/_TEENDOK.md', implode("\n", $lines)."\n"); + } + + /** + * Mindig LF sorvéggel írunk: a csomag Windowson készül, de Linux célgépre megy. + */ + private function put(string $path, string $contents): void + { + $directory = dirname($path); + + if (! $this->files->isDirectory($directory)) { + $this->files->makeDirectory($directory, 0775, true); + } + + $this->files->put($path, str_replace("\r\n", "\n", $contents)); + } +} diff --git a/app/Services/Deployment/GitRepository.php b/app/Services/Deployment/GitRepository.php index 4d84b2f..1fa9e10 100644 --- a/app/Services/Deployment/GitRepository.php +++ b/app/Services/Deployment/GitRepository.php @@ -4,6 +4,7 @@ use RuntimeException; use Symfony\Component\Process\Process; +use ZipArchive; /** * Csak olvasó git wrapper a deployment csomagolóhoz. @@ -227,6 +228,98 @@ public function changedFiles(string $from, string $to): array return $files; } + /** + * A megadott commit szerinti fájltartalmat írja ki a célmappába, relatív útvonalakat megőrizve. + * + * Szándékosan a git objektumtárból dolgozunk (git archive) és nem a working tree-ből: + * így garantáltan a commitolt állapot kerül a csomagba (nem szivárog ki félkész + * módosítás), és a .gitattributes `text=auto eol=lf` miatt LF sorvégekkel, ami a + * Linux célgépeknek kell - a working tree másolása ezt egyik esetben sem garantálná. + * + * @param array $paths + * @return array a ténylegesen kiírt útvonalak + */ + public function archiveTo(string $commit, array $paths, string $targetDirectory): array + { + // Pathspec nélkül a git archive a TELJES fát csomagolná - üres listánál ezért + // nem hívhatjuk meg egyáltalán. + if ($paths === []) { + return []; + } + + $resolved = $this->resolveCommit($commit); + + if (! $resolved) { + throw new RuntimeException('Ismeretlen commit azonosító.'); + } + + $written = []; + + // Az útvonalak argumentumként mennek: Windowson a CreateProcess parancssora + // ~32 000 karakter, ezért darabolunk. + foreach (array_chunk($paths, 100) as $chunk) { + $archivePath = tempnam(sys_get_temp_dir(), 'deployment-archive-'); + + if ($archivePath === false) { + throw new RuntimeException('Nem hozható létre ideiglenes fájl a csomagoláshoz.'); + } + + try { + $this->run(array_merge( + ['archive', '--format=zip', '--output='.$archivePath, $resolved, '--'], + $chunk, + )); + + $written = array_merge($written, $this->extract($archivePath, $targetDirectory)); + } finally { + @unlink($archivePath); + } + } + + return $written; + } + + /** + * @return array + */ + private function extract(string $archivePath, string $targetDirectory): array + { + $zip = new ZipArchive; + + if ($zip->open($archivePath) !== true) { + throw new RuntimeException('A git archive kimenete nem nyitható meg.'); + } + + $entries = []; + + try { + for ($index = 0; $index < $zip->numFiles; $index++) { + $name = $zip->getNameIndex($index); + + if ($name === false || str_ends_with($name, '/')) { + continue; + } + + // A célmappából kimutató bejegyzés nem fordulhat elő valódi git archive-ban, + // de a kiírás előtti ellenőrzés olcsó - és ez az egyetlen pont, ahol az + // alkalmazás a repón kívülre írhatna. + if (! $this->isSafePath($name)) { + throw new RuntimeException(sprintf('Gyanús útvonal a csomagban: %s', $name)); + } + + $entries[] = $name; + } + + if ($entries !== [] && ! $zip->extractTo($targetDirectory, $entries)) { + throw new RuntimeException('A fájlok kicsomagolása nem sikerült.'); + } + } finally { + $zip->close(); + } + + return $entries; + } + /** * A repo gyökeréből ki nem mutató, relatív útvonal-e. */ diff --git a/resources/views/filament/pages/deployment-package.blade.php b/resources/views/filament/pages/deployment-package.blade.php index 2c5a43b..44d84c1 100644 --- a/resources/views/filament/pages/deployment-package.blade.php +++ b/resources/views/filament/pages/deployment-package.blade.php @@ -35,10 +35,33 @@ (a kezdő commit kizárva, a záró beleértve).

- Ez a felület jelenleg csak olvas — a csomag előállítása a következő fázisban készül el. + A csomag ide készül: {{ config('deployment.output_path') }}

+ @if ($this->lastPackage) +
+

Elkészült: {{ $this->lastPackage['name'] }}

+

+ {{ $this->lastPackage['counts']['copied'] }} fájl másolva, + {{ $this->lastPackage['counts']['deleted'] }} törlendő, + {{ $this->lastPackage['counts']['skipped'] }} kézzel kivéve. +

+

{{ $this->lastPackage['path'] }}

+ + @if ($this->lastPackage['counts']['missing'] > 0) +

+ Figyelem: {{ $this->lastPackage['counts']['missing'] }} fájl nem került a csomagba, + a listájuk a _MANIFEST.txt végén van. +

+ @endif + +

+ A lépéseket a csomagban lévő _TEENDOK.md tartalmazza. +

+
+ @endif + @if ($this->gitError)

Git hiba

@@ -183,6 +206,7 @@ class="{{ $buttonClass }} ml-1 {{ $isTo ? 'border-primary-600 bg-primary-600 tex + @@ -192,8 +216,19 @@ class="{{ $buttonClass }} ml-1 {{ $isTo ? 'border-primary-600 bg-primary-600 tex @foreach ($changeSet['files'] as $file) @php $style = $statusStyles[$file['status']] ?? ['label' => $file['status'], 'class' => 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200']; + $isSkipped = in_array($file['path'], $this->skippedPaths, true); @endphp - + +
Csomagba Állapot Útvonal Művelet
+ @if ($file['action'] === 'copy') + + @else + + @endif + {{ $file['status'] }} · {{ $style['label'] }} @@ -207,7 +242,7 @@ class="{{ $buttonClass }} ml-1 {{ $isTo ? 'border-primary-600 bg-primary-600 tex @switch($file['action']) - @case('copy') másolandó @break + @case('copy') {{ $isSkipped ? 'kézzel kivéve' : 'másolandó' }} @break @case('delete') törlendő a célgépen @break @default kihagyva (kizárási lista) @endswitch @@ -218,6 +253,32 @@ class="{{ $buttonClass }} ml-1 {{ $isTo ? 'border-primary-600 bg-primary-600 tex
@endif + + @php + $packageCount = count(array_diff($changeSet['copied'], $this->skippedPaths)); + @endphp + +
+

+ A csomagba {{ $packageCount }} fájl kerül + @if ($changeSet['counts']['deleted'] > 0) + , és {{ $changeSet['counts']['deleted'] }} fájl a törlendők listájára + @endif + @if ($target) + · cél: {{ $target['label'] }} + @endif +

+ + +
@endif diff --git a/tests/Feature/DeploymentPackageTest.php b/tests/Feature/DeploymentPackageTest.php index dd27870..42a040c 100644 --- a/tests/Feature/DeploymentPackageTest.php +++ b/tests/Feature/DeploymentPackageTest.php @@ -6,6 +6,7 @@ use App\Models\User; use App\Services\Deployment\ChangeSetAnalyzer; use App\Services\Deployment\DeploymentGuard; +use App\Services\Deployment\DeploymentPackageBuilder; use App\Services\Deployment\GitRepository; use App\Services\FeatureFlagRegistrar; use Illuminate\Foundation\Testing\RefreshDatabase; @@ -96,6 +97,7 @@ function deploymentTestRepository(): array $log = trim(deploymentGit($path, ['log', '--reverse', '--pretty=format:%H'])); Config::set('deployment.repo_path', $path); + Config::set('deployment.output_path', $path.DIRECTORY_SEPARATOR.'_kimenet'); return ['path' => $path, 'commits' => explode("\n", $log)]; } @@ -293,6 +295,112 @@ function deploymentCleanup(string $path): void deploymentCleanup($repository['path']); }); +test('a csomag a commitolt tartalmat viszi, nem a working tree állapotát', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + // A working tree szándékos elrontása a commit után: ez nem kerülhet a csomagba. + File::put($repository['path'].'/app/Árlista.php', "build('e2e', 'main', $first, $third); + + expect(File::get($package['path'].'/app/Árlista.php'))->not->toContain('NEM KERÜLHET') + // A .gitattributes eol=lf miatt a git archive LF-fel ír, Windowson is. + ->and(File::get($package['path'].'/app/Árlista.php'))->not->toContain("\r\n"); + + deploymentCleanup($repository['path']); +}); + +test('a csomagból kimarad a kizárt és a kézzel kivett fájl, a törlendők külön listára kerülnek', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third, ['app/Elso.php']); + + expect(File::exists($package['path'].'/app/Atnevezett.php'))->toBeTrue() + ->and(File::exists($package['path'].'/app/Elso.php'))->toBeFalse() + ->and(File::exists($package['path'].'/.env'))->toBeFalse() + ->and(File::exists($package['path'].'/app/Regi.php'))->toBeFalse() + ->and($package['counts']['skipped'])->toBe(1) + ->and($package['counts']['missing'])->toBe(0); + + $torlendo = File::get($package['path'].'/_TORLENDO.txt'); + + expect($torlendo)->toContain('app/Regi.php') + ->and($torlendo)->toContain('app/Atnevezendo.php'); + + deploymentCleanup($repository['path']); +}); + +test('a csomag rögzíti a kirakott verziót', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third); + + $version = json_decode(File::get($package['path'].'/public/deploy-version.json'), true); + + expect($version['commit'])->toBe($third) + ->and($version['target'])->toBe('e2e') + ->and($version['branch'])->toBe('main'); + + deploymentCleanup($repository['path']); +}); + +test('a limit fölötti fájlszámnál nem készül csomag', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + Config::set('deployment.max_files', 1); + + expect(fn () => app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third)) + ->toThrow(RuntimeException::class); + + expect(File::exists(config('deployment.output_path')))->toBeFalse(); + + deploymentCleanup($repository['path']); +}); + +test('a csomagoló szerveren akkor sem indul el, ha közvetlenül hívják', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + Config::set('app.stage', 'PROD'); + + expect(fn () => app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third)) + ->toThrow(RuntimeException::class); + + deploymentCleanup($repository['path']); +}); + +test('a felületről indított csomagolás létrehozza a mappát', function () { + deploymentAllowEnvironment(); + $repository = deploymentTestRepository(); + [$first, , $third] = $repository['commits']; + + $this->actingAs(deploymentDeveloper()); + + $component = Livewire::test(DeploymentPackage::class) + ->call('selectFrom', $first) + ->call('selectTo', $third) + ->call('toggleFile', 'app/Elso.php') + ->call('createPackage'); + + $package = $component->get('lastPackage'); + + expect($package)->not->toBeNull() + ->and($package['name'])->toStartWith('deploy_e2e_') + ->and(File::isDirectory($package['path']))->toBeTrue() + ->and($package['skipped'])->toBe(['app/Elso.php']); + + deploymentCleanup($repository['path']); +}); + test('az oldal nem fogad el kamu commit azonosítót', function () { deploymentAllowEnvironment(); $repository = deploymentTestRepository();