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
172 lines
5.8 KiB
Python
Executable File
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())
|