Replace deleted_files with manifest-based updater cleanup, add release workflow

- Add manifest.json generation script (scripts/generate-manifest.php)
- Add Updater::cleanStaleFiles() that removes files not in manifest
- Add /api/v1/update/clean endpoint with backward compatibility
- Add configurable update_protected_paths in config/invoiceshelf.php
- Update frontend to use clean step instead of delete step
- Add GitHub Actions release workflow triggered on version tags
- Add .github/release.yml for auto-generated changelog categories
- Update Makefile to include manifest generation and scripts directory
This commit is contained in:
Darko Gjorgjijoski
2026-04-06 19:27:33 +02:00
parent 2bdfb6a8b6
commit e64529468c
12 changed files with 297 additions and 7 deletions

View File

@@ -131,6 +131,80 @@ class Updater
return true;
}
public static function cleanStaleFiles(): array
{
$manifestPath = base_path('manifest.json');
if (! File::exists($manifestPath)) {
return ['success' => true, 'cleaned' => 0];
}
$manifest = json_decode(File::get($manifestPath), true);
if (! is_array($manifest)) {
return ['success' => false, 'error' => 'Invalid manifest'];
}
$manifestLookup = array_flip($manifest);
$protectedPaths = config('invoiceshelf.update_protected_paths', []);
$cleaned = 0;
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(base_path(), \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($iterator as $file) {
$relativePath = substr($file->getPathname(), strlen(base_path()) + 1);
if (static::isProtectedPath($relativePath, $protectedPaths)) {
continue;
}
if ($file->isFile() && ! isset($manifestLookup[$relativePath])) {
File::delete($file->getPathname());
$cleaned++;
}
}
// Second pass: remove empty directories
$dirIterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator(base_path(), \RecursiveDirectoryIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($dirIterator as $item) {
if (! $item->isDir()) {
continue;
}
$relativePath = substr($item->getPathname(), strlen(base_path()) + 1);
if (static::isProtectedPath($relativePath, $protectedPaths)) {
continue;
}
$entries = scandir($item->getPathname());
if (count($entries) <= 2) {
@rmdir($item->getPathname());
}
}
return ['success' => true, 'cleaned' => $cleaned];
}
private static function isProtectedPath(string $relativePath, array $protectedPaths): bool
{
foreach ($protectedPaths as $protected) {
if ($relativePath === $protected || str_starts_with($relativePath, $protected.'/')) {
return true;
}
}
return false;
}
public static function migrateUpdate()
{
Artisan::call('migrate --force');