Files
wehub-resource-sync 7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

172 lines
5.8 KiB
Python
Executable File

#!/usr/bin/env python3
"""Remind contributors about news fragments.
Always exits 0 (non-blocking). Two messages:
- When any file under changelog.d/ is staged: confirm that the fragment
will be rolled into the next release's notes by towncrier and validate
the filename matches the expected pattern.
- When source changes are substantial but no fragment is staged: nudge
the contributor to add one.
Replaces the old shared docs/release_notes/<version>.md model — see
changelog.d/README.md for the rationale and conventions.
"""
import subprocess
import sys
import tomllib
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _changelog_fragments import classify_fragment, load_categories
from _commit_analysis import analyze_commit
# Minimum added source lines before the nudge fires
MIN_SOURCE_ADDED = 20
REPO_ROOT = Path(__file__).resolve().parent.parent
PYPROJECT = REPO_ROOT / "pyproject.toml"
def _load_categories():
"""Load the canonical category list, falling back to a sensible
default if pyproject.toml or its towncrier section is unreadable —
this hook is non-blocking, so a degraded mode is preferable to a
hard failure here (the blocking check-changelog-fragments hook is
the one that fails loud on a broken config)."""
try:
return load_categories(PYPROJECT)
except (
OSError,
tomllib.TOMLDecodeError,
KeyError,
TypeError,
ValueError,
):
return ("breaking", "security", "feature", "bugfix", "removal", "misc")
CATEGORIES = _load_categories()
# Color helpers: only emit ANSI when stdout is a TTY. CI logs and Windows
# terminals without VT processing render the raw escape sequences as
# visible garbage.
_USE_COLOR = sys.stdout.isatty()
_CYAN = "\033[36m" if _USE_COLOR else ""
_YELLOW = "\033[33m" if _USE_COLOR else ""
_RESET = "\033[0m" if _USE_COLOR else ""
def _fragments_staged():
"""Return the list of staged files under changelog.d/, excluding
README.md and other non-fragment files."""
result = subprocess.run(
["git", "diff", "--cached", "--name-only", "--", "changelog.d/"],
capture_output=True,
text=True,
)
files = [line for line in result.stdout.strip().splitlines() if line]
return [
f for f in files if f.endswith(".md") and Path(f).name != "README.md"
]
def _print_staged_notice(staged):
"""Inform the committer that a news fragment was staged."""
print()
print(f" {_CYAN}News Fragment Staged{_RESET}")
print(" " + "-" * 40)
for f in staged:
print(f" - {f}")
print()
print(" Files under changelog.d/ are rendered into")
print(" docs/release_notes/<version>.md at release prep time by")
print(" `pdm run towncrier build --version <X.Y.Z> --yes`, then")
print(" surfaced in the GitHub release body by")
print(" .github/workflows/release.yml.")
# Validate filenames — non-blocking, but a typo'd category silently
# falls through towncrier's "no fragments matched" branch and the
# contributor's note vanishes from the release.
issues = []
for f in staged:
kind, value = classify_fragment(Path(f).name, CATEGORIES)
if kind != "ok":
issues.append((f, kind, value))
if issues:
print()
print(f" {_YELLOW}⚠ Fragment filename problems:{_RESET}")
for f, kind, value in issues:
if kind == "bad-name":
print(
f" {f} — does not match `<id>.<category>.md` or "
f"`+<slug>.<category>.md`"
)
else:
print(
f" {f} — unknown category `{value}`. Use one of: "
f"{', '.join(CATEGORIES)}"
)
print()
print(" See changelog.d/README.md for the convention.")
print()
print(" Format tips:")
print(" - One sentence is usually enough; longer prose is fine for")
print(" breaking changes that need a 'what to do' line.")
print(" - Markdown is supported. The PR/issue link is auto-appended")
print(" based on the fragment id (no need to add `(#NNNN)`).")
print(" - Skip dependency bumps, internal CI tweaks, and refactors")
print(" with no user-visible behavior — the auto-PR-list catches")
print(" those without a fragment.")
print()
def _print_missing_notice(analysis):
"""Nudge the committer to add a news fragment for a substantial change."""
print()
print(f" {_CYAN}News Fragment Reminder{_RESET}")
print(" " + "-" * 40)
print(
f" You're adding {analysis.total_source_added} lines across "
f"{len(analysis.source_files)} source file(s)"
)
print(" but no changelog.d/ fragment is staged.")
print()
print(" Changed source files:")
for f in analysis.source_files:
print(f" - {f.path} (+{f.added})")
print()
print(" If this change is user-facing, drop a fragment under")
print(" changelog.d/ named `<PR-number>.<category>.md` (categories:")
print(f" {', '.join(CATEGORIES)}). See changelog.d/README.md.")
print()
def main():
staged = _fragments_staged()
# Always inform when a fragment is staged — contributors should know
# the file gets rendered into the release, and any naming mistakes
# need to surface before the fragment silently goes ignored.
if staged:
_print_staged_notice(staged)
return 0
analysis = analyze_commit()
# Silent exit: no source files staged
if not analysis.source_files:
return 0
# Silent exit: trivial change (less than MIN_SOURCE_ADDED added lines)
if analysis.total_source_added < MIN_SOURCE_ADDED:
return 0
_print_missing_notice(analysis)
return 0
if __name__ == "__main__":
sys.exit(main())