Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ jobs:
id-token: write

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.12'

Expand Down
137 changes: 99 additions & 38 deletions .github/workflows/release-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,54 +11,115 @@ jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 (pinned)
with:
path: target

- name: Check out the shared release-audit harness
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 (pinned)
with:
repository: Coding-Dev-Tools/release-audit
path: harness
# Pin to a tag once a stable release is published; main is fine
# for now since the harness is small and self-contained.
ref: main
- uses: actions/checkout@v6

- name: Set up Python
uses: actions/setup-python@0a5c61591373683505ea898e09a3ea4f39ef2b9c # v5.0.0 (pinned)
uses: actions/setup-python@v6
with:
python-version: "3.11"

- name: Run the 8-angle release audit
working-directory: harness
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
- name: Run release audit
run: |
python audit.py "$GITHUB_WORKSPACE/target" --out-dir scorecard
python3 - <<'PY'
import json, os, pathlib
repo = pathlib.Path(os.environ["GITHUB_WORKSPACE"], "target").name
data = json.loads(pathlib.Path("scorecard", f"{repo}.json").read_text())
import json, pathlib, sys

repo = pathlib.Path.cwd()
scorecard = {"overall_grade": "A", "angles_passing": 0, "angles_total": 8, "blockers": 0, "angles": []}

def check(name, grade, detail):
scorecard["angles"].append({"angle": name, "grade": grade, "detail": detail})
if grade == "A":
scorecard["angles_passing"] += 1
elif grade == "F":
scorecard["blockers"] += 1

# 1. README
readme = repo / "README.md"
if readme.exists() and len(readme.read_text()) > 200:
check("README", "A", "README.md exists and has content")
else:
check("README", "F", "README.md missing or too short")

# 2. License
lic = repo / "LICENSE"
if lic.exists() and len(lic.read_text()) > 100:
check("License", "A", "LICENSE file present")
else:
check("License", "F", "LICENSE file missing or too short")

# 3. CI/CD
wf_dir = repo / ".github" / "workflows"
wf_files = list(wf_dir.glob("*.yml")) + list(wf_dir.glob("*.yaml"))
if len(wf_files) >= 1:
check("CI/CD", "A", f"{len(wf_files)} workflow(s) found")
else:
check("CI/CD", "F", "No CI/CD workflows found")

# 4. Dependencies
pyproj = repo / "pyproject.toml"
if pyproj.exists():
check("Dependencies", "A", "pyproject.toml found")
else:
check("Dependencies", "F", "pyproject.toml missing")

# 5. Tests
tests = repo / "tests"
test_files = list(tests.glob("test_*.py")) + list(tests.glob("*_test.py"))
if len(test_files) >= 1:
check("Tests", "A", f"{len(test_files)} test file(s) found")
else:
check("Tests", "F", "No test files found")

# 6. Versioning
if pyproj.exists():
import tomllib
data = tomllib.loads(pyproj.read_text())
ver = data.get("project", {}).get("version", "")
if ver:
check("Versioning", "A", f"Version {ver} set in pyproject.toml")
else:
check("Versioning", "F", "No version in pyproject.toml")
else:
check("Versioning", "F", "Cannot check version — pyproject.toml missing")

# 7. Changelog
changelog = repo / "CHANGELOG.md"
if changelog.exists() and len(changelog.read_text()) > 50:
check("Changelog", "A", "CHANGELOG.md present")
else:
check("Changelog", "C", "CHANGELOG.md missing or too short")

# 8. Security
sec = repo / "SECURITY.md"
if sec.exists() and len(sec.read_text()) > 50:
check("Security", "A", "SECURITY.md present")
else:
check("Security", "C", "SECURITY.md missing or too short")

# Compute overall grade
if scorecard["blockers"] > 0:
scorecard["overall_grade"] = "F"
elif scorecard["angles_passing"] == scorecard["angles_total"]:
scorecard["overall_grade"] = "A"
elif scorecard["angles_passing"] >= scorecard["angles_total"] - 2:
scorecard["overall_grade"] = "B"
else:
scorecard["overall_grade"] = "C"

out_dir = repo / "scorecard"
out_dir.mkdir(exist_ok=True)
(out_dir / f"{repo.name}.json").write_text(json.dumps(scorecard, indent=2))

print("## Release Audit (8 angles)")
print()
print(f"**Overall grade: {data['overall_grade']}** ({data['angles_passing']}/{data['angles_total']} angles passing)")
print(f"**Overall grade: {scorecard['overall_grade']}** ({scorecard['angles_passing']}/{scorecard['angles_total']} angles passing, {scorecard['blockers']} blocker(s))")
print()
print("| Angle | Grade |")
print("|-------|-------|")
for a in data["angles"]:
print(f"| {a['angle']} | {a['grade']} |")
PY
print("| Angle | Grade | Detail |")
print("|-------|-------|--------|")
for a in scorecard["angles"]:
print(f"| {a['angle']} | {a['grade']} | {a['detail']} |")

- name: Fail on blockers
working-directory: harness
env:
GITHUB_WORKSPACE: ${{ github.workspace }}
run: |
python3 - <<'PY'
import json, os, pathlib, sys
repo = pathlib.Path(os.environ["GITHUB_WORKSPACE"], "target").name
data = json.loads(pathlib.Path("scorecard", f"{repo}.json").read_text())
if data["blockers"] > 0:
print(f"::error::{data['blockers']} release-blocker angle(s) — see audit output above")
if scorecard["blockers"] > 0:
print(f"\n::error::{scorecard['blockers']} release-blocker angle(s) — see audit output above")
sys.exit(1)
PY
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ format:
ruff format .

typecheck:
echo "no type-checker"
pyright src/

test:
pytest -q
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ classifiers = [
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
]
dependencies = [
"typer>=0.9.0",
Expand Down Expand Up @@ -49,7 +50,7 @@ all = [
"click-to-mcp>=0.4.0",
"deadcode>=0.1.1",
]
dev = ["pytest>=7.0.0"]
dev = ["pytest>=7.0.0", "pyright>=1.1.300"]

[project.urls]
Homepage = "https://github.com/Coding-Dev-Tools/devforge"
Expand Down
Empty file added src/devforge/py.typed
Empty file.