Compare commits

..

3 Commits

Author SHA1 Message Date
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
6 changed files with 172 additions and 135 deletions
+14 -6
View File
@@ -17,11 +17,15 @@ existing /acdl/... parameters to /nova/... and deletes the old ones.)
import json
import os
import sys
import urllib.error
import urllib.request
try:
import boto3
from botocore.exceptions import ClientError
except ImportError:
boto3 = None
ClientError = Exception # type: ignore[assignment,misc]
# 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).
@@ -109,10 +113,12 @@ def publish_to_ssm(outputs, environment, contract_id):
Overwrite=True,
)
results[name] = param_name
except Exception as e:
# Don't fail the pipeline if one output fails to publish, but log it
except (ClientError, OSError) as e:
# 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
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
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:
return False # not in a PR context or no token
try:
import urllib.request
url = f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments"
data = json.dumps({"body": comment_text}).encode()
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")
urllib.request.urlopen(req, timeout=10)
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
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
+54 -72
View File
@@ -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]:
+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"
if not force:
return "skipped-mismatch"
except Exception: # ParameterNotFound → proceed to put
pass
except client.exceptions.ParameterNotFound:
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 = {
"Name": dest_name,
+36 -52
View File
@@ -113,6 +113,38 @@ fi
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).
# No AWS credentials, no Checkov, no DynamoDB. Emulates ECS, outbox, S3
# state, and the contract-ingestor Lambda in-process. Exits 0 on success.
@@ -142,8 +174,8 @@ stream() {
fi
}
CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID
WORK="/tmp/acdl_platform_run_v18"
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" # spike UUID (override via NOVA_CONTRACT_ID)
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
TF_DIR="$WORK/tf"
rm -rf "$WORK"; mkdir -p "$TF_DIR"
@@ -325,31 +357,7 @@ if [ "$APPLY_ONLY" = "1" ]; then
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
fi
if [ "$RESOLVED_ENV" != "dev" ]; then
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
run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" " before apply" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; exit 1; }
echo ""
echo "=== Step 5: terraform apply -auto-approve ==="
@@ -441,31 +449,7 @@ RESOLVED_ENV=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV="$ENVIRONMENT_OVERRIDE"
fi
if [ "$RESOLVED_ENV" != "dev" ]; then
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
run_hitl_gate "$CONTRACT_ID" "$RESOLVED_ENV" "" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; exit 1; }
echo ""
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
+49 -1
View File
@@ -74,4 +74,52 @@ class TestMapPath:
def test_preserves_value_segment_exactly(self):
# 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):
call_count["n"] += 1
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)
with mock.patch("core.output_publisher._ssm_client", return_value=ssm):
@@ -272,10 +276,11 @@ class TestPostGithubComment:
assert "/issues/5/comments" in captured["url"]
def test_returns_false_on_exception(self, monkeypatch):
import urllib.error
monkeypatch.setenv("GITHUB_TOKEN", "tok")
monkeypatch.setenv("GITHUB_REPOSITORY", "acdl/acdl")
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
def test_uses_gh_token_fallback(self, monkeypatch):