fix(release): lint the changelog entry inside the file it lands in
The entry was linted alone, so rules that compare it with the rest of CHANGELOG.md never fired. infinito-nexus 16.0.0 passed this check with "-" bullets on top of older "*" entries, and the repository's own markdown lint then reported 400 MD004 findings and refused the push. update_changelog now also lints the merged file and reports only the findings the existing file does not already produce (lint_changelog_context), so pre-existing issues are not blamed on the entry. markdownlint-cli2 runs with --no-globs, so a target config with its own globs no longer lints the whole repository and blocks a release on unrelated files. Without markdownlint-cli2 the built-in check now requires "-" list markers (MD004 dash), the strictest style. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1f6fe52855
commit
32e896fd09
4 files changed
+90
-17
No files matched your search
@@ -6,10 +6,13 @@ import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
from collections import Counter
|
||||
|
||||
_HEADING = re.compile(r"^\s*(#{1,6})\s+(.*?)\s*#*\s*$")
|
||||
_INLINE_CODE = re.compile(r"`([^`\n]+)`")
|
||||
_FINDING = re.compile(r"\bMD\d{3}\b")
|
||||
_LOCATION = re.compile(r"^.*?:\d+(?::\d+)?\s+")
|
||||
_NON_DASH_ITEM = re.compile(r"^\s*[*+]\s")
|
||||
_MULTI_BLANK = re.compile(r"\n{3,}")
|
||||
_ENTRY_LEVEL = 3
|
||||
|
||||
@@ -59,6 +62,35 @@ def lint_changelog_entry(reference_path: str, entry: str) -> list[str]:
|
||||
return _builtin_lint(document)
|
||||
|
||||
|
||||
def lint_changelog_context(reference_path: str, existing: str, merged: str) -> list[str]:
|
||||
"""Return the findings *merged* has beyond those *existing* already has.
|
||||
|
||||
Args:
|
||||
reference_path: path of the target CHANGELOG.md; its directory supplies
|
||||
the markdownlint config.
|
||||
existing: the target file before the entry is inserted.
|
||||
merged: the target file with the entry inserted.
|
||||
|
||||
Returns:
|
||||
Findings of *merged* that *existing* does not produce. Empty when
|
||||
``markdownlint-cli2`` is unavailable.
|
||||
"""
|
||||
if not shutil.which("markdownlint-cli2"):
|
||||
return []
|
||||
known = Counter(
|
||||
_LOCATION.sub("", finding)
|
||||
for finding in (_markdownlint(reference_path, existing) if existing.strip() else [])
|
||||
)
|
||||
new: list[str] = []
|
||||
for finding in _markdownlint(reference_path, merged):
|
||||
key = _LOCATION.sub("", finding)
|
||||
if known[key]:
|
||||
known[key] -= 1
|
||||
else:
|
||||
new.append(finding)
|
||||
return new
|
||||
|
||||
|
||||
def _markdownlint(reference_path: str, document: str) -> list[str]:
|
||||
directory = os.path.dirname(os.path.abspath(reference_path)) or "."
|
||||
fd, tmp_path = tempfile.mkstemp(
|
||||
@@ -69,7 +101,7 @@ def _markdownlint(reference_path: str, document: str) -> list[str]:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
handle.write(document)
|
||||
proc = subprocess.run(
|
||||
["markdownlint-cli2", name],
|
||||
["markdownlint-cli2", "--no-globs", name],
|
||||
cwd=directory,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
@@ -96,6 +128,8 @@ def _builtin_lint(document: str) -> list[str]:
|
||||
errors.append(f"line {number}: trailing whitespace (MD009)")
|
||||
if "`" in line:
|
||||
errors.append(f"line {number}: backtick is not allowed (use *italic*)")
|
||||
if _NON_DASH_ITEM.match(line):
|
||||
errors.append(f"line {number}: list items use '-' (MD004)")
|
||||
if "\n\n\n" in document:
|
||||
errors.append("multiple consecutive blank lines (MD012)")
|
||||
return errors
|
||||
@@ -7,6 +7,7 @@ from datetime import date
|
||||
|
||||
from .changelog_lint import (
|
||||
ChangelogLintError,
|
||||
lint_changelog_context,
|
||||
lint_changelog_entry,
|
||||
transform_changelog_message,
|
||||
)
|
||||
@@ -86,9 +87,22 @@ def update_changelog(
|
||||
except EOFError:
|
||||
print("[INFO] Re-opening the editor so you can fix the entry...")
|
||||
|
||||
changelog = ""
|
||||
if os.path.exists(changelog_path):
|
||||
try:
|
||||
with open(changelog_path, encoding="utf-8") as f:
|
||||
changelog = f.read()
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
print(f"[WARN] Could not read existing CHANGELOG.md: {exc}")
|
||||
|
||||
def _findings_for(entry: str) -> list[str]:
|
||||
return lint_changelog_entry(changelog_path, entry) or lint_changelog_context(
|
||||
changelog_path, changelog, _insert_after_h1(changelog, entry)
|
||||
)
|
||||
|
||||
if message is not None:
|
||||
body, entry = _entry_for(message)
|
||||
findings = lint_changelog_entry(changelog_path, entry)
|
||||
findings = _findings_for(entry)
|
||||
if findings:
|
||||
_print_findings(findings)
|
||||
raise ChangelogLintError(
|
||||
@@ -106,7 +120,7 @@ def update_changelog(
|
||||
)
|
||||
raw = _open_editor_for_changelog(attempt, rejected)
|
||||
body, entry = _entry_for(raw or f"Release {new_version}")
|
||||
findings = lint_changelog_entry(changelog_path, entry)
|
||||
findings = _findings_for(entry)
|
||||
if not findings:
|
||||
break
|
||||
_print_findings(findings)
|
||||
@@ -114,14 +128,6 @@ def update_changelog(
|
||||
rejected = findings
|
||||
_wait_for_reader()
|
||||
|
||||
changelog = ""
|
||||
if os.path.exists(changelog_path):
|
||||
try:
|
||||
with open(changelog_path, encoding="utf-8") as f:
|
||||
changelog = f.read()
|
||||
except (OSError, UnicodeDecodeError) as exc:
|
||||
print(f"[WARN] Could not read existing CHANGELOG.md: {exc}")
|
||||
|
||||
new_changelog = _insert_after_h1(changelog, entry)
|
||||
|
||||
print("\n================ CHANGELOG ENTRY ================")
|
||||
|
||||
Reference in new issue
Block a user