mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
3529 invalid comments in 528 files, grandfathered in while the linter only saw the working-tree diff. Removal ran through the linter's own collectors, so detection and deletion cannot drift: a comment whose opener line is blank before it and whose closer line is blank after it loses all its lines, anything else loses only the comment region, and a line left holding a bare delimiter goes with it. Five files keep their comments behind a nocheck marker instead. Four are upstream configs whose inline docs are upstream's own (mautrix, systemd journald, nextcloud-oidc-login, MediaWiki). In littlejs the commented-out entries ARE the deactivation mechanism for an example tile, so deleting them would delete configuration. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
186 lines
5.8 KiB
Python
186 lines
5.8 KiB
Python
"""Lint: enabled + shared services declare a ``bond`` importance factor.
|
|
|
|
``bond`` is a number in the closed range ``0..1`` expressing how strongly a
|
|
service is tied to the overall integration:
|
|
|
|
- ``bond: 1`` — fully bonded, always required.
|
|
- ``bond: 0`` — unbonded, never required (safe to omit).
|
|
- floats in between rank partial / optional coupling strength.
|
|
|
|
This is groundwork for tooling that later scores how important each service is
|
|
for the whole integration and decides which services may be dropped. (Named
|
|
``bond`` rather than ``bound`` to avoid confusion with the resource *bounds* /
|
|
limits enforced by ``test_compose_resource_limits``.)
|
|
|
|
Enforced rule: any service entry that is **both enabled and shared** MUST
|
|
declare a valid ``bond``. ``enabled`` / ``shared`` count as set when present
|
|
and truthy — either literal ``true`` or a non-false template expression (e.g.
|
|
``"{{ 'web-app-keycloak' in group_names }}"``).
|
|
|
|
Missing / invalid values emit a ``::warning`` annotation each so CI annotates
|
|
the source line and **fail the test** so the regression blocks the merge.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
import unittest
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import yaml
|
|
|
|
from utils.annotations.message import in_github_actions, warning
|
|
from utils.cache.files import read_text
|
|
from utils.cache.yaml import load_yaml_any
|
|
from utils.roles.mapping import ROLE_FILE_META_SERVICES
|
|
|
|
from . import PROJECT_ROOT
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class BondFinding:
|
|
role: str
|
|
service: str
|
|
problem: str
|
|
config_path: Path
|
|
line: int
|
|
|
|
|
|
def _load_yaml(path: Path) -> dict:
|
|
try:
|
|
data = load_yaml_any(str(path), default_if_missing={})
|
|
except yaml.YAMLError:
|
|
return {}
|
|
return data if isinstance(data, dict) else {}
|
|
|
|
|
|
def _is_truthy(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value
|
|
if value is None:
|
|
return False
|
|
return str(value).strip().lower() not in ("", "false", "0", "no", "off")
|
|
|
|
|
|
def _valid_bond(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return False
|
|
if isinstance(value, (int, float)):
|
|
return 0.0 <= float(value) <= 1.0
|
|
return False
|
|
|
|
|
|
def _find_service_line(config_path: Path, service_name: str) -> int:
|
|
"""1-based line of ``<service_name>:`` at the root of meta/services.yml.
|
|
Falls back to 1 when unparsable so the annotation still points at the file.
|
|
"""
|
|
pattern = re.compile(rf"^{re.escape(service_name)}\s*:\s*$")
|
|
try:
|
|
for i, raw in enumerate(read_text(str(config_path)).splitlines(), start=1):
|
|
if pattern.match(raw):
|
|
return i
|
|
except OSError:
|
|
return 1
|
|
return 1
|
|
|
|
|
|
def _collect_findings(root: Path) -> list[BondFinding]:
|
|
findings: list[BondFinding] = []
|
|
roles_dir = root / "roles"
|
|
for role_dir in sorted(roles_dir.iterdir()):
|
|
if not role_dir.is_dir():
|
|
continue
|
|
config_path = role_dir / ROLE_FILE_META_SERVICES
|
|
if not config_path.is_file():
|
|
continue
|
|
|
|
services = _load_yaml(config_path)
|
|
if not isinstance(services, dict):
|
|
continue
|
|
|
|
for service_key, raw_conf in services.items():
|
|
if not isinstance(raw_conf, dict):
|
|
continue
|
|
if not (
|
|
_is_truthy(raw_conf.get("enabled"))
|
|
and _is_truthy(raw_conf.get("shared"))
|
|
):
|
|
continue
|
|
|
|
line = _find_service_line(config_path, service_key)
|
|
if "bond" not in raw_conf:
|
|
findings.append(
|
|
BondFinding(
|
|
role_dir.name,
|
|
service_key,
|
|
"bond is not set",
|
|
config_path,
|
|
line,
|
|
)
|
|
)
|
|
elif not _valid_bond(raw_conf["bond"]):
|
|
findings.append(
|
|
BondFinding(
|
|
role_dir.name,
|
|
service_key,
|
|
f"bond={raw_conf['bond']!r} is not a number in 0..1",
|
|
config_path,
|
|
line,
|
|
)
|
|
)
|
|
|
|
findings.sort(key=lambda f: (f.role, f.service))
|
|
return findings
|
|
|
|
|
|
def _emit_warning(finding: BondFinding, root: Path) -> None:
|
|
rel = finding.config_path.relative_to(root).as_posix()
|
|
warning(
|
|
f"{finding.role}: services.{finding.service}.{finding.problem}",
|
|
title="Missing service bond",
|
|
file=rel,
|
|
line=finding.line,
|
|
)
|
|
|
|
|
|
def _print_summary(findings: list[BondFinding], root: Path) -> None:
|
|
if not findings:
|
|
return
|
|
print()
|
|
print(f"[WARNING] enabled+shared services without a valid bond ({len(findings)}):")
|
|
for f in findings:
|
|
rel = f.config_path.relative_to(root).as_posix()
|
|
print(f"- {rel}:{f.line} - services.{f.service}: {f.problem} ({f.role})")
|
|
|
|
|
|
class TestServiceBond(unittest.TestCase):
|
|
def test_enabled_shared_services_declare_bond(self) -> None:
|
|
"""Fail when an enabled + shared service is missing a valid ``bond``."""
|
|
root = PROJECT_ROOT
|
|
findings = _collect_findings(root)
|
|
|
|
for finding in findings:
|
|
_emit_warning(finding, root)
|
|
|
|
if not in_github_actions():
|
|
_print_summary(findings, root)
|
|
|
|
if findings:
|
|
lines = [
|
|
f"{f.config_path.relative_to(root).as_posix()}:{f.line}: "
|
|
f"services.{f.service}: {f.problem} ({f.role})"
|
|
for f in findings
|
|
]
|
|
self.fail(
|
|
f"enabled+shared services must declare a `bond` (number in 0..1) "
|
|
f"on {len(findings)} entries:\n" + "\n".join(lines)
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|