From f5b22c414943bbb1498c8f4839be29a2e2ecca21 Mon Sep 17 00:00:00 2001 From: GitCaddy Date: Wed, 14 Jan 2026 09:26:21 +0000 Subject: [PATCH] feat: Add build cache cleanup and CLI cleanup command MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cleanup for common build tool caches (Go, npm, NuGet, Gradle, Maven, pip, Cargo) - Build caches cleaned for files older than 7 days - Add gitcaddy-runner cleanup CLI command for manual cleanup trigger - Fixes disk space issues from accumulated CI build artifacts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- internal/app/cmd/cmd.go | 27 ++++++++++ internal/pkg/cleanup/cleanup.go | 90 +++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+) diff --git a/internal/app/cmd/cmd.go b/internal/app/cmd/cmd.go index 7101edf..ab92fd5 100644 --- a/internal/app/cmd/cmd.go +++ b/internal/app/cmd/cmd.go @@ -10,6 +10,7 @@ import ( "github.com/spf13/cobra" + "gitea.com/gitea/act_runner/internal/pkg/cleanup" "gitea.com/gitea/act_runner/internal/pkg/config" "gitea.com/gitea/act_runner/internal/pkg/ver" ) @@ -79,6 +80,32 @@ func Execute(ctx context.Context) { cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "Port of the cache server") rootCmd.AddCommand(cacheCmd) + + // ./gitcaddy-runner cleanup + cleanupCmd := &cobra.Command{ + Use: "cleanup", + Short: "Manually trigger cleanup to free disk space", + Args: cobra.MaximumNArgs(0), + RunE: func(_ *cobra.Command, _ []string) error { + cfg, err := config.LoadDefault(configFile) + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + result, err := cleanup.RunCleanup(ctx, cfg) + if err != nil { + return fmt.Errorf("cleanup failed: %w", err) + } + fmt.Printf("Cleanup completed: freed %d bytes, deleted %d files in %s\n", result.BytesFreed, result.FilesDeleted, result.Duration) + if len(result.Errors) > 0 { + fmt.Printf("Warnings: %d errors occurred\n", len(result.Errors)) + for _, e := range result.Errors { + fmt.Printf(" - %s\n", e) + } + } + return nil + }, + } + rootCmd.AddCommand(cleanupCmd) // hide completion command rootCmd.CompletionOptions.HiddenDefaultCmd = true diff --git a/internal/pkg/cleanup/cleanup.go b/internal/pkg/cleanup/cleanup.go index 41528d9..ff4de09 100644 --- a/internal/pkg/cleanup/cleanup.go +++ b/internal/pkg/cleanup/cleanup.go @@ -73,6 +73,16 @@ func RunCleanup(ctx context.Context, cfg *config.Config) (*CleanupResult, error) log.Infof("Cleaned temp: freed %d bytes, deleted %d files", bytes, files) } + // 5. Clean build tool caches (older than 7 days) + // These can grow very large from Go, npm, nuget, gradle, maven builds + if bytes, files, err := cleanBuildCaches(7 * 24 * time.Hour); err != nil { + result.Errors = append(result.Errors, fmt.Errorf("build cache cleanup: %w", err)) + } else { + result.BytesFreed += bytes + result.FilesDeleted += files + log.Infof("Cleaned build caches: freed %d bytes, deleted %d files", bytes, files) + } + result.Duration = time.Since(start) log.Infof("Cleanup completed: freed %s in %s", formatBytes(result.BytesFreed), result.Duration) @@ -251,6 +261,86 @@ func dirSize(path string) int64 { return size } + +// cleanBuildCaches removes old build tool caches that accumulate from CI jobs +// These are cleaned more aggressively (files older than 7 days) since they can grow very large +func cleanBuildCaches(maxAge time.Duration) (int64, int, error) { + home := os.Getenv("HOME") + if home == "" { + home = "/root" // fallback for runners typically running as root + } + + var totalBytesFreed int64 + var totalFilesDeleted int + + // Build cache directories to clean + // Format: {path, description} + cacheDirs := []struct { + path string + desc string + }{ + {filepath.Join(home, ".cache", "go-build"), "Go build cache"}, + {filepath.Join(home, ".cache", "golangci-lint"), "golangci-lint cache"}, + {filepath.Join(home, ".npm", "_cacache"), "npm cache"}, + {filepath.Join(home, ".cache", "pnpm"), "pnpm cache"}, + {filepath.Join(home, ".cache", "yarn"), "yarn cache"}, + {filepath.Join(home, ".nuget", "packages"), "NuGet cache"}, + {filepath.Join(home, ".gradle", "caches"), "Gradle cache"}, + {filepath.Join(home, ".m2", "repository"), "Maven cache"}, + {filepath.Join(home, ".cache", "pip"), "pip cache"}, + {filepath.Join(home, ".cargo", "registry", "cache"), "Cargo cache"}, + {filepath.Join(home, ".rustup", "tmp"), "Rustup temp"}, + } + + cutoff := time.Now().Add(-maxAge) + + for _, cache := range cacheDirs { + if _, err := os.Stat(cache.path); os.IsNotExist(err) { + continue + } + + var bytesFreed int64 + var filesDeleted int + + err := filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error { + if err != nil { + return nil // Skip errors + } + if info.IsDir() { + return nil + } + if info.ModTime().Before(cutoff) { + size := info.Size() + if err := os.Remove(path); err == nil { + bytesFreed += size + filesDeleted++ + } + } + return nil + }) + + if err == nil && (bytesFreed > 0 || filesDeleted > 0) { + log.Infof("Cleaned %s: freed %s, deleted %d files", cache.desc, formatBytes(bytesFreed), filesDeleted) + totalBytesFreed += bytesFreed + totalFilesDeleted += filesDeleted + } + + // Also remove empty directories + filepath.Walk(cache.path, func(path string, info os.FileInfo, err error) error { + if err != nil || !info.IsDir() || path == cache.path { + return nil + } + entries, _ := os.ReadDir(path) + if len(entries) == 0 { + os.Remove(path) + } + return nil + }) + } + + return totalBytesFreed, totalFilesDeleted, nil +} + // formatBytes formats bytes into human readable string func formatBytes(bytes int64) string { const unit = 1024