d2d.emegrendeles.hu/app/Services/Deployment/ChangeSetAnalyzer.php
E98Developer 2965f231ce ADD Deployment csomagoló queue worker és scheduler újraindítás a teendők közé
A 2026-08-17-i eset általánosítása: a d2d/e2e Docker Swarmban ugyanazt a kódot több
service futtatja közös volume-ról, és a queue:work hosszan futó folyamat a betöltött
osztályokat a memóriájában tartja. A fájlmásolás után a worker RÉGI kóddal dolgozta fel
a jobot - a felület jónak látszott, a háttérfeladat némán a régi viselkedést hozta.

- config: 'runtime' minta (app/, config/, database/, routes/, bootstrap/, composer.lock),
  mert a restart nem csak a jobok változásakor kell; targetenként a service nevek és a
  Swarm manager elérése
- a felületen figyelmeztetés, a teendőkben a konkrét ssh + docker service update parancs
- a restart szándékosan a cache ürítés UTÁN: a frissen induló worker különben a régi,
  cache-elt konfigot töltené be
- a d2d schedulernél jelezzük, hogy 0 replikán fut (a restart üresjárat lehet), a t2t-nél
  pedig azt, hogy queue service még nincs, de a névkonvenció alapján t2t_queue lesz

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 06:41:19 +02:00

206 lines
8.1 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',
];
}
$seeders = $this->matching($copied, 'seeder');
if ($seeders !== []) {
$warnings[] = [
'level' => 'warning',
'title' => sprintf('Seeder a csomagban (%d db)', count($seeders)),
'body' => 'A seeder felmásolása önmagában nem csinál semmit - el kell dönteni, hogy le kell-e futtatni a célgépen. '
.'Ha feature flageket ír, a lefutása után azok azonnal hatnak.',
];
}
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 ($this->matching($copied, 'runtime') !== []) {
$jobs = count($this->matching($copied, 'job'));
$consoles = count($this->matching($copied, 'console'));
$detail = [];
if ($jobs > 0) {
$detail[] = $jobs.' job';
}
if ($consoles > 0) {
$detail[] = $consoles.' konzol parancs';
}
$warnings[] = [
'level' => 'warning',
'title' => 'Hosszan futó konténerek érintve'.($detail === [] ? '' : ' ('.implode(', ', $detail).')'),
'body' => 'A queue worker és a scheduler külön konténerben fut, és a betöltött osztályokat a memóriájában '
.'tartja - a fájlok felmásolása után is a RÉGI kóddal dolgozik, amíg újra nem indul. Ez némán rossz '
.'viselkedést okoz (a job "elkészül", de nem azt csinálja, amit az új kód). A konkrét restart parancsok '
.'a csomag _TEENDOK.md fájljában 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)));
}
}