mirror of
https://github.com/tiennm99/awesome-ai-dev-tools.git
synced 2026-10-11 03:13:05 +00:00
feat: flag repos that stopped receiving pushes
The maintenance criterion was unfalsifiable, so entries went years without a push before anyone noticed by hand. The updater already fetches pushedAt; warn when it is older than 90 days, alongside the existing rename and archival annotations. Archived repos keep their own warning rather than getting two. Quantify the criterion in the README and CONTRIBUTING: dropped after 6 months idle, flagged past 3, with removal still a human decision. Also correct two CONTRIBUTING claims the code contradicts — duplicates are rejected by CI rather than ignored at run time, and renames do carry history over once canonicalKeyMigrations has the old key.
This commit is contained in:
1 parent
2e2ad56add
commit
ce81c85645
4 files changed
+108
-7
No files matched your search
@@ -35,11 +35,13 @@ Optional fields: `notes` (for clarifications or caveats)
|
||||
|
||||
## Handling Duplicates and Changes
|
||||
|
||||
**Duplicate repos:** If a repo appears twice (same owner/repo), the second entry is ignored in the next daily run.
|
||||
**Duplicate repos:** CI rejects them. `go run . -check` fails on a case-insensitive `owner/repo` match, so a duplicate never reaches a daily run.
|
||||
|
||||
**Renamed repos:** If a GitHub repo is renamed after being tracked, update the `repo` field in `data/agents.yml`. The updater will fetch fresh metadata under the new name. Historical data in `data/history.jsonl` remains under the old key and is not migrated.
|
||||
**Renamed repos:** GitHub redirects the old slug, so the updater keeps working — it prints a `::warning::` naming the new slug. Update `owner`/`repo` in `data/agents.yml` to the new slug and add the old key to `canonicalKeyMigrations` in `history.go`, or the repo's star history detaches and its deltas show `—`.
|
||||
|
||||
**Deprecation:** To remove an agent, delete its entry from `data/agents.yml`. The next run will drop it from the README; historical data is preserved.
|
||||
**Deprecation:** To remove an agent, delete its entry from `data/agents.yml`. The next run drops it from the README; its history stays in `data/history.jsonl`, so re-adding the entry later restores its star chart.
|
||||
|
||||
**Staleness:** An entry with no push in **6 months** is dropped. Past **3 months** the updater prints a `::warning::` naming the repo and its days idle, so the daily run surfaces candidates without anyone auditing the list by hand. Removal stays a human decision: a repo can go quiet between releases, and a historically significant one (`gpt-engineer`) is kept with a `notes` marker instead.
|
||||
|
||||
## PR Review
|
||||
|
||||
|
||||
@@ -68,6 +68,12 @@ var httpClient = &http.Client{Timeout: 30 * time.Second}
|
||||
// a network call or token.
|
||||
var graphqlURL = "https://api.github.com/graphql"
|
||||
|
||||
// staleWarnAfter is how long a tracked repo may go without a push before the
|
||||
// updater flags it for review. It mirrors the review window in the README's
|
||||
// inclusion criteria; dropping an entry stays a human decision, so this only
|
||||
// annotates the run.
|
||||
const staleWarnAfter = 90 * 24 * time.Hour
|
||||
|
||||
// chunkSize is the max aliases per GraphQL request (GitHub node-limit safety margin).
|
||||
const chunkSize = 50
|
||||
|
||||
@@ -111,14 +117,20 @@ func fetchStats(token string, agents []Agent) ([]Stat, error) {
|
||||
lang = node.PrimaryLanguage.Name
|
||||
}
|
||||
|
||||
// Drift detection: surface renames/archival as GitHub Actions run
|
||||
// annotations so a human notices without polling every repo by hand.
|
||||
// Drift detection: surface renames, archival and staleness as GitHub
|
||||
// Actions run annotations so a human notices without polling every
|
||||
// repo by hand.
|
||||
canonicalKey := a.Owner + "/" + a.Repo
|
||||
if node.NameWithOwner != "" && !strings.EqualFold(node.NameWithOwner, canonicalKey) {
|
||||
fmt.Printf("::warning::repo %s renamed to %s — update data/agents.yml and add a canonicalKeyMigrations entry\n", canonicalKey, node.NameWithOwner)
|
||||
}
|
||||
if node.IsArchived {
|
||||
switch {
|
||||
case node.IsArchived:
|
||||
// Archived already implies no further pushes; one warning is enough.
|
||||
fmt.Printf("::warning::repo %s is archived — consider removing or annotating\n", canonicalKey)
|
||||
case !node.PushedAt.IsZero() && timeNow().Sub(node.PushedAt) > staleWarnAfter:
|
||||
days := int(timeNow().Sub(node.PushedAt).Hours() / 24)
|
||||
fmt.Printf("::warning::repo %s has no push in %d days — review against the maintenance criterion\n", canonicalKey, days)
|
||||
}
|
||||
|
||||
stats = append(stats, Stat{
|
||||
|
||||
@@ -6,10 +6,12 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// aliasRe extracts the numeric alias index from a GraphQL query fragment
|
||||
@@ -227,3 +229,86 @@ func TestFetchStats_DriftWarnings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// captureStdout redirects os.Stdout for the duration of fn and returns what
|
||||
// was written, so the ::warning:: annotations fetchStats emits are assertable.
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Pipe: %v", err)
|
||||
}
|
||||
orig := os.Stdout
|
||||
os.Stdout = w
|
||||
defer func() { os.Stdout = orig }()
|
||||
|
||||
fn()
|
||||
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatalf("close pipe writer: %v", err)
|
||||
}
|
||||
out, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatalf("read pipe: %v", err)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func TestFetchStats_StaleWarning(t *testing.T) {
|
||||
fixed := time.Date(2026, 9, 11, 0, 0, 0, 0, time.UTC)
|
||||
origNow := timeNow
|
||||
timeNow = func() time.Time { return fixed }
|
||||
defer func() { timeNow = origNow }()
|
||||
|
||||
agents := []Agent{
|
||||
{Owner: "org", Repo: "fresh", Category: "cli"},
|
||||
{Owner: "org", Repo: "stale", Category: "cli"},
|
||||
{Owner: "org", Repo: "stale-and-archived", Category: "cli"},
|
||||
}
|
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
node := func(name, pushedAt string, archived bool) map[string]any {
|
||||
return map[string]any{
|
||||
"stargazerCount": 5,
|
||||
"nameWithOwner": name,
|
||||
"pushedAt": pushedAt,
|
||||
"isArchived": archived,
|
||||
}
|
||||
}
|
||||
out, _ := json.Marshal(map[string]any{"data": map[string]any{
|
||||
// 2 days idle: inside the window, no warning.
|
||||
"r0": node("org/fresh", "2026-09-09T00:00:00Z", false),
|
||||
// 120 days idle: past staleWarnAfter.
|
||||
"r1": node("org/stale", "2026-05-14T00:00:00Z", false),
|
||||
// Also stale, but archived — the archived warning covers it.
|
||||
"r2": node("org/stale-and-archived", "2025-01-01T00:00:00Z", true),
|
||||
}})
|
||||
_, _ = w.Write(out) // httptest write error is not actionable in a test fake
|
||||
}))
|
||||
defer srv.Close()
|
||||
withGraphQLURL(t, srv)
|
||||
|
||||
var stats []Stat
|
||||
var fetchErr error
|
||||
logs := captureStdout(t, func() { stats, fetchErr = fetchStats("test-token", agents) })
|
||||
|
||||
if fetchErr != nil {
|
||||
t.Fatalf("fetchStats: %v (staleness should warn, not fail)", fetchErr)
|
||||
}
|
||||
if len(stats) != 3 {
|
||||
t.Fatalf("expected 3 stats, got %d", len(stats))
|
||||
}
|
||||
|
||||
if want := "::warning::repo org/stale has no push in 120 days"; !strings.Contains(logs, want) {
|
||||
t.Errorf("missing stale warning %q in:\n%s", want, logs)
|
||||
}
|
||||
if strings.Contains(logs, "org/fresh has no push") {
|
||||
t.Errorf("warned about a repo pushed 2 days ago:\n%s", logs)
|
||||
}
|
||||
if strings.Contains(logs, "org/stale-and-archived has no push") {
|
||||
t.Errorf("stale warning duplicates the archived warning:\n%s", logs)
|
||||
}
|
||||
if want := "::warning::repo org/stale-and-archived is archived"; !strings.Contains(logs, want) {
|
||||
t.Errorf("missing archived warning %q in:\n%s", want, logs)
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,9 @@
|
||||
|
||||
1. **An AI coding agent or assistant itself** — a tool that autonomously writes, edits, or reviews code. Frameworks, wrappers, observability layers, and workspaces *for* agents are out of scope.
|
||||
2. **Notable**: roughly **10,000+ GitHub stars** (the current list floor).
|
||||
3. **Open source and actively maintained** (archived repos are kept only for historical significance, marked as such).
|
||||
3. **Open source and actively maintained** — no push in **6 months** means the entry is dropped. The daily run flags anything past **3 months** for review, so the ranking reflects tools people can actually use today.
|
||||
|
||||
Archived or abandoned repos are kept only when they are historically significant, and are marked as such in [`data/agents.yml`](data/agents.yml).
|
||||
|
||||
Add an agent to [`data/agents.yml`](data/agents.yml):
|
||||
|
||||
|
||||
Reference in new issue
Block a user