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()