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:
parent
7ad9c6fea8
commit
bfc1484611
@ -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
|
||||
*/
|
||||
|
||||
48
app/Models/DeploymentPackage.php
Normal file
48
app/Models/DeploymentPackage.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
*/
|
||||
|
||||
@ -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.
|
||||
*
|
||||
|
||||
@ -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');
|
||||
}
|
||||
};
|
||||
@ -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>
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user