$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)); } }