mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-10 04:36:40 +00:00
Every version whose tree holds a locale/<code>/LC_MESSAGES/docs.po with at least one translation now gets one site per such language next to the English one, built with `-D language=<code> -D html_copy_source=0` after the English site is published. Sphinx reads one docs.po per language (locale_dirs, gettext_compact = "docs"); YAML pages stay untranslated through an extension that runs before the Locale transform. The deployed working tree is built as the unlisted version `deployed` from a snapshot baked into the image, because the git mirror only holds what is pushed to main. The server answers /<version>/<code>/ and /api/languages/<version>, a language without a site is a 404 rather than the English page, and a switcher on every page lists the built languages and keeps the page path. min_storage grows to 13GB for the translated sites. The 47 docs.po catalogs hold the 24869 documentation messages; German is partially machine-translated so far. The raw-docker and primary-domain lints skip .po files, which carry the Markdown's own commands and hosts as translation sources. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
275 lines
6.7 KiB
Python
275 lines
6.7 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import subprocess
|
|
import unittest
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from utils.annotations.suppress import is_suppressed_at, is_suppressed_in_head
|
|
from utils.cache.files import iter_project_files, read_text
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class Finding:
|
|
file: str
|
|
line_no: int
|
|
line: str
|
|
rule: str
|
|
suggestion: str
|
|
|
|
|
|
WHITELIST_SUFFIXES: tuple[str, ...] = (
|
|
".md",
|
|
".po",
|
|
".js",
|
|
".json",
|
|
".png",
|
|
".jpg",
|
|
".jpeg",
|
|
".gif",
|
|
".svg",
|
|
".ico",
|
|
".pdf",
|
|
".lock",
|
|
)
|
|
|
|
WHITELIST_FILENAMES: tuple[str, ...] = ("LICENSE",)
|
|
|
|
WHITELIST_PATH_FRAGMENTS: tuple[str, ...] = (
|
|
"/.git/",
|
|
"/.venv/",
|
|
"/.mypy_cache/",
|
|
"/.pytest_cache/",
|
|
"/node_modules/",
|
|
"/dist/",
|
|
"/build/",
|
|
"/.github/",
|
|
"/scripts/",
|
|
"/.tox/",
|
|
"/cli/",
|
|
"infinito_nexus.egg-info/",
|
|
)
|
|
|
|
|
|
_CMD_PREFIX = r"""
|
|
(?:
|
|
^\s* # line start
|
|
| [;&(]\s* # ; & (
|
|
| \|\s* # pipe
|
|
| &&\s* # &&
|
|
| \|\|\s* # ||
|
|
| \$(?:\(|\{)\s* # $( or ${
|
|
)
|
|
"""
|
|
|
|
_DOCKER_BIN = r"(?:sudo\s+)?(?:/usr/bin/|/bin/|/usr/local/bin/)?docker"
|
|
_DOCKER_COMPOSE_BIN = r"(?:sudo\s+)?(?:/usr/bin/|/bin/|/usr/local/bin/)?docker-compose"
|
|
|
|
_DOCKER_SUBCOMMANDS = (
|
|
"run",
|
|
"exec",
|
|
"ps",
|
|
"inspect",
|
|
"logs",
|
|
"pull",
|
|
"push",
|
|
"build",
|
|
"login",
|
|
"logout",
|
|
"tag",
|
|
"rm",
|
|
"rmi",
|
|
"start",
|
|
"stop",
|
|
"restart",
|
|
"kill",
|
|
"cp",
|
|
"info",
|
|
"version",
|
|
"events",
|
|
"stats",
|
|
"system",
|
|
"container",
|
|
"image",
|
|
"volume",
|
|
"network",
|
|
"manifest",
|
|
"buildx",
|
|
"builder",
|
|
"context",
|
|
)
|
|
|
|
_COMPOSE_VERBS = (
|
|
"up",
|
|
"down",
|
|
"pull",
|
|
"push",
|
|
"build",
|
|
"config",
|
|
"ps",
|
|
"logs",
|
|
"exec",
|
|
"run",
|
|
"start",
|
|
"stop",
|
|
"restart",
|
|
"rm",
|
|
"create",
|
|
"images",
|
|
"top",
|
|
)
|
|
|
|
RE_DOCKER_CMD = re.compile(
|
|
rf"{_CMD_PREFIX}{_DOCKER_BIN}\s+(?:{'|'.join(map(re.escape, _DOCKER_SUBCOMMANDS))})\b",
|
|
re.IGNORECASE | re.VERBOSE,
|
|
)
|
|
|
|
RE_DOCKER_COMPOSE_CMD = re.compile(
|
|
rf"{_CMD_PREFIX}{_DOCKER_BIN}\s+compose\s+(?:{'|'.join(map(re.escape, _COMPOSE_VERBS))})\b",
|
|
re.IGNORECASE | re.VERBOSE,
|
|
)
|
|
|
|
RE_DOCKER_DASH_COMPOSE_CMD = re.compile(
|
|
rf"{_CMD_PREFIX}{_DOCKER_COMPOSE_BIN}\s+(?:{'|'.join(map(re.escape, _COMPOSE_VERBS))})\b",
|
|
re.IGNORECASE | re.VERBOSE,
|
|
)
|
|
|
|
RULES: tuple[tuple[str, re.Pattern, str], ...] = (
|
|
(
|
|
"docker compose usage",
|
|
RE_DOCKER_COMPOSE_CMD,
|
|
"Use 'compose <verb> ...' instead of 'docker compose <verb> ...'.",
|
|
),
|
|
(
|
|
"docker-compose usage",
|
|
RE_DOCKER_DASH_COMPOSE_CMD,
|
|
"Use 'compose <verb> ...' instead of 'docker-compose <verb> ...'.",
|
|
),
|
|
(
|
|
"docker CLI usage",
|
|
RE_DOCKER_CMD,
|
|
"Use 'container <cmd> ...' instead of calling 'docker <cmd> ...' directly.",
|
|
),
|
|
)
|
|
|
|
|
|
def git_ls_files(root: Path) -> list[Path]:
|
|
try:
|
|
out = subprocess.check_output(
|
|
["git", "-C", str(root), "ls-files", "-z"],
|
|
stderr=subprocess.STDOUT,
|
|
)
|
|
rels = [p for p in out.decode("utf-8", errors="replace").split("\0") if p]
|
|
return [root / p for p in rels]
|
|
except Exception:
|
|
results: list[Path] = []
|
|
for path_str in iter_project_files():
|
|
try:
|
|
rel = Path(path_str).relative_to(root).as_posix()
|
|
except ValueError:
|
|
continue
|
|
if any(fragment in f"/{rel}/" for fragment in WHITELIST_PATH_FRAGMENTS):
|
|
continue
|
|
results.append(Path(path_str))
|
|
return results
|
|
|
|
|
|
def is_whitelisted(path: Path, root: Path) -> bool:
|
|
rel = path.relative_to(root).as_posix()
|
|
|
|
if path.name in WHITELIST_FILENAMES:
|
|
return True
|
|
if any(rel.endswith(suf) for suf in WHITELIST_SUFFIXES):
|
|
return True
|
|
|
|
rel_wrapped = f"/{rel}/"
|
|
return bool(any(fragment in rel_wrapped for fragment in WHITELIST_PATH_FRAGMENTS))
|
|
|
|
|
|
SUPPRESS_RULE: str = "raw-docker"
|
|
HEAD_SCAN_LINES: int = 30
|
|
|
|
|
|
def scan_file(path: Path, root: Path) -> list[Finding]:
|
|
findings: list[Finding] = []
|
|
rel = path.relative_to(root).as_posix()
|
|
|
|
try:
|
|
text = read_text(str(path))
|
|
except (OSError, UnicodeDecodeError):
|
|
return findings
|
|
|
|
lines = text.splitlines()
|
|
if is_suppressed_in_head(lines, SUPPRESS_RULE, scan_lines=HEAD_SCAN_LINES):
|
|
return findings
|
|
|
|
for idx, line in enumerate(lines, start=1):
|
|
for rule_name, pattern, suggestion in RULES:
|
|
if pattern.search(line):
|
|
if is_suppressed_at(lines, idx, SUPPRESS_RULE, mode="same-or-above"):
|
|
break
|
|
findings.append(
|
|
Finding(
|
|
file=rel,
|
|
line_no=idx,
|
|
line=line.rstrip("\n"),
|
|
rule=rule_name,
|
|
suggestion=suggestion,
|
|
)
|
|
)
|
|
break
|
|
|
|
return findings
|
|
|
|
|
|
def format_findings(findings: Sequence[Finding]) -> str:
|
|
lines: list[str] = []
|
|
lines.append("Forbidden raw Docker command invocations detected.")
|
|
lines.append("")
|
|
lines.append("Why this matters:")
|
|
lines.append(
|
|
"- We enforce a convenience wrapper ('container' / 'compose') so the container engine can be switched quickly"
|
|
)
|
|
lines.append(
|
|
" (e.g., Docker -> Podman) without refactoring command strings across the repo."
|
|
)
|
|
lines.append("")
|
|
lines.append("Fix rules:")
|
|
lines.append("- 'docker <cmd> ...' -> 'container <cmd> ...'")
|
|
lines.append("- 'docker compose <verb> ...' -> 'compose <verb> ...'")
|
|
lines.append("- 'docker-compose <verb> ...' -> 'compose <verb> ...'")
|
|
lines.append("")
|
|
lines.append("Findings:")
|
|
for f in findings:
|
|
lines.append(f"- {f.file}:{f.line_no}: {f.line.strip()}")
|
|
lines.append(f" -> {f.suggestion}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
class TestNoRawDockerCommands(unittest.TestCase):
|
|
def test_no_raw_docker_commands_in_repo(self) -> None:
|
|
root = PROJECT_ROOT
|
|
files = git_ls_files(root)
|
|
|
|
findings: list[Finding] = []
|
|
for p in files:
|
|
if not p.is_file():
|
|
continue
|
|
if is_whitelisted(p, root):
|
|
continue
|
|
findings.extend(scan_file(p, root))
|
|
|
|
if findings:
|
|
self.fail(format_findings(findings))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|