Files
awesome-docker/internal/scorer/scorer.go
2026-02-28 01:00:35 +01:00

172 lines
4.6 KiB
Markdown

package scorer
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/veggiemonk/awesome-docker/internal/cache"
"github.com/veggiemonk/awesome-docker/internal/checker"
)
// Status represents the health status of an entry.
type Status string
const (
StatusHealthy Status = "healthy"
StatusInactive Status = "inactive" // 1-2 years since last push
StatusStale Status = "stale" // 2+ years since last push
StatusArchived Status = "archived"
StatusDead Status = "dead" // disabled or 404
)
// ScoredEntry is a repo with its computed health status.
type ScoredEntry struct {
URL string
Name string
Status Status
Stars int
Forks int
LastPush time.Time
}
// ReportSummary contains grouped status counts.
type ReportSummary struct {
Healthy int `json:"healthy"`
Inactive int `json:"inactive"`
Stale int `json:"stale"`
Archived int `json:"archived"`
Dead int `json:"dead"`
}
// ReportData is the full machine-readable report model.
type ReportData struct {
GeneratedAt time.Time `json:"generated_at"`
Total int `json:"total"`
Summary ReportSummary `json:"summary"`
Entries []ScoredEntry `json:"entries"`
ByStatus map[Status][]ScoredEntry `json:"by_status"`
}
// Score computes the health status of a GitHub repo.
func Score(info checker.RepoInfo) Status {
if info.IsDisabled {
return StatusDead
}
if info.IsArchived {
return StatusArchived
}
twoYearsAgo := time.Now().AddDate(-2, 0, 0)
oneYearAgo := time.Now().AddDate(-1, 0, 0)
if info.PushedAt.Before(twoYearsAgo) {
return StatusStale
}
if info.PushedAt.Before(oneYearAgo) {
return StatusInactive
}
return StatusHealthy
}
// ScoreAll scores a batch of repo infos.
func ScoreAll(infos []checker.RepoInfo) []ScoredEntry {
results := make([]ScoredEntry, len(infos))
for i, info := range infos {
results[i] = ScoredEntry{
URL: info.URL,
Name: fmt.Sprintf("%s/%s", info.Owner, info.Name),
Status: Score(info),
Stars: info.Stars,
Forks: info.Forks,
LastPush: info.PushedAt,
}
}
return results
}
// ToCacheEntries converts scored entries to cache format.
func ToCacheEntries(scored []ScoredEntry) []cache.HealthEntry {
entries := make([]cache.HealthEntry, len(scored))
now := time.Now().UTC()
for i, s := range scored {
entries[i] = cache.HealthEntry{
URL: s.URL,
Name: s.Name,
Status: string(s.Status),
Stars: s.Stars,
Forks: s.Forks,
LastPush: s.LastPush,
CheckedAt: now,
}
}
return entries
}
// GenerateReport produces a Markdown health report.
func GenerateReport(scored []ScoredEntry) string {
var b strings.Builder
data := BuildReportData(scored)
groups := data.ByStatus
fmt.Fprintf(&b, "# Health Report\n\n")
fmt.Fprintf(&b, "**Generated:** %s\n\n", data.GeneratedAt.Format(time.RFC3339))
fmt.Fprintf(&b, "**Total:** %d repositories\n\n", data.Total)
fmt.Fprintf(&b, "## Summary\n\n")
fmt.Fprintf(&b, "- Healthy: %d\n", data.Summary.Healthy)
fmt.Fprintf(&b, "- Inactive (1-2 years): %d\n", data.Summary.Inactive)
fmt.Fprintf(&b, "- Stale (2+ years): %d\n", data.Summary.Stale)
fmt.Fprintf(&b, "- Archived: %d\n", data.Summary.Archived)
fmt.Fprintf(&b, "- Dead: %d\n\n", data.Summary.Dead)
writeSection := func(title string, status Status) {
entries := groups[status]
if len(entries) == 0 {
return
}
fmt.Fprintf(&b, "## %s\n\n", title)
for _, e := range entries {
fmt.Fprintf(&b, "- [%s](%s) - Stars: %d - Last push: %s\n",
e.Name, e.URL, e.Stars, e.LastPush.Format("2006-01-02"))
}
b.WriteString("\n")
}
writeSection("Archived (should mark :skull:)", StatusArchived)
writeSection("Stale (2+ years inactive)", StatusStale)
writeSection("Inactive (1-2 years)", StatusInactive)
return b.String()
}
// BuildReportData returns full report data for machine-readable and markdown rendering.
func BuildReportData(scored []ScoredEntry) ReportData {
groups := map[Status][]ScoredEntry{}
for _, s := range scored {
groups[s.Status] = append(groups[s.Status], s)
}
return ReportData{
GeneratedAt: time.Now().UTC(),
Total: len(scored),
Summary: ReportSummary{
Healthy: len(groups[StatusHealthy]),
Inactive: len(groups[StatusInactive]),
Stale: len(groups[StatusStale]),
Archived: len(groups[StatusArchived]),
Dead: len(groups[StatusDead]),
},
Entries: scored,
ByStatus: groups,
}
}
// GenerateJSONReport returns the full report as pretty-printed JSON.
func GenerateJSONReport(scored []ScoredEntry) ([]byte, error) {
data := BuildReportData(scored)
return json.MarshalIndent(data, "", " ")
}