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:
@ -89,17 +89,63 @@ def _recipe_extra_env(recipe: str, domain: str) -> dict[str, str]:
|
||||
return {str(k): str(v) for k, v in (ee or {}).items()}
|
||||
|
||||
|
||||
def deploy_app(recipe: str, domain: str, version: str | None = None, secrets: bool = True) -> None:
|
||||
def _record_deploy() -> None:
|
||||
"""Increment the per-run deploy counter (DG4.1: one deploy per run). No-op unless the
|
||||
orchestrator set CCCI_DEPLOY_COUNT_FILE — so it never affects standalone/manual use."""
|
||||
path = os.environ.get("CCCI_DEPLOY_COUNT_FILE")
|
||||
if not path:
|
||||
return
|
||||
n = 0
|
||||
with contextlib.suppress(OSError, ValueError), open(path) as f:
|
||||
n = int(f.read().strip() or "0")
|
||||
with contextlib.suppress(OSError), open(path, "w") as f:
|
||||
f.write(str(n + 1))
|
||||
|
||||
|
||||
def _run_install_steps(hook: tuple[str, str], recipe: str, domain: str) -> None:
|
||||
"""Run a recipe's custom install-steps hook (install_steps.sh) during the install tier — after
|
||||
`abra app new` + env defaults + secret generate, before deploy (Phase 1d DG5). The hook gets the
|
||||
app .env path + domain so it can insert secrets / set env / seed before the app comes up."""
|
||||
source, path = hook
|
||||
env_path = os.path.expanduser(f"~/.abra/servers/default/{domain}.env")
|
||||
print(f" install-steps hook ({source}): {path}", flush=True)
|
||||
subprocess.run(
|
||||
["bash", path],
|
||||
check=True,
|
||||
env=dict(
|
||||
os.environ,
|
||||
CCCI_APP_DOMAIN=domain,
|
||||
CCCI_RECIPE=recipe,
|
||||
CCCI_APP_ENV=env_path,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def deploy_app(
|
||||
recipe: str,
|
||||
domain: str,
|
||||
version: str | None = None,
|
||||
secrets: bool = True,
|
||||
install_steps_hook: tuple[str, str] | None = None,
|
||||
) -> None:
|
||||
"""Create + configure + deploy an app. Forces LETS_ENCRYPT_ENV='' so traefik serves the
|
||||
wildcard cert via the file provider and NEVER attempts ACME (adversary finding A1). Applies any
|
||||
per-recipe EXTRA_ENV (recipe_meta.py) before deploy."""
|
||||
per-recipe EXTRA_ENV (recipe_meta.py) and the custom install-steps hook (Phase 1d) before deploy."""
|
||||
_record_deploy()
|
||||
abra.app_config_remove(domain) # clear any stale .env from a prior crashed run
|
||||
abra.app_new(recipe, domain, version=version, secrets=secrets)
|
||||
# Pin DOMAIN to the run domain explicitly. `abra app new -D` fills it for recipes whose
|
||||
# .env.sample uses a literal placeholder, but NOT for ones using a `{{ .Domain }}` Go-template
|
||||
# (this abra version leaves it unexpanded → deploy fails "can't evaluate field Domain"). Setting
|
||||
# it ourselves is recipe-agnostic and canonical (the run domain IS the app's domain).
|
||||
abra.env_set(domain, "DOMAIN", domain)
|
||||
abra.env_set(domain, "LETS_ENCRYPT_ENV", "")
|
||||
for k, v in _recipe_extra_env(recipe, domain).items():
|
||||
abra.env_set(domain, k, v)
|
||||
if secrets:
|
||||
abra.secret_generate(domain)
|
||||
if install_steps_hook:
|
||||
_run_install_steps(install_steps_hook, recipe, domain)
|
||||
abra.deploy(domain)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user