feat: Add build cache cleanup and CLI cleanup command
Some checks failed
CI / build-and-test (push) Failing after 30s
Some checks failed
CI / build-and-test (push) Failing after 30s
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/spf13/cobra"
|
"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/config"
|
||||||
"gitea.com/gitea/act_runner/internal/pkg/ver"
|
"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")
|
cacheCmd.Flags().Uint16VarP(&cacheArgs.Port, "port", "p", 0, "Port of the cache server")
|
||||||
rootCmd.AddCommand(cacheCmd)
|
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
|
// hide completion command
|
||||||
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
rootCmd.CompletionOptions.HiddenDefaultCmd = true
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
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)
|
result.Duration = time.Since(start)
|
||||||
log.Infof("Cleanup completed: freed %s in %s", formatBytes(result.BytesFreed), result.Duration)
|
log.Infof("Cleanup completed: freed %s in %s", formatBytes(result.BytesFreed), result.Duration)
|
||||||
|
|
||||||
@@ -251,6 +261,86 @@ func dirSize(path string) int64 {
|
|||||||
return size
|
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
|
// formatBytes formats bytes into human readable string
|
||||||
func formatBytes(bytes int64) string {
|
func formatBytes(bytes int64) string {
|
||||||
const unit = 1024
|
const unit = 1024
|
||||||
|
|||||||
Reference in New Issue
Block a user