mirror of
https://github.com/veggiemonk/awesome-docker.git
synced 2026-08-15 13:17:18 +02:00
feat: add prune subcommand, drop archived/stale entries (#1441)
* feat: add prune subcommand, drop archived/stale entries, add container-explorer Add a new `awesome-docker prune` subcommand that removes README entries whose repository health status matches a configurable set (default: archived,stale). URLs are read from the local health cache, or from a markdown report file via --from-report when the cache is outdated. Apply it against the issue #1439 health report to remove 5 entries that survived the recent reorg: stitchocker, docker-consul, blockbridge-docker-volume, docker-explorer, dockdash. Add google/container-explorer in the Security section as the actively maintained successor to the now-archived google/docker-explorer. Co-Authored-By: Claude <noreply@anthropic.com> * golangci-lint config * fix: address golangci-lint findings Fixes errcheck on bufio.Writer.WriteString, gocritic rangeValCopy via indexed loops with pointer locals, gosec G703 on user-supplied CLI output path, noctx by switching to exec.CommandContext with a timeout in the TUI url opener, prealloc in the scorer test, plus fieldalignment struct reorders and golines line breaks from --fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
+199
-24
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
"github.com/veggiemonk/awesome-docker/internal/checker"
|
||||
"github.com/veggiemonk/awesome-docker/internal/linter"
|
||||
"github.com/veggiemonk/awesome-docker/internal/parser"
|
||||
"github.com/veggiemonk/awesome-docker/internal/pruner"
|
||||
"github.com/veggiemonk/awesome-docker/internal/scorer"
|
||||
"github.com/veggiemonk/awesome-docker/internal/tui"
|
||||
)
|
||||
@@ -27,11 +29,11 @@ const (
|
||||
)
|
||||
|
||||
type checkSummary struct {
|
||||
ExternalTotal int
|
||||
GitHubTotal int
|
||||
Broken []checker.LinkResult
|
||||
Redirected []checker.LinkResult
|
||||
GitHubErrors []error
|
||||
ExternalTotal int
|
||||
GitHubTotal int
|
||||
GitHubSkipped bool
|
||||
}
|
||||
|
||||
@@ -52,6 +54,7 @@ func main() {
|
||||
validateCmd(),
|
||||
ciCmd(),
|
||||
browseCmd(),
|
||||
pruneCmd(),
|
||||
)
|
||||
|
||||
if err := root.Execute(); err != nil {
|
||||
@@ -154,7 +157,7 @@ func runLinkChecks(prMode bool) (checkSummary, error) {
|
||||
func runHealth(ctx context.Context) error {
|
||||
token := os.Getenv("GITHUB_TOKEN")
|
||||
if token == "" {
|
||||
return fmt.Errorf("GITHUB_TOKEN environment variable is required")
|
||||
return errors.New("GITHUB_TOKEN environment variable is required")
|
||||
}
|
||||
|
||||
doc, err := parseReadme()
|
||||
@@ -174,9 +177,12 @@ func runHealth(ctx context.Context) error {
|
||||
}
|
||||
if len(infos) == 0 {
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("failed to fetch GitHub metadata for all repositories (%d errors); check network/DNS and GITHUB_TOKEN", len(errs))
|
||||
return fmt.Errorf(
|
||||
"failed to fetch GitHub metadata for all repositories (%d errors); check network/DNS and GITHUB_TOKEN",
|
||||
len(errs),
|
||||
)
|
||||
}
|
||||
return fmt.Errorf("no GitHub repositories found in README")
|
||||
return errors.New("no GitHub repositories found in README")
|
||||
}
|
||||
|
||||
scored := scorer.ScoreAll(infos)
|
||||
@@ -211,11 +217,12 @@ func scoredFromCache() ([]scorer.ScoredEntry, error) {
|
||||
return nil, fmt.Errorf("load cache: %w", err)
|
||||
}
|
||||
if len(hc.Entries) == 0 {
|
||||
return nil, fmt.Errorf("no cache data, run 'health' first")
|
||||
return nil, errors.New("no cache data, run 'health' first")
|
||||
}
|
||||
|
||||
scored := make([]scorer.ScoredEntry, 0, len(hc.Entries))
|
||||
for _, e := range hc.Entries {
|
||||
for i := range hc.Entries {
|
||||
e := &hc.Entries[i]
|
||||
scored = append(scored, scorer.ScoredEntry{
|
||||
URL: e.URL,
|
||||
Name: e.Name,
|
||||
@@ -409,7 +416,11 @@ func checkCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
if len(summary.Broken) > 0 && len(summary.GitHubErrors) > 0 {
|
||||
return fmt.Errorf("found %d broken links and %d GitHub API errors", len(summary.Broken), len(summary.GitHubErrors))
|
||||
return fmt.Errorf(
|
||||
"found %d broken links and %d GitHub API errors",
|
||||
len(summary.Broken),
|
||||
len(summary.GitHubErrors),
|
||||
)
|
||||
}
|
||||
if len(summary.Broken) > 0 {
|
||||
return fmt.Errorf("found %d broken links", len(summary.Broken))
|
||||
@@ -560,20 +571,40 @@ func ciBrokenLinksCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeGitHubOutput(githubOutput, "has_errors", strconv.FormatBool(hasErrors)); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"has_errors",
|
||||
strconv.FormatBool(hasErrors),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeGitHubOutput(githubOutput, "check_exit_code", strconv.Itoa(exitCode)); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"check_exit_code",
|
||||
strconv.Itoa(exitCode),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeGitHubOutput(githubOutput, "broken_count", strconv.Itoa(len(summary.Broken))); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"broken_count",
|
||||
strconv.Itoa(len(summary.Broken)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeGitHubOutput(githubOutput, "github_error_count", strconv.Itoa(len(summary.GitHubErrors))); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"github_error_count",
|
||||
strconv.Itoa(len(summary.GitHubErrors)),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if runErr != nil {
|
||||
if err := writeGitHubOutput(githubOutput, "run_error", sanitizeOutputValue(runErr.Error())); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"run_error",
|
||||
sanitizeOutputValue(runErr.Error()),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -582,7 +613,11 @@ func ciBrokenLinksCmd() *cobra.Command {
|
||||
fmt.Printf("CI broken-links run error: %v\n", runErr)
|
||||
}
|
||||
if hasErrors {
|
||||
fmt.Printf("CI broken-links found %d broken links and %d GitHub errors\n", len(summary.Broken), len(summary.GitHubErrors))
|
||||
fmt.Printf(
|
||||
"CI broken-links found %d broken links and %d GitHub errors\n",
|
||||
len(summary.Broken),
|
||||
len(summary.GitHubErrors),
|
||||
)
|
||||
} else {
|
||||
fmt.Println("CI broken-links found no errors")
|
||||
}
|
||||
@@ -592,7 +627,11 @@ func ciBrokenLinksCmd() *cobra.Command {
|
||||
return runErr
|
||||
}
|
||||
if hasErrors {
|
||||
return fmt.Errorf("found %d broken links and %d GitHub API errors", len(summary.Broken), len(summary.GitHubErrors))
|
||||
return fmt.Errorf(
|
||||
"found %d broken links and %d GitHub API errors",
|
||||
len(summary.Broken),
|
||||
len(summary.GitHubErrors),
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -600,7 +639,8 @@ func ciBrokenLinksCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&issueFile, "issue-file", "broken_links_issue.md", "Path to write issue markdown body")
|
||||
cmd.Flags().StringVar(&githubOutput, "github-output", "", "Path to GitHub output file (typically $GITHUB_OUTPUT)")
|
||||
cmd.Flags().
|
||||
StringVar(&githubOutput, "github-output", "", "Path to GitHub output file (typically $GITHUB_OUTPUT)")
|
||||
cmd.Flags().BoolVar(&strict, "strict", false, "Return non-zero when errors are found")
|
||||
return cmd
|
||||
}
|
||||
@@ -629,25 +669,49 @@ func ciHealthReportCmd() *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if err := writeGitHubOutput(githubOutput, "has_report", strconv.FormatBool(hasReport)); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"has_report",
|
||||
strconv.FormatBool(hasReport),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeGitHubOutput(githubOutput, "health_ok", strconv.FormatBool(healthOK)); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"health_ok",
|
||||
strconv.FormatBool(healthOK),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeGitHubOutput(githubOutput, "report_ok", strconv.FormatBool(reportOK)); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"report_ok",
|
||||
strconv.FormatBool(reportOK),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeGitHubOutput(githubOutput, "has_errors", strconv.FormatBool(hasErrors)); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"has_errors",
|
||||
strconv.FormatBool(hasErrors),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if healthErr != nil {
|
||||
if err := writeGitHubOutput(githubOutput, "health_error", sanitizeOutputValue(healthErr.Error())); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"health_error",
|
||||
sanitizeOutputValue(healthErr.Error()),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if reportErr != nil {
|
||||
if err := writeGitHubOutput(githubOutput, "report_error", sanitizeOutputValue(reportErr.Error())); err != nil {
|
||||
if err := writeGitHubOutput(
|
||||
githubOutput,
|
||||
"report_error",
|
||||
sanitizeOutputValue(reportErr.Error()),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -677,7 +741,8 @@ func ciHealthReportCmd() *cobra.Command {
|
||||
}
|
||||
|
||||
cmd.Flags().StringVar(&issueFile, "issue-file", "health_report.txt", "Path to write health issue markdown body")
|
||||
cmd.Flags().StringVar(&githubOutput, "github-output", "", "Path to GitHub output file (typically $GITHUB_OUTPUT)")
|
||||
cmd.Flags().
|
||||
StringVar(&githubOutput, "github-output", "", "Path to GitHub output file (typically $GITHUB_OUTPUT)")
|
||||
cmd.Flags().BoolVar(&strict, "strict", false, "Return non-zero when health/report fails")
|
||||
return cmd
|
||||
}
|
||||
@@ -693,7 +758,7 @@ func browseCmd() *cobra.Command {
|
||||
return fmt.Errorf("load cache: %w", err)
|
||||
}
|
||||
if len(hc.Entries) == 0 {
|
||||
return fmt.Errorf("no cache data; run 'awesome-docker health' first")
|
||||
return errors.New("no cache data; run 'awesome-docker health' first")
|
||||
}
|
||||
return tui.Run(hc.Entries)
|
||||
},
|
||||
@@ -701,3 +766,113 @@ func browseCmd() *cobra.Command {
|
||||
cmd.Flags().StringVar(&cachePath, "cache", healthCachePath, "Path to health cache YAML")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func pruneCmd() *cobra.Command {
|
||||
var (
|
||||
statuses []string
|
||||
dryRun bool
|
||||
keepCache bool
|
||||
fromReport string
|
||||
)
|
||||
cmd := &cobra.Command{
|
||||
Use: "prune",
|
||||
Short: "Remove README entries by health status (default: archived, stale)",
|
||||
Long: `Remove entries from README.md whose repository health status matches the
|
||||
given list (default: "archived,stale"). The matching cache entries are also
|
||||
dropped so the next health refresh starts from a clean slate.
|
||||
|
||||
By default the target URL list is read from the local health cache. Refresh
|
||||
it first with:
|
||||
|
||||
awesome-docker health
|
||||
|
||||
If the local cache is outdated (e.g. you only have the markdown report from
|
||||
a CI-generated issue), point --from-report at the saved markdown report file
|
||||
and the URLs will be parsed from its section headings instead.
|
||||
|
||||
Use --dry-run to preview what would be removed without writing files.`,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
var (
|
||||
targets map[string]cache.HealthEntry
|
||||
hc *cache.HealthCache
|
||||
)
|
||||
|
||||
if fromReport != "" {
|
||||
f, err := os.Open(fromReport) //nolint:gosec
|
||||
if err != nil {
|
||||
return fmt.Errorf("open report: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
targets, err = pruner.TargetsFromReport(f, statuses)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse report: %w", err)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
hc, err = cache.LoadHealthCache(healthCachePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load cache: %w", err)
|
||||
}
|
||||
if len(hc.Entries) == 0 {
|
||||
return errors.New("no cache data; run 'awesome-docker health' first")
|
||||
}
|
||||
targets = pruner.TargetURLs(hc, statuses)
|
||||
}
|
||||
|
||||
if len(targets) == 0 {
|
||||
fmt.Printf("No entries match statuses %v; nothing to do\n", statuses)
|
||||
return nil
|
||||
}
|
||||
|
||||
res, err := pruner.PruneREADME(readmePath, targets, dryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prune readme: %w", err)
|
||||
}
|
||||
|
||||
byStatus := map[string]int{}
|
||||
for _, r := range res.Removed {
|
||||
byStatus[r.Status]++
|
||||
}
|
||||
action := "Removed"
|
||||
if dryRun {
|
||||
action = "Would remove"
|
||||
}
|
||||
fmt.Printf("%s %d entries from %s\n", action, len(res.Removed), readmePath)
|
||||
for s, n := range byStatus {
|
||||
fmt.Printf(" %s: %d\n", s, n)
|
||||
}
|
||||
for _, r := range res.Removed {
|
||||
fmt.Printf(" - [%s] %s (%s)\n", r.Status, r.Name, r.URL)
|
||||
}
|
||||
if len(res.NotFound) > 0 {
|
||||
fmt.Printf(
|
||||
"\n%d target URLs not found in README (already pruned or URL drift):\n",
|
||||
len(res.NotFound),
|
||||
)
|
||||
for _, u := range res.NotFound {
|
||||
fmt.Printf(" %s\n", u)
|
||||
}
|
||||
}
|
||||
|
||||
if !keepCache && hc != nil {
|
||||
dropped, err := pruner.PruneCache(healthCachePath, hc, targets, dryRun)
|
||||
if err != nil {
|
||||
return fmt.Errorf("prune cache: %w", err)
|
||||
}
|
||||
cacheAction := "Removed"
|
||||
if dryRun {
|
||||
cacheAction = "Would remove"
|
||||
}
|
||||
fmt.Printf("\n%s %d cache entries from %s\n", cacheAction, dropped, healthCachePath)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().
|
||||
StringSliceVar(&statuses, "status", []string{"archived", "stale"}, "Comma-separated health statuses to prune (archived,stale,inactive,dead)")
|
||||
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Print what would be removed without writing files")
|
||||
cmd.Flags().BoolVar(&keepCache, "keep-cache", false, "Do not remove pruned URLs from the health cache")
|
||||
cmd.Flags().
|
||||
StringVar(&fromReport, "from-report", "", "Read target URLs from a markdown health report file instead of the cache")
|
||||
return cmd
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user