ADD Deployment csomagoló phase3 napló, környezetkövetés, ZIP letöltés és törlő szkript

A csomagolás mostantól nyomot hagy: melyik környezetre mi készült, mi lett kirakva, és
mennyi a lemaradás - ez az, ami a "melyik verzió fut az e2e-n" kérdést kiveszi a fejből.

- deployment_packages tábla + modell (BaseAuditable): target, tartomány, darabszámok,
  mappa/ZIP útvonal, deployed_at. A csomagolás és a tényleges kirakás két külön esemény.
- Targetenkénti előtöltés: a kezdő commit az adott környezetre utoljára kirakottként
  megjelölt csomag to_commit-je. Rebase után nem létező hash esetén inkább üres marad,
  mint hogy hamis tartományt mutasson.
- "Környezetek állapota" panel: napló szerinti állapot + commit-lemaradás
  (git rev-list --count), és gombra a szerver deploy-version.json-jának lekérdezése.
  Ha a kettő eltér, az elmaradt vagy félbemaradt feltöltés jele. Szándékosan gombra fut,
  nem minden rendereléskor: így nem indul kimenő kérés magától.
- ZIP a kész mappából (a fájllista a zip létrejötte előtt készül, így nem csomagolja
  magát), letöltés a naplóból. A DB-ből jövő útvonalat kiírás előtt a kimeneti mappához
  kötjük, hogy egy módosított rekord se tehessen letölthetővé tetszőleges fájlt.
- _torles.sh: alapból dry run, --confirm kell a törléshez, app-gyökér ellenőrzéssel,
  abszolút útvonal és .. kiszűrésével, LF sorvéggel és BOM nélkül. Lefuttatva ellenőrizve.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
E98Developer 2026-08-16 07:20:34 +02:00
parent 7ad9c6fea8
commit bfc1484611
7 changed files with 619 additions and 3 deletions

View File

@ -2,6 +2,7 @@
namespace App\Filament\Pages;
use App\Models\DeploymentPackage as DeploymentPackageRecord;
use App\Services\Deployment\ChangeSetAnalyzer;
use App\Services\Deployment\DeploymentGuard;
use App\Services\Deployment\DeploymentPackageBuilder;
@ -15,8 +16,12 @@
use Filament\Pages\Page;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Schema;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Http;
use Livewire\Attributes\Computed;
use RuntimeException;
use Symfony\Component\HttpFoundation\BinaryFileResponse;
use Throwable;
/**
* Deployment csomagoló: célkörnyezet, commit-tartomány, diff-előnézet és csomagolás.
@ -57,6 +62,9 @@ class DeploymentPackage extends Page implements HasForms
/** @var array<string, mixed>|null az utoljára elkészített csomag adatai */
public ?array $lastPackage = null;
/** @var array<string, array<string, mixed>> targetenként a szerverről lekérdezett élő verzió */
public array $liveVersions = [];
public static function canAccess(): bool
{
return app(DeploymentGuard::class)->isAllowed(auth()->user());
@ -69,12 +77,37 @@ public function mount(): void
/** @var array<int, int> $limits */
$limits = (array) config('deployment.commit_limits', [50]);
$target = app(DeploymentTargets::class)->defaultKey();
$this->form->fill([
'target' => app(DeploymentTargets::class)->defaultKey(),
'target' => $target,
'branch' => $this->defaultBranch(),
'limit' => $limits[0] ?? 50,
'search' => null,
]);
$this->preloadFromCommit($target);
}
/**
* A kezdő commit előtöltése az adott környezetre utoljára kirakott csomagból.
*
* Ez a funkció lényegi része: a "hol tart ez a környezet" kérdésre nem emlékezetből
* kell válaszolni, így nem marad ki fájl a következő csomagból.
*/
private function preloadFromCommit(?string $targetKey): void
{
if (! $targetKey) {
return;
}
$lastDeployed = DeploymentPackageRecord::lastDeployedFor($targetKey);
// Rebase/force push után a korábban rögzített hash már nem létezik - ilyenkor
// inkább nem töltünk elő semmit, mint hogy hamis tartományt mutassunk.
$this->fromCommit = $lastDeployed
? app(GitRepository::class)->resolveCommit($lastDeployed->to_commit)
: null;
}
public function form(Schema $form): Schema
@ -91,6 +124,7 @@ public function form(Schema $form): Schema
->options(app(DeploymentTargets::class)->options())
->required()
->live()
->afterStateUpdated(fn (?string $state) => $this->preloadFromCommit($state))
->helperText(fn (): ?string => $this->target()['remote_path'] ?? null),
Select::make('branch')
->label('Branch')
@ -191,6 +225,20 @@ public function createPackage(): void
return;
}
DeploymentPackageRecord::create([
'name' => $this->lastPackage['name'],
'target' => $this->lastPackage['target'],
'branch' => $this->lastPackage['branch'],
'from_commit' => $this->lastPackage['from'],
'to_commit' => $this->lastPackage['to'],
'folder' => $this->lastPackage['path'],
'zip_path' => $this->lastPackage['zip'],
'copied_count' => $this->lastPackage['counts']['copied'],
'deleted_count' => $this->lastPackage['counts']['deleted'],
'skipped_count' => $this->lastPackage['counts']['skipped'],
'missing_count' => $this->lastPackage['counts']['missing'],
]);
Notification::make()
->title('A csomag elkészült')
->body(sprintf(
@ -202,6 +250,137 @@ public function createPackage(): void
->send();
}
/**
* A csomag ZIP-jének letöltése.
*/
public function downloadPackage(int $id): ?BinaryFileResponse
{
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
$package = DeploymentPackageRecord::find($id);
$zipPath = $package?->zip_path;
// Az útvonal adatbázisból jön, ezért kiírás előtt ellenőrizzük, hogy a csomagoló
// saját kimeneti mappáján belül van - egy elrontott vagy módosított rekord így sem
// tud tetszőleges fájlt letölthetővé tenni.
if (! $zipPath || ! is_file($zipPath) || ! $this->isInsideOutputPath($zipPath)) {
Notification::make()
->title('A ZIP nem érhető el')
->body('A csomag ZIP fájlja már nincs a lemezen.')
->danger()
->send();
return null;
}
return response()->download($zipPath);
}
/**
* A csomag megjelölése kirakottként - innentől ez lesz a következő tartomány kezdete.
*/
public function markDeployed(int $id): void
{
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
$package = DeploymentPackageRecord::find($id);
if (! $package) {
return;
}
$package->deployed_at = now();
$package->save();
$this->preloadFromCommit($package->target);
Notification::make()
->title('Megjelölve kirakottként')
->body(sprintf('A(z) %s környezet innentől a %s commiton áll.', $package->target, substr((string) $package->to_commit, 0, 7)))
->success()
->send();
}
/**
* Az élő állapot lekérdezése a célkörnyezetről (deploy-version.json).
*
* Szándékosan gombra fut és nem minden rendereléskor: így nem indul kimenő kérés
* magától, és nem lassítja az oldalt, ha egy környezet nem válaszol.
*/
public function fetchLiveVersion(string $targetKey): void
{
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
$target = app(DeploymentTargets::class)->find($targetKey);
if (! $target || ! $target['domain']) {
return;
}
try {
$response = Http::connectTimeout(3)->timeout(5)->get('https://'.$target['domain'].'/deploy-version.json');
$this->liveVersions[$targetKey] = $response->successful()
? ['ok' => true, 'data' => $response->json()]
: ['ok' => false, 'error' => 'HTTP '.$response->status()];
} catch (Throwable $exception) {
$this->liveVersions[$targetKey] = ['ok' => false, 'error' => $exception->getMessage()];
}
}
/**
* Környezetenkénti állapot: mi van kirakva és mennyi a lemaradás.
*
* @return array<int, array<string, mixed>>
*/
#[Computed]
public function environmentStatuses(): array
{
$branch = $this->data['branch'] ?? null;
$statuses = [];
foreach (app(DeploymentTargets::class)->all() as $key => $target) {
$lastDeployed = DeploymentPackageRecord::lastDeployedFor($key);
$live = $this->liveVersions[$key] ?? null;
$liveCommit = $live['ok'] ?? false ? ($live['data']['commit'] ?? null) : null;
$statuses[] = [
'target' => $target,
'package' => $lastDeployed,
'behind' => $lastDeployed && $branch
? app(GitRepository::class)->countCommitsBetween($lastDeployed->to_commit, $branch)
: null,
'live' => $live,
// A napló azt mondja meg, mit jelöltünk kirakottnak; a szerver azt, mi fut
// valójában. Ha a kettő eltér, az elmaradt vagy félbemaradt feltöltés jele.
'mismatch' => $liveCommit && $lastDeployed && $liveCommit !== $lastDeployed->to_commit,
];
}
return $statuses;
}
/**
* @return \Illuminate\Support\Collection<int, DeploymentPackageRecord>
*/
#[Computed]
public function recentPackages(): Collection
{
return DeploymentPackageRecord::query()
->when($this->data['target'] ?? null, fn ($query, $target) => $query->forTarget($target))
->latest('id')
->limit(10)
->get();
}
private function isInsideOutputPath(string $path): bool
{
$base = realpath((string) config('deployment.output_path'));
$real = realpath($path);
return $base !== false && $real !== false && str_starts_with($real, $base);
}
/**
* @return array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null
*/

View File

@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Egy elkészített deployment csomag naplóbejegyzése.
*
* Nem csak audit: a legutóbb kirakott csomag `to_commit`-je adja a következő csomag
* alapértelmezett kezdő commitját, környezetenként külön - ez az, ami a "mi maradt ki
* a másolásból" hibát megelőzi.
*/
class DeploymentPackage extends BaseAuditable
{
use SoftDeletes;
protected $casts = [
'deployed_at' => 'datetime',
'copied_count' => 'integer',
'deleted_count' => 'integer',
'skipped_count' => 'integer',
'missing_count' => 'integer',
];
public function scopeForTarget(Builder $query, string $target): Builder
{
return $query->where('target', $target);
}
/**
* Az adott környezetre utoljára kirakottként megjelölt csomag.
*/
public static function lastDeployedFor(string $target): ?self
{
return static::query()
->forTarget($target)
->whereNotNull('deployed_at')
->latest('deployed_at')
->first();
}
public function isDeployed(): bool
{
return $this->deployed_at !== null;
}
}

View File

@ -5,6 +5,7 @@
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Carbon;
use RuntimeException;
use ZipArchive;
/**
* A kijelölt commit-tartományból állítja elő a feltölthető csomagot.
@ -29,7 +30,7 @@ public function __construct(
/**
* @param array<int, string> $skippedPaths a felületen kézzel kivett fájlok
* @return array{
* name:string, path:string, target:string, branch:string,
* name:string, path:string, zip:?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<int, array{path:string, status:string, size:int, sha1:string}>,
@ -122,8 +123,13 @@ public function build(
$this->writeVersionMarker($path, $manifest, $target);
$this->writeManifest($path, $manifest, $target);
$this->writeDeleteList($path, $manifest, $target);
$this->writeDeleteScript($path, $manifest, $target);
$this->writeInstructions($path, $manifest, $target);
// A ZIP készül utoljára, hogy a kísérő fájlokat is tartalmazza. A fájllistát
// előbb gyűjtjük össze, mint ahogy a zip létrejön, így nem tudja magát becsomagolni.
$manifest['zip'] = $this->createZip($path, $manifest['name']);
return $manifest;
}
@ -292,7 +298,11 @@ private function writeInstructions(string $path, array $manifest, array $target)
];
if ($manifest['deleted'] !== []) {
$steps[] = sprintf('Töröld a `_TORLENDO.txt`-ben felsorolt %d fájlt a célgépen.', $manifest['counts']['deleted']);
$steps[] = sprintf(
'Töröld a `_TORLENDO.txt`-ben felsorolt %d fájlt a célgépen. Nagyobb mennyiségnél a mellékelt '
.'szkript is használható **átnézés után**: `bash _torles.sh` kilistázza, `bash _torles.sh --confirm` törli.',
$manifest['counts']['deleted'],
);
}
foreach ($manifest['warnings'] as $warning) {
@ -338,6 +348,86 @@ private function writeInstructions(string $path, array $manifest, array $target)
$this->put($path.'/_TEENDOK.md', implode("\n", $lines)."\n");
}
/**
* Törlő szkript nagyobb mennyiséghez - a kézi törlés (_TORLENDO.txt) marad az alapeset.
*
* Ezért alapból csak kilistáz, a tényleges törléshez explicit --confirm kell; és mivel
* Windowson generáljuk Linux célgépre, LF sorvéggel és BOM nélkül kell kiírni, különben
* a shebang sor törik el.
*
* @param array<string, mixed> $manifest
* @param array<string, mixed> $target
*/
private function writeDeleteScript(string $path, array $manifest, array $target): void
{
$lines = [
'#!/usr/bin/env bash',
'# '.$manifest['name'].' - a célgépen törlendő fájlok ('.$target['label'].')',
'# Futtatás az app gyökeréből:',
'# bash _torles.sh -> csak kilistázza (dry run)',
'# bash _torles.sh --confirm -> ténylegesen töröl',
'set -euo pipefail',
'',
'[[ -f artisan && -d app ]] || { echo "HIBA: nem az app gyökerében futsz"; exit 1; }',
'',
'CONFIRM=0',
'[[ "${1:-}" == "--confirm" ]] && CONFIRM=1',
'',
'FILES=(',
];
foreach ($manifest['deleted'] as $deleted) {
$lines[] = ' "'.$deleted.'"';
}
$lines = array_merge($lines, [
')',
'',
'if (( ${#FILES[@]} == 0 )); then echo "Nincs törlendő fájl."; exit 0; fi',
'',
'for f in "${FILES[@]}"; do',
' case "$f" in /*|*..*) echo "KIHAGYVA (gyanús útvonal): $f"; continue ;; esac',
' [[ -f "$f" ]] || { echo "NINCS MEG: $f"; continue; }',
' if (( CONFIRM )); then rm -f -- "$f"; echo "TÖRÖLVE: $f"; else echo "TÖRÖLNÉ: $f"; fi',
'done',
'',
'if (( ! CONFIRM )); then echo; echo "Ez csak lista volt. Tényleges törlés: bash _torles.sh --confirm"; fi',
]);
$this->put($path.'/_torles.sh', implode("\n", $lines)."\n");
}
/**
* A kész mappa ZIP-be csomagolása letöltéshez és archiváláshoz.
*/
private function createZip(string $path, string $name): ?string
{
$entries = [];
foreach ($this->files->allFiles($path, true) as $file) {
$entries[str_replace('\\', '/', $file->getRelativePathname())] = $file->getPathname();
}
if ($entries === []) {
return null;
}
$zipPath = $path.DIRECTORY_SEPARATOR.$name.'.zip';
$zip = new ZipArchive;
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
throw new RuntimeException('A ZIP fájl nem hozható létre.');
}
foreach ($entries as $relative => $absolute) {
$zip->addFile($absolute, $relative);
}
$zip->close();
return $zipPath;
}
/**
* Mindig LF sorvéggel írunk: a csomag Windowson készül, de Linux célgépre megy.
*/

View File

@ -228,6 +228,29 @@ public function changedFiles(string $from, string $to): array
return $files;
}
/**
* Hány commit van a kettő között (from kizárva, to beleértve).
*
* A környezetek lemaradásának kimutatásához: "az e2e 7 committal van elmaradva".
*/
public function countCommitsBetween(string $from, string $to): ?int
{
$fromHash = $this->resolveCommit($from);
if (! $fromHash) {
return null;
}
try {
$this->assertReference($to);
$count = trim($this->run(['rev-list', '--count', $fromHash.'..'.$to, '--']));
} catch (RuntimeException) {
return null;
}
return is_numeric($count) ? (int) $count : null;
}
/**
* A megadott commit szerinti fájltartalmat írja ki a célmappába, relatív útvonalakat megőrizve.
*

View File

@ -0,0 +1,46 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('deployment_packages', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('target', 32);
$table->string('branch')->nullable();
$table->string('from_commit', 40);
$table->string('to_commit', 40);
$table->string('folder', 1024);
$table->string('zip_path', 1024)->nullable();
$table->unsignedInteger('copied_count')->default(0);
$table->unsignedInteger('deleted_count')->default(0);
$table->unsignedInteger('skipped_count')->default(0);
$table->unsignedInteger('missing_count')->default(0);
$table->text('note')->nullable();
// A csomagolás és a tényleges kirakás két külön esemény: a deployed_at csak
// akkor kap értéket, amikor a fejlesztő megjelöli, hogy fel is töltötte.
$table->timestamp('deployed_at')->nullable();
$table->userIdFields();
$table->timestamps();
$table->softDeletes();
$table->index(['target', 'deployed_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('deployment_packages');
}
};

View File

@ -6,6 +6,8 @@
$changeSet = $this->changeSet;
$highlighted = $this->highlightedHashes;
$target = $this->target();
$environments = $this->environmentStatuses;
$recentPackages = $this->recentPackages;
$statusStyles = [
'A' => ['label' => 'új', 'class' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'],
@ -39,6 +41,66 @@
</p>
</div>
<div class="{{ $cardClass }}">
<h6 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">Környezetek állapota</h6>
<div class="grid gap-4 md:grid-cols-2">
@foreach ($environments as $environment)
<div class="p-4 border rounded-lg {{ $environment['mismatch'] ? $warningStyles['danger'] : 'border-gray-200 dark:border-gray-700' }}">
<div class="flex items-start justify-between gap-3">
<div>
<p class="font-semibold text-gray-900 dark:text-white">{{ $environment['target']['label'] }}</p>
<p class="text-xs font-mono text-gray-500 dark:text-gray-400">{{ $environment['target']['remote_path'] }}</p>
</div>
<button type="button" wire:click="fetchLiveVersion('{{ $environment['target']['key'] }}')"
wire:loading.attr="disabled" wire:target="fetchLiveVersion('{{ $environment['target']['key'] }}')"
class="{{ $buttonClass }} border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700">
Élő állapot
</button>
</div>
<div class="mt-3 space-y-1 text-sm text-gray-600 dark:text-gray-300">
@if ($environment['package'])
<p>
Utoljára kirakva:
<code>{{ substr($environment['package']->to_commit, 0, 7) }}</code>
· {{ $environment['package']->deployed_at?->format('Y.m.d H:i') }}
</p>
@if ($environment['behind'] !== null)
<p>
Lemaradás a kiválasztott branchhez képest:
<strong>{{ $environment['behind'] }}</strong> commit
</p>
@endif
@else
<p class="text-gray-400">Még nincs kirakottként megjelölt csomag.</p>
@endif
@if ($environment['live'])
@if ($environment['live']['ok'])
<p>
A szerver szerint:
<code>{{ substr($environment['live']['data']['commit'] ?? '?', 0, 7) }}</code>
@if (($environment['live']['data']['packaged_at'] ?? null))
· {{ \Illuminate\Support\Carbon::parse($environment['live']['data']['packaged_at'])->format('Y.m.d H:i') }}
@endif
</p>
@else
<p class="text-gray-400">Élő állapot nem lekérdezhető: {{ $environment['live']['error'] }}</p>
@endif
@endif
@if ($environment['mismatch'])
<p class="font-semibold">
A szerveren futó commit eltér a naplózottól elmaradt vagy félbemaradt feltöltés.
</p>
@endif
</div>
</div>
@endforeach
</div>
</div>
@if ($this->lastPackage)
<div class="p-4 border rounded-lg {{ $this->lastPackage['counts']['missing'] > 0 ? $warningStyles['warning'] : 'border-emerald-300 bg-emerald-50 text-emerald-900 dark:border-emerald-800 dark:bg-emerald-950 dark:text-emerald-200' }}">
<p class="font-semibold">Elkészült: {{ $this->lastPackage['name'] }}</p>
@ -282,5 +344,74 @@ class="px-4 py-2 text-sm font-semibold text-white rounded bg-primary-600 hover:b
</div>
@endif
@if ($recentPackages->isNotEmpty())
<div class="{{ $cardClass }}">
<h6 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
Korábbi csomagok
<span class="ml-1 text-sm font-normal text-gray-500 dark:text-gray-400">
({{ $target ? $target['label'] : 'összes' }})
</span>
</h6>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs uppercase text-gray-600 border-b border-gray-200 dark:text-gray-300 dark:border-gray-700">
<tr>
<th class="py-2 pr-4">Csomag</th>
<th class="py-2 pr-4">Tartomány</th>
<th class="py-2 pr-4">Fájlok</th>
<th class="py-2 pr-4">Készült</th>
<th class="py-2 pr-4">Kirakva</th>
<th class="py-2 text-right">Műveletek</th>
</tr>
</thead>
<tbody>
@foreach ($recentPackages as $package)
<tr wire:key="package-{{ $package->id }}" class="border-b border-gray-100 dark:border-gray-700">
<td class="py-2 pr-4 font-mono text-xs break-all text-gray-800 dark:text-gray-100">{{ $package->name }}</td>
<td class="py-2 pr-4 font-mono text-xs whitespace-nowrap text-gray-600 dark:text-gray-300">
{{ substr($package->from_commit, 0, 7) }} {{ substr($package->to_commit, 0, 7) }}
</td>
<td class="py-2 pr-4 whitespace-nowrap text-gray-600 dark:text-gray-300">
{{ $package->copied_count }}
@if ($package->deleted_count > 0)
<span class="text-gray-400">/ {{ $package->deleted_count }} törl.</span>
@endif
</td>
<td class="py-2 pr-4 whitespace-nowrap text-gray-600 dark:text-gray-300">
{{ $package->created_at?->format('Y.m.d H:i') }}
</td>
<td class="py-2 pr-4 whitespace-nowrap">
@if ($package->isDeployed())
<span class="px-2 py-0.5 text-xs font-semibold rounded bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200">
{{ $package->deployed_at?->format('Y.m.d H:i') }}
</span>
@else
<span class="text-gray-400"></span>
@endif
</td>
<td class="py-2 text-right whitespace-nowrap">
@if ($package->zip_path)
<button type="button" wire:click="downloadPackage({{ $package->id }})"
class="{{ $buttonClass }} border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700">
ZIP
</button>
@endif
@unless ($package->isDeployed())
<button type="button" wire:click="markDeployed({{ $package->id }})"
wire:confirm="Megjelöljük kirakottként? Innentől ez lesz a következő csomag kezdő commitja ezen a környezeten."
class="{{ $buttonClass }} ml-1 border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700">
Kirakva
</button>
@endunless
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
</div>
</x-filament-panels::page>

View File

@ -1,6 +1,7 @@
<?php
use App\Filament\Pages\DeploymentPackage;
use App\Models\DeploymentPackage as DeploymentPackageRecord;
use App\Models\FeatureFlag;
use App\Models\Role;
use App\Models\User;
@ -401,6 +402,104 @@ function deploymentCleanup(string $path): void
deploymentCleanup($repository['path']);
});
test('a csomagolás naplóbejegyzést és letölthető ZIP-et hagy maga után', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();
[$first, , $third] = $repository['commits'];
$this->actingAs(deploymentDeveloper());
Livewire::test(DeploymentPackage::class)
->call('selectFrom', $first)
->call('selectTo', $third)
->call('createPackage');
$record = DeploymentPackageRecord::query()->latest('id')->first();
expect($record)->not->toBeNull()
->and($record->target)->toBe('e2e')
->and($record->to_commit)->toBe($third)
->and($record->deployed_at)->toBeNull()
->and(File::exists($record->zip_path))->toBeTrue();
Livewire::test(DeploymentPackage::class)
->call('downloadPackage', $record->id)
->assertFileDownloaded();
deploymentCleanup($repository['path']);
});
test('a kirakottként megjelölt csomag adja a következő tartomány kezdetét', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();
[$first, $second, $third] = $repository['commits'];
$this->actingAs(deploymentDeveloper());
$record = DeploymentPackageRecord::create([
'name' => 'deploy_e2e_teszt',
'target' => 'e2e',
'branch' => 'main',
'from_commit' => $first,
'to_commit' => $second,
'folder' => $repository['path'],
]);
Livewire::test(DeploymentPackage::class)
->call('markDeployed', $record->id)
->assertSet('fromCommit', $second);
expect($record->refresh()->deployed_at)->not->toBeNull();
// Új oldalbetöltésnél is a kirakott commitról indul a tartomány.
Livewire::test(DeploymentPackage::class)->assertSet('fromCommit', $second);
deploymentCleanup($repository['path']);
});
test('a kimeneti mappán kívüli ZIP útvonal nem tölthető le', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();
[$first, , $third] = $repository['commits'];
$this->actingAs(deploymentDeveloper());
// Elrontott (vagy szándékosan átírt) rekord: a fájl létezik, de a csomagoló
// kimeneti mappáján kívül van.
$record = DeploymentPackageRecord::create([
'name' => 'deploy_e2e_hamis',
'target' => 'e2e',
'branch' => 'main',
'from_commit' => $first,
'to_commit' => $third,
'folder' => $repository['path'],
'zip_path' => base_path('composer.json'),
]);
Livewire::test(DeploymentPackage::class)
->call('downloadPackage', $record->id)
->assertNoFileDownloaded();
deploymentCleanup($repository['path']);
});
test('a törlő szkript dry runban fut és ellenőrzi az app gyökeret', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();
[$first, , $third] = $repository['commits'];
$package = app(DeploymentPackageBuilder::class)->build('e2e', 'main', $first, $third);
$script = File::get($package['path'].'/_torles.sh');
expect($script)->toStartWith('#!/usr/bin/env bash')
->and($script)->toContain('[[ -f artisan && -d app ]]')
->and($script)->toContain('"app/Regi.php"')
// Windowson generáljuk, Linuxon fut: CRLF-fel a shebang sor törne el.
->and($script)->not->toContain("\r\n");
deploymentCleanup($repository['path']);
});
test('az oldal nem fogad el kamu commit azonosítót', function () {
deploymentAllowEnvironment();
$repository = deploymentTestRepository();