diff --git a/core/regression_verify.py b/core/regression_verify.py index 4092023..594d7d5 100755 --- a/core/regression_verify.py +++ b/core/regression_verify.py @@ -146,14 +146,18 @@ def _check_environment_schema_validation() -> Tuple[Status, str]: ]) -def _check_resolver_static_assets() -> Tuple[Status, str]: - """CAP-003: contract_resolver resolves static-assets to a Target Stack.""" +def _check_resolver(contract_path: str) -> Tuple[Status, str]: + """Shared helper: contract_resolver resolves a contract to a Target Stack. + + Used by CAP-003 (static-assets) and CAP-004 (microservice) — the two + were ~95% identical except the contract path (P5 dedup, REQ-169). + """ with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t: out = t.name try: return _check_subprocess([ "python3", "core/contract_resolver.py", - "contracts/static-assets.yml", out, + contract_path, out, ]) finally: try: @@ -162,20 +166,14 @@ def _check_resolver_static_assets() -> Tuple[Status, str]: pass +def _check_resolver_static_assets() -> Tuple[Status, str]: + """CAP-003: contract_resolver resolves static-assets to a Target Stack.""" + return _check_resolver("contracts/static-assets.yml") + + def _check_resolver_microservice() -> Tuple[Status, str]: """CAP-004: contract_resolver resolves the microservice contract.""" - with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t: - out = t.name - try: - return _check_subprocess([ - "python3", "core/contract_resolver.py", - "contracts/microservice.yml", out, - ]) - finally: - try: - os.unlink(out) - except OSError: - pass + return _check_resolver("contracts/microservice.yml") def _check_adapter_emits_terraform() -> Tuple[Status, str]: @@ -325,21 +323,24 @@ def _load_aws_env() -> Dict[str, str]: return env -def _check_live_terraform_plan_microservice() -> Tuple[Status, str]: - """CAP-013: terraform init+validate+plan against live AWS for the - microservice stack (D-093 live-AWS tier of the headline E2E). +def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status, str]: + """Shared helper: terraform init+validate+plan against live AWS for a + contract (D-093 live-AWS tier of the headline E2E). - Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in .env.secrets; - NOVA_* only — the ACDL_* fallback was removed in v1.15 P5, REQ-164). - Runs in a temp dir; does NOT apply (plan only).""" + Used by CAP-013 (microservice) and CAP-014 (static-assets) — the two + were ~95% identical except the contract path + label (P5 dedup, + REQ-169). Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in + .env.secrets; NOVA_* only — the ACDL_* fallback was removed in v1.15 + P5, REQ-164). Runs in a temp dir; does NOT apply (plan only). + """ import tempfile, os - work = tempfile.mkdtemp(prefix="nova_regr_live_") + work = tempfile.mkdtemp(prefix=f"nova_regr_live_{label}_") stack_path = os.path.join(work, "stack.json") tf_dir = os.path.join(work, "tf") os.makedirs(tf_dir, exist_ok=True) rc, out, err = _run_subprocess([ "python3", "core/contract_resolver.py", - "contracts/microservice.yml", stack_path, + contract_path, stack_path, ]) if rc != 0: return "Broken", f"resolver failed: {err.strip()[-200:]}" @@ -366,47 +367,19 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]: ) if rc != 0: return "Decayed", f"terraform plan failed: {err.strip()[-200:]}" - return "Verified", "terraform init+validate+plan OK (live AWS, microservice)" + return "Verified", f"terraform init+validate+plan OK (live AWS, {label})" + + +def _check_live_terraform_plan_microservice() -> Tuple[Status, str]: + """CAP-013: terraform init+validate+plan against live AWS for the + microservice stack (D-093 live-AWS tier of the headline E2E).""" + return _check_live_terraform_plan("contracts/microservice.yml", "microservice") def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]: """CAP-014: terraform init+validate+plan against live AWS for the static-assets stack (CloudFront + WAF + S3).""" - import tempfile, os - work = tempfile.mkdtemp(prefix="nova_regr_live_sa_") - stack_path = os.path.join(work, "stack.json") - tf_dir = os.path.join(work, "tf") - os.makedirs(tf_dir, exist_ok=True) - rc, out, err = _run_subprocess([ - "python3", "core/contract_resolver.py", - "contracts/static-assets.yml", stack_path, - ]) - if rc != 0: - return "Broken", f"resolver failed: {err.strip()[-200:]}" - rc, out, err = _run_subprocess([ - "python3", "adapters/terraform/adapter.py", stack_path, tf_dir, - ]) - if rc != 0: - return "Broken", f"adapter failed: {err.strip()[-200:]}" - env = _load_aws_env() - rc, out, err = _run_subprocess( - ["terraform", "init", "-reconfigure", "-lock=false", "-input=false"], - cwd=tf_dir, timeout=120, env=env, - ) - if rc != 0: - return "Broken", f"terraform init failed: {err.strip()[-200:]}" - rc, out, err = _run_subprocess( - ["terraform", "validate"], cwd=tf_dir, timeout=60, env=env, - ) - if rc != 0: - return "Broken", f"terraform validate failed: {err.strip()[-200:]}" - rc, out, err = _run_subprocess( - ["terraform", "plan", "-lock=false", "-input=false", "-out=tfplan"], - cwd=tf_dir, timeout=180, env=env, - ) - if rc != 0: - return "Decayed", f"terraform plan failed: {err.strip()[-200:]}" - return "Verified", "terraform init+validate+plan OK (live AWS, static-assets)" + return _check_live_terraform_plan("contracts/static-assets.yml", "static-assets") def _check_dynamodb_outbox_table() -> Tuple[Status, str]: @@ -474,16 +447,30 @@ def _check_lifecycle_module_terraform(module: str) -> Tuple[Status, str]: ["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30) if rc != 0: return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}" + status, detail = _assert_contracts_resolve(ROOT / "modules" / "l1" / module, "l1") + if status != "Verified": + return status, detail + return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve" + + +def _assert_contracts_resolve(module_dir: Path, level: str) -> Tuple[Status, str]: + """Shared helper: assert an L1/L2 module's example contracts resolve. + + Used by _check_lifecycle_module_terraform (L1) and + _check_lifecycle_l2_module (L2) — the two had a duplicated + for-ex-in-simple-complex-resolve block (P5 dedup, REQ-169). + ``level`` is "l1" or "l2" (selects the examples dir parent). + """ for ex in ["simple", "complex"]: - contract = ROOT / "modules" / "l1" / module / "examples" / f"{ex}.yml" + contract = module_dir / "examples" / f"{ex}.yml" if not contract.is_file(): - return "Broken", f"modules/l1/{module}/examples/{ex}.yml missing" + return "Broken", f"{module_dir.relative_to(ROOT)}/examples/{ex}.yml missing" rc, out, err = _run_subprocess([ "python3", "core/contract_resolver.py", str(contract), "/dev/null", ], timeout=30) if rc != 0: return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}" - return "Verified", f"terraform files present + fmt -check passes + simple/complex contracts resolve" + return "Verified", "" def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]: @@ -492,16 +479,11 @@ def _check_lifecycle_l2_module(module: str) -> Tuple[Status, str]: This is an offline proxy, not live pipeline evidence; the live apply/modify/destroy is verified by the modules-lifecycle workflow run, not by this gate.""" - for ex in ["simple", "complex"]: - contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml" - if not contract.is_file(): - return "Broken", f"modules/l2/{module}/examples/{ex}.yml missing" - rc, out, err = _run_subprocess([ - "python3", "core/contract_resolver.py", str(contract), "/dev/null", - ], timeout=30) - if rc != 0: - return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}" - return "Verified", f"L2 composition resolves (simple + complex contracts; offline proxy)" + module_dir = ROOT / "modules" / "l2" / module + status, detail = _assert_contracts_resolve(module_dir, "l2") + if status != "Verified": + return status, detail + return "Verified", "L2 composition resolves (simple + complex contracts; offline proxy)" def _check_cap_017_dynamodb() -> Tuple[Status, str]: