feat(1d): G0 — generic install + deploy-once orchestrator (DG1 green on hedgedoc)
- harness/generic.py: recipe-agnostic assert_serving (converged + real HTTP, 404-excluded + not Traefik 404 body + CA-verified trusted wildcard cert), op helpers, backup_capable detect - harness/discovery.py: per-op overlay resolution (repo-local > cc-ci > generic), custom + hook - tests/_generic/: assertion-only tiers (install/upgrade/backup/restore) on the shared deployment - run_recipe_ci.py: deploy-ONCE orchestrator, per-op summary, deploy-count guard (DG4.1) - conftest live_app fixture; lifecycle deploy-count + install-steps hook + pin DOMAIN to run domain DG1 cold-verified green on hedgedoc (pure generic, deploy-count=1, clean teardown). G0 CLAIMED. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
71
runner/harness/discovery.py
Normal file
71
runner/harness/discovery.py
Normal file
@ -0,0 +1,71 @@
|
||||
"""Overlay / custom-test / install-steps discovery + precedence (Phase 1d, plan §2.5, DG4/DG5).
|
||||
|
||||
The generic is the default for each lifecycle op; a recipe's `test_<op>.py` OVERRIDES it. Sources,
|
||||
in precedence order (machine-docs/DECISIONS.md):
|
||||
|
||||
lifecycle op (install/upgrade/backup/restore) — exactly ONE assertion file runs:
|
||||
repo-local tests/test_<op>.py (upstream-authoritative, wins same-name collisions)
|
||||
> cc-ci tests/<recipe>/test_<op>.py
|
||||
> generic tests/_generic/test_<op>.py <- always present; the floor
|
||||
|
||||
custom (non-lifecycle) test_*.py — ALL run, additively, from BOTH locations (opt-in).
|
||||
|
||||
install-steps hook — install_steps.sh: repo-local > cc-ci, or none.
|
||||
|
||||
Repo-local = the recipe repo's own tests/ dir, snapshotted after fetch (it survives abra
|
||||
re-checking-out the recipe to a version tag — see the run orchestrator).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import glob
|
||||
import os
|
||||
|
||||
LIFECYCLE_OPS = ("install", "upgrade", "backup", "restore")
|
||||
|
||||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
GENERIC_DIR = os.path.join(ROOT, "tests", "_generic")
|
||||
|
||||
|
||||
def cc_ci_dir(recipe: str) -> str:
|
||||
return os.path.join(ROOT, "tests", recipe)
|
||||
|
||||
|
||||
def resolve_op(recipe: str, op: str, repo_local_dir: str | None) -> tuple[str, str]:
|
||||
"""Return (source, path) for the single assertion file to run for `op`:
|
||||
source in {"repo-local","cc-ci","generic"}. The generic file is the floor and always exists."""
|
||||
fname = f"test_{op}.py"
|
||||
if repo_local_dir:
|
||||
p = os.path.join(repo_local_dir, fname)
|
||||
if os.path.isfile(p):
|
||||
return ("repo-local", p)
|
||||
p = os.path.join(cc_ci_dir(recipe), fname)
|
||||
if os.path.isfile(p):
|
||||
return ("cc-ci", p)
|
||||
return ("generic", os.path.join(GENERIC_DIR, fname))
|
||||
|
||||
|
||||
def custom_tests(recipe: str, repo_local_dir: str | None) -> list[tuple[str, str]]:
|
||||
"""All non-lifecycle test_*.py from cc-ci's tests/<recipe>/ and the recipe's repo-local tests/.
|
||||
These have no generic equivalent and run only when present (opt-in), additively from both."""
|
||||
lifecycle_names = {f"test_{op}.py" for op in LIFECYCLE_OPS}
|
||||
found: list[tuple[str, str]] = []
|
||||
for source, d in (("cc-ci", cc_ci_dir(recipe)), ("repo-local", repo_local_dir)):
|
||||
if not d or not os.path.isdir(d):
|
||||
continue
|
||||
for p in sorted(glob.glob(os.path.join(d, "test_*.py"))):
|
||||
if os.path.basename(p) not in lifecycle_names:
|
||||
found.append((source, p))
|
||||
return found
|
||||
|
||||
|
||||
def install_steps(recipe: str, repo_local_dir: str | None) -> tuple[str, str] | None:
|
||||
"""The custom install-steps hook (install_steps.sh) for a recipe, or None. repo-local > cc-ci."""
|
||||
if repo_local_dir:
|
||||
p = os.path.join(repo_local_dir, "install_steps.sh")
|
||||
if os.path.isfile(p):
|
||||
return ("repo-local", p)
|
||||
p = os.path.join(cc_ci_dir(recipe), "install_steps.sh")
|
||||
if os.path.isfile(p):
|
||||
return ("cc-ci", p)
|
||||
return None
|
||||
Reference in New Issue
Block a user