ci: harden contributor checks (#254)

* ci: harden contributor checks

* ci: pin setup-uv action release

* ci: split workflow checks

* docs: clarify required checks
This commit is contained in:
Elliot Chen
2026-06-06 10:47:16 +08:00
committed by GitHub
parent 3527ea3eb2
commit 873e7535fb
12 changed files with 529 additions and 225 deletions

View File

@ -0,0 +1,114 @@
"""Validate commit subjects against the EverOS Conventional Commits policy."""
from __future__ import annotations
import os
import re
import subprocess
import sys
ZERO_SHA = "0" * 40
ALLOWED_TYPES = (
"feat",
"fix",
"refactor",
"test",
"docs",
"style",
"perf",
"chore",
"build",
"ci",
"revert",
)
TITLE_RE = re.compile(
rf"^({'|'.join(ALLOWED_TYPES)})(\([A-Za-z0-9._/-]+\))?(!)?: .+"
)
MAX_TITLE_LENGTH = 72
def _run_git(args: list[str]) -> str:
return subprocess.check_output(["git", *args], text=True).strip()
def _default_range() -> str:
event_name = os.getenv("GITHUB_EVENT_NAME", "")
before = os.getenv("GITHUB_EVENT_BEFORE", "") or os.getenv("GITHUB_EVENT_BEFORE_SHA", "")
after = os.getenv("GITHUB_SHA", "HEAD")
pr_base = os.getenv("GITHUB_PR_BASE_SHA", "")
if event_name.startswith("pull_request") and pr_base:
return f"{pr_base}..HEAD"
if before and before != ZERO_SHA:
return f"{before}..{after}"
try:
_run_git(["rev-parse", "--verify", f"{after}^"])
except subprocess.CalledProcessError:
return after
return f"{after}^..{after}"
def _commit_rows(commit_range: str) -> list[tuple[str, str, str]]:
output = _run_git(
[
"log",
"--format=%H%x00%s%x00%P",
commit_range,
]
)
if not output:
return []
rows = []
for line in output.splitlines():
commit, subject, parents = line.split("\x00", 2)
rows.append((commit, subject, parents))
return rows
def _is_exempt(subject: str, parents: str) -> bool:
if len(parents.split()) > 1:
return True
return subject.startswith(("Revert ", "fixup!", "squash!"))
def _validate(commit_range: str) -> list[str]:
failures: list[str] = []
for commit, subject, parents in _commit_rows(commit_range):
if _is_exempt(subject, parents):
continue
short = commit[:12]
if len(subject) > MAX_TITLE_LENGTH:
failures.append(
f"{short}: subject is {len(subject)} chars; max is {MAX_TITLE_LENGTH}: {subject}"
)
continue
if not TITLE_RE.match(subject):
allowed = ", ".join(ALLOWED_TYPES)
failures.append(
f"{short}: invalid subject: {subject}\n"
f" expected: <type>[(scope)][!]: <description>\n"
f" allowed types: {allowed}"
)
return failures
def main() -> int:
commit_range = sys.argv[1] if len(sys.argv) > 1 else _default_range()
failures = _validate(commit_range)
if failures:
print(f"Commit message check failed for range {commit_range}:")
print("\n".join(failures))
return 1
print(f"Commit messages follow Conventional Commits for range {commit_range}.")
return 0
if __name__ == "__main__":
raise SystemExit(main())

109
scripts/check_docs.py Normal file
View File

@ -0,0 +1,109 @@
"""Validate contributor-facing Markdown and use-case documentation."""
from __future__ import annotations
import re
import sys
from pathlib import Path
SKIP_DIRS = {".git", "node_modules", ".venv", ".uv-cache"}
USE_CASE_TABLES = {
Path("README.md"): "## Use Cases",
Path("use-cases/README.md"): "## Use Cases",
}
PRIMARY_LINK_RE = re.compile(
r"^\[(?:Code|Plugin|Live Demo|Learn more)\]\(([^)]+)\)",
flags=re.M,
)
def _markdown_files() -> list[Path]:
return sorted(
path
for path in Path(".").rglob("*.md")
if not any(part in SKIP_DIRS for part in path.parts)
)
def _check_active_relative_links() -> list[str]:
missing: list[str] = []
root = Path.cwd().resolve()
for path in _markdown_files():
active = re.sub(r"<!--.*?-->", "", path.read_text(), flags=re.S)
for raw in re.findall(r"\[[^\]]*\]\(([^)]+)\)", active):
link = raw.split("#", 1)[0]
if not link or link.startswith(("http://", "https://", "mailto:")):
continue
target = (path.parent / link).resolve()
try:
target.relative_to(root)
except ValueError:
missing.append(f"{path}: {raw} -> outside repository")
continue
if not target.exists():
missing.append(f"{path}: {raw} -> missing")
return missing
def _check_use_case_banner_links() -> tuple[list[str], list[str]]:
failures: list[str] = []
warnings: list[str] = []
for path, heading in USE_CASE_TABLES.items():
text = path.read_text()
start = text.find(heading)
if start == -1:
failures.append(f"{path}: missing {heading}")
continue
table_start = text.find("<table>", start)
table_end = text.find("</table>", table_start)
if table_start == -1 or table_end == -1:
failures.append(f"{path}: missing use-case table")
continue
table = text[table_start:table_end]
cells = re.findall(r"<td[^>]*>(.*?)</td>", table, flags=re.S)
for index, cell in enumerate(cells, start=1):
title_match = re.search(r"####\s+(.+)", cell)
title = title_match.group(1).strip() if title_match else f"use case {index}"
banner_match = re.search(r"\[!\[[^\]]*\]\([^)]+\)\]\(([^)]+)\)", cell)
primary_match = PRIMARY_LINK_RE.search(cell)
if not banner_match and primary_match:
warnings.append(f"{path}: {title}: primary link has no linked banner")
elif banner_match and not primary_match:
failures.append(f"{path}: {title}: missing primary link")
elif (
banner_match
and primary_match
and banner_match.group(1) != primary_match.group(1)
):
failures.append(
f"{path}: {title}: banner link {banner_match.group(1)} "
f"does not match primary link {primary_match.group(1)}"
)
return failures, warnings
def main() -> int:
failures = _check_active_relative_links()
use_case_failures, warnings = _check_use_case_banner_links()
failures.extend(use_case_failures)
if warnings:
print("\n".join(f"warning: {warning}" for warning in warnings))
if failures:
print("\n".join(failures))
return 1
print("Documentation checks passed.")
return 0
if __name__ == "__main__":
raise SystemExit(main())