@@ -11,54 +11,115 @@ jobs:
1111 audit :
1212 runs-on : ubuntu-latest
1313 steps :
14- - uses : actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 (pinned)
15- with :
16- path : target
17-
18- - name : Check out the shared release-audit harness
19- uses : actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 (pinned)
20- with :
21- repository : Coding-Dev-Tools/release-audit
22- path : harness
23- # Pin to a tag once a stable release is published; main is fine
24- # for now since the harness is small and self-contained.
25- ref : main
14+ - uses : actions/checkout@v6
2615
2716 - name : Set up Python
28- uses : actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 (pinned)
17+ uses : actions/setup-python@v6
2918 with :
3019 python-version : " 3.11"
3120
32- - name : Run the 8-angle release audit
33- working-directory : harness
34- env :
35- GITHUB_WORKSPACE : ${{ github.workspace }}
21+ - name : Run release audit
3622 run : |
37- python audit.py "$GITHUB_WORKSPACE/target" --out-dir scorecard
3823 python3 - <<'PY'
39- import json, os, pathlib
40- repo = pathlib.Path(os.environ["GITHUB_WORKSPACE"], "target").name
41- data = json.loads(pathlib.Path("scorecard", f"{repo}.json").read_text())
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"
110+ out_dir.mkdir(exist_ok=True)
111+ (out_dir / f"{repo.name}.json").write_text(json.dumps(scorecard, indent=2))
112+
42113 print("## Release Audit (8 angles)")
43114 print()
44- print(f"**Overall grade: {data ['overall_grade']}** ({data ['angles_passing']}/{data ['angles_total']} angles passing)")
115+ print(f"**Overall grade: {scorecard ['overall_grade']}** ({scorecard ['angles_passing']}/{scorecard ['angles_total']} angles passing, {scorecard['blockers']} blocker(s) )")
45116 print()
46- print("| Angle | Grade |")
47- print("|-------|-------|")
48- for a in data["angles"]:
49- print(f"| {a['angle']} | {a['grade']} |")
50- PY
117+ print("| Angle | Grade | Detail |")
118+ print("|-------|-------|--------|")
119+ for a in scorecard["angles"]:
120+ print(f"| {a['angle']} | {a['grade']} | {a['detail']} |")
51121
52- - name : Fail on blockers
53- working-directory : harness
54- env :
55- GITHUB_WORKSPACE : ${{ github.workspace }}
56- run : |
57- python3 - <<'PY'
58- import json, os, pathlib, sys
59- repo = pathlib.Path(os.environ["GITHUB_WORKSPACE"], "target").name
60- data = json.loads(pathlib.Path("scorecard", f"{repo}.json").read_text())
61- if data["blockers"] > 0:
62- print(f"::error::{data['blockers']} release-blocker angle(s) — see audit output above")
122+ if scorecard["blockers"] > 0:
123+ print(f"\n::error::{scorecard['blockers']} release-blocker angle(s) — see audit output above")
63124 sys.exit(1)
64125 PY
0 commit comments