Compare commits

...

6 Commits

Author SHA1 Message Date
Jon Chery 93ae9e4a39 verify(P7): contract-resolver-envloader-and-kind — 4-layer verify PASS + ship
VERIFY: structural — envloader dedup + kind field; behavioral — 49 tests + CI PASS; quality — fragile is_l2 heuristic replaced.

---ci---
project: acdl
phase: 7
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-171]
  partial: []
---/ci---
2026-08-01 12:38:56 +00:00
Jon Chery d3179fff37 verify(P6): run-platform-deadcode-and-hitl-fn — 4-layer verify PASS + ship
VERIFY: structural — HITL fn extracted + deadcode/config; behavioral — syntax clean + CI PASS; quality — ~14 lines saved.

---ci---
project: acdl
phase: 6
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-170]
  partial: []
---/ci---
2026-08-01 12:36:40 +00:00
Jon Chery c029b102a3 verify(P5): regression-verify-dedup — 4-layer verify PASS + ship
VERIFY: structural — 3 shared helpers extracted; behavioral — 611 tests + CI PASS; quality — behavior preserved, ~70 lines saved.

---ci---
project: acdl
phase: 5
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-169]
  partial: []
---/ci---
2026-08-01 12:31:08 +00:00
Jon Chery 2806c6c3ed verify(P4): migrate-ssm-except-narrowing — 4-layer verify PASS + ship
VERIFY: structural — narrowed excepts; behavioral — 48 tests + CI PASS; security — non-ParameterNotFound errors surface; quality — 2 new + 2 updated tests.

---ci---
project: acdl
phase: 4
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-168]
  partial: []
---/ci---
2026-08-01 12:24:30 +00:00
Jon Chery e048acd4dd verify(P3): dead-code-and-stale-prefix-cleanup — 4-layer verify PASS + ship
VERIFY: structural — dead export + stale comments + prefixes gone; behavioral — 616 tests + CI PASS; quality — env-override test updated.

---ci---
project: acdl
phase: 3
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-167]
  partial: []
---/ci---
2026-08-01 12:20:40 +00:00
Jon Chery 9421442afd verify(P2): user-facing ACDL→Nova sweep — 4-layer verify PASS + ship
VERIFY: structural — all user-facing strings Nova; behavioral — 79 tests
+ CI PASS; security — no creds; quality — new onboarding Nova-header test.
REQ-166 complete. Internal ship_phase.sh helper added.

---ci---
project: acdl
phase: 2
milestone: v1.16
status: complete
phase_role: execution
requirements:
  covered: [REQ-166]
  partial: []
---/ci---
2026-08-01 12:12:45 +00:00
29 changed files with 312 additions and 221 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# ACDL Adapters # Nova Adapters
## Overview ## Overview
+2 -2
View File
@@ -1,7 +1,7 @@
# Kyverno Adapter # Kyverno Adapter
The Kyverno adapter translates Kyverno `PolicyReport` results to the The Kyverno adapter translates Kyverno `PolicyReport` results to the
normalized ACDL normalized Nova
[`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema [`PolicyCheckResult`](../../schemas/policy_check_result.schema.json) schema
(engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern. (engine: `"kyverno"`), mirroring the Checkov/Wiz adapter pattern.
@@ -15,7 +15,7 @@ publishes results to `PolicyReport` resources.
## When to use it ## When to use it
Kyverno is the right engine **when the platform emits Kubernetes Kyverno is the right engine **when the platform emits Kubernetes
manifests** (a K8s-native stack). The ACDL platform today emits Terraform manifests** (a K8s-native stack). The Nova platform today emits Terraform
only (D-053), so this adapter is **ready but inactive**: it ships now so only (D-053), so this adapter is **ready but inactive**: it ships now so
the schema path, severity/result mapping and sample policies are in place the schema path, severity/result mapping and sample policies are in place
ahead of the GitOps reconciler that will emit K8s manifests (roadmap). ahead of the GitOps reconciler that will emit K8s manifests (roadmap).
+1 -1
View File
@@ -1,4 +1,4 @@
"""Kyverno adapter — translate Kyverno PolicyReport results to ACDL PolicyCheckResult records. """Kyverno adapter — translate Kyverno PolicyReport results to Nova PolicyCheckResult records.
Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests Kyverno is a Kubernetes-native policy engine. It evaluates K8s manifests
and produces PolicyReport resources. This adapter translates those results and produces PolicyReport resources. This adapter translates those results
+1 -1
View File
@@ -1,4 +1,4 @@
"""ACDL Terraform adapter — stateless assembler (v1.11 RESTART, P56a). """Nova Terraform adapter — stateless assembler (v1.11 RESTART, P56a).
A STATELESS ASSEMBLER. It owns no module content no resource shape, no A STATELESS ASSEMBLER. It owns no module content no resource shape, no
nested HCL blocks, no defaults, no type-specific logic. It reads the nested HCL blocks, no defaults, no type-specific logic. It reads the
+1 -1
View File
@@ -1,4 +1,4 @@
"""Wiz adapter — translate Wiz API results to ACDL PolicyCheckResult records. """Wiz adapter — translate Wiz API results to Nova PolicyCheckResult records.
Wiz is a SaaS security platform with a GraphQL API. This adapter Wiz is a SaaS security platform with a GraphQL API. This adapter
translates Wiz issue records to the normalized PolicyCheckResult schema translates Wiz issue records to the normalized PolicyCheckResult schema
+1 -1
View File
@@ -1,4 +1,4 @@
"""ACDL Confidence Signal (REQ-19). """Nova Confidence Signal (REQ-19).
The platform's certified answer to "is this safe to proceed?" (vision The platform's certified answer to "is this safe to proceed?" (vision
tenet: "Safety is Computed, Not Assumed"). Every delivery action produces tenet: "Safety is Computed, Not Assumed"). Every delivery action produces
+16 -26
View File
@@ -1,4 +1,4 @@
"""ACDL Contract Resolver — resolve a consumer contract to a Target Stack instance. """Nova Contract Resolver — resolve a consumer contract to a Target Stack instance.
The contract resolver is the bridge between the consumer's declared intent The contract resolver is the bridge between the consumer's declared intent
(a contract YAML) and the platform's executable representation (a Target (a contract YAML) and the platform's executable representation (a Target
@@ -50,22 +50,13 @@ from core import env
def _load_env(env_name, repo_root): def _load_env(env_name, repo_root):
"""Load the environment onboarding JSON for env_name. """Load the environment onboarding JSON for env_name.
Mirrors core.environment_check.load() but is self-contained so the P7 (REQ-171): delegates to core.environment_check.load() (dedup
resolver works both as a package import (`from core.contract_resolver the two were verbatim duplicates). The environment_check module is
import resolve`) and as a script (`python3 core/contract_resolver.py`). in the same core/ package, so the import works both as a package
Emits a stderr warning when account_id is the placeholder and env != dev. import and as a script (`python3 core/contract_resolver.py`).
""" """
env_file = os.path.join(repo_root, "core", "environments", f"{env_name}.json") from core import environment_check
if not os.path.isfile(env_file): return environment_check.load(env_name, root=repo_root)
raise FileNotFoundError(f"no environment file for '{env_name}' at {env_file}")
env = _load_json(env_file)
if env.get("account_id") == "000000000000" and env_name != "dev":
sys.stderr.write(
f"WARNING: environment '{env_name}' has the placeholder account_id "
f"000000000000 — replace it with the real {env_name} account id "
f"before deploying (onboarding scaffold).\n"
)
return env
def _load_json(path): def _load_json(path):
@@ -471,7 +462,7 @@ def resolve(contract_path, repo_root=None, environment_override=None):
Args: Args:
contract_path: Path to the contract YAML file. contract_path: Path to the contract YAML file.
repo_root: Root of the ACDL repo (defaults to two levels up from this file). repo_root: Root of the Nova repo (defaults to two levels up from this file).
environment_override: When set (dev/qa/prod/dr), overrides the environment_override: When set (dev/qa/prod/dr), overrides the
contract's 'environment' field BEFORE schema validation, so contract's 'environment' field BEFORE schema validation, so
interpolation context is consistent (D-088). Used by interpolation context is consistent (D-088). Used by
@@ -534,10 +525,14 @@ def resolve(contract_path, repo_root=None, environment_override=None):
f"module '{module_name}' version '{version}' not found in registry") f"module '{module_name}' version '{version}' not found in registry")
module_inputs = module_entry.get("inputs", {}) module_inputs = module_entry.get("inputs", {})
# Determine if L1 or L2 # Determine if L1 or L2 — prefer the registry `kind` field (P7,
# REQ-171); fall back to the path heuristic for entries that
# predate the kind field.
entry = registry[module_name][version] entry = registry[module_name][version]
interface_path = entry["interface"] interface_path = entry["interface"]
is_l2 = "l2" in interface_path or "composition" in interface_path is_l2 = entry.get("kind") == "l2" or (
"kind" not in entry and ("l2" in interface_path or "composition" in interface_path)
)
if is_l2: if is_l2:
fragment = _resolve_l2(module_name, version, module_inputs, fragment = _resolve_l2(module_name, version, module_inputs,
@@ -580,13 +575,8 @@ def resolve(contract_path, repo_root=None, environment_override=None):
merged_outputs.update(fragment.get("outputs", {})) merged_outputs.update(fragment.get("outputs", {}))
all_resources.extend(fragment["resources"]) all_resources.extend(fragment["resources"])
# Determine stack kind: L2 if any module is L2 or if multi-module # Determine stack kind: L2 if any module is L2 or if multi-module (P7)
if multi_module: kind = "l2" if (multi_module or any_l2) else "l1"
kind = "l2"
elif any_l2:
kind = "l2"
else:
kind = "l1"
stack_instance = { stack_instance = {
"version": "1.0.0", "version": "1.0.0",
+2 -2
View File
@@ -56,9 +56,9 @@ def load(env_name, root=None):
def _onboarding_message(env_name): def _onboarding_message(env_name):
return ( return (
"=== ACDL Environment Onboarding ===\n" "=== Nova Environment Onboarding ===\n"
f"No environment named '{env_name}' is bound to this repository.\n\n" f"No environment named '{env_name}' is bound to this repository.\n\n"
"ACDL environments are platform-managed. The platform provisions on\n" "Nova environments are platform-managed. The platform provisions on\n"
"your behalf:\n" "your behalf:\n"
" - an AWS account (or a scoped partition of one)\n" " - an AWS account (or a scoped partition of one)\n"
" - a network (VPC + subnets)\n" " - a network (VPC + subnets)\n"
+2 -2
View File
@@ -142,7 +142,7 @@ def _report_error(payload):
raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}") raise RuntimeError(f"failed to read GitHub token from Secrets Manager: {e}")
owner, repo = PLATFORM_REPO.split("/") owner, repo = PLATFORM_REPO.split("/")
title = f"[ACDL-ALERT] Deploy failure: {consumer_repo} / {contract_id}" title = f"[NOVA-ALERT] Deploy failure: {consumer_repo} / {contract_id}"
# Check for an existing open issue with the same title (idempotency) # Check for an existing open issue with the same title (idempotency)
# URL-encode the contract_id to prevent search-query injection (P1-1). # URL-encode the contract_id to prevent search-query injection (P1-1).
@@ -188,7 +188,7 @@ def _report_error(payload):
{stack_trace} {stack_trace}
``` ```
_This issue was auto-created by the ACDL platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._ _This issue was auto-created by the Nova platform Lambda (D-055). The consumer's onboarding-granted Lambda-invoke permission is the only grant needed._
""" """
if existing: if existing:
+6 -6
View File
@@ -13,7 +13,8 @@ evidence event) runs end-to-end against the local tier with no AWS:
Each adapter exposes the same interface as the live counterpart so the Each adapter exposes the same interface as the live counterpart so the
caller code path is unchanged; only the I/O target swaps. Selection is caller code path is unchanged; only the I/O target swaps. Selection is
gated on the NOVA_LOCAL_TIER env var (set by run_platform.sh --local). gated on the NOVA_LOCAL_TIER env var (set by run_platform.sh --local).
Dual-read via core/env.py: NOVA_* preferred, ACDL_* fallback until P5. Env vars read via core/env.py (NOVA_* only; the ACDL_* fallback was
removed in v1.15 P5, REQ-164).
""" """
from __future__ import annotations from __future__ import annotations
@@ -68,7 +69,7 @@ class FlatFileOutbox:
@classmethod @classmethod
def create(cls, dir: Optional[Path] = None) -> "FlatFileOutbox": def create(cls, dir: Optional[Path] = None) -> "FlatFileOutbox":
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_outbox_")) d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_outbox_"))
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
out = cls(dir=d) out = cls(dir=d)
# Re-read the chain tail if the file already exists. # Re-read the chain tail if the file already exists.
@@ -249,7 +250,7 @@ class LocalS3StateBackend:
@classmethod @classmethod
def create(cls, dir: Optional[Path] = None) -> "LocalS3StateBackend": def create(cls, dir: Optional[Path] = None) -> "LocalS3StateBackend":
d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="acdl_tfstate_")) d = Path(dir) if dir else Path(tempfile.mkdtemp(prefix="nova_tfstate_"))
d.mkdir(parents=True, exist_ok=True) d.mkdir(parents=True, exist_ok=True)
return cls(state_dir=d) return cls(state_dir=d)
@@ -499,9 +500,8 @@ def run_local_e2e(contract_path: str, repo_root: Optional[Path] = None) -> Dict[
if __name__ == "__main__": if __name__ == "__main__":
contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml" contract = sys.argv[1] if len(sys.argv) > 1 else "contracts/microservice.yml"
# Set both so the dual-read in is_local_tier() finds NOVA_* (preferred); # Set so is_local_tier() finds NOVA_LOCAL_TIER (NOVA_* only; the
# the ACDL_* alias stays for any unmigrated reader until P5. # ACDL_* alias was removed in v1.15 P5, REQ-164).
os.environ["NOVA_LOCAL_TIER"] = "1" os.environ["NOVA_LOCAL_TIER"] = "1"
# P5 (REQ-164): ACDL_LOCAL_TIER legacy alias removed (NOVA_* only)
result = run_local_e2e(contract) result = run_local_e2e(contract)
print(json.dumps(result, indent=2)) print(json.dumps(result, indent=2))
+14 -6
View File
@@ -17,11 +17,15 @@ existing /acdl/... parameters to /nova/... and deletes the old ones.)
import json import json
import os import os
import sys import sys
import urllib.error
import urllib.request
try: try:
import boto3 import boto3
from botocore.exceptions import ClientError
except ImportError: except ImportError:
boto3 = None boto3 = None
ClientError = Exception # type: ignore[assignment,misc]
# Repo root on sys.path so `from core import env` resolves to THIS package # Repo root on sys.path so `from core import env` resolves to THIS package
# when run as a script (avoids editable-installed third-party `core` shadow). # when run as a script (avoids editable-installed third-party `core` shadow).
@@ -109,10 +113,12 @@ def publish_to_ssm(outputs, environment, contract_id):
Overwrite=True, Overwrite=True,
) )
results[name] = param_name results[name] = param_name
except Exception as e: except (ClientError, OSError) as e:
# Don't fail the pipeline if one output fails to publish, but log it # P4 (REQ-168): narrow from bare `except Exception` to AWS +
# OS errors. Don't fail the pipeline if one output fails to
# publish, but log it with context.
import sys import sys
print(f"WARNING: SSM put_parameter failed for {name}: {e}", file=sys.stderr) print(f"WARNING: SSM put_parameter failed for {name}: {type(e).__name__}: {e}", file=sys.stderr)
results[name] = None results[name] = None
return results return results
@@ -171,7 +177,6 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
if not token or not repo or not pr_number: if not token or not repo or not pr_number:
return False # not in a PR context or no token return False # not in a PR context or no token
try: try:
import urllib.request
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments" url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
data = json.dumps({"body": comment_text}).encode() data = json.dumps({"body": comment_text}).encode()
req = urllib.request.Request(url, data=data, method="POST") req = urllib.request.Request(url, data=data, method="POST")
@@ -179,9 +184,12 @@ def post_github_comment(comment_text, token=None, repo=None, pr_number=None):
req.add_header("Accept", "application/vnd.github+json") req.add_header("Accept", "application/vnd.github+json")
urllib.request.urlopen(req, timeout=10) urllib.request.urlopen(req, timeout=10)
return True return True
except Exception as e: except (OSError, urllib.error.URLError, urllib.error.HTTPError) as e:
# P4 (REQ-168): narrow from bare `except Exception` to network +
# HTTP errors. Don't fail the pipeline if the PR comment can't be
# posted, but log it with context.
import sys import sys
print(f"WARNING: GitHub PR comment failed: {e}", file=sys.stderr) print(f"WARNING: GitHub PR comment failed: {type(e).__name__}: {e}", file=sys.stderr)
return False return False
+58 -76
View File
@@ -146,14 +146,18 @@ def _check_environment_schema_validation() -> Tuple[Status, str]:
]) ])
def _check_resolver_static_assets() -> Tuple[Status, str]: def _check_resolver(contract_path: str) -> Tuple[Status, str]:
"""CAP-003: contract_resolver resolves static-assets to a Target Stack.""" """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: with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t:
out = t.name out = t.name
try: try:
return _check_subprocess([ return _check_subprocess([
"python3", "core/contract_resolver.py", "python3", "core/contract_resolver.py",
"contracts/static-assets.yml", out, contract_path, out,
]) ])
finally: finally:
try: try:
@@ -162,25 +166,19 @@ def _check_resolver_static_assets() -> Tuple[Status, str]:
pass 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]: def _check_resolver_microservice() -> Tuple[Status, str]:
"""CAP-004: contract_resolver resolves the microservice contract.""" """CAP-004: contract_resolver resolves the microservice contract."""
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as t: return _check_resolver("contracts/microservice.yml")
out = t.name
try:
return _check_subprocess([
"python3", "core/contract_resolver.py",
"contracts/microservice.yml", out,
])
finally:
try:
os.unlink(out)
except OSError:
pass
def _check_adapter_emits_terraform() -> Tuple[Status, str]: def _check_adapter_emits_terraform() -> Tuple[Status, str]:
"""CAP-005: terraform adapter compiles a resolved stack to .tf files.""" """CAP-005: terraform adapter compiles a resolved stack to .tf files."""
work = tempfile.mkdtemp(prefix="acdl_regr_") work = tempfile.mkdtemp(prefix="nova_regr_")
stack_path = os.path.join(work, "stack.json") stack_path = os.path.join(work, "stack.json")
tf_dir = os.path.join(work, "tf") tf_dir = os.path.join(work, "tf")
os.makedirs(tf_dir, exist_ok=True) os.makedirs(tf_dir, exist_ok=True)
@@ -211,7 +209,7 @@ def _check_interpolation() -> Tuple[Status, str]:
"import sys; sys.path.insert(0,'.'); " "import sys; sys.path.insert(0,'.'); "
"from core.contract_resolver import _expand_vars; " "from core.contract_resolver import _expand_vars; "
"ctx={'env':{'environment':'qa','account_id':'123'},'contract':{'id':'assets'}}; " "ctx={'env':{'environment':'qa','account_id':'123'},'contract':{'id':'assets'}}; "
"assert _expand_vars('acdl-${env.environment}-${contract.id}', ctx)=='acdl-qa-assets'; " "assert _expand_vars('nova-${env.environment}-${contract.id}', ctx)=='nova-qa-assets'; "
"print('interpolation ok')", "print('interpolation ok')",
]) ])
@@ -231,7 +229,7 @@ def _check_confidence_signal() -> Tuple[Status, str]:
def _check_outbox_writer() -> Tuple[Status, str]: def _check_outbox_writer() -> Tuple[Status, str]:
"""CAP-008: outbox_writer writes a hash-chained event to a temp file.""" """CAP-008: outbox_writer writes a hash-chained event to a temp file."""
work = tempfile.mkdtemp(prefix="acdl_outbox_") work = tempfile.mkdtemp(prefix="nova_outbox_")
event_path = os.path.join(work, "event.json") event_path = os.path.join(work, "event.json")
event = { event = {
"contractId": "regression-test", "eventType": "CONFIDENCE_COMPUTED", "contractId": "regression-test", "eventType": "CONFIDENCE_COMPUTED",
@@ -315,7 +313,7 @@ def _load_aws_env() -> Dict[str, str]:
continue continue
if "=" in line: if "=" in line:
k, v = line.split("=", 1) k, v = line.split("=", 1)
# P5 (REQ-164): dual-read fallback removed — NOVA_* only. # NOVA_* only (ACDL_* fallback removed in v1.15 P5, REQ-164).
if k == "NOVA_AWS_ACCESS_KEY_ID": if k == "NOVA_AWS_ACCESS_KEY_ID":
env["AWS_ACCESS_KEY_ID"] = v env["AWS_ACCESS_KEY_ID"] = v
elif k == "NOVA_AWS_SECRET_ACCESS_KEY": elif k == "NOVA_AWS_SECRET_ACCESS_KEY":
@@ -325,21 +323,24 @@ def _load_aws_env() -> Dict[str, str]:
return env return env
def _check_live_terraform_plan_microservice() -> Tuple[Status, str]: def _check_live_terraform_plan(contract_path: str, label: str) -> Tuple[Status, str]:
"""CAP-013: terraform init+validate+plan against live AWS for the """Shared helper: terraform init+validate+plan against live AWS for a
microservice stack (D-093 live-AWS tier of the headline E2E). contract (D-093 live-AWS tier of the headline E2E).
Requires AWS credentials (NOVA_AWS_ACCESS_KEY_ID etc. in .env.secrets; Used by CAP-013 (microservice) and CAP-014 (static-assets) the two
dual-read NOVA_* first, ACDL_* fallback per G-106). were ~95% identical except the contract path + label (P5 dedup,
Runs in a temp dir; does NOT apply (plan only).""" 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 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") stack_path = os.path.join(work, "stack.json")
tf_dir = os.path.join(work, "tf") tf_dir = os.path.join(work, "tf")
os.makedirs(tf_dir, exist_ok=True) os.makedirs(tf_dir, exist_ok=True)
rc, out, err = _run_subprocess([ rc, out, err = _run_subprocess([
"python3", "core/contract_resolver.py", "python3", "core/contract_resolver.py",
"contracts/microservice.yml", stack_path, contract_path, stack_path,
]) ])
if rc != 0: if rc != 0:
return "Broken", f"resolver failed: {err.strip()[-200:]}" return "Broken", f"resolver failed: {err.strip()[-200:]}"
@@ -366,47 +367,19 @@ def _check_live_terraform_plan_microservice() -> Tuple[Status, str]:
) )
if rc != 0: if rc != 0:
return "Decayed", f"terraform plan failed: {err.strip()[-200:]}" 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]: def _check_live_terraform_plan_static_assets() -> Tuple[Status, str]:
"""CAP-014: terraform init+validate+plan against live AWS for the """CAP-014: terraform init+validate+plan against live AWS for the
static-assets stack (CloudFront + WAF + S3).""" static-assets stack (CloudFront + WAF + S3)."""
import tempfile, os return _check_live_terraform_plan("contracts/static-assets.yml", "static-assets")
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)"
def _check_dynamodb_outbox_table() -> Tuple[Status, str]: 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) ["terraform", "fmt", "-check", "-diff", str(tf_dir)], timeout=30)
if rc != 0: if rc != 0:
return "Broken", f"terraform fmt -check failed: {err.strip()[-200:]}" 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"]: 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(): 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([ rc, out, err = _run_subprocess([
"python3", "core/contract_resolver.py", str(contract), "/dev/null", "python3", "core/contract_resolver.py", str(contract), "/dev/null",
], timeout=30) ], timeout=30)
if rc != 0: if rc != 0:
return "Broken", f"{ex}.yml resolver failed: {err.strip()[-200:]}" 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]: 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 This is an offline proxy, not live pipeline evidence; the live
apply/modify/destroy is verified by the modules-lifecycle workflow apply/modify/destroy is verified by the modules-lifecycle workflow
run, not by this gate.""" run, not by this gate."""
for ex in ["simple", "complex"]: module_dir = ROOT / "modules" / "l2" / module
contract = ROOT / "modules" / "l2" / module / "examples" / f"{ex}.yml" status, detail = _assert_contracts_resolve(module_dir, "l2")
if not contract.is_file(): if status != "Verified":
return "Broken", f"modules/l2/{module}/examples/{ex}.yml missing" return status, detail
rc, out, err = _run_subprocess([ return "Verified", "L2 composition resolves (simple + complex contracts; offline proxy)"
"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)"
def _check_cap_017_dynamodb() -> Tuple[Status, str]: def _check_cap_017_dynamodb() -> Tuple[Status, str]:
+28 -14
View File
@@ -4,7 +4,8 @@
"interface": "modules/l1/s3/interface.json", "interface": "modules/l1/s3/interface.json",
"terraform_dir": "modules/l1/s3/terraform", "terraform_dir": "modules/l1/s3/terraform",
"published_at": "2026-07-21T19:00:00Z", "published_at": "2026-07-21T19:00:00Z",
"deprecated": false "deprecated": false,
"kind": "l1"
} }
}, },
"vpc": { "vpc": {
@@ -12,7 +13,8 @@
"interface": "modules/l1/vpc/interface.json", "interface": "modules/l1/vpc/interface.json",
"published_at": "2026-07-21T21:30:00Z", "published_at": "2026-07-21T21:30:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/vpc/terraform" "terraform_dir": "modules/l1/vpc/terraform",
"kind": "l1"
} }
}, },
"ecs-cluster": { "ecs-cluster": {
@@ -20,7 +22,8 @@
"interface": "modules/l1/ecs-cluster/interface.json", "interface": "modules/l1/ecs-cluster/interface.json",
"published_at": "2026-07-21T21:30:00Z", "published_at": "2026-07-21T21:30:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/ecs-cluster/terraform" "terraform_dir": "modules/l1/ecs-cluster/terraform",
"kind": "l1"
} }
}, },
"ecs-service": { "ecs-service": {
@@ -28,7 +31,8 @@
"interface": "modules/l1/ecs-service/interface.json", "interface": "modules/l1/ecs-service/interface.json",
"published_at": "2026-07-21T21:30:00Z", "published_at": "2026-07-21T21:30:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/ecs-service/terraform" "terraform_dir": "modules/l1/ecs-service/terraform",
"kind": "l1"
} }
}, },
"iam-role": { "iam-role": {
@@ -36,7 +40,8 @@
"interface": "modules/l1/iam-role/interface.json", "interface": "modules/l1/iam-role/interface.json",
"published_at": "2026-07-21T21:30:00Z", "published_at": "2026-07-21T21:30:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/iam-role/terraform" "terraform_dir": "modules/l1/iam-role/terraform",
"kind": "l1"
} }
}, },
"alb": { "alb": {
@@ -44,7 +49,8 @@
"interface": "modules/l1/alb/interface.json", "interface": "modules/l1/alb/interface.json",
"published_at": "2026-07-21T21:30:00Z", "published_at": "2026-07-21T21:30:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/alb/terraform" "terraform_dir": "modules/l1/alb/terraform",
"kind": "l1"
} }
}, },
"ecr": { "ecr": {
@@ -52,7 +58,8 @@
"interface": "modules/l1/ecr/interface.json", "interface": "modules/l1/ecr/interface.json",
"published_at": "2026-07-21T21:30:00Z", "published_at": "2026-07-21T21:30:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/ecr/terraform" "terraform_dir": "modules/l1/ecr/terraform",
"kind": "l1"
} }
}, },
"cloudfront": { "cloudfront": {
@@ -60,7 +67,8 @@
"interface": "modules/l1/cloudfront/interface.json", "interface": "modules/l1/cloudfront/interface.json",
"published_at": "2026-07-22T19:00:00Z", "published_at": "2026-07-22T19:00:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/cloudfront/terraform" "terraform_dir": "modules/l1/cloudfront/terraform",
"kind": "l1"
} }
}, },
"waf": { "waf": {
@@ -68,7 +76,8 @@
"interface": "modules/l1/waf/interface.json", "interface": "modules/l1/waf/interface.json",
"published_at": "2026-07-22T19:00:00Z", "published_at": "2026-07-22T19:00:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/waf/terraform" "terraform_dir": "modules/l1/waf/terraform",
"kind": "l1"
} }
}, },
"rds": { "rds": {
@@ -76,7 +85,8 @@
"interface": "modules/l1/rds/interface.json", "interface": "modules/l1/rds/interface.json",
"published_at": "2026-07-22T20:00:00Z", "published_at": "2026-07-22T20:00:00Z",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/rds/terraform" "terraform_dir": "modules/l1/rds/terraform",
"kind": "l1"
} }
}, },
"kms-key": { "kms-key": {
@@ -84,7 +94,8 @@
"interface": "modules/l1/kms-key/interface.json", "interface": "modules/l1/kms-key/interface.json",
"published_at": "2026-07-22T20:00", "published_at": "2026-07-22T20:00",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/kms-key/terraform" "terraform_dir": "modules/l1/kms-key/terraform",
"kind": "l1"
} }
}, },
"uptime": { "uptime": {
@@ -92,21 +103,24 @@
"interface": "modules/l1/uptime/interface.json", "interface": "modules/l1/uptime/interface.json",
"published_at": "2026-07-22T21:00", "published_at": "2026-07-22T21:00",
"deprecated": false, "deprecated": false,
"terraform_dir": "modules/l1/uptime/terraform" "terraform_dir": "modules/l1/uptime/terraform",
"kind": "l1"
} }
}, },
"static-assets": { "static-assets": {
"1.0.0": { "1.0.0": {
"interface": "modules/l2/static-assets/composition.json", "interface": "modules/l2/static-assets/composition.json",
"published_at": "2026-07-22T15:00:00Z", "published_at": "2026-07-22T15:00:00Z",
"deprecated": false "deprecated": false,
"kind": "l2"
} }
}, },
"microservice": { "microservice": {
"1.0.0": { "1.0.0": {
"interface": "modules/l2/microservice/composition.json", "interface": "modules/l2/microservice/composition.json",
"published_at": "2026-07-22T15:00:00Z", "published_at": "2026-07-22T15:00:00Z",
"deprecated": false "deprecated": false,
"kind": "l2"
} }
} }
} }
+12 -2
View File
@@ -110,8 +110,18 @@ def copy_one_param(client, source_name: str, dest_name: str, force: bool = False
return "skipped-equal" return "skipped-equal"
if not force: if not force:
return "skipped-mismatch" return "skipped-mismatch"
except Exception: # ParameterNotFound → proceed to put except client.exceptions.ParameterNotFound:
pass pass # target doesn't exist yet → proceed to put
except Exception as e:
# P4 (REQ-168): narrow the broad swallow — only ParameterNotFound
# is an expected "proceed to put" condition. Any other AWS error
# (auth, throttling, service) must surface, not be swallowed.
import sys
sys.stderr.write(
f"migrate_ssm_paths: get_parameter({dest_name}) failed: "
f"{type(e).__name__}: {e}\n"
)
raise
put_kwargs = { put_kwargs = {
"Name": dest_name, "Name": dest_name,
+2 -2
View File
@@ -36,14 +36,14 @@ import json, sys
stage = '''$STAGE''' stage = '''$STAGE'''
status = '''$STATUS''' status = '''$STATUS'''
details = json.loads('''$DETAILS''') details = json.loads('''$DETAILS''')
lines = [f'### ACDL Stage: {stage} — {status}', ''] lines = [f'### Nova Stage: {stage} — {status}', '']
if details: if details:
lines.append('| Metric | Value |') lines.append('| Metric | Value |')
lines.append('|--------|-------|') lines.append('|--------|-------|')
for k, v in details.items(): for k, v in details.items():
lines.append(f'| {k} | {v} |') lines.append(f'| {k} | {v} |')
lines.append('') lines.append('')
lines.append('> _Auto-posted by the ACDL deploy pipeline (D-055)._') lines.append('> _Auto-posted by the Nova deploy pipeline (D-055)._')
print('\n'.join(lines)) print('\n'.join(lines))
") ")
+1 -1
View File
@@ -36,7 +36,7 @@ banner() {
fail() { echo "FAIL: $*" >&2; exit 1; } fail() { echo "FAIL: $*" >&2; exit 1; }
echo "=== ACDL CI Pipeline (local reproduction) ===" echo "=== Nova CI Pipeline (local reproduction) ==="
echo "contract: pipelines/ci.yml (3 stages)" echo "contract: pipelines/ci.yml (3 stages)"
echo "" echo ""
+3 -3
View File
@@ -14,8 +14,8 @@
# parity with the L1 matrix, but $2 is accepted-but-ignored here (documented, # parity with the L1 matrix, but $2 is accepted-but-ignored here (documented,
# not a bug). # not a bug).
# #
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred, # Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (NOVA_* only; ACDL_*
# ACDL_* fallback until P5) default "plan" = no-op # fallback removed in v1.15 P5) default "plan" = no-op
# (plan mode never applies resources, so there is nothing to destroy). # (plan mode never applies resources, so there is nothing to destroy).
# Set to "full" for the real `--destroy` against live AWS. # Set to "full" for the real `--destroy` against live AWS.
set -euo pipefail set -euo pipefail
@@ -25,7 +25,7 @@ cd "$ROOT"
MODULE="$1" MODULE="$1"
# Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy. # Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy.
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). # NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
if [ "$LIFECYCLE_MODE" != "full" ]; then if [ "$LIFECYCLE_MODE" != "full" ]; then
+3 -5
View File
@@ -17,8 +17,8 @@
# positional args for parity with the L1 matrix, but $3 is accepted-but- # positional args for parity with the L1 matrix, but $3 is accepted-but-
# ignored here (documented, not a bug). # ignored here (documented, not a bug).
# #
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred, # Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (NOVA_* only; ACDL_*
# ACDL_* fallback until P5) default "plan" runs # fallback removed in v1.15 P5) default "plan" runs
# `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for # `run_platform.sh --plan-only` (fast, no AWS mutation). Set to "full" for
# the real `--apply` against live AWS. # the real `--apply` against live AWS.
set -euo pipefail set -euo pipefail
@@ -29,14 +29,12 @@ MODULE="$1"
EXAMPLE="$2" # simple or complex EXAMPLE="$2" # simple or complex
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). # NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml" CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
# Point terraform_remote_state to the CI VPC state (not the platform VPC). # Point terraform_remote_state to the CI VPC state (not the platform VPC).
# Set both NOVA_* (preferred by the dual-read helper) and ACDL_* (legacy
# fallback) so any unmigrated reader finds the key until P5.
export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate"
+4 -4
View File
@@ -6,8 +6,8 @@
# For VPC-dependent modules, injects CI VPC outputs into the complex contract # For VPC-dependent modules, injects CI VPC outputs into the complex contract
# before destroy (so terraform can find the resources in the right VPC). # before destroy (so terraform can find the resources in the right VPC).
# #
# Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (dual-read NOVA_* preferred, # Lifecycle mode (REQ-134): NOVA_LIFECYCLE_MODE (NOVA_* only; ACDL_*
# ACDL_* fallback until P5) default "plan" = no-op # fallback removed in v1.15 P5) default "plan" = no-op
# (plan mode never applies resources, so there is nothing to destroy; the # (plan mode never applies resources, so there is nothing to destroy; the
# script exits 0 so the pipeline matrix cell stays green). Set to "full" # script exits 0 so the pipeline matrix cell stays green). Set to "full"
# for the real `--destroy` against live AWS. # for the real `--destroy` against live AWS.
@@ -20,7 +20,7 @@ CI_VPC_OUTPUTS="${2:-}"
# Lifecycle mode: "plan" (default) skips destroy (nothing was applied); # Lifecycle mode: "plan" (default) skips destroy (nothing was applied);
# "full" runs the real terraform destroy. # "full" runs the real terraform destroy.
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). # NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
if [ "$LIFECYCLE_MODE" != "full" ]; then if [ "$LIFECYCLE_MODE" != "full" ]; then
@@ -33,7 +33,7 @@ CONTRACT="modules/l1/${MODULE}/examples/complex.yml"
VPC_DEPENDENT="alb ecs-service rds uptime" VPC_DEPENDENT="alb ecs-service rds uptime"
if echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then if echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then
TMP_CONTRACT="/tmp/acdl-lifecycle-${MODULE}-complex.yml" TMP_CONTRACT="/tmp/nova-lifecycle-${MODULE}-complex.yml"
python3 -c " python3 -c "
import yaml, json import yaml, json
+3 -3
View File
@@ -11,7 +11,7 @@
# from the long-lived platform VPC. # from the long-lived platform VPC.
# #
# Lifecycle mode (REQ-134): the NOVA_LIFECYCLE_MODE env var selects the # Lifecycle mode (REQ-134): the NOVA_LIFECYCLE_MODE env var selects the
# tier (dual-read NOVA_* preferred, ACDL_* fallback until P5). Default # tier (NOVA_* only; ACDL_* fallback removed in v1.15 P5). Default
# "plan" runs `run_platform.sh --plan-only` (fast, no AWS # "plan" runs `run_platform.sh --plan-only` (fast, no AWS
# mutation, validates the contract->resolver->adapter->plan chain for # mutation, validates the contract->resolver->adapter->plan chain for
# every module). Set to "full" to run the real `--apply` (terraform apply # every module). Set to "full" to run the real `--apply` (terraform apply
@@ -26,7 +26,7 @@ EXAMPLE="$2" # simple or complex
CI_VPC_OUTPUTS="${3:-}" CI_VPC_OUTPUTS="${3:-}"
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS). # Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). # NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}" LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml" CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml"
@@ -38,7 +38,7 @@ VPC_DEPENDENT="alb ecs-service rds uptime"
# (only meaningful in full mode; plan mode ignores VPC outputs) # (only meaningful in full mode; plan mode ignores VPC outputs)
if [ "$LIFECYCLE_MODE" = "full" ] && echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then if [ "$LIFECYCLE_MODE" = "full" ] && echo "$VPC_DEPENDENT" | grep -qw "$MODULE" && [ -n "$CI_VPC_OUTPUTS" ] && [ -f "$CI_VPC_OUTPUTS" ]; then
# Generate a temporary contract with CI VPC outputs injected # Generate a temporary contract with CI VPC outputs injected
TMP_CONTRACT="/tmp/acdl-lifecycle-${MODULE}-${EXAMPLE}.yml" TMP_CONTRACT="/tmp/nova-lifecycle-${MODULE}-${EXAMPLE}.yml"
python3 -c " python3 -c "
import yaml, json, sys import yaml, json, sys
+1 -1
View File
@@ -26,7 +26,7 @@ done
CONTRACT="contracts/$MODULE.yaml" CONTRACT="contracts/$MODULE.yaml"
[ -f "$CONTRACT" ] || { echo "FAIL: no sample contract at $CONTRACT for module '$MODULE'" >&2; exit 1; } [ -f "$CONTRACT" ] || { echo "FAIL: no sample contract at $CONTRACT for module '$MODULE'" >&2; exit 1; }
WORK="/tmp/acdl_pattern_plan_$MODULE" WORK="/tmp/nova_pattern_plan_$MODULE"
rm -rf "$WORK"; mkdir -p "$WORK" rm -rf "$WORK"; mkdir -p "$WORK"
echo "=== Pattern plan: $MODULE ===" echo "=== Pattern plan: $MODULE ==="
+37 -54
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# scripts/run_platform.sh - the ACDL platform pipeline. # scripts/run_platform.sh - the Nova platform pipeline.
# #
# Usage: # Usage:
# run_platform.sh <contract.yml> (full e2e with AWS) # run_platform.sh <contract.yml> (full e2e with AWS)
@@ -113,6 +113,38 @@ fi
fail() { echo "FAIL: $*" >&2; exit 1; } fail() { echo "FAIL: $*" >&2; exit 1; }
# run_hitl_gate <contract_id> <resolved_env> <context>
# REQ-108: for qa/prod/dr, call hitl_gates.attest before apply. Dev skips.
# Extracted from the two duplicated inline blocks (P6, REQ-170).
run_hitl_gate() {
local _cid="$1" _env="$2" _ctx="$3"
if [ "$_env" = "dev" ]; then
echo "Environment is $_env — autonomous (no HITL gate)."
return 0
fi
echo "Environment is $_env — HITL attestation gate required$_ctx."
local _approver="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}"
if [ -z "$_approver" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset)" >&2
echo " the gate would block in a real CI run. Passing for local." >&2
fi
python3 -c "
import os, sys
sys.path.insert(0, '.')
from core.hitl_gates import attest
from core import env as _envhelper
contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID']
env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV']
approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test'
ok, reason = attest(contract_id, env, approver)
if ok:
print(f'HITL PASS: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$_cid" NOVA_HITL_ENV="$_env" NOVA_HITL_APPROVER="$_approver"
}
# --local: run the headline E2E against the local emulating tier (D-092). # --local: run the headline E2E against the local emulating tier (D-092).
# No AWS credentials, no Checkov, no DynamoDB. Emulates ECS, outbox, S3 # No AWS credentials, no Checkov, no DynamoDB. Emulates ECS, outbox, S3
# state, and the contract-ingestor Lambda in-process. Exits 0 on success. # state, and the contract-ingestor Lambda in-process. Exits 0 on success.
@@ -142,15 +174,14 @@ stream() {
fi fi
} }
CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" # spike UUID (override via NOVA_CONTRACT_ID)
WORK="/tmp/acdl_platform_run_v18" WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
TF_DIR="$WORK/tf" TF_DIR="$WORK/tf"
rm -rf "$WORK"; mkdir -p "$TF_DIR" rm -rf "$WORK"; mkdir -p "$TF_DIR"
echo "=== Step 0: environment onboarding check ===" echo "=== Step 0: environment onboarding check ==="
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE"
export ACDL_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE" # legacy fallback, removed in P5
python3 core/environment_check.py --env="$ENVIRONMENT_OVERRIDE" || { python3 core/environment_check.py --env="$ENVIRONMENT_OVERRIDE" || {
echo "FAIL: environment not bound — see the onboarding prompt above" >&2 echo "FAIL: environment not bound — see the onboarding prompt above" >&2
exit 1 exit 1
@@ -326,31 +357,7 @@ if [ "$APPLY_ONLY" = "1" ]; then
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE" RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
fi fi
if [ "$RESOLVED_ENV" != "dev" ]; then run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" " before apply" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
echo "Environment is $RESOLVED_ENV — HITL attestation gate required before apply."
APPROVER="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}"
if [ -z "$APPROVER" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset)" >&2
echo " the gate would block in a real CI run. Passing for local." >&2
fi
python3 -c "
import os, sys
sys.path.insert(0, '.')
from core.hitl_gates import attest
from core import env as _envhelper
contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID']
env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV']
approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test'
ok, reason = attest(contract_id, env, approver)
if ok:
print(f'HITL PASS: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
echo "" echo ""
echo "=== Step 5: terraform apply -auto-approve ===" echo "=== Step 5: terraform apply -auto-approve ==="
@@ -442,31 +449,7 @@ RESOLVED_ENV=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE" RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
fi fi
if [ "$RESOLVED_ENV" != "dev" ]; then run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" "" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
echo "Environment is $RESOLVED_ENV — HITL attestation gate required."
APPROVER="${GITHUB_ACTOR:-${GITEA_ACTOR:-}}"
if [ -z "$APPROVER" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/GITEA_ACTOR unset); " >&2
echo " the gate would block in a real CI run. Passing for local." >&2
fi
python3 -c "
import os, sys
sys.path.insert(0, '.')
from core.hitl_gates import attest
from core import env as _envhelper
contract_id = _envhelper.get_env('HITL_CONTRACT_ID') or os.environ['NOVA_HITL_CONTRACT_ID']
env = _envhelper.get_env('HITL_ENV') or os.environ['NOVA_HITL_ENV']
approver = _envhelper.get_env('HITL_APPROVER', '') or 'local-test'
ok, reason = attest(contract_id, env, approver)
if ok:
print(f'HITL PASS: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" NOVA_HITL_CONTRACT_ID="$CONTRACT_ID" NOVA_HITL_ENV="$RESOLVED_ENV" NOVA_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
echo "" echo ""
echo "=== Step 8: write evidence event to DynamoDB outbox ===" echo "=== Step 8: write evidence event to DynamoDB outbox ==="
+1 -1
View File
@@ -26,7 +26,7 @@ done
INSTANCE="modules/l1/$PRIMITIVE/instance.json" INSTANCE="modules/l1/$PRIMITIVE/instance.json"
[ -f "$INSTANCE" ] || { echo "FAIL: no instance.json for primitive '$PRIMITIVE'" >&2; exit 1; } [ -f "$INSTANCE" ] || { echo "FAIL: no instance.json for primitive '$PRIMITIVE'" >&2; exit 1; }
WORK="/tmp/acdl_primitive_plan_$PRIMITIVE" WORK="/tmp/nova_primitive_plan_$PRIMITIVE"
rm -rf "$WORK"; mkdir -p "$WORK" rm -rf "$WORK"; mkdir -p "$WORK"
echo "=== Primitive plan: $PRIMITIVE ===" echo "=== Primitive plan: $PRIMITIVE ==="
+1 -1
View File
@@ -19,7 +19,7 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$ROOT" cd "$ROOT"
echo "=== Nova Regression VERIFY (D-091) ===" echo "=== Nova Regression VERIFY (D-091) ==="
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5). # NOVA_* env vars only (ACDL_* fallback removed in v1.15 P5, REQ-164).
echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}" echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}"
echo "" echo ""
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# scripts/ship_phase.sh — internal CIAgent per-phase ship helper (v1.16)
# Usage: bash scripts/ship_phase.sh <phase_num> <req_id> <phase_slug> <release_body>
set -euo pipefail
PHASE="$1"; REQ="$2"; SLUG="$3"; BODY="$4"
MS="milestone/v1.16-nova-simplification"
BR="phase/$(printf '%02d' "$PHASE")-${SLUG}"
cd "$(git rev-parse --show-toplevel)"
git checkout "$MS" 2>/dev/null
git merge --squash "$BR" 2>&1 | tail -2
MSG="verify(P${PHASE}): ${SLUG} — 4-layer verify PASS + ship
${BODY}
---ci---
project: acdl
phase: ${PHASE}
milestone: v1.16
status: complete
phase_role: execution
requirements:
covered: [${REQ}]
partial: []
---/ci---"
git commit -q -m "$MSG"
PREV=$(git tag -l "v1.15.*" --sort=-version:refname | head -1)
PATCH=$(($(echo "$PREV" | sed 's/v1.15.//')))
NEWPATCH=$((PATCH + 1))
TAG="v1.15.${NEWPATCH}"
git tag -a "$TAG" -m "${TAG}: v1.16 P${PHASE}${SLUG}"
git push origin "$MS" --tags 2>&1 | grep -E "new tag|new branch" | head -2
python3 - "$TAG" "$PREV" <<'PYEOF'
import json, subprocess, sys, urllib.request, urllib.error
tag, prev = sys.argv[1], sys.argv[2]
tok = [l.split("=",1)[1].strip() for l in open(".env.secrets") if l.startswith("NOVA_GITEA_TOKEN=")][0]
body = subprocess.check_output(["git","log",f"{prev}..{tag}","--oneline"]).decode()
payload = {"tag_name":tag,"name":f"Nova {tag} — v1.16 P{tag.split('.')[-1]}","body":body}
req = urllib.request.Request("https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/releases", data=json.dumps(payload).encode(), headers={"Authorization":f"token {tok}","Content-Type":"application/json"}, method="POST")
try:
r = urllib.request.urlopen(req, timeout=30); d = json.loads(r.read()); print(f"release_id: {d.get('id')} tag: {tag}")
except urllib.error.HTTPError as e:
if e.code == 409: print(f"release exists for {tag}")
else: print(f"HTTP {e.code}: {e.read().decode()[:120]}")
except Exception as e: print(f"ERROR: {e}")
PYEOF
echo "SHIPPED ${TAG}"
+3 -2
View File
@@ -78,6 +78,7 @@ def test_run_platform_sh_has_environment_flag():
text = (ROOT / "scripts" / "run_platform.sh").read_text() text = (ROOT / "scripts" / "run_platform.sh").read_text()
assert "--environment" in text assert "--environment" in text
assert "ENVIRONMENT_OVERRIDE" in text assert "ENVIRONMENT_OVERRIDE" in text
# P2 (REQ-159): NOVA_* preferred; ACDL_* kept as dual-read fallback until P5. # P3 (REQ-167): NOVA_* only; the dead ACDL_ENVIRONMENT_OVERRIDE export
# (comment said "removed in P5" but the line was present) is gone.
assert "NOVA_ENVIRONMENT_OVERRIDE" in text assert "NOVA_ENVIRONMENT_OVERRIDE" in text
assert "ACDL_ENVIRONMENT_OVERRIDE" in text # legacy fallback, removed in P5 assert "ACDL_ENVIRONMENT_OVERRIDE" not in text
+6
View File
@@ -31,6 +31,12 @@ class TestEnvironmentCheck:
assert "state backend" in msg.lower() assert "state backend" in msg.lower()
assert "IAM role" in msg assert "IAM role" in msg
def test_onboarding_message_says_nova_not_acdl(self):
"""P2 (REQ-166): the onboarding message is rebranded Nova."""
msg = _onboarding_message("qa")
assert "Nova Environment Onboarding" in msg
assert "ACDL" not in msg
def test_contract_with_dev_environment_passes(self): def test_contract_with_dev_environment_passes(self):
ok, msg = check(contract_path=str(ROOT / "contracts/static-assets.yml"), root=ROOT) ok, msg = check(contract_path=str(ROOT / "contracts/static-assets.yml"), root=ROOT)
assert ok is True assert ok is True
+49 -1
View File
@@ -74,4 +74,52 @@ class TestMapPath:
def test_preserves_value_segment_exactly(self): def test_preserves_value_segment_exactly(self):
# Hyphens, dots, underscores in output names are preserved # Hyphens, dots, underscores in output names are preserved
assert map_path("/acdl/dev/c-1/my.output-name_2") == "/nova/dev/c-1/my.output-name_2" assert map_path("/acdl/dev/c-1/my.output-name_2") == "/nova/dev/c-1/my.output-name_2"
class TestNarrowedException:
"""P4 (REQ-168): the copy_one_param except is narrowed to
ParameterNotFound; non-ParameterNotFound errors surface (not swallowed)."""
def test_parameter_not_found_proceeds_to_put(self):
"""A ParameterNotFound on the dest get_parameter (target absent) is
the expected 'proceed to put' path not an error."""
from unittest import mock
import migrate_ssm_paths as m
class FakeExceptions:
ParameterNotFound = type("ParameterNotFound", (Exception,), {})
fake_client = mock.Mock()
fake_client.exceptions = FakeExceptions
# source get_parameter succeeds; dest get_parameter raises ParameterNotFound
fake_client.get_parameter.side_effect = [
{"Parameter": {"Value": "v", "Type": "String", "KeyId": None}},
FakeExceptions.ParameterNotFound(),
]
fake_client.put_parameter.return_value = {"Version": 1}
result = m.copy_one_param(fake_client, "/acdl/dev/c/out", "/nova/dev/c/out")
assert result == "copied"
fake_client.put_parameter.assert_called_once()
def test_non_parameter_not_found_error_is_raised(self):
"""A non-ParameterNotFound AWS error (e.g. ThrottlingException) on
the dest get_parameter is raised, not swallowed (P4, REQ-168)."""
from unittest import mock
import migrate_ssm_paths as m
class FakeExceptions:
ParameterNotFound = type("ParameterNotFound", (Exception,), {})
class ThrottlingException(Exception):
pass
fake_client = mock.Mock()
fake_client.exceptions = FakeExceptions
# source get_parameter succeeds; dest get_parameter raises Throttling
fake_client.get_parameter.side_effect = [
{"Parameter": {"Value": "v", "Type": "String", "KeyId": None}},
ThrottlingException("slow down"),
]
with pytest.raises(ThrottlingException):
m.copy_one_param(fake_client, "/acdl/dev/c/out", "/nova/dev/c/out")
fake_client.put_parameter.assert_not_called()
+7 -2
View File
@@ -134,7 +134,11 @@ class TestPublishToSsm:
def flaky_put(**kwargs): def flaky_put(**kwargs):
call_count["n"] += 1 call_count["n"] += 1
if "bad" in kwargs["Name"]: if "bad" in kwargs["Name"]:
raise Exception("simulated failure") from botocore.exceptions import ClientError
raise ClientError(
{"Error": {"Code": "InternalError", "Message": "simulated"}},
"PutParameter",
)
return real_put(**kwargs) return real_put(**kwargs)
with mock.patch("core.output_publisher._ssm_client", return_value=ssm): with mock.patch("core.output_publisher._ssm_client", return_value=ssm):
@@ -272,10 +276,11 @@ class TestPostGithubComment:
assert "/issues/5/comments" in captured["url"] assert "/issues/5/comments" in captured["url"]
def test_returns_false_on_exception(self, monkeypatch): def test_returns_false_on_exception(self, monkeypatch):
import urllib.error
monkeypatch.setenv("GITHUB_TOKEN", "tok") monkeypatch.setenv("GITHUB_TOKEN", "tok")
monkeypatch.setenv("GITHUB_REPOSITORY", "acdl/acdl") monkeypatch.setenv("GITHUB_REPOSITORY", "acdl/acdl")
monkeypatch.setenv("GITHUB_REF", "refs/pull/1/merge") monkeypatch.setenv("GITHUB_REF", "refs/pull/1/merge")
with mock.patch("urllib.request.urlopen", side_effect=Exception("boom")): with mock.patch("urllib.request.urlopen", side_effect=urllib.error.URLError("boom")):
assert post_github_comment("body") is False assert post_github_comment("body") is False
def test_uses_gh_token_fallback(self, monkeypatch): def test_uses_gh_token_fallback(self, monkeypatch):