chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:12:00 +08:00
commit 3de48288cb
2986 changed files with 1131193 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
"""Shared Markdown-section parsing for the PR-template tooling.
`validate.py` (the merge gate) and the release-time changelog harvester
(`.github/scripts/changelog/generate.py`) both need to pull a named `##`
section out of a PR body. Keeping that logic in one place means the gate and
the harvester can never drift on what counts as the "## Changelog" section.
"""
from __future__ import annotations
import re
_HEADING_RE = re.compile(r"(?im)^\s*##\s+(.+?)\s*$")
_HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
_CHECKBOX_RE = re.compile(r"(?im)^\s*-\s*\[(?P<mark>[ xX])\]\s*(?P<label>.+?)\s*$")
def strip_html_comments(text: str) -> str:
"""Drop ``<!-- ... -->`` comments (template guidance lives in these)."""
return _HTML_COMMENT_RE.sub("", text)
def heading_spans(body: str) -> dict[str, tuple[int, int]]:
"""Map each lowercased ``## heading`` to the (start, end) span of its body.
The span runs from just after the heading line to the start of the next
``##`` heading (or end of document). Later duplicate headings win, matching
the existing validator behaviour.
"""
matches = list(_HEADING_RE.finditer(body))
spans: dict[str, tuple[int, int]] = {}
for idx, match in enumerate(matches):
title = match.group(1).strip().lower()
start = match.end()
end = matches[idx + 1].start() if idx + 1 < len(matches) else len(body)
spans[title] = (start, end)
return spans
def section(body: str, spans: dict[str, tuple[int, int]], heading: str) -> str:
"""Return the raw text under *heading*, or ``""`` if it is absent."""
span = spans.get(heading.lower())
if span is None:
return ""
return body[span[0] : span[1]]
def section_text(body: str, heading: str) -> str:
"""Convenience: raw text under *heading* parsed straight from *body*."""
return section(body, heading_spans(body), heading)
# --- checkbox parsing (shared by the gate and the harvester) ----------------
def checked_labels(section_raw: str, expected_labels: tuple[str, ...]) -> set[str]:
"""Return the canonical labels whose checkbox is ticked in *section_raw*."""
expected_by_lower = {label.lower(): label for label in expected_labels}
checked: set[str] = set()
for match in _CHECKBOX_RE.finditer(section_raw):
label = match.group("label").strip()
canonical = expected_by_lower.get(label.lower())
if canonical and match.group("mark").lower() == "x":
checked.add(canonical)
return checked
# --- "## Changelog" section format ------------------------------------------
#
# The section holds a free-text, user-voice one-liner describing the change (the
# author may hard-wrap it — we take the first line). The category/tag is NOT
# written here; it is derived from the "Type of change" checkboxes via TYPE_TAGS.
# The section is optional: an author deletes it (or leaves the `<…>` placeholder)
# when the change isn't noteworthy, and the PR is then omitted from the changelog.
# The same parser backs the PR gate (validate.py) and the harvester (generate.py).
# "Type of change" checkbox label -> bracket tag rendered in CHANGELOG.md.
TYPE_TAGS: dict[str, str] = {
"UI / frontend change": "UI",
"Bug fix": "Bug fix",
"Feature": "Feature",
"Docs": "Docs",
"Refactor / chore": "Chore",
"Test / CI": "Test/CI",
"Breaking change": "Breaking",
}
_PLACEHOLDER_RE = re.compile(r"^\s*<.*>\s*$")
# Markers meaning "nothing to announce" — the section is optional and deletable,
# but authors (and the old template's `skip` sentinel) still write these; treat
# them as an absent section rather than leaking them in as literal entries.
_OMIT_MARKERS = frozenset({"skip", "n/a", "na", "none", "-"})
def is_placeholder(line: str) -> bool:
"""True when *line* is the untouched ``<…>`` template placeholder."""
return bool(_PLACEHOLDER_RE.match(line))
def changelog_description(section_raw: str) -> str:
"""First meaningful line of a "## Changelog" section.
Strips HTML comments, then returns the first non-blank line — unless that
line is the ``<…>`` placeholder or an omit marker (``skip``/``n/a``/…), in
which case the section counts as absent and this returns ``""``. Multi-line /
wrapped bodies collapse to their first line.
"""
for raw in strip_html_comments(section_raw).splitlines():
line = raw.strip()
if not line:
continue
if is_placeholder(line) or line.lower() in _OMIT_MARKERS:
return ""
return line
return ""
def type_tag(labels: set[str]) -> str:
"""Render the bracket tag for the checked Type-of-change *labels*.
Joined with ` / ` in TYPE_TAGS declaration order (e.g. ``[UI / Bug fix]``).
Returns ``""`` when no known type is checked.
"""
tags = [tag for label, tag in TYPE_TAGS.items() if label in labels]
return f"[{' / '.join(tags)}]" if tags else ""
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""Add PR-template scaffolding without deleting the author's text."""
from __future__ import annotations
import re
import sys
from pathlib import Path
from validate import TEST_LABELS, TYPE_LABELS
_HEADING_RE = re.compile(r"(?im)^\s*##\s+(.+?)\s*$")
def _has_heading(body: str, heading: str) -> bool:
wanted = heading.strip().lower()
return any(match.group(1).strip().lower() == wanted for match in _HEADING_RE.finditer(body))
def _append_section(body: str, heading: str, content: str) -> str:
if _has_heading(body, heading):
return body
return body.rstrip() + f"\n\n## {heading}\n\n{content.rstrip()}\n"
def _checkbox_block(labels: tuple[str, ...]) -> str:
return "\n".join(f"- [ ] {label}" for label in labels) + "\n"
def format_body(body: str) -> str:
"""Return *body* with missing PR-template sections appended.
Existing prose is preserved verbatim. When the body has no Summary
heading, the existing text is placed under Summary so it remains the
main description instead of being pushed below the template.
"""
body = body.strip()
if not body:
body = "## Summary\n\n"
elif not _has_heading(body, "Summary"):
body = f"## Summary\n\n{body}"
body = _append_section(
body,
"Test Plan",
"How was this change tested? Describe the steps, commands, or scenarios "
"used to verify it (autoformat added this section — please replace it).",
)
body = _append_section(
body,
"Demo",
"<!-- Video or images demonstrating the change. Mandatory for UI / "
"frontend changes; use 'N/A' otherwise. -->",
)
body = _append_section(
body,
"ELI5",
"<!-- Optional: explain the change in plain language. -->",
)
body = _append_section(
body,
"Diagram",
"```mermaid\nflowchart LR\n A[Before] --> B[Change]\n B --> C[After]\n```",
)
body = _append_section(body, "Type of change", _checkbox_block(TYPE_LABELS))
body = _append_section(body, "Test coverage", _checkbox_block(TEST_LABELS))
body = _append_section(
body,
"Coverage notes",
"<!-- Optional; required if you checked 'Manual verification completed' "
"or 'Not applicable' above. -->",
)
body = _append_section(
body,
"Changelog",
"<!-- One line, in the user's voice, describing the user-facing change; "
"the category comes from the 'Type of change' boxes above. DELETE this "
"section if the change isn't noteworthy (a Breaking change must keep it). "
"-->\n\n<Add a line to describe the change, else delete this section>",
)
return body.rstrip() + "\n"
def main() -> int:
if len(sys.argv) != 3:
print("usage: format_body.py INPUT OUTPUT", file=sys.stderr)
return 2
source = Path(sys.argv[1])
dest = Path(sys.argv[2])
dest.write_text(format_body(source.read_text()))
return 0
if __name__ == "__main__":
raise SystemExit(main())
+182
View File
@@ -0,0 +1,182 @@
#!/usr/bin/env python3
"""Validate that a PR description follows the repository template.
The GitHub workflow passes the PR body in PR_BODY. The script is also
unit-tested directly so changes to the template gate are reviewed like
normal code.
"""
from __future__ import annotations
import os
import re
import sys
from pathlib import Path
# Share the Markdown-section + changelog parsing with the release-time harvester
# (.github/scripts/changelog/generate.py) so the gate and the harvester can
# never disagree on what the "## Changelog" section means.
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _md import changelog_description
from _md import checked_labels as _checked_labels
from _md import heading_spans as _heading_spans
from _md import section as _section
from _md import strip_html_comments as _strip_html_comments
REQUIRED_HEADINGS = (
"Summary",
"Test Plan",
"Type of change",
"Test coverage",
)
TYPE_LABELS = (
"Bug fix",
"Feature",
"UI / frontend change",
"Refactor / chore",
"Docs",
"Test / CI",
"Breaking change",
)
TEST_LABELS = (
"Unit tests added / updated",
"Integration tests added / updated",
"E2E tests added / updated",
"Manual verification completed",
"Existing tests cover this change",
"Not applicable",
)
PLACEHOLDER_FRAGMENTS = (
"what changed and why",
"check all that apply",
"describe below",
"how was this change tested",
)
class ValidationResult:
def __init__(self, ok: bool, errors: list[str]) -> None:
self.ok = ok
self.errors = errors
_CHECKBOX_RE = re.compile(r"(?im)^\s*-\s*\[(?P<mark>[ xX])\]\s*(?P<label>.+?)\s*$")
def _missing_labels(section: str, expected_labels: tuple[str, ...]) -> list[str]:
present = {match.group("label").strip().lower() for match in _CHECKBOX_RE.finditer(section)}
return [label for label in expected_labels if label.lower() not in present]
def _meaningful_text(section: str) -> str:
text = _strip_html_comments(section)
text = re.sub(r"(?im)^\s*-\s*\[[ xX]\].*$", "", text)
return text.strip()
def _contains_placeholder(text: str) -> bool:
lowered = text.lower()
return any(fragment in lowered for fragment in PLACEHOLDER_FRAGMENTS)
def validate_pr_body(body: str) -> ValidationResult:
body = body.lstrip("\ufeff")
errors: list[str] = []
spans = _heading_spans(body)
for heading in REQUIRED_HEADINGS:
if heading.lower() not in spans:
errors.append(f"Missing required section: ## {heading}")
summary = _meaningful_text(_section(body, spans, "Summary"))
if not summary:
errors.append("Summary must describe what changed and why.")
elif _contains_placeholder(summary):
errors.append("Summary still contains template placeholder text.")
test_plan = _meaningful_text(_section(body, spans, "Test Plan"))
if not test_plan:
errors.append("Test Plan must describe how the change was tested.")
elif _contains_placeholder(test_plan):
errors.append("Test Plan still contains template placeholder text.")
type_section = _section(body, spans, "Type of change")
missing_type_labels = _missing_labels(type_section, TYPE_LABELS)
if missing_type_labels:
errors.append(
"Type of change is missing template checkbox(es): " + ", ".join(missing_type_labels)
)
checked_types = _checked_labels(type_section, TYPE_LABELS)
if not checked_types:
errors.append("Check at least one Type of change checkbox.")
# The Demo section is mandatory for UI / frontend changes — reviewers need
# a screenshot or recording of the new behaviour. It stays optional for
# everything else.
if "UI / frontend change" in checked_types:
demo = _meaningful_text(_section(body, spans, "Demo"))
if not demo:
errors.append(
"Demo is required for UI / frontend changes — attach a screenshot "
"or screen recording demonstrating the new behaviour."
)
elif _contains_placeholder(demo):
errors.append("Demo still contains template placeholder text.")
test_section = _section(body, spans, "Test coverage")
missing_test_labels = _missing_labels(test_section, TEST_LABELS)
if missing_test_labels:
errors.append(
"Test coverage is missing template checkbox(es): " + ", ".join(missing_test_labels)
)
checked_tests = _checked_labels(test_section, TEST_LABELS)
if not checked_tests:
errors.append("Check at least one Test coverage checkbox.")
# Coverage notes are optional in general, but required whenever "Manual
# verification completed" or "Not applicable" is checked — those choices
# need a written justification.
if checked_tests & {"Manual verification completed", "Not applicable"}:
coverage_notes = _meaningful_text(_section(body, spans, "Coverage notes"))
if not coverage_notes:
errors.append(
"Coverage notes are required when 'Manual verification completed' or "
"'Not applicable' is selected — describe what you verified or why "
"automated coverage is not needed."
)
elif _contains_placeholder(coverage_notes):
errors.append("Coverage notes still contains template placeholder text.")
# The Changelog section is optional — an author deletes it (or leaves the
# `<…>` placeholder) when the change isn't noteworthy, and the PR is simply
# omitted from the changelog. The one exception: a Breaking change is always
# noteworthy, so it must carry a real description line.
if "Breaking change" in checked_types:
changelog_section = _section(body, spans, "Changelog") if "changelog" in spans else ""
if not changelog_description(changelog_section):
errors.append(
"A Breaking change must describe the change in the Changelog section "
"(otherwise it would be omitted from the changelog)."
)
return ValidationResult(ok=not errors, errors=errors)
def main() -> int:
body = os.environ["PR_BODY"]
result = validate_pr_body(body)
if result.ok:
print("PR template validation passed.")
return 0
print("PR template validation failed:")
for error in result.errors:
print(f"- {error}")
return 1
if __name__ == "__main__":
sys.exit(main())