"""Reject private release artifacts and internal references in the work tree."""
from __future__ import annotations
import argparse
import os
import re
import sys
from pathlib import Path
FORBIDDEN_DIRECTORY_NAMES = {".pytest_cache", "__pycache__", ".sisyphus"}
FORBIDDEN_FILE_SUFFIXES = {".pyc", ".pyo", ".swn", ".swo", ".swp"}
TEXT_SUFFIXES = {
".adoc",
".c",
".cc",
".cfg",
".cmake",
".conf",
".cpp",
".css",
".csv",
".cxx",
".go",
".h",
".hpp",
".html",
".hxx",
".in",
".ini",
".java",
".js",
".json",
".kt",
".md",
".properties",
".py",
".rs",
".rst",
".sh",
".toml",
".ts",
".tsx",
".txt",
".xml",
".yaml",
".yml",
}
TEXT_FILENAMES = {
".gitignore",
".mailmap",
"CODEOWNERS",
"LICENSE",
"NOTICE",
"VERSION",
}
def _absolute_path(*parts: str) -> str:
return "/" + "/".join(parts)
HOST_HOME_ROOT = _absolute_path("home")
GENERIC_PLACEHOLDER_ROOT = _absolute_path("path", "to")
PRIVATE_TEMP_PATH = _absolute_path("tmp", "opencode")
PRIVATE_GO_WORKSPACE_PATH = _absolute_path("go", "ibmw")
PRIVATE_PATH_CONTENT = (
("host home path", re.compile(re.escape(HOST_HOME_ROOT) + r"(?:/|\b)")),
(
"generic absolute path placeholder",
re.compile(re.escape(GENERIC_PLACEHOLDER_ROOT) + r"(?:/|\b)"),
),
("private temporary path", re.compile(re.escape(PRIVATE_TEMP_PATH) + r"(?:/|\b)")),
(
"private Go workspace path",
re.compile(re.escape(PRIVATE_GO_WORKSPACE_PATH) + r"(?:/|\b)", re.I),
),
)
FORBIDDEN_CONTENT = PRIVATE_PATH_CONTENT + (
("verification notes", re.compile(r"\bIBMW_VERIFICATION_NOTES\b", re.I)),
("project status", re.compile(r"\bPROJECT_STATUS\b", re.I)),
("internal refactor plan", re.compile(r"\bREFACTOR_PLAN_Y9\b", re.I)),
(
"private documentation data",
re.compile(r"(?<![A-Za-z0-9_.-])docs/data/", re.I),
),
("internal HR milestone", re.compile(r"(?<![A-Za-z0-9_])HR-[A-Za-z0-9][A-Za-z0-9._-]*", re.I)),
("internal QW milestone", re.compile(r"(?<![A-Za-z0-9_])QW-[A-Za-z0-9][A-Za-z0-9._-]*", re.I)),
("internal section milestone", re.compile(r"§Y[A-Za-z0-9._-]*", re.I)),
("internal V2 notes marker", re.compile(r"\bV2\s+NOTES\b", re.I)),
("internal commit marker", re.compile(r"\bTHIS\s+COMMIT\b", re.I)),
(
"legacy project owner order",
re.compile(re.escape("openEuler Embedded SIG & IB-Robot Group")),
),
(
"legacy project authors",
re.compile(re.escape("The Ibmw Authors")),
),
(
"legacy XML maintainer",
re.compile(re.escape("openEuler Embedded SIG & IB-Robot Group")),
),
)
NON_PATH_DEFINITION_SOURCES = {
Path("scripts/check_docs_governance.py"),
Path("scripts/check_release_hygiene.py"),
Path("scripts/tests/test_docs_governance.sh"),
Path("scripts/tests/test_release_hygiene.sh"),
}
def display(path: Path) -> str:
return path.as_posix() or "."
def is_text_candidate(path: Path) -> bool:
return path.name in TEXT_FILENAMES or path.suffix.lower() in TEXT_SUFFIXES
def check_tree(repo: Path) -> list[str]:
errors: list[str] = []
for current, directory_names, file_names in os.walk(repo, topdown=True):
current_path = Path(current)
relative_current = current_path.relative_to(repo)
kept_directories: list[str] = []
for name in directory_names:
if name == ".git":
continue
relative = relative_current / name
if name in FORBIDDEN_DIRECTORY_NAMES:
errors.append(f"forbidden cache/internal path: {display(relative)}")
continue
kept_directories.append(name)
directory_names[:] = kept_directories
for name in file_names:
relative = relative_current / name
path = current_path / name
if name in FORBIDDEN_DIRECTORY_NAMES:
errors.append(f"forbidden cache/internal path: {display(relative)}")
continue
if path.suffix.lower() in FORBIDDEN_FILE_SUFFIXES:
errors.append(f"forbidden release file: {display(relative)}")
continue
if not is_text_candidate(path):
continue
try:
lines = path.read_text(encoding="utf-8").splitlines()
except (OSError, UnicodeError) as exc:
errors.append(f"cannot read text candidate {display(relative)}: {exc}")
continue
patterns = (
PRIVATE_PATH_CONTENT
if relative in NON_PATH_DEFINITION_SOURCES
else FORBIDDEN_CONTENT
)
for line_number, line in enumerate(lines, 1):
for label, pattern in patterns:
if pattern.search(line):
errors.append(
f"forbidden release content ({label}): "
f"{display(relative)}:{line_number}"
)
return errors
def parser() -> argparse.ArgumentParser:
cli = argparse.ArgumentParser(description=__doc__)
cli.add_argument(
"--repo-root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="work-tree root (default: parent of scripts/)",
)
return cli
def main() -> int:
repo = parser().parse_args().repo_root.resolve()
if not repo.is_dir():
print(f"ERROR: not a directory: {repo}", file=sys.stderr)
return 2
errors = check_tree(repo)
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
print(f"release hygiene: FAIL ({len(errors)} issue(s))", file=sys.stderr)
return 1
print("release hygiene: PASS")
return 0
if __name__ == "__main__":
raise SystemExit(main())