mirror of
https://github.com/Cacti/cacti.git
synced 2026-10-30 00:26:50 +09:00
* chore: resolve concurrent CHANGELOG appends with a union merge Every PR lands on the same two anchors, so merging one reliably conflicts the others on CHANGELOG alone. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> (cherry picked from commit6bc8e63ecb) * feat(changelog): take entries as fragment files instead of CHANGELOG edits Every pull request edited one of two lines, so merging one conflicted the rest. A file per change gives concurrent PRs nothing to collide over. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> (cherry picked from commita8a0bfa29a) * chore(security): drop line numbers from the tracked sink baselines Both verifiers already strip the line before comparing, so the tracked numbers carried no signal and rewrote 212 lines of baseline on any edit that shifted code, conflicting every open pull request. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> (cherry picked from commitccfd3d374b) * fix(ci): rebase stale pull request branches instead of merging them A merge update adds a commit per run, so branches accumulate snapshots of old develop and re-conflict on every shared file they carry. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> (cherry picked from commite9e33c77cd) * test: match the escaping call rather than the exact source line The literal assertions here required the uncast form while PhpBinaryPathNullCoercionTest required the cast, so the two could not both pass and whichever landed second broke the other. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> (cherry picked from commit98cfc84756) * fix(changelog): handle security entries in fragments and validation Review caught the missing type. Security entries also take GHSA identifiers and open a release section ahead of issue and feature entries, none of which the first pass handled. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> * docs: require Composer-managed dependencies Prevent agents from committing third-party package trees and require composer.json plus composer.lock updates instead. Signed-off-by: Thomas Vincent <thomasvincent@gmail.com> --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
131 lines
4.0 KiB
Python
Executable File
131 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Validate the repository CHANGELOG grouping and entry format."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
RELEASE_HEADING = re.compile(r"^(?:\d+\.\d+\.\d+(?:-[\w.]+)?|\d+\.\d+\.x)$")
|
|
ENTRY = re.compile(r"^-(issue|feature|security)(?:#[0-9A-Za-z-]+)?:\s+\S.*$")
|
|
RANK_NAMES = {0: "security", 1: "issue", 2: "feature"}
|
|
NUMBERED_ENTRY = re.compile(r"^-(?:issue|feature)#\d+:|^-security#(?:\d+|GHSA-[0-9A-Za-z-]+):")
|
|
FRAGMENT = re.compile(r"^(\d+|GHSA-[0-9A-Za-z-]+)\.(issue|feature|security)$")
|
|
|
|
|
|
def check_fragments(directory: Path) -> list[str]:
|
|
"""Validates changelog.d fragments, which replace direct CHANGELOG edits."""
|
|
errors: list[str] = []
|
|
if not directory.is_dir():
|
|
return errors
|
|
|
|
for item in sorted(directory.iterdir()):
|
|
if item.name == "README.md" or item.name.startswith("."):
|
|
continue
|
|
if not item.is_file():
|
|
errors.append(f"{item}: changelog.d holds fragment files only")
|
|
continue
|
|
match = FRAGMENT.fullmatch(item.name)
|
|
if match is None:
|
|
errors.append(f"{item}: fragment must be named <number>.issue, <number>.feature, or <number|GHSA-id>.security")
|
|
continue
|
|
if match.group(2) != "security" and not match.group(1).isdigit():
|
|
errors.append(f"{item}: only a security fragment may use a GHSA identifier")
|
|
continue
|
|
|
|
text = item.read_text(encoding="utf-8")
|
|
body = text.strip()
|
|
if not body:
|
|
errors.append(f"{item}: fragment is empty")
|
|
elif len(body.splitlines()) > 1:
|
|
errors.append(f"{item}: fragment must be a single line")
|
|
elif body.startswith("-"):
|
|
errors.append(f"{item}: fragment holds the description only, without the -issue or -feature prefix")
|
|
|
|
return errors
|
|
|
|
|
|
def main() -> int:
|
|
path = Path("CHANGELOG")
|
|
if not path.is_file():
|
|
print("CHANGELOG is missing", file=sys.stderr)
|
|
return 1
|
|
|
|
lines = path.read_text(encoding="utf-8").splitlines()
|
|
try:
|
|
start = lines.index("1.3.0-dev")
|
|
except ValueError:
|
|
print("CHANGELOG is missing the 1.3.0-dev section", file=sys.stderr)
|
|
return 1
|
|
|
|
end = len(lines)
|
|
for index in range(start + 1, len(lines)):
|
|
if RELEASE_HEADING.fullmatch(lines[index]):
|
|
end = index
|
|
break
|
|
|
|
errors: list[str] = check_fragments(Path("changelog.d"))
|
|
highest_rank = 0
|
|
|
|
for index in range(start + 1, end):
|
|
line = lines[index].rstrip()
|
|
number = index + 1
|
|
if not line:
|
|
continue
|
|
if not line.startswith(("-issue", "-feature", "-security")):
|
|
if line.startswith("-"):
|
|
errors.append(f"line {number}: malformed CHANGELOG entry: {line}")
|
|
continue
|
|
if not ENTRY.fullmatch(line):
|
|
errors.append(f"line {number}: malformed CHANGELOG entry: {line}")
|
|
continue
|
|
|
|
if line.startswith("-security"):
|
|
kind, rank = "security", 0
|
|
elif line.startswith("-issue"):
|
|
kind, rank = "issue", 1
|
|
else:
|
|
kind, rank = "feature", 2
|
|
|
|
if rank < highest_rank:
|
|
errors.append(f"line {number}: {kind} entry appears after {RANK_NAMES[highest_rank]} entries")
|
|
else:
|
|
highest_rank = rank
|
|
|
|
if end < len(lines) and lines[end - 1].strip():
|
|
errors.append(f"line {end + 1}: release headings must be separated by a blank line")
|
|
|
|
# Existing releases contain legacy unnumbered entries. New entries must be
|
|
# traceable to an issue or pull request, so enforce that only on PR additions.
|
|
base_ref = os.environ.get("GITHUB_BASE_REF")
|
|
if base_ref:
|
|
result = subprocess.run(
|
|
["git", "diff", "--unified=0", f"origin/{base_ref}...HEAD", "--", "CHANGELOG"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=False,
|
|
)
|
|
if result.returncode == 0:
|
|
for added in result.stdout.splitlines():
|
|
if added.startswith(("+++", "---")):
|
|
continue
|
|
line = added[1:] if added.startswith("+") else ""
|
|
if line.startswith(("-issue", "-feature")) and not NUMBERED_ENTRY.match(line):
|
|
errors.append(f"new CHANGELOG entries must include an issue or pull-request number: {line}")
|
|
|
|
if errors:
|
|
print("CHANGELOG validation failed:", file=sys.stderr)
|
|
print("\n".join(f"- {error}" for error in errors), file=sys.stderr)
|
|
return 1
|
|
|
|
print("CHANGELOG format is valid")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|