From 09eed3eeb3a22ae159d846541528e3f93bc5cb17 Mon Sep 17 00:00:00 2001 From: E98Developer Date: Sun, 16 Aug 2026 06:45:09 +0200 Subject: [PATCH] =?UTF-8?q?ADD=20Deployment=20csomagol=C3=B3=20phase1=20v?= =?UTF-8?q?=C3=A9delem,=20git=20olvas=C3=B3=20=C3=A9s=20read-only=20commit?= =?UTF-8?q?/diff=20fel=C3=BClet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app/Filament/Pages/DeploymentPackage.php | 303 ++++++++++++++++++ app/Services/Deployment/ChangeSetAnalyzer.php | 170 ++++++++++ app/Services/Deployment/DeploymentGuard.php | 69 ++++ app/Services/Deployment/DeploymentTargets.php | 54 ++++ app/Services/Deployment/GitRepository.php | 273 ++++++++++++++++ config/deployment.php | 127 ++++++++ .../components/speed-button-nav-bar.blade.php | 7 + .../pages/deployment-package.blade.php | 225 +++++++++++++ .../views/layout/speedButtonNavBar.blade.php | 12 + tests/Feature/DeploymentPackageTest.php | 290 +++++++++++++++++ 10 files changed, 1530 insertions(+) create mode 100644 app/Filament/Pages/DeploymentPackage.php create mode 100644 app/Services/Deployment/ChangeSetAnalyzer.php create mode 100644 app/Services/Deployment/DeploymentGuard.php create mode 100644 app/Services/Deployment/DeploymentTargets.php create mode 100644 app/Services/Deployment/GitRepository.php create mode 100644 config/deployment.php create mode 100644 resources/views/filament/pages/deployment-package.blade.php create mode 100644 tests/Feature/DeploymentPackageTest.php diff --git a/app/Filament/Pages/DeploymentPackage.php b/app/Filament/Pages/DeploymentPackage.php new file mode 100644 index 0000000..8d4144a --- /dev/null +++ b/app/Filament/Pages/DeploymentPackage.php @@ -0,0 +1,303 @@ +|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 $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 $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 + */ + #[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, + * copied: array, + * deleted: array, + * counts: array{total:int, copied:int, deleted:int, excluded:int}, + * warnings: array + * }|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 + */ + #[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 + */ + 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; + } +} diff --git a/app/Services/Deployment/ChangeSetAnalyzer.php b/app/Services/Deployment/ChangeSetAnalyzer.php new file mode 100644 index 0000000..6ad59fe --- /dev/null +++ b/app/Services/Deployment/ChangeSetAnalyzer.php @@ -0,0 +1,170 @@ + $changedFiles + * @param array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null $target + * @return array{ + * files: array, + * copied: array, + * deleted: array, + * counts: array{total:int, copied:int, deleted:int, excluded:int}, + * warnings: array + * } + */ + public function analyze(array $changedFiles, ?array $target = null, ?string $branch = null): array + { + /** @var array $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 $copied + * @param array $deleted + * @param array{key:string, label:string, domain:string, remote_path:string, expected_branch:?string}|null $target + * @return array + */ + 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 $paths + * @return array + */ + private function matching(array $paths, string $group): array + { + /** @var array $patterns */ + $patterns = (array) config('deployment.attention.'.$group, []); + + return array_values(array_filter($paths, fn (string $path): bool => Str::is($patterns, $path))); + } +} diff --git a/app/Services/Deployment/DeploymentGuard.php b/app/Services/Deployment/DeploymentGuard.php new file mode 100644 index 0000000..623d421 --- /dev/null +++ b/app/Services/Deployment/DeploymentGuard.php @@ -0,0 +1,69 @@ +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 $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); + } + } +} diff --git a/app/Services/Deployment/DeploymentTargets.php b/app/Services/Deployment/DeploymentTargets.php new file mode 100644 index 0000000..aabf9a1 --- /dev/null +++ b/app/Services/Deployment/DeploymentTargets.php @@ -0,0 +1,54 @@ + + */ + public function all(): array + { + /** @var array> $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 + */ + 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()); + } +} diff --git a/app/Services/Deployment/GitRepository.php b/app/Services/Deployment/GitRepository.php new file mode 100644 index 0000000..8e13031 --- /dev/null +++ b/app/Services/Deployment/GitRepository.php @@ -0,0 +1,273 @@ +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 + */ + 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 + */ + 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 + */ + 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 $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 $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(); + } +} diff --git a/config/deployment.php b/config/deployment.php new file mode 100644 index 0000000..8e57347 --- /dev/null +++ b/config/deployment.php @@ -0,0 +1,127 @@ + 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', + ], + ], + +]; diff --git a/resources/views/filament/components/speed-button-nav-bar.blade.php b/resources/views/filament/components/speed-button-nav-bar.blade.php index 601a218..b2c71a3 100644 --- a/resources/views/filament/components/speed-button-nav-bar.blade.php +++ b/resources/views/filament/components/speed-button-nav-bar.blade.php @@ -40,6 +40,13 @@ ['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