CI-GATE.0-2: establish repository CI canary

This commit is contained in:
2026-08-04 19:01:59 +03:00
parent 64a5bdd04c
commit 49cc0e2fb8
9 changed files with 1637 additions and 15 deletions

View File

@@ -0,0 +1,151 @@
from __future__ import annotations
import re
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[3]
WORKFLOW_PATH = PROJECT_ROOT / ".gitea" / "workflows" / "ci-required.yml"
DEV_LOCK_PATH = PROJECT_ROOT / "app" / "requirements-dev.lock"
PRODUCTION_LOCK_PATH = PROJECT_ROOT / "app" / "requirements.lock"
CHECKOUT_REFERENCE = (
"https://github.com/actions/checkout"
"@34e114876b0b11c390a56381ad16ebd13914f8d5"
)
EXPECTED_JOB_IDS = (
"quality",
"offline-regression",
"loopback-integration",
)
def _read(path: Path) -> str:
return path.read_text(encoding="utf-8")
def _logical_requirements(lock_text: str) -> tuple[str, ...]:
records: list[str] = []
current: list[str] = []
for raw_line in lock_text.splitlines():
stripped = raw_line.strip()
if not stripped or stripped.startswith("#"):
continue
current.append(stripped.removesuffix("\\").rstrip())
if stripped.endswith("\\"):
continue
records.append(" ".join(current))
current.clear()
assert not current, "Последняя запись lock-файла не завершена"
return tuple(records)
def _locked_versions(lock_text: str) -> dict[str, str]:
versions: dict[str, str] = {}
for record in _logical_requirements(lock_text):
match = re.match(r"^([A-Za-z0-9_.-]+)==([^ ;]+)", record)
assert match is not None, f"Не распознана lock-запись: {record}"
normalized_name = match.group(1).lower().replace("_", "-")
version = match.group(2)
previous = versions.setdefault(normalized_name, version)
assert previous == version, (
f"Для {normalized_name} обнаружены версии {previous} и {version}"
)
return versions
def test_workflow_is_manual_canary_only() -> None:
workflow = _read(WORKFLOW_PATH)
trigger = workflow.split("\non:\n", maxsplit=1)[1].split(
"\nenv:\n",
maxsplit=1,
)[0]
assert trigger.strip() == "workflow_dispatch: {}"
assert "push:" not in trigger
assert "pull_request:" not in trigger
assert "schedule:" not in trigger
assert "workflow_run:" not in trigger
def test_workflow_has_stable_jobs_and_runner_label() -> None:
workflow = _read(WORKFLOW_PATH)
jobs = workflow.split("\njobs:\n", maxsplit=1)[1]
job_ids = tuple(
re.findall(
r"^ ([a-z][a-z0-9-]*):\n name: \1$",
jobs,
flags=re.MULTILINE,
)
)
assert job_ids == EXPECTED_JOB_IDS
assert jobs.count(" runs-on: dzentra-python312\n") == len(
EXPECTED_JOB_IDS
)
def test_workflow_uses_only_immutable_checkout_reference() -> None:
workflow = _read(WORKFLOW_PATH)
action_references = tuple(
re.findall(r"^\s+uses:\s+(\S+)$", workflow, flags=re.MULTILINE)
)
assert action_references == (CHECKOUT_REFERENCE,) * len(EXPECTED_JOB_IDS)
assert workflow.count(" persist-credentials: false\n") == len(
EXPECTED_JOB_IDS
)
def test_workflow_avoids_unsupported_gitea_125_controls() -> None:
workflow = _read(WORKFLOW_PATH)
for key in (
"concurrency",
"continue-on-error",
"environment",
"permissions",
"timeout-minutes",
):
assert re.search(rf"^\s*{re.escape(key)}:", workflow, re.MULTILINE) is None
def test_workflow_is_offline_and_secret_free_after_dependency_install() -> None:
workflow = _read(WORKFLOW_PATH)
assert workflow.count(" --require-hashes \\\n") == len(
EXPECTED_JOB_IDS
)
assert workflow.count(" -r app/requirements-dev.lock\n") == len(
EXPECTED_JOB_IDS
)
assert "secrets." not in workflow
assert "DZENTRA_RUN_LIVE_TESTS" not in workflow
assert "DZENTRA_RUN_POSTGRES_TESTS" not in workflow
assert "docker " not in workflow.lower()
def test_dev_lock_has_hash_for_every_requirement() -> None:
lock_text = _read(DEV_LOCK_PATH)
records = _logical_requirements(lock_text)
assert lock_text.startswith(
"# Сгенерированный hash-lock developer/CI-зависимостей; "
"вручную не редактировать.\n"
)
assert records
for record in records:
assert "==" in record
assert "--hash=sha256:" in record
assert "https://" not in record
assert "git+" not in record
def test_dev_lock_preserves_all_production_versions() -> None:
production_versions = _locked_versions(_read(PRODUCTION_LOCK_PATH))
dev_versions = _locked_versions(_read(DEV_LOCK_PATH))
assert production_versions.items() <= dev_versions.items()