d2d.emegrendeles.hu/app/Services/Deployment/ChangeSetAnalyzer.php
E98Developer 09eed3eeb3 ADD Deployment csomagoló phase1 védelem, git olvasó és read-only commit/diff felület
Két commit közötti fájlok összegyűjtését előkészítő felület első fázisa: célkörnyezet
választás (e2e/d2d), commitlista, diff-előnézet figyelmeztetésekkel. Ez a fázis még
semmit nem ír a fájlrendszerre, a csomagolás a phase2-ben érkezik.

- DeploymentGuard: .env kapcsoló + hardkódolt stage whitelist + developer szerep +
  feature flag; a flag szándékosan nem biztonsági réteg, csak láthatóság-vezérlés
- GitRepository: csak olvasó wrapper, argumentum-tömbös Symfony Process (nincs shell),
  hash- és referencia-validáció, core.quotePath=false az ékezetes útvonalakhoz
- ChangeSetAnalyzer: kizárási lista, törlendő/másolandó szétválasztás (átnevezésnél
  mindkettő), figyelmeztetések migrációra, composer.lock-ra és a nem verziókövetett
  fordított assetekre

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-16 06:45:09 +02:00

171 lines
6.6 KiB
PHP

<?php
namespace App\Services\Deployment;
use Illuminate\Support\Str;
/**
* A git diff nyers kimenetéből állítja elő a csomagolható halmazt és a figyelmeztetéseket.
*
* Szándékosan nem hív gitet és nem ír fájlt: tisztán a listát és a szabályokat kezeli,
* így a fázis 2 csomagolója ugyanezt az eredményt tudja majd felhasználni.
*/
class ChangeSetAnalyzer
{
/**
* @param array<int, array{status:string, path:string, old_path:?string}> $changedFiles
* @param array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null $target
* @return array{
* files: array<int, array{status:string, path:string, old_path:?string, excluded:bool, action:string}>,
* copied: array<int, string>,
* deleted: array<int, string>,
* counts: array{total:int, copied:int, deleted:int, excluded:int},
* warnings: array<int, array{level:string, title:string, body:string}>
* }
*/
public function analyze(array $changedFiles, ?array $target = null, ?string $branch = null): array
{
/** @var array<int, string> $excludePatterns */
$excludePatterns = (array) config('deployment.exclude', []);
$files = [];
$copied = [];
$deleted = [];
$excludedCount = 0;
foreach ($changedFiles as $file) {
$excluded = Str::is($excludePatterns, $file['path']);
// Törlés esetén nincs mit másolni: a fájl a célgépen létezik, itt már nem.
// Átnevezésnél mindkettő kell - az új útvonal másolandó, a régi törlendő.
$action = $excluded
? 'skip'
: ($file['status'] === 'D' ? 'delete' : 'copy');
if ($excluded) {
$excludedCount++;
} elseif ($action === 'delete') {
$deleted[] = $file['path'];
} else {
$copied[] = $file['path'];
if ($file['old_path'] !== null && ! Str::is($excludePatterns, $file['old_path'])) {
$deleted[] = $file['old_path'];
}
}
$files[] = array_merge($file, ['excluded' => $excluded, 'action' => $action]);
}
return [
'files' => $files,
'copied' => $copied,
'deleted' => $deleted,
'counts' => [
'total' => count($files),
'copied' => count($copied),
'deleted' => count($deleted),
'excluded' => $excludedCount,
],
'warnings' => $this->warnings($copied, $deleted, $excludedCount, $target, $branch),
];
}
/**
* @param array<int, string> $copied
* @param array<int, string> $deleted
* @param array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null $target
* @return array<int, array{level:string, title:string, body:string}>
*/
private function warnings(array $copied, array $deleted, int $excludedCount, ?array $target, ?string $branch): array
{
$warnings = [];
$maxFiles = (int) config('deployment.max_files', 500);
if ($copied === [] && $deleted === []) {
$warnings[] = [
'level' => 'info',
'title' => 'Nincs csomagolható változás',
'body' => 'A két commit között nincs olyan fájl, ami a célgépre kerülne.',
];
}
if (count($copied) > $maxFiles) {
$warnings[] = [
'level' => 'danger',
'title' => sprintf('Túl sok fájl (%d db, limit %d)', count($copied), $maxFiles),
'body' => 'Ekkora tartomány kézi másolással már nehezen ellenőrizhető. Szűkítsd a commit-tartományt, vagy bontsd több csomagra.',
];
}
$migrations = $this->matching($copied, 'migration');
if ($migrations !== []) {
$warnings[] = [
'level' => 'warning',
'title' => sprintf('Migráció a csomagban (%d db)', count($migrations)),
'body' => 'A fájlok felmásolása után a célgépen le kell futtatni: php artisan migrate',
];
}
if ($this->matching($copied, 'composer') !== []) {
$warnings[] = [
'level' => 'warning',
'title' => 'Függőség változott (composer)',
'body' => 'A vendor/ nincs verziókövetve, ezért nem kerül a csomagba - a célgépen composer install szükséges.',
];
}
if ($this->matching($copied, 'asset') !== []) {
$warnings[] = [
'level' => 'warning',
'title' => 'Frontend forrás változott',
'body' => 'A fordított assetek (public/build, public/js, public/css, mix-manifest.json) a .gitignore miatt NEM kerülnek a csomagba. Futtass npm run build-ot, és a fázis 4-től külön csatolhatók lesznek.',
];
}
if ($deleted !== []) {
$warnings[] = [
'level' => 'warning',
'title' => sprintf('Törlendő fájl a célgépen (%d db)', count($deleted)),
'body' => 'Ezeket a másolás nem intézi el, kézzel kell törölni - a csomag _TORLENDO.txt fájlja fogja tartalmazni őket.',
];
}
if ($excludedCount > 0) {
$warnings[] = [
'level' => 'info',
'title' => sprintf('Kihagyott fájl (%d db)', $excludedCount),
'body' => 'A kizárási lista (.env, storage/, vendor/, node_modules/, .git*) alapján ezek soha nem kerülnek csomagba.',
];
}
if ($target && $target['expected_branch'] && $branch && $branch !== $target['expected_branch']) {
$warnings[] = [
'level' => 'warning',
'title' => 'Nem a szokásos branch',
'body' => sprintf(
'A(z) "%s" környezetre általában a %s branchről megy csomag, most viszont a %s van kiválasztva.',
$target['label'],
$target['expected_branch'],
$branch,
),
];
}
return $warnings;
}
/**
* @param array<int, string> $paths
* @return array<int, string>
*/
private function matching(array $paths, string $group): array
{
/** @var array<int, string> $patterns */
$patterns = (array) config('deployment.attention.'.$group, []);
return array_values(array_filter($paths, fn (string $path): bool => Str::is($patterns, $path)));
}
}