76 lines
2.3 KiB
Python
76 lines
2.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Reject profile export archives before publication.
|
|
|
|
``.gitignore`` and ``.dockerignore`` are useful first-line filters, but both
|
|
can be bypassed (for example with ``git add -f`` or a non-standard build
|
|
context). This check is the blocking, executable policy at the CI and image
|
|
publication boundaries. It intentionally checks the filesystem rather than
|
|
Git's index so a generated archive cannot enter a build after checkout.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
from pathlib import Path
|
|
|
|
_PROFILE_ARCHIVE_SUFFIXES = (".tar.gz", ".tgz")
|
|
|
|
|
|
def find_forbidden_profile_archives(root: Path) -> list[Path]:
|
|
"""Return profile archive paths anywhere in the checkout."""
|
|
root = root.resolve()
|
|
if not root.is_dir():
|
|
raise ValueError(f"repository root is not a directory: {root}")
|
|
|
|
offenders: list[Path] = []
|
|
for directory, dirnames, filenames in os.walk(root, followlinks=False):
|
|
dirnames[:] = [
|
|
name
|
|
for name in dirnames
|
|
if name not in {".git", ".venv", "venv", "node_modules", "__pycache__"}
|
|
]
|
|
for name in (*dirnames, *filenames):
|
|
if name.casefold().endswith(_PROFILE_ARCHIVE_SUFFIXES):
|
|
offenders.append((Path(directory) / name).relative_to(root))
|
|
|
|
return sorted(offenders, key=lambda path: path.as_posix().casefold())
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
parser = argparse.ArgumentParser(
|
|
description="Reject profile export archives in the checkout."
|
|
)
|
|
parser.add_argument(
|
|
"--root",
|
|
type=Path,
|
|
default=Path.cwd(),
|
|
help="repository root to inspect (default: current directory)",
|
|
)
|
|
args = parser.parse_args(argv)
|
|
|
|
try:
|
|
offenders = find_forbidden_profile_archives(args.root)
|
|
except ValueError as exc:
|
|
parser.error(str(exc))
|
|
|
|
if not offenders:
|
|
print("No profile export archives detected in the checkout.")
|
|
return 0
|
|
|
|
print(
|
|
"::error::profile export archives are forbidden "
|
|
"in source and Docker build contexts"
|
|
)
|
|
for path in offenders:
|
|
print(f" {path.as_posix()}")
|
|
print(
|
|
"Move the archive outside the checkout or pass an explicit external "
|
|
"output path to the profile export command."
|
|
)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|