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>
This commit is contained in:
parent
8bcfeba0f9
commit
09eed3eeb3
303
app/Filament/Pages/DeploymentPackage.php
Normal file
303
app/Filament/Pages/DeploymentPackage.php
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Filament\Pages;
|
||||||
|
|
||||||
|
use App\Services\Deployment\ChangeSetAnalyzer;
|
||||||
|
use App\Services\Deployment\DeploymentGuard;
|
||||||
|
use App\Services\Deployment\DeploymentTargets;
|
||||||
|
use App\Services\Deployment\GitRepository;
|
||||||
|
use Filament\Forms\Components\Select;
|
||||||
|
use Filament\Forms\Components\TextInput;
|
||||||
|
use Filament\Forms\Concerns\InteractsWithForms;
|
||||||
|
use Filament\Forms\Contracts\HasForms;
|
||||||
|
use Filament\Notifications\Notification;
|
||||||
|
use Filament\Pages\Page;
|
||||||
|
use Filament\Schemas\Components\Grid;
|
||||||
|
use Filament\Schemas\Schema;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deployment csomagoló - fázis 1: célkörnyezet, commitlista és diff-előnézet.
|
||||||
|
*
|
||||||
|
* Ez az oldal (a fázis 1-ben) semmit nem ír a fájlrendszerre, csak olvassa a git
|
||||||
|
* történetet. A tényleges csomagolás a fázis 2-ben érkezik.
|
||||||
|
*
|
||||||
|
* A hozzáférést a DeploymentGuard dönti el (env kapcsoló + stage whitelist +
|
||||||
|
* developer szerep + feature flag), és a mount()-on kívül minden akció elején
|
||||||
|
* újra lefut - a felület elrejtése önmagában nem védelem.
|
||||||
|
*/
|
||||||
|
class DeploymentPackage extends Page implements HasForms
|
||||||
|
{
|
||||||
|
use InteractsWithForms;
|
||||||
|
|
||||||
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-rocket-launch';
|
||||||
|
|
||||||
|
protected string $view = 'filament.pages.deployment-package';
|
||||||
|
|
||||||
|
protected static ?string $title = 'Deployment csomagoló';
|
||||||
|
|
||||||
|
protected static ?string $navigationLabel = 'Deployment csomagoló';
|
||||||
|
|
||||||
|
protected static bool $shouldRegisterNavigation = false;
|
||||||
|
|
||||||
|
/** @var array<string, mixed>|null */
|
||||||
|
public ?array $data = [];
|
||||||
|
|
||||||
|
public ?string $fromCommit = null;
|
||||||
|
|
||||||
|
public ?string $toCommit = null;
|
||||||
|
|
||||||
|
public ?string $gitError = null;
|
||||||
|
|
||||||
|
public static function canAccess(): bool
|
||||||
|
{
|
||||||
|
return app(DeploymentGuard::class)->isAllowed(auth()->user());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(): void
|
||||||
|
{
|
||||||
|
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
|
||||||
|
|
||||||
|
/** @var array<int, int> $limits */
|
||||||
|
$limits = (array) config('deployment.commit_limits', [50]);
|
||||||
|
|
||||||
|
$this->form->fill([
|
||||||
|
'target' => app(DeploymentTargets::class)->defaultKey(),
|
||||||
|
'branch' => $this->defaultBranch(),
|
||||||
|
'limit' => $limits[0] ?? 50,
|
||||||
|
'search' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function form(Schema $form): Schema
|
||||||
|
{
|
||||||
|
/** @var array<int, int> $limits */
|
||||||
|
$limits = (array) config('deployment.commit_limits', [50]);
|
||||||
|
|
||||||
|
return $form
|
||||||
|
->schema([
|
||||||
|
Grid::make(4)
|
||||||
|
->schema([
|
||||||
|
Select::make('target')
|
||||||
|
->label('Célkörnyezet')
|
||||||
|
->options(app(DeploymentTargets::class)->options())
|
||||||
|
->required()
|
||||||
|
->live()
|
||||||
|
->helperText(fn (): ?string => $this->target()['remote_path'] ?? null),
|
||||||
|
Select::make('branch')
|
||||||
|
->label('Branch')
|
||||||
|
->options($this->branchOptions())
|
||||||
|
->required()
|
||||||
|
->searchable()
|
||||||
|
->live()
|
||||||
|
->afterStateUpdated(fn () => $this->clearRange()),
|
||||||
|
Select::make('limit')
|
||||||
|
->label('Commitok száma')
|
||||||
|
->options(array_combine($limits, $limits))
|
||||||
|
->required()
|
||||||
|
->live(),
|
||||||
|
TextInput::make('search')
|
||||||
|
->label('Keresés')
|
||||||
|
->placeholder('hash, üzenet vagy szerző')
|
||||||
|
->helperText('A betöltött listán belül szűr.')
|
||||||
|
->live(debounce: 400),
|
||||||
|
]),
|
||||||
|
])
|
||||||
|
->statePath('data');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectFrom(string $hash): void
|
||||||
|
{
|
||||||
|
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
|
||||||
|
|
||||||
|
$this->fromCommit = $this->resolveOrWarn($hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function selectTo(string $hash): void
|
||||||
|
{
|
||||||
|
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
|
||||||
|
|
||||||
|
$this->toCommit = $this->resolveOrWarn($hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function swapRange(): void
|
||||||
|
{
|
||||||
|
app(DeploymentGuard::class)->ensureAllowed(auth()->user());
|
||||||
|
|
||||||
|
[$this->fromCommit, $this->toCommit] = [$this->toCommit, $this->fromCommit];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function clearRange(): void
|
||||||
|
{
|
||||||
|
$this->fromCommit = null;
|
||||||
|
$this->toCommit = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null
|
||||||
|
*/
|
||||||
|
public function target(): ?array
|
||||||
|
{
|
||||||
|
return app(DeploymentTargets::class)->find($this->data['target'] ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, array{hash:string, short:string, author:string, date:string, subject:string, file_count:?int}>
|
||||||
|
*/
|
||||||
|
#[Computed]
|
||||||
|
public function commits(): array
|
||||||
|
{
|
||||||
|
$branch = $this->data['branch'] ?? null;
|
||||||
|
|
||||||
|
if (! $branch) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$commits = app(GitRepository::class)->commits($branch, (int) ($this->data['limit'] ?? 50));
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
$this->gitError = $exception->getMessage();
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$search = trim((string) ($this->data['search'] ?? ''));
|
||||||
|
|
||||||
|
if ($search === '') {
|
||||||
|
return $commits;
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
$commits,
|
||||||
|
fn (array $commit): bool => str_contains(mb_strtolower($commit['subject'].' '.$commit['author'].' '.$commit['hash']), mb_strtolower($search)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A kijelölt tartomány elemzése, vagy null, ha még nincs két commit kiválasztva.
|
||||||
|
*
|
||||||
|
* @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}>
|
||||||
|
* }|null
|
||||||
|
*/
|
||||||
|
#[Computed]
|
||||||
|
public function changeSet(): ?array
|
||||||
|
{
|
||||||
|
if (! $this->fromCommit || ! $this->toCommit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$repository = app(GitRepository::class);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$changedFiles = $repository->changedFiles($this->fromCommit, $this->toCommit);
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
$this->gitError = $exception->getMessage();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$changeSet = app(ChangeSetAnalyzer::class)->analyze(
|
||||||
|
$changedFiles,
|
||||||
|
$this->target(),
|
||||||
|
$this->data['branch'] ?? null,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ($this->fromCommit === $this->toCommit) {
|
||||||
|
array_unshift($changeSet['warnings'], [
|
||||||
|
'level' => 'warning',
|
||||||
|
'title' => 'Azonos commitok',
|
||||||
|
'body' => 'A kezdő és a záró commit ugyanaz, így a tartomány üres.',
|
||||||
|
]);
|
||||||
|
} elseif (! $repository->isAncestor($this->fromCommit, $this->toCommit)) {
|
||||||
|
array_unshift($changeSet['warnings'], [
|
||||||
|
'level' => 'danger',
|
||||||
|
'title' => 'Fordított vagy szétágazó tartomány',
|
||||||
|
'body' => 'A kezdő commit nem őse a zárónak, ezért a lista nem a "mi került bele azóta" kérdésre válaszol. Ellenőrizd a sorrendet (Csere gomb), vagy azt, hogy ugyanazon az ágon vagy-e.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $changeSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A kijelölt tartományba eső commitok - csak a lista kiemeléséhez.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
#[Computed]
|
||||||
|
public function highlightedHashes(): array
|
||||||
|
{
|
||||||
|
if (! $this->fromCommit || ! $this->toCommit) {
|
||||||
|
return array_values(array_filter([$this->fromCommit, $this->toCommit]));
|
||||||
|
}
|
||||||
|
|
||||||
|
$hashes = array_column($this->commits(), 'hash');
|
||||||
|
$fromIndex = array_search($this->fromCommit, $hashes, true);
|
||||||
|
$toIndex = array_search($this->toCommit, $hashes, true);
|
||||||
|
|
||||||
|
if ($fromIndex === false || $toIndex === false) {
|
||||||
|
return array_values(array_filter([$this->fromCommit, $this->toCommit]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// A lista fentről lefelé a legfrissebbtől halad, tehát a záró commit van előrébb.
|
||||||
|
[$start, $end] = $fromIndex <= $toIndex ? [$fromIndex, $toIndex] : [$toIndex, $fromIndex];
|
||||||
|
|
||||||
|
return array_slice($hashes, $start, $end - $start + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function branchOptions(): array
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$branches = app(GitRepository::class)->branches();
|
||||||
|
} catch (RuntimeException $exception) {
|
||||||
|
$this->gitError = $exception->getMessage();
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_combine($branches, $branches) ?: [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRepositoryAvailable(): bool
|
||||||
|
{
|
||||||
|
return app(GitRepository::class)->isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function defaultBranch(): ?string
|
||||||
|
{
|
||||||
|
$repository = app(GitRepository::class);
|
||||||
|
$current = $repository->currentBranch();
|
||||||
|
|
||||||
|
if ($current) {
|
||||||
|
return $current;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->branchOptions() === [] ? null : array_key_first($this->branchOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveOrWarn(string $hash): ?string
|
||||||
|
{
|
||||||
|
$resolved = app(GitRepository::class)->resolveCommit($hash);
|
||||||
|
|
||||||
|
if (! $resolved) {
|
||||||
|
Notification::make()
|
||||||
|
->title('Ismeretlen commit')
|
||||||
|
->body('A kiválasztott commit nem található a repóban.')
|
||||||
|
->danger()
|
||||||
|
->send();
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
170
app/Services/Deployment/ChangeSetAnalyzer.php
Normal file
170
app/Services/Deployment/ChangeSetAnalyzer.php
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
<?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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
69
app/Services/Deployment/DeploymentGuard.php
Normal file
69
app/Services/Deployment/DeploymentGuard.php
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Deployment;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Laravel\Pennant\Feature;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A deployment csomagoló hozzáférés-ellenőrzése egy helyen.
|
||||||
|
*
|
||||||
|
* Az alkalmazás kódja manuális fájlmásolással kerül a szerverekre, tehát ez az oldal
|
||||||
|
* fizikailag ott lesz az éles környezetben is. Ezért négy, egymástól független feltétel
|
||||||
|
* van, és mindet MINDEN belépési ponton ellenőrizni kell (oldal mount, akciók, később a
|
||||||
|
* csomagoló service) - ha csak a felületet védjük, az látszatvédelem.
|
||||||
|
*
|
||||||
|
* A négyből a tényleges védelmet az .env kapcsoló és a stage whitelist adja, a
|
||||||
|
* jogosultságot a szerepkör. A feature flag NEM biztonsági réteg: a menü-láthatóságot
|
||||||
|
* vezérli, és lehetővé teszi a deploy nélküli kikapcsolást, illetve a név szerinti
|
||||||
|
* átadást (feature_flag_overrides) developer szerep osztogatása nélkül.
|
||||||
|
*/
|
||||||
|
class DeploymentGuard
|
||||||
|
{
|
||||||
|
public function isAllowed(?User $user = null): bool
|
||||||
|
{
|
||||||
|
return $this->denialReason($user) === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Az első nem teljesülő feltétel magyarázata, vagy null, ha minden rendben.
|
||||||
|
*/
|
||||||
|
public function denialReason(?User $user = null): ?string
|
||||||
|
{
|
||||||
|
if (! config('deployment.enabled')) {
|
||||||
|
return 'A deployment csomagoló ki van kapcsolva (DEPLOYMENT_PACKAGE_ENABLED).';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var array<int, string> $stages */
|
||||||
|
$stages = (array) config('deployment.stages', []);
|
||||||
|
|
||||||
|
if (! in_array((string) config('app.stage'), $stages, true)) {
|
||||||
|
return sprintf(
|
||||||
|
'A deployment csomagoló csak fejlesztői környezetben használható (engedélyezett stage: %s, jelenlegi: %s).',
|
||||||
|
implode(', ', $stages),
|
||||||
|
(string) config('app.stage'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user) {
|
||||||
|
return 'Bejelentkezés szükséges.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $user->hasRole((string) config('deployment.role'))) {
|
||||||
|
return 'A deployment csomagoló csak fejlesztői szerepkörrel érhető el.';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Feature::for($user)->active((string) config('deployment.feature_flag'))) {
|
||||||
|
return 'A deployment csomagoló feature flag nincs bekapcsolva erre a felhasználóra.';
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ensureAllowed(?User $user = null): void
|
||||||
|
{
|
||||||
|
if ($reason = $this->denialReason($user)) {
|
||||||
|
abort(403, $reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
app/Services/Deployment/DeploymentTargets.php
Normal file
54
app/Services/Deployment/DeploymentTargets.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Deployment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A célkörnyezetek (e2e, d2d) feloldása a config/deployment.php-ból.
|
||||||
|
*
|
||||||
|
* A target nem utólagos címke: már a csomagoláskor rögzül, mert befolyásolja a
|
||||||
|
* mappanevet, a teendők listáját, a kezdő commit előtöltését és a naplót is.
|
||||||
|
*/
|
||||||
|
class DeploymentTargets
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}>
|
||||||
|
*/
|
||||||
|
public function all(): array
|
||||||
|
{
|
||||||
|
/** @var array<string, array<string, string>> $targets */
|
||||||
|
$targets = (array) config('deployment.targets', []);
|
||||||
|
|
||||||
|
$resolved = [];
|
||||||
|
|
||||||
|
foreach ($targets as $key => $target) {
|
||||||
|
$resolved[$key] = array_merge(
|
||||||
|
['label' => $key, 'domain' => '', 'remote_path' => '', 'expected_branch' => null],
|
||||||
|
$target,
|
||||||
|
['key' => $key],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null
|
||||||
|
*/
|
||||||
|
public function find(?string $key): ?array
|
||||||
|
{
|
||||||
|
return $key ? ($this->all()[$key] ?? null) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function options(): array
|
||||||
|
{
|
||||||
|
return array_map(fn (array $target): string => $target['label'], $this->all());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function defaultKey(): ?string
|
||||||
|
{
|
||||||
|
return array_key_first($this->all());
|
||||||
|
}
|
||||||
|
}
|
||||||
273
app/Services/Deployment/GitRepository.php
Normal file
273
app/Services/Deployment/GitRepository.php
Normal file
@ -0,0 +1,273 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Deployment;
|
||||||
|
|
||||||
|
use RuntimeException;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Csak olvasó git wrapper a deployment csomagolóhoz.
|
||||||
|
*
|
||||||
|
* Minden hívás argumentum-tömbbel megy a Symfony Process-nek, soha nem shell-stringgel,
|
||||||
|
* így a felületről érkező érték nem tud parancsot injektálni. Ettől függetlenül minden
|
||||||
|
* bemenetet külön is validálunk: a git a kötőjellel kezdődő értéket kapcsolóként
|
||||||
|
* értelmezné, a `..` pedig commit-tartományt jelent, nem fájlnevet.
|
||||||
|
*/
|
||||||
|
class GitRepository
|
||||||
|
{
|
||||||
|
/** Mezőelválasztó a git log formátumban (ASCII unit separator). */
|
||||||
|
private const FIELD_SEPARATOR = "\x1f";
|
||||||
|
|
||||||
|
/** Rekordelválasztó a git log formátumban (ASCII record separator). */
|
||||||
|
private const RECORD_SEPARATOR = "\x1e";
|
||||||
|
|
||||||
|
private const COMMIT_PATTERN = '/^[0-9a-f]{7,40}$/';
|
||||||
|
|
||||||
|
private const REFERENCE_PATTERN = '#^[A-Za-z0-9][A-Za-z0-9._/-]*$#';
|
||||||
|
|
||||||
|
public function isAvailable(): bool
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$this->run(['rev-parse', '--git-dir']);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (RuntimeException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function currentBranch(): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$branch = trim($this->run(['rev-parse', '--abbrev-ref', 'HEAD']));
|
||||||
|
} catch (RuntimeException) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ($branch === '' || $branch === 'HEAD') ? null : $branch;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function branches(): array
|
||||||
|
{
|
||||||
|
$output = $this->run([
|
||||||
|
'for-each-ref',
|
||||||
|
'--format=%(refname:short)',
|
||||||
|
'--sort=-committerdate',
|
||||||
|
'refs/heads',
|
||||||
|
'refs/remotes',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return collect(explode("\n", $output))
|
||||||
|
->map(fn (string $line): string => trim($line))
|
||||||
|
->filter()
|
||||||
|
// Az "origin/HEAD" csak egy mutató az alapértelmezett branchre, nem önálló ág.
|
||||||
|
->reject(fn (string $branch): bool => str_ends_with($branch, '/HEAD'))
|
||||||
|
->unique()
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Commitlista egy referenciáról, legfrissebbtől visszafelé.
|
||||||
|
*
|
||||||
|
* A --shortstat miatt minden commit után megjelenik a "N files changed" sor is,
|
||||||
|
* ezt az utolsó mezőből (a tárgyból) bányásszuk ki - így nem kell commitonként
|
||||||
|
* külön git hívás a fájlszámhoz.
|
||||||
|
*
|
||||||
|
* @return array<int, array{hash:string, short:string, author:string, date:string, subject:string, file_count:?int}>
|
||||||
|
*/
|
||||||
|
public function commits(string $reference, int $limit): array
|
||||||
|
{
|
||||||
|
$this->assertReference($reference);
|
||||||
|
|
||||||
|
$format = self::RECORD_SEPARATOR.implode(self::FIELD_SEPARATOR, ['%H', '%h', '%an', '%aI', '%s']);
|
||||||
|
|
||||||
|
$output = $this->run([
|
||||||
|
'log',
|
||||||
|
'--max-count='.max(1, min($limit, 500)),
|
||||||
|
'--no-merges',
|
||||||
|
'--shortstat',
|
||||||
|
'--pretty=format:'.$format,
|
||||||
|
$reference,
|
||||||
|
'--',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$commits = [];
|
||||||
|
|
||||||
|
foreach (explode(self::RECORD_SEPARATOR, $output) as $record) {
|
||||||
|
if (trim($record) === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$fields = explode(self::FIELD_SEPARATOR, $record, 5);
|
||||||
|
|
||||||
|
if (count($fields) < 5) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[$hash, $short, $author, $date, $tail] = $fields;
|
||||||
|
|
||||||
|
$commits[] = [
|
||||||
|
'hash' => $hash,
|
||||||
|
'short' => $short,
|
||||||
|
'author' => $author,
|
||||||
|
'date' => $date,
|
||||||
|
'subject' => trim(explode("\n", $tail, 2)[0]),
|
||||||
|
'file_count' => preg_match('/(\d+)\s+files?\s+changed/', $tail, $matches) === 1
|
||||||
|
? (int) $matches[1]
|
||||||
|
: null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $commits;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Teljes hash, ha a megadott érték létező commitra mutat - különben null.
|
||||||
|
*/
|
||||||
|
public function resolveCommit(string $hash): ?string
|
||||||
|
{
|
||||||
|
if (preg_match(self::COMMIT_PATTERN, $hash) !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resolved = trim($this->run(['rev-parse', '--verify', '--quiet', $hash.'^{commit}']));
|
||||||
|
} catch (RuntimeException) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $resolved === '' ? null : $resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Őse-e az első commit a másodiknak? (Fordított tartomány felismeréséhez.)
|
||||||
|
*/
|
||||||
|
public function isAncestor(string $ancestor, string $descendant): bool
|
||||||
|
{
|
||||||
|
if (! $this->resolveCommit($ancestor) || ! $this->resolveCommit($descendant)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A merge-base 1-es kilépési kóddal jelzi a "nem őse" esetet, ez nem hiba,
|
||||||
|
// ezért itt nem a run() dobó változatát használjuk.
|
||||||
|
return $this->process(['merge-base', '--is-ancestor', $ancestor, $descendant])->run() === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A két commit között változott fájlok (from kizárva, to beleértve).
|
||||||
|
*
|
||||||
|
* @return array<int, array{status:string, path:string, old_path:?string}>
|
||||||
|
*/
|
||||||
|
public function changedFiles(string $from, string $to): array
|
||||||
|
{
|
||||||
|
$fromHash = $this->resolveCommit($from);
|
||||||
|
$toHash = $this->resolveCommit($to);
|
||||||
|
|
||||||
|
if (! $fromHash || ! $toHash) {
|
||||||
|
throw new RuntimeException('Ismeretlen commit azonosító.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// A core.quotePath=false nélkül a git a nem-ASCII fájlneveket idézőjelbe teszi
|
||||||
|
// és oktálisan escape-eli ("app/\303\251kezet.php") - ékezetes útvonalaknál ez
|
||||||
|
// használhatatlan lenne.
|
||||||
|
$output = $this->run([
|
||||||
|
'-c', 'core.quotePath=false',
|
||||||
|
'diff',
|
||||||
|
'--name-status',
|
||||||
|
'--find-renames',
|
||||||
|
$fromHash.'..'.$toHash,
|
||||||
|
'--',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$files = [];
|
||||||
|
|
||||||
|
foreach (explode("\n", $output) as $line) {
|
||||||
|
$line = rtrim($line, "\r\n");
|
||||||
|
|
||||||
|
if ($line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = explode("\t", $line);
|
||||||
|
$status = strtoupper(substr($parts[0], 0, 1));
|
||||||
|
|
||||||
|
// Átnevezés (R) és másolás (C) esetén két útvonal jön: régi és új.
|
||||||
|
$isTwoPath = in_array($status, ['R', 'C'], true) && count($parts) >= 3;
|
||||||
|
$path = $isTwoPath ? $parts[2] : ($parts[1] ?? '');
|
||||||
|
$oldPath = $isTwoPath ? $parts[1] : null;
|
||||||
|
|
||||||
|
if (! $this->isSafePath($path) || ($oldPath !== null && ! $this->isSafePath($oldPath))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$files[] = [
|
||||||
|
'status' => $status,
|
||||||
|
'path' => $path,
|
||||||
|
'old_path' => $oldPath,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
usort($files, fn (array $a, array $b): int => strcmp($a['path'], $b['path']));
|
||||||
|
|
||||||
|
return $files;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A repo gyökeréből ki nem mutató, relatív útvonal-e.
|
||||||
|
*/
|
||||||
|
private function isSafePath(string $path): bool
|
||||||
|
{
|
||||||
|
if ($path === '' || str_starts_with($path, '/') || str_contains($path, "\0")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows meghajtó-előtag (C:/...) és szülőkönyvtár-hivatkozás sem fordulhat elő
|
||||||
|
// valódi git útvonalban, viszont fájlkiírásnál kitörhetne a célmappából.
|
||||||
|
return preg_match('#^[A-Za-z]:#', $path) !== 1
|
||||||
|
&& preg_match('#(^|/)\.\.(/|$)#', $path) !== 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assertReference(string $reference): void
|
||||||
|
{
|
||||||
|
if (preg_match(self::REFERENCE_PATTERN, $reference) !== 1 || str_contains($reference, '..')) {
|
||||||
|
throw new RuntimeException(sprintf('Érvénytelen git referencia: %s', $reference));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
private function process(array $arguments): Process
|
||||||
|
{
|
||||||
|
return new Process(
|
||||||
|
array_merge([(string) config('deployment.git_binary') ?: 'git'], $arguments),
|
||||||
|
(string) config('deployment.repo_path') ?: base_path(),
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
(float) config('deployment.timeout', 60),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
private function run(array $arguments): string
|
||||||
|
{
|
||||||
|
$process = $this->process($arguments);
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
if (! $process->isSuccessful()) {
|
||||||
|
throw new RuntimeException(sprintf(
|
||||||
|
'A git parancs hibára futott (%s): %s',
|
||||||
|
implode(' ', $arguments),
|
||||||
|
trim($process->getErrorOutput()) ?: trim($process->getOutput()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $process->getOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
127
config/deployment.php
Normal file
127
config/deployment.php
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Fő kapcsoló (1. védelmi réteg)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| A deployment csomagoló KIZÁRÓLAG fejlesztői gépen futhat. Mivel az
|
||||||
|
| alkalmazás kódja manuális fájlmásolással kerül a szerverekre, ez az
|
||||||
|
| oldal fizikailag ott lesz minden környezeten - ezért a bekapcsolás
|
||||||
|
| .env-ből történik, ami viszont nincs verziókövetve és nem másoljuk.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enabled' => env('DEPLOYMENT_PACKAGE_ENABLED', false),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Stage whitelist (2. védelmi réteg)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Szándékosan hardkódolt lista, nem env-ből jön: ez fogja meg azt az esetet,
|
||||||
|
| ha valaki a fenti kapcsolót tévedésből bemásolja egy szerver .env-jébe.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'stages' => ['local'],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Jogosultság (3. réteg) és láthatóság (4. réteg)
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| A feature flag NEM biztonsági réteg - a védelmet a fenti kettő és a
|
||||||
|
| szerepkör adja. A flag a menü-láthatóságot vezérli, illetve deploy nélküli
|
||||||
|
| kikapcsolást és név szerinti átadást tesz lehetővé (feature_flag_overrides).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'role' => 'developer',
|
||||||
|
|
||||||
|
'feature_flag' => 'DeploymentPackage',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Git
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'repo_path' => base_path(),
|
||||||
|
|
||||||
|
'git_binary' => env('DEPLOYMENT_GIT_BINARY', 'git'),
|
||||||
|
|
||||||
|
'timeout' => 60,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Csomagolás
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
*/
|
||||||
|
|
||||||
|
'output_path' => env('DEPLOYMENT_OUTPUT_PATH', storage_path('app/private/deployments')),
|
||||||
|
|
||||||
|
'max_files' => 500,
|
||||||
|
|
||||||
|
'commit_limits' => [50, 100, 200],
|
||||||
|
|
||||||
|
/*
|
||||||
|
| Ezek soha nem kerülnek csomagba, akkor sem, ha a diffben szerepelnének.
|
||||||
|
| A minták Str::is() szintaxisúak, a repo gyökeréhez képest relatív úton.
|
||||||
|
*/
|
||||||
|
'exclude' => [
|
||||||
|
'.env',
|
||||||
|
'.env.*',
|
||||||
|
'auth.json',
|
||||||
|
'.git*',
|
||||||
|
'storage/*',
|
||||||
|
'vendor/*',
|
||||||
|
'node_modules/*',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
| Fájlok, amik csomagolhatók, de külön figyelmeztetést érdemelnek, mert
|
||||||
|
| önmagukban nem elegendők (kell melléjük egy parancs a célgépen).
|
||||||
|
*/
|
||||||
|
'attention' => [
|
||||||
|
'migration' => ['database/migrations/*'],
|
||||||
|
'composer' => ['composer.json', 'composer.lock'],
|
||||||
|
'asset' => [
|
||||||
|
'resources/js/*',
|
||||||
|
'resources/css/*',
|
||||||
|
'resources/modern/*',
|
||||||
|
'vite.config.js',
|
||||||
|
'webpack.mix.js',
|
||||||
|
'package.json',
|
||||||
|
'package-lock.json',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Célkörnyezetek
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Az expected_branch csak figyelmeztetést vezérel, nem tiltást.
|
||||||
|
| A remote_path a shared storage útvonala (ld. docs/cicd-context.md).
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'targets' => [
|
||||||
|
'e2e' => [
|
||||||
|
'label' => 'e2e — t2t éles',
|
||||||
|
'domain' => 'e2e.emegrendeles.hu',
|
||||||
|
'remote_path' => '/delirest/test.t2t.emegrendeles.hu/app/',
|
||||||
|
'expected_branch' => 'main',
|
||||||
|
],
|
||||||
|
'd2d' => [
|
||||||
|
'label' => 'd2d — fejlesztői',
|
||||||
|
'domain' => 'd2d.emegrendeles.hu',
|
||||||
|
'remote_path' => '/delirest/d2d.emegrendeles.hu/app/',
|
||||||
|
'expected_branch' => 'test',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
@ -40,6 +40,13 @@
|
|||||||
['displayName' => 'Egyéni felülbírálások', 'link' => \App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource::getUrl()],
|
['displayName' => 'Egyéni felülbírálások', 'link' => \App\Filament\Resources\FeatureFlagOverrides\FeatureFlagOverrideResource::getUrl()],
|
||||||
]];
|
]];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A deployment csomagoló kizárólag fejlesztői gépen érhető el (DeploymentGuard:
|
||||||
|
// .env kapcsoló + stage whitelist + developer szerep + feature flag), ezért a
|
||||||
|
// menüpont a szervereken akkor sem jelenik meg, ha a kód oda is felmásolódik.
|
||||||
|
if (\App\Filament\Pages\DeploymentPackage::canAccess()) {
|
||||||
|
$modules[] = ['name' => 'deploymentPackage', 'displayName' => 'Deployment', 'icon' => 'upload', 'link' => \App\Filament\Pages\DeploymentPackage::getUrl(), 'roles' => ['root','developer']];
|
||||||
|
}
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<nav class="fi-speed-button-nav flex items-center h-full ml-4 w-full">
|
<nav class="fi-speed-button-nav flex items-center h-full ml-4 w-full">
|
||||||
|
|||||||
225
resources/views/filament/pages/deployment-package.blade.php
Normal file
225
resources/views/filament/pages/deployment-package.blade.php
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
@php
|
||||||
|
// A fázis 1 felülete csak olvas: a git történetet listázza és a kijelölt tartomány
|
||||||
|
// diffjét mutatja. A számított property-ket egy helyen kérjük le, hogy a hibabanner
|
||||||
|
// (gitError) már a renderelés elején beállított állapotot lássa.
|
||||||
|
$commits = $this->commits;
|
||||||
|
$changeSet = $this->changeSet;
|
||||||
|
$highlighted = $this->highlightedHashes;
|
||||||
|
$target = $this->target();
|
||||||
|
|
||||||
|
$statusStyles = [
|
||||||
|
'A' => ['label' => 'új', 'class' => 'bg-emerald-100 text-emerald-800 dark:bg-emerald-900 dark:text-emerald-200'],
|
||||||
|
'M' => ['label' => 'módosult', 'class' => 'bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200'],
|
||||||
|
'D' => ['label' => 'törölt', 'class' => 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200'],
|
||||||
|
'R' => ['label' => 'átnevezett', 'class' => 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'],
|
||||||
|
'C' => ['label' => 'másolt', 'class' => 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200'],
|
||||||
|
];
|
||||||
|
|
||||||
|
$warningStyles = [
|
||||||
|
'danger' => 'border-red-300 bg-red-50 text-red-900 dark:border-red-800 dark:bg-red-950 dark:text-red-200',
|
||||||
|
'warning' => 'border-amber-300 bg-amber-50 text-amber-900 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200',
|
||||||
|
'info' => 'border-gray-300 bg-gray-50 text-gray-800 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300',
|
||||||
|
];
|
||||||
|
|
||||||
|
$cardClass = 'p-6 bg-white border border-gray-200 rounded-lg shadow dark:bg-gray-800 dark:border-gray-700';
|
||||||
|
$buttonClass = 'px-2 py-1 text-xs font-semibold rounded border transition';
|
||||||
|
@endphp
|
||||||
|
|
||||||
|
<x-filament-panels::page>
|
||||||
|
<div class="space-y-6">
|
||||||
|
|
||||||
|
<div class="{{ $cardClass }}">
|
||||||
|
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">Deployment csomagoló</h5>
|
||||||
|
<p class="font-normal text-gray-700 dark:text-gray-400">
|
||||||
|
Válaszd ki a célkörnyezetet, majd két commitot: a köztük változott fájlok kerülnek majd a csomagba
|
||||||
|
(a kezdő commit kizárva, a záró beleértve).
|
||||||
|
</p>
|
||||||
|
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Ez a felület jelenleg <strong>csak olvas</strong> — a csomag előállítása a következő fázisban készül el.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($this->gitError)
|
||||||
|
<div class="p-4 border rounded-lg {{ $warningStyles['danger'] }}">
|
||||||
|
<p class="font-semibold">Git hiba</p>
|
||||||
|
<p class="mt-1 text-sm">{{ $this->gitError }}</p>
|
||||||
|
</div>
|
||||||
|
@elseif (! $this->isRepositoryAvailable())
|
||||||
|
<div class="p-4 border rounded-lg {{ $warningStyles['danger'] }}">
|
||||||
|
<p class="font-semibold">Nem érhető el git repó</p>
|
||||||
|
<p class="mt-1 text-sm">
|
||||||
|
A(z) <code>{{ config('deployment.repo_path') }}</code> útvonalon nincs git working copy,
|
||||||
|
vagy a git parancs nem található.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<div class="{{ $cardClass }}">
|
||||||
|
{{ $this->form }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="{{ $cardClass }}">
|
||||||
|
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||||
|
<div class="flex flex-wrap items-center gap-3 text-sm">
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">Tartomány:</span>
|
||||||
|
|
||||||
|
@if ($this->fromCommit)
|
||||||
|
<code class="px-2 py-1 rounded bg-gray-100 dark:bg-gray-900">{{ substr($this->fromCommit, 0, 7) }}</code>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-400">nincs kezdő commit</span>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<span class="text-gray-400">→</span>
|
||||||
|
|
||||||
|
@if ($this->toCommit)
|
||||||
|
<code class="px-2 py-1 rounded bg-gray-100 dark:bg-gray-900">{{ substr($this->toCommit, 0, 7) }}</code>
|
||||||
|
@else
|
||||||
|
<span class="text-gray-400">nincs záró commit</span>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($changeSet)
|
||||||
|
<span class="text-gray-500 dark:text-gray-400">
|
||||||
|
· {{ $changeSet['counts']['copied'] }} másolandó
|
||||||
|
@if ($changeSet['counts']['deleted'] > 0)
|
||||||
|
, {{ $changeSet['counts']['deleted'] }} törlendő
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<button type="button" wire:click="swapRange"
|
||||||
|
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">
|
||||||
|
Csere
|
||||||
|
</button>
|
||||||
|
<button type="button" wire:click="clearRange"
|
||||||
|
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">
|
||||||
|
Törlés
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($target)
|
||||||
|
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Cél: <strong>{{ $target['label'] }}</strong> — a fájlok ide kerülnek majd feltöltésre:
|
||||||
|
<code>{{ $target['remote_path'] }}</code>
|
||||||
|
</p>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="{{ $cardClass }}">
|
||||||
|
<h6 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Commitok
|
||||||
|
<span class="ml-1 text-sm font-normal text-gray-500 dark:text-gray-400">({{ count($commits) }} db)</span>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
@if ($commits === [])
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">Nincs megjeleníthető commit.</p>
|
||||||
|
@else
|
||||||
|
<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">Kijelölés</th>
|
||||||
|
<th class="py-2 pr-4">Hash</th>
|
||||||
|
<th class="py-2 pr-4">Dátum</th>
|
||||||
|
<th class="py-2 pr-4">Szerző</th>
|
||||||
|
<th class="py-2 pr-4">Üzenet</th>
|
||||||
|
<th class="py-2 text-right">Fájl</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($commits as $commit)
|
||||||
|
@php
|
||||||
|
$isFrom = $this->fromCommit === $commit['hash'];
|
||||||
|
$isTo = $this->toCommit === $commit['hash'];
|
||||||
|
$inRange = in_array($commit['hash'], $highlighted, true);
|
||||||
|
@endphp
|
||||||
|
<tr wire:key="commit-{{ $commit['hash'] }}"
|
||||||
|
class="border-b border-gray-100 dark:border-gray-700 {{ $inRange ? 'bg-primary-50 dark:bg-gray-900' : '' }}">
|
||||||
|
<td class="py-2 pr-4 whitespace-nowrap">
|
||||||
|
<button type="button" wire:click="selectFrom('{{ $commit['hash'] }}')"
|
||||||
|
class="{{ $buttonClass }} {{ $isFrom ? 'border-primary-600 bg-primary-600 text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700' }}">
|
||||||
|
Kezdő
|
||||||
|
</button>
|
||||||
|
<button type="button" wire:click="selectTo('{{ $commit['hash'] }}')"
|
||||||
|
class="{{ $buttonClass }} ml-1 {{ $isTo ? 'border-primary-600 bg-primary-600 text-white' : 'border-gray-300 text-gray-700 hover:bg-gray-100 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700' }}">
|
||||||
|
Záró
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4 font-mono text-xs whitespace-nowrap">{{ $commit['short'] }}</td>
|
||||||
|
<td class="py-2 pr-4 whitespace-nowrap text-gray-600 dark:text-gray-300">
|
||||||
|
{{ \Illuminate\Support\Carbon::parse($commit['date'])->format('Y.m.d H:i') }}
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4 whitespace-nowrap text-gray-600 dark:text-gray-300">{{ $commit['author'] }}</td>
|
||||||
|
<td class="py-2 pr-4 text-gray-800 dark:text-gray-100">{{ $commit['subject'] }}</td>
|
||||||
|
<td class="py-2 text-right text-gray-500 dark:text-gray-400">{{ $commit['file_count'] ?? '–' }}</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ($changeSet)
|
||||||
|
<div class="{{ $cardClass }}">
|
||||||
|
<h6 class="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
||||||
|
Változott fájlok
|
||||||
|
<span class="ml-1 text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
({{ $changeSet['counts']['total'] }} db)
|
||||||
|
</span>
|
||||||
|
</h6>
|
||||||
|
|
||||||
|
@foreach ($changeSet['warnings'] as $warning)
|
||||||
|
<div class="p-4 mb-3 border rounded-lg {{ $warningStyles[$warning['level']] ?? $warningStyles['info'] }}">
|
||||||
|
<p class="font-semibold">{{ $warning['title'] }}</p>
|
||||||
|
<p class="mt-1 text-sm">{{ $warning['body'] }}</p>
|
||||||
|
</div>
|
||||||
|
@endforeach
|
||||||
|
|
||||||
|
@if ($changeSet['files'] !== [])
|
||||||
|
<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">Állapot</th>
|
||||||
|
<th class="py-2 pr-4">Útvonal</th>
|
||||||
|
<th class="py-2">Művelet</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach ($changeSet['files'] as $file)
|
||||||
|
@php
|
||||||
|
$style = $statusStyles[$file['status']] ?? ['label' => $file['status'], 'class' => 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'];
|
||||||
|
@endphp
|
||||||
|
<tr wire:key="file-{{ $loop->index }}" class="border-b border-gray-100 dark:border-gray-700 {{ $file['excluded'] ? 'opacity-50' : '' }}">
|
||||||
|
<td class="py-2 pr-4 whitespace-nowrap">
|
||||||
|
<span class="px-2 py-0.5 text-xs font-semibold rounded {{ $style['class'] }}">
|
||||||
|
{{ $file['status'] }} · {{ $style['label'] }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="py-2 pr-4 font-mono text-xs break-all text-gray-800 dark:text-gray-100">
|
||||||
|
{{ $file['path'] }}
|
||||||
|
@if ($file['old_path'])
|
||||||
|
<span class="block text-gray-400">← {{ $file['old_path'] }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td class="py-2 whitespace-nowrap text-gray-600 dark:text-gray-300">
|
||||||
|
@switch($file['action'])
|
||||||
|
@case('copy') másolandó @break
|
||||||
|
@case('delete') törlendő a célgépen @break
|
||||||
|
@default kihagyva (kizárási lista)
|
||||||
|
@endswitch
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</x-filament-panels::page>
|
||||||
@ -173,6 +173,18 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
];
|
];
|
||||||
|
// A deployment csomagoló kizárólag fejlesztői gépen érhető el (DeploymentGuard) -
|
||||||
|
// ugyanaz a canAccess() hívás, mint a modern speed-button-nav-bar.blade.php-ban.
|
||||||
|
if (\App\Filament\Pages\DeploymentPackage::canAccess()) {
|
||||||
|
$Modules['deploymentPackage'] = [
|
||||||
|
'name' => 'deploymentPackage',
|
||||||
|
'DisplayName' => 'Deployment',
|
||||||
|
'icon' => 'upload',
|
||||||
|
'link' => \App\Filament\Pages\DeploymentPackage::getUrl(),
|
||||||
|
'noAjax' => true,
|
||||||
|
'roles' => ['root', 'developer'],
|
||||||
|
];
|
||||||
|
}
|
||||||
$Modules=json_decode(json_encode($Modules));
|
$Modules=json_decode(json_encode($Modules));
|
||||||
?>
|
?>
|
||||||
{{--
|
{{--
|
||||||
|
|||||||
290
tests/Feature/DeploymentPackageTest.php
Normal file
290
tests/Feature/DeploymentPackageTest.php
Normal file
@ -0,0 +1,290 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Filament\Pages\DeploymentPackage;
|
||||||
|
use App\Models\FeatureFlag;
|
||||||
|
use App\Models\Role;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Services\Deployment\ChangeSetAnalyzer;
|
||||||
|
use App\Services\Deployment\DeploymentGuard;
|
||||||
|
use App\Services\Deployment\GitRepository;
|
||||||
|
use App\Services\FeatureFlagRegistrar;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\Config;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Livewire\Livewire;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
uses(TestCase::class, RefreshDatabase::class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fejlesztői gépet szimuláló környezet: az .env kapcsoló és a stage whitelist rendben.
|
||||||
|
*/
|
||||||
|
function deploymentAllowEnvironment(): void
|
||||||
|
{
|
||||||
|
Config::set('deployment.enabled', true);
|
||||||
|
Config::set('app.stage', 'local');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deploymentDeveloper(bool $flagEnabled = true): User
|
||||||
|
{
|
||||||
|
$role = Role::firstOrCreate(['name' => 'developer'], ['display_name' => 'Developer']);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
$user->addRole($role);
|
||||||
|
|
||||||
|
FeatureFlag::create([
|
||||||
|
'name' => 'DeploymentPackage',
|
||||||
|
'label' => 'Deployment csomagoló',
|
||||||
|
'enabled' => $flagEnabled,
|
||||||
|
'stages' => null,
|
||||||
|
'roles' => ['developer'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app(FeatureFlagRegistrar::class)->registerAll();
|
||||||
|
|
||||||
|
return $user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
function deploymentGit(string $path, array $arguments): string
|
||||||
|
{
|
||||||
|
$process = new Process(array_merge(['git'], $arguments), $path);
|
||||||
|
$process->mustRun();
|
||||||
|
|
||||||
|
return $process->getOutput();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determinisztikus teszt-repó: átnevezés, törlés, ékezetes fájlnév és migráció is van benne.
|
||||||
|
*
|
||||||
|
* @return array{path:string, commits:array<int, string>}
|
||||||
|
*/
|
||||||
|
function deploymentTestRepository(): array
|
||||||
|
{
|
||||||
|
$path = sys_get_temp_dir().DIRECTORY_SEPARATOR.'deployment-package-test-'.uniqid();
|
||||||
|
|
||||||
|
File::makeDirectory($path.'/app', 0777, true);
|
||||||
|
File::makeDirectory($path.'/database/migrations', 0777, true);
|
||||||
|
|
||||||
|
deploymentGit($path, ['init']);
|
||||||
|
deploymentGit($path, ['config', 'user.email', 'teszt@example.com']);
|
||||||
|
deploymentGit($path, ['config', 'user.name', 'Teszt']);
|
||||||
|
deploymentGit($path, ['config', 'commit.gpgsign', 'false']);
|
||||||
|
|
||||||
|
File::put($path.'/app/Elso.php', "<?php\n// elso\n");
|
||||||
|
File::put($path.'/app/Regi.php', "<?php\n// regi\n");
|
||||||
|
File::put($path.'/app/Atnevezendo.php', "<?php\n// atnevezendo\n");
|
||||||
|
deploymentGit($path, ['add', '-A']);
|
||||||
|
deploymentGit($path, ['commit', '-m', 'első commit']);
|
||||||
|
deploymentGit($path, ['branch', '-M', 'main']);
|
||||||
|
|
||||||
|
File::put($path.'/app/Elso.php', "<?php\n// elso modositva\n");
|
||||||
|
File::put($path.'/app/Árlista.php', "<?php\n// ekezetes\n");
|
||||||
|
File::put($path.'/database/migrations/2026_01_01_000000_teszt.php', "<?php\n// migracio\n");
|
||||||
|
File::put($path.'/.env', "APP_KEY=titok\n");
|
||||||
|
deploymentGit($path, ['add', '-A', '-f']);
|
||||||
|
deploymentGit($path, ['commit', '-m', 'második commit']);
|
||||||
|
|
||||||
|
deploymentGit($path, ['mv', 'app/Atnevezendo.php', 'app/Atnevezett.php']);
|
||||||
|
deploymentGit($path, ['rm', 'app/Regi.php']);
|
||||||
|
deploymentGit($path, ['commit', '-m', 'harmadik commit']);
|
||||||
|
|
||||||
|
$log = trim(deploymentGit($path, ['log', '--reverse', '--pretty=format:%H']));
|
||||||
|
|
||||||
|
Config::set('deployment.repo_path', $path);
|
||||||
|
|
||||||
|
return ['path' => $path, 'commits' => explode("\n", $log)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function deploymentCleanup(string $path): void
|
||||||
|
{
|
||||||
|
// A .git/objects tartalma Windowson csak olvasható, ezért törlés előtt fel kell oldani.
|
||||||
|
foreach (File::allFiles($path, true) as $file) {
|
||||||
|
@chmod($file->getPathname(), 0777);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
File::deleteDirectory($path);
|
||||||
|
} catch (Throwable) {
|
||||||
|
// A takarítás nem futtathat el tesztet - a temp mappa maradványa nem befolyásol semmit.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test('a kikapcsolt env kapcsoló mindenkitől elveszi a hozzáférést', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
Config::set('deployment.enabled', false);
|
||||||
|
|
||||||
|
$developer = deploymentDeveloper();
|
||||||
|
|
||||||
|
expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse()
|
||||||
|
->and(app(DeploymentGuard::class)->denialReason($developer))->toContain('DEPLOYMENT_PACKAGE_ENABLED');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a nem fejlesztői stage-en akkor sem érhető el, ha a kapcsoló be van kapcsolva', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
Config::set('app.stage', 'PROD');
|
||||||
|
|
||||||
|
$developer = deploymentDeveloper();
|
||||||
|
|
||||||
|
expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse()
|
||||||
|
->and(app(DeploymentGuard::class)->denialReason($developer))->toContain('fejlesztői környezetben');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a developer szerep nélküli felhasználó nem éri el', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
|
||||||
|
Role::create(['name' => 'admin', 'display_name' => 'Admin']);
|
||||||
|
$admin = User::factory()->create();
|
||||||
|
$admin->addRole(Role::where('name', 'admin')->first());
|
||||||
|
|
||||||
|
FeatureFlag::create([
|
||||||
|
'name' => 'DeploymentPackage',
|
||||||
|
'label' => 'Deployment csomagoló',
|
||||||
|
'enabled' => true,
|
||||||
|
'stages' => null,
|
||||||
|
'roles' => null,
|
||||||
|
]);
|
||||||
|
app(FeatureFlagRegistrar::class)->registerAll();
|
||||||
|
|
||||||
|
expect(app(DeploymentGuard::class)->isAllowed($admin))->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a kikapcsolt feature flag elrejti a felületet a fejlesztő elől is', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
|
||||||
|
$developer = deploymentDeveloper(flagEnabled: false);
|
||||||
|
|
||||||
|
expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeFalse()
|
||||||
|
->and(DeploymentPackage::canAccess())->toBeFalse();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('minden feltétel teljesülése esetén engedélyezett', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
|
||||||
|
$developer = deploymentDeveloper();
|
||||||
|
|
||||||
|
expect(app(DeploymentGuard::class)->isAllowed($developer))->toBeTrue()
|
||||||
|
->and(app(DeploymentGuard::class)->denialReason($developer))->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a jogosulatlan felhasználó 403-at kap az oldal betöltésekor', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
|
||||||
|
$developer = deploymentDeveloper(flagEnabled: false);
|
||||||
|
$this->actingAs($developer);
|
||||||
|
|
||||||
|
expect(fn () => Livewire::test(DeploymentPackage::class))->toThrow(HttpException::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a commitlista a legfrissebbtől visszafelé, fájlszámmal együtt jön', function () {
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
|
||||||
|
$commits = app(GitRepository::class)->commits('main', 10);
|
||||||
|
|
||||||
|
expect($commits)->toHaveCount(3)
|
||||||
|
->and($commits[0]['subject'])->toBe('harmadik commit')
|
||||||
|
->and($commits[2]['subject'])->toBe('első commit')
|
||||||
|
->and($commits[0]['hash'])->toHaveLength(40)
|
||||||
|
->and($commits[0]['file_count'])->toBe(2);
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a diff felismeri az új, módosult, törölt és átnevezett fájlokat', function () {
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
[$first, , $third] = $repository['commits'];
|
||||||
|
|
||||||
|
$files = collect(app(GitRepository::class)->changedFiles($first, $third))->keyBy('path');
|
||||||
|
|
||||||
|
expect($files->get('app/Elso.php')['status'])->toBe('M')
|
||||||
|
->and($files->get('app/Regi.php')['status'])->toBe('D')
|
||||||
|
->and($files->get('app/Atnevezett.php')['status'])->toBe('R')
|
||||||
|
->and($files->get('app/Atnevezett.php')['old_path'])->toBe('app/Atnevezendo.php')
|
||||||
|
// Ékezetes útvonal: core.quotePath=false nélkül "app/\303\201rlista.php" jönne vissza.
|
||||||
|
->and($files->has('app/Árlista.php'))->toBeTrue();
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az elemzés kizárja a tiltott fájlokat és külön kezeli a törlendőket', function () {
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
[$first, , $third] = $repository['commits'];
|
||||||
|
|
||||||
|
$changedFiles = app(GitRepository::class)->changedFiles($first, $third);
|
||||||
|
$changeSet = app(ChangeSetAnalyzer::class)->analyze($changedFiles);
|
||||||
|
|
||||||
|
expect($changeSet['copied'])->toContain('app/Árlista.php', 'app/Atnevezett.php', 'app/Elso.php')
|
||||||
|
->and($changeSet['copied'])->not->toContain('.env')
|
||||||
|
->and($changeSet['counts']['excluded'])->toBe(1)
|
||||||
|
// A törölt fájl mellett az átnevezés régi útvonala is törlendő a célgépen.
|
||||||
|
->and($changeSet['deleted'])->toContain('app/Regi.php', 'app/Atnevezendo.php');
|
||||||
|
|
||||||
|
$titles = collect($changeSet['warnings'])->pluck('title')->implode(' | ');
|
||||||
|
|
||||||
|
expect($titles)->toContain('Migráció')
|
||||||
|
->and($titles)->toContain('Törlendő fájl');
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az érvénytelen commit azonosítót nem adja tovább a gitnek', function () {
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
|
||||||
|
$git = app(GitRepository::class);
|
||||||
|
|
||||||
|
expect($git->resolveCommit('../../etc/passwd'))->toBeNull()
|
||||||
|
->and($git->resolveCommit('main; rm -rf /'))->toBeNull()
|
||||||
|
->and($git->resolveCommit('HEAD'))->toBeNull()
|
||||||
|
->and($git->resolveCommit($repository['commits'][0]))->toBe($repository['commits'][0]);
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a fordított tartomány felismerhető', function () {
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
[$first, , $third] = $repository['commits'];
|
||||||
|
|
||||||
|
$git = app(GitRepository::class);
|
||||||
|
|
||||||
|
expect($git->isAncestor($first, $third))->toBeTrue()
|
||||||
|
->and($git->isAncestor($third, $first))->toBeFalse();
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az oldal kilistázza a commitokat és a kijelölt tartomány fájljait', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
[$first, , $third] = $repository['commits'];
|
||||||
|
|
||||||
|
$this->actingAs(deploymentDeveloper());
|
||||||
|
|
||||||
|
Livewire::test(DeploymentPackage::class)
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('harmadik commit')
|
||||||
|
->call('selectFrom', $first)
|
||||||
|
->call('selectTo', $third)
|
||||||
|
->assertSet('fromCommit', $first)
|
||||||
|
->assertSet('toCommit', $third)
|
||||||
|
->assertSee('app/Atnevezett.php')
|
||||||
|
->assertSee('app/Regi.php');
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('az oldal nem fogad el kamu commit azonosítót', function () {
|
||||||
|
deploymentAllowEnvironment();
|
||||||
|
$repository = deploymentTestRepository();
|
||||||
|
|
||||||
|
$this->actingAs(deploymentDeveloper());
|
||||||
|
|
||||||
|
Livewire::test(DeploymentPackage::class)
|
||||||
|
->call('selectFrom', '../../etc/passwd')
|
||||||
|
->assertSet('fromCommit', null);
|
||||||
|
|
||||||
|
deploymentCleanup($repository['path']);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user