diff --git a/.github/workflows/pr-lint.yml b/.github/workflows/pr-lint.yml index cc95f0d..003c3cf 100644 --- a/.github/workflows/pr-lint.yml +++ b/.github/workflows/pr-lint.yml @@ -1,8 +1,10 @@ name: PR Lint (links + format) -# Runs on pull requests that touch the list. Two independent checks: -# 1. link-check - lychee looks for dead links in README.md. -# 2. format-lint - warns when a newly added entry has no description. +# Runs on pull requests that touch the list. Three independent checks: +# 1. link-check - lychee looks for dead links in README.md. +# 2. format-lint - warns when a newly added entry has no description. +# 3. openness-check - verifies the repos a PR ADDS: gone, archived, unlicensed, +# or no real source code. Warns, never blocks. on: pull_request: @@ -122,3 +124,20 @@ jobs: # Warn only. Format nits never block a PR. sys.exit(0) PY + + openness-check: + name: Check added repositories + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + with: + fetch-depth: 0 # need the base commit to diff against + + - name: Verify open-source claims of added repos + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # full rate limit + BASE_SHA: ${{ github.event.pull_request.base.sha }} + # Checks only repos this PR adds. Posts warnings to the job summary and + # inline annotations; never fails the PR (the maintainer decides). + run: python3 scripts/health_report.py --diff-base "$BASE_SHA" diff --git a/scripts/health_report.py b/scripts/health_report.py index 36740b9..205f975 100644 --- a/scripts/health_report.py +++ b/scripts/health_report.py @@ -26,6 +26,7 @@ import datetime as dt import json import os import re +import subprocess import sys import time import urllib.error @@ -169,6 +170,53 @@ def scan_repos(repos, skull_set, now, token, limit=0): return findings, checked, rate_limited +def added_readme_text(base): + """The lines a PR adds to README.md, for scoping the check to new entries.""" + try: + out = subprocess.run( + ["git", "diff", "--unified=0", base, "HEAD", "--", "README.md"], + capture_output=True, text=True, check=False, + ).stdout + except Exception as exc: + print(f"::warning::could not compute diff ({exc})", file=sys.stderr) + return "" + return "\n".join( + line[1:] for line in out.splitlines() + if line.startswith("+") and not line.startswith("+++") + ) + + +def run_pr_check(base, now, token, limit): + """Check only the repos a PR adds. Warns, never blocks. Returns exit code 0.""" + added = added_readme_text(base) + repos = extract_repos(added) + findings, checked, rate_limited = scan_repos( + repos, extract_skull_repos(added), now, token, limit + ) + new = [(s, w) for s, w, sk in findings if not sk] + for slug, why in new: + print(f"::warning::{slug} - {why}") # inline annotation on the PR + + lines = ["## Open-source check of added repositories", "", + f"Checked {checked} newly added repo(s)."] + if new: + lines += ["", "Review these before merge (warnings, not blockers):"] + lines += [f"- {slug} - {why}" for slug, why in new] + else: + lines.append("No open-source concerns in the added repositories.") + if rate_limited: + lines += ["", "_GitHub rate limit hit; some repos were not checked._"] + summary = "\n".join(lines) + "\n" + + step = os.environ.get("GITHUB_STEP_SUMMARY") + if step: + with open(step, "a", encoding="utf-8") as fh: + fh.write(summary) + else: + sys.stdout.write(summary) + return 0 # warn only; the maintainer decides + + def parse_dead_links(path): """Return (dead_links, scan_ran). dead_links = [(url, reason), ...].""" try: @@ -235,12 +283,17 @@ def main(): ap.add_argument("--lychee", default="lychee/out.json") ap.add_argument("--limit", type=int, default=0, help="cap repos checked (0 = all)") ap.add_argument("--out", default=None) + ap.add_argument("--diff-base", default=None, + help="PR mode: check only repos added since this git ref (warns, never blocks)") args = ap.parse_args() now = dt.datetime.now(dt.timezone.utc) today = now.date().isoformat() token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") + if args.diff_base: + sys.exit(run_pr_check(args.diff_base, now, token, args.limit)) + with open(args.readme, encoding="utf-8") as fh: readme = fh.read() repos = extract_repos(readme)