@@ -21,33 +21,105 @@ jobs:
2121 - name : Run release audit
2222 run : |
2323 python3 - <<'PY'
24- import json, os, pathlib, sys
25-
26- repo = pathlib.Path.cwd().name
27- scorecard = {"overall_grade": "A", "angles_passing": 8, "angles_total": 8, "blockers": 0, "angles": []}
28- angles = [
29- ("README", "A"),
30- ("License", "A"),
31- ("CI/CD", "A"),
32- ("Dependencies", "A"),
33- ("Tests", "A"),
34- ("Versioning", "A"),
35- ("Changelog", "A"),
36- ("Security", "A"),
37- ]
38- for name, grade in angles:
39- scorecard["angles"].append({"angle": name, "grade": grade})
40-
41- out_dir = pathlib.Path("scorecard")
24+ import json, pathlib, sys
25+
26+ repo = pathlib.Path.cwd()
27+ scorecard = {"overall_grade": "A", "angles_passing": 0, "angles_total": 8, "blockers": 0, "angles": []}
28+
29+ def check(name, grade, detail):
30+ scorecard["angles"].append({"angle": name, "grade": grade, "detail": detail})
31+ if grade == "A":
32+ scorecard["angles_passing"] += 1
33+ elif grade == "F":
34+ scorecard["blockers"] += 1
35+
36+ # 1. README
37+ readme = repo / "README.md"
38+ if readme.exists() and len(readme.read_text()) > 200:
39+ check("README", "A", "README.md exists and has content")
40+ else:
41+ check("README", "F", "README.md missing or too short")
42+
43+ # 2. License
44+ lic = repo / "LICENSE"
45+ if lic.exists() and len(lic.read_text()) > 100:
46+ check("License", "A", "LICENSE file present")
47+ else:
48+ check("License", "F", "LICENSE file missing or too short")
49+
50+ # 3. CI/CD
51+ wf_dir = repo / ".github" / "workflows"
52+ wf_files = list(wf_dir.glob("*.yml")) + list(wf_dir.glob("*.yaml"))
53+ if len(wf_files) >= 1:
54+ check("CI/CD", "A", f"{len(wf_files)} workflow(s) found")
55+ else:
56+ check("CI/CD", "F", "No CI/CD workflows found")
57+
58+ # 4. Dependencies
59+ pyproj = repo / "pyproject.toml"
60+ if pyproj.exists():
61+ check("Dependencies", "A", "pyproject.toml found")
62+ else:
63+ check("Dependencies", "F", "pyproject.toml missing")
64+
65+ # 5. Tests
66+ tests = repo / "tests"
67+ test_files = list(tests.glob("test_*.py")) + list(tests.glob("*_test.py"))
68+ if len(test_files) >= 1:
69+ check("Tests", "A", f"{len(test_files)} test file(s) found")
70+ else:
71+ check("Tests", "F", "No test files found")
72+
73+ # 6. Versioning
74+ if pyproj.exists():
75+ import tomllib
76+ data = tomllib.loads(pyproj.read_text())
77+ ver = data.get("project", {}).get("version", "")
78+ if ver:
79+ check("Versioning", "A", f"Version {ver} set in pyproject.toml")
80+ else:
81+ check("Versioning", "F", "No version in pyproject.toml")
82+ else:
83+ check("Versioning", "F", "Cannot check version — pyproject.toml missing")
84+
85+ # 7. Changelog
86+ changelog = repo / "CHANGELOG.md"
87+ if changelog.exists() and len(changelog.read_text()) > 50:
88+ check("Changelog", "A", "CHANGELOG.md present")
89+ else:
90+ check("Changelog", "C", "CHANGELOG.md missing or too short")
91+
92+ # 8. Security
93+ sec = repo / "SECURITY.md"
94+ if sec.exists() and len(sec.read_text()) > 50:
95+ check("Security", "A", "SECURITY.md present")
96+ else:
97+ check("Security", "C", "SECURITY.md missing or too short")
98+
99+ # Compute overall grade
100+ if scorecard["blockers"] > 0:
101+ scorecard["overall_grade"] = "F"
102+ elif scorecard["angles_passing"] == scorecard["angles_total"]:
103+ scorecard["overall_grade"] = "A"
104+ elif scorecard["angles_passing"] >= scorecard["angles_total"] - 2:
105+ scorecard["overall_grade"] = "B"
106+ else:
107+ scorecard["overall_grade"] = "C"
108+
109+ out_dir = repo / "scorecard"
42110 out_dir.mkdir(exist_ok=True)
43- (out_dir / f"{repo}.json").write_text(json.dumps(scorecard, indent=2))
111+ (out_dir / f"{repo.name }.json").write_text(json.dumps(scorecard, indent=2))
44112
45113 print("## Release Audit (8 angles)")
46114 print()
47- print(f"**Overall grade: {scorecard['overall_grade']}** ({scorecard['angles_passing']}/{scorecard['angles_total']} angles passing)")
115+ print(f"**Overall grade: {scorecard['overall_grade']}** ({scorecard['angles_passing']}/{scorecard['angles_total']} angles passing, {scorecard['blockers']} blocker(s) )")
48116 print()
49- print("| Angle | Grade |")
50- print("|-------|-------|")
117+ print("| Angle | Grade | Detail | ")
118+ print("|-------|-------|--------| ")
51119 for a in scorecard["angles"]:
52- print(f"| {a['angle']} | {a['grade']} |")
120+ print(f"| {a['angle']} | {a['grade']} | {a['detail']} |")
121+
122+ if scorecard["blockers"] > 0:
123+ print(f"\n::error::{scorecard['blockers']} release-blocker angle(s) — see audit output above")
124+ sys.exit(1)
53125 PY
0 commit comments