Skip to content

Commit e108cf6

Browse files
Check 5+ GitHub stars only on newly added listings.
Non-GitHub URLs and existing README entries are skipped so moved sections do not get rescanned. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 151e2eb commit e108cf6

4 files changed

Lines changed: 165 additions & 1 deletion

File tree

.github/workflows/awesome.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,19 @@ jobs:
6363

6464
- name: Check README list order
6565
run: python3 scripts/check-list-order.py README.md
66+
67+
min-stars:
68+
runs-on: ubuntu-latest
69+
70+
steps:
71+
- name: Checkout repository
72+
uses: actions/checkout@v5.0.0
73+
with:
74+
fetch-depth: 0
75+
76+
- name: Check new GitHub listings have 5+ stars
77+
env:
78+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
79+
run: |
80+
git fetch origin "${GITHUB_BASE_REF:-main}"
81+
python3 scripts/check-min-stars.py README.md

.pre-commit-config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ repos:
2323
description: "Require alphabetical resource lists in README.md"
2424
pass_filenames: false
2525
require_serial: true
26+
- id: min-stars
27+
name: README min GitHub stars
28+
entry: python3 scripts/check-min-stars.py README.md
29+
language: system
30+
files: README\.md$
31+
description: "Require 5+ stars on newly added GitHub listings"
32+
pass_filenames: false
33+
require_serial: true
2634

2735
# Markdown Formatting and Linting
2836
- repo: local

CONTRIBUTING.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@ pip install pre-commit && pre-commit install
2828
pre-commit run --all-files # Run all checks
2929
pre-commit run typos # Check spelling
3030
python3 scripts/check-list-order.py --fix README.md # Sort section lists
31+
python3 scripts/check-min-stars.py README.md # 5+ stars on new GitHub listings
3132
```
3233

33-
This runs the same tools as CI: awesome-lint, markdown formatting, spell check, list order, and link validation.
34+
This runs the same tools as CI: awesome-lint, markdown formatting, spell check, list order, min stars, and link validation.
3435

3536
## Requirements
3637

scripts/check-min-stars.py

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
#!/usr/bin/env python3
2+
"""Fail if newly added GitHub README listings have fewer than 5 stars."""
3+
4+
from __future__ import annotations
5+
6+
import argparse
7+
import json
8+
import os
9+
import re
10+
import subprocess
11+
import sys
12+
from pathlib import Path
13+
14+
ITEM_RE = re.compile(r"^- \[([^\]]+)\]\(([^)]+)\)")
15+
GH_REPO_RE = re.compile(
16+
r"^https?://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+)/?(?:\.git)?$",
17+
re.IGNORECASE,
18+
)
19+
SKIP_OWNERS = frozenset({"topics", "orgs", "marketplace", "settings", "github"})
20+
MIN_STARS = 5
21+
DEFAULT_README = Path("README.md")
22+
23+
24+
def git_ok(args: list[str]) -> subprocess.CompletedProcess[str]:
25+
return subprocess.run(["git", *args], capture_output=True, text=True)
26+
27+
28+
def resolve_base(explicit: str | None) -> str | None:
29+
if explicit:
30+
return explicit
31+
base_ref = os.environ.get("GITHUB_BASE_REF")
32+
if base_ref:
33+
return f"origin/{base_ref}"
34+
for ref in ("origin/main", "origin/master", "main", "master"):
35+
if git_ok(["rev-parse", "--verify", ref]).returncode == 0:
36+
return ref
37+
return None
38+
39+
40+
def listing_repos(line: str) -> str | None:
41+
item = ITEM_RE.match(line.strip())
42+
if not item:
43+
return None
44+
match = GH_REPO_RE.match(item.group(2).strip())
45+
if not match:
46+
return None
47+
owner, name = match.group(1), match.group(2)
48+
if name.endswith(".git"):
49+
name = name[:-4]
50+
if owner.lower() in SKIP_OWNERS:
51+
return None
52+
return f"{owner}/{name}"
53+
54+
55+
def repos_from(text: str) -> dict[str, str]:
56+
found: dict[str, str] = {}
57+
for line in text.splitlines():
58+
slug = listing_repos(line)
59+
if slug:
60+
found.setdefault(slug.lower(), slug)
61+
return found
62+
63+
64+
def added_repos(base: str, readme: Path) -> list[str]:
65+
shown = git_ok(["show", f"{base}:{readme.as_posix()}"])
66+
old_text = shown.stdout if shown.returncode == 0 else ""
67+
old = repos_from(old_text)
68+
current = repos_from(readme.read_text())
69+
return [current[key] for key in current.keys() - old.keys()]
70+
71+
72+
def star_counts(slugs: list[str]) -> dict[str, int | None]:
73+
counts: dict[str, int | None] = {}
74+
for offset in range(0, len(slugs), 40):
75+
batch = slugs[offset : offset + 40]
76+
parts = []
77+
for i, slug in enumerate(batch):
78+
owner, name = slug.split("/", 1)
79+
parts.append(
80+
f'r{i}: repository(owner:{json.dumps(owner)}, name:{json.dumps(name)})'
81+
"{stargazerCount}"
82+
)
83+
payload = json.dumps({"query": "query{" + " ".join(parts) + "}"})
84+
result = subprocess.run(
85+
["gh", "api", "graphql", "--input", "-"],
86+
input=payload,
87+
capture_output=True,
88+
text=True,
89+
)
90+
if result.returncode != 0:
91+
for slug in batch:
92+
counts[slug] = None
93+
continue
94+
data = (json.loads(result.stdout).get("data") or {})
95+
for i, slug in enumerate(batch):
96+
repo = data.get(f"r{i}")
97+
counts[slug] = None if not repo else int(repo["stargazerCount"])
98+
return counts
99+
100+
101+
def main() -> int:
102+
parser = argparse.ArgumentParser(description=__doc__)
103+
parser.add_argument("readme", nargs="?", type=Path, default=DEFAULT_README)
104+
parser.add_argument("--base", help="git ref to diff against (default: origin/main)")
105+
parser.add_argument("--min-stars", type=int, default=MIN_STARS)
106+
args = parser.parse_args()
107+
108+
base = resolve_base(args.base)
109+
if base is None:
110+
print("no git base ref; skipping star check")
111+
return 0
112+
113+
repos = added_repos(base, args.readme)
114+
if not repos:
115+
print("no new GitHub listings")
116+
return 0
117+
118+
counts = star_counts(repos)
119+
failures: list[str] = []
120+
for slug in repos:
121+
stars = counts.get(slug)
122+
if stars is None:
123+
failures.append(f"{slug}: not a public GitHub repository")
124+
continue
125+
if stars < args.min_stars:
126+
failures.append(f"{slug}: {stars} stars (need {args.min_stars}+)")
127+
128+
if failures:
129+
print("new GitHub listings below the star bar:", file=sys.stderr)
130+
for line in failures:
131+
print(f" {line}", file=sys.stderr)
132+
return 1
133+
134+
print(f"checked {len(repos)} new GitHub listing(s); all have {args.min_stars}+ stars")
135+
return 0
136+
137+
138+
if __name__ == "__main__":
139+
raise SystemExit(main())

0 commit comments

Comments
 (0)