feat(P2): Nova rebrand — code/env-vars/consumer-path (REQ-158/159/160)

core/env.py dual-read helper (D-108); 21 ACDL_*→NOVA_* env vars migrated
across core/scripts/adapters/tests/workflows + .env/.env.secrets (key
rename, values stay). G-106 binding: run_platform.sh:288-289 +
regression_verify.py:309-312 dual-read (NOVA first, ACDL fallback).
G-108 binding: Gitea NOVA_* secrets created via API + workflow secrets:
refs updated (deploy.yml + modules-lifecycle.yml, .gitea + .github).
acdl_tagging.py→nova_tagging.py (D-109 warn mode, nova:* enforced).
.acdl/→.nova/ consumer path (resolver + deploy workflow + schema +
tests + docs). Test fixtures updated; pytest + run_ci.sh PASS.

---ci---
project: acdl
phase: 2
milestone: v1.15
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-07-30 01:25:24 +00:00
parent 0bc70a3d95
commit d5bae868a4
48 changed files with 593 additions and 297 deletions
+7 -5
View File
@@ -408,14 +408,16 @@ class TestAdapterDedupMergesSameModule:
class TestAdapterRemoteStateKeyOverride:
"""P2-2 (v1.14, REQ-139): ACDL_REMOTE_STATE_KEY env var overrides the
default 'platform/terraform.tfstate' key in the emitted
"""P2-2 (v1.14, REQ-139): NOVA_REMOTE_STATE_KEY env var (P2 renamed from
ACDL_REMOTE_STATE_KEY; dual-read NOVA_* preferred, ACDL_* fallback until
P5) overrides the default 'platform/terraform.tfstate' key in the emitted
data terraform_remote_state block. This is the load-bearing correctness
mechanism for the microservice L2 lifecycle (remote state points at the
CI VPC, not the platform VPC)."""
def test_default_remote_state_key(self, tmp_path, monkeypatch):
"""When ACDL_REMOTE_STATE_KEY is unset, the default key is used."""
"""When NOVA_REMOTE_STATE_KEY is unset, the default key is used."""
monkeypatch.delenv("NOVA_REMOTE_STATE_KEY", raising=False)
monkeypatch.delenv("ACDL_REMOTE_STATE_KEY", raising=False)
stack = {
"resources": [
@@ -431,9 +433,9 @@ class TestAdapterRemoteStateKeyOverride:
assert "platform/terraform.tfstate" in main_tf
def test_env_override_remote_state_key(self, tmp_path, monkeypatch):
"""When ACDL_REMOTE_STATE_KEY is set, the emitted data block uses
"""When NOVA_REMOTE_STATE_KEY is set, the emitted data block uses
the overridden key (e.g. 'spike/ci-vpc/terraform.tfstate')."""
monkeypatch.setenv("ACDL_REMOTE_STATE_KEY", "spike/ci-vpc/terraform.tfstate")
monkeypatch.setenv("NOVA_REMOTE_STATE_KEY", "spike/ci-vpc/terraform.tfstate")
stack = {
"resources": [
{"id": "s3", "type": "aws:s3:bucket", "module": "s3@1.0.0", "inputs": {"bucket_name": "test", "region": "us-east-1"}}
+3 -1
View File
@@ -102,6 +102,8 @@ def test_dr_blocks_on_missing_dr_drill():
def test_signature_skip_when_key_unset(monkeypatch, capsys):
"""D-089: signature verification is skipped when the signing key is unset."""
# P2: dual-read — both NOVA_* and ACDL_* must be unset for the skip.
monkeypatch.delenv("NOVA_ATTESTATION_SIGNING_KEY_ID", raising=False)
monkeypatch.delenv("ACDL_ATTESTATION_SIGNING_KEY_ID", raising=False)
artifact = {"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"type": "x", "payload": {}, "signature": "sig"}
@@ -112,7 +114,7 @@ def test_signature_skip_when_key_unset(monkeypatch, capsys):
def test_signature_required_when_key_set(monkeypatch):
"""When the signing key is set, a missing signature fails."""
monkeypatch.setenv("ACDL_ATTESTATION_SIGNING_KEY_ID", "kms-key-id")
monkeypatch.setenv("NOVA_ATTESTATION_SIGNING_KEY_ID", "kms-key-id")
artifact = {"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
"type": "x", "payload": {}} # no signature
assert _verify_signature(artifact) is False
+6 -6
View File
@@ -67,10 +67,10 @@ class TestToPcr:
class TestRuleMapTagging:
def test_acdl_tag_naming_is_real_rule(self):
# D-054 / D-043 closure: ACDL_TAG_NAMING is now a real custom Checkov
# rule, not a synthetic SKIPPED record.
assert RULE_MAP["ACDL_TAG_NAMING"] == ("tagging-standard", "medium")
def test_nova_tag_naming_is_real_rule(self):
# D-054 / D-043 closure, P2 rename (REQ-158): NOVA_TAG_NAMING is now
# a real custom Checkov rule, not a synthetic SKIPPED record.
assert RULE_MAP["NOVA_TAG_NAMING"] == ("tagging-standard", "medium")
class TestAdapt:
@@ -100,13 +100,13 @@ class TestAdapt:
def test_adapt_does_not_emit_synthetic_tag_naming(self, tmp_path):
# D-043 closure: adapt() no longer appends a synthetic SKIPPED
# ACDL_TAG_NAMING record. The custom Checkov rule (loaded via
# NOVA_TAG_NAMING record. The custom Checkov rule (loaded via
# --external-checks-dir) produces real PASS/FAIL records instead.
data = self._sample_checkov_json()
f = tmp_path / "checkov.json"
f.write_text(json.dumps(data))
results = adapt(str(f), "c-1")
tag = [r for r in results if r["ruleId"] == "ACDL_TAG_NAMING"]
tag = [r for r in results if r["ruleId"] == "NOVA_TAG_NAMING"]
assert tag == [] # no synthetic record
def test_adapt_has_passed_and_failed(self, tmp_path):
+3 -1
View File
@@ -78,4 +78,6 @@ def test_run_platform_sh_has_environment_flag():
text = (ROOT / "scripts" / "run_platform.sh").read_text()
assert "--environment" in text
assert "ENVIRONMENT_OVERRIDE" in text
assert "ACDL_ENVIRONMENT_OVERRIDE" in text
# P2 (REQ-159): NOVA_* preferred; ACDL_* kept as dual-read fallback until P5.
assert "NOVA_ENVIRONMENT_OVERRIDE" in text
assert "ACDL_ENVIRONMENT_OVERRIDE" in text # legacy fallback, removed in P5
+56
View File
@@ -0,0 +1,56 @@
"""Unit tests for the dual-read env helper (core/env.py, D-108, REQ-159).
Covers the four cases:
- both NOVA_* and ACDL_* set (NOVA wins)
- only NOVA_* set
- only ACDL_* set (fallback)
- neither set (default returned)
The ACDL_* fallback is the intentional dual-read source and is removed
in P5 (REQ-164). These fixtures deliberately keep the ACDL_* names as
the fallback source — they are the one allowed ACDL_* reference.
"""
from __future__ import annotations
import pytest
from core import env
@pytest.fixture(autouse=True)
def _isolate_env(monkeypatch):
"""Ensure no ACDL_*/NOVA_* leakage between tests."""
for key in list(__import__("os").environ):
if key.startswith(("ACDL_", "NOVA_")):
monkeypatch.delenv(key, raising=False)
yield
def test_both_set_nova_wins(monkeypatch):
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value")
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "nova-value"
def test_only_nova_set(monkeypatch):
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "nova-value")
assert env.get_env("AWS_ACCOUNT_ID") == "nova-value"
def test_only_acdl_set_fallback(monkeypatch):
# ACDL_* is the intentional dual-read fallback source (removed in P5).
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value"
def test_neither_set_returns_default():
assert env.get_env("AWS_ACCOUNT_ID") is None
assert env.get_env("AWS_ACCOUNT_ID", default="581513795199") == "581513795199"
def test_blank_nova_falls_back_to_acdl(monkeypatch):
# An explicitly-empty NOVA key must not shadow the ACDL fallback.
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "")
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value"
+11 -8
View File
@@ -2,9 +2,10 @@
The lifecycle scripts (run_lifecycle_test.sh, run_lifecycle_destroy.sh,
run_l2_lifecycle_test.sh, run_l2_lifecycle_destroy.sh) wrap run_platform.sh.
REQ-134 (v1.12) adds the ACDL_LIFECYCLE_MODE env var: default "plan" runs
`run_platform.sh --plan-only` (fast, no AWS mutation); "full" runs the real
`--apply`/`--destroy` against live AWS.
REQ-134 (v1.12) adds the NOVA_LIFECYCLE_MODE env var (P2 renamed from
ACDL_LIFECYCLE_MODE, dual-read NOVA_* preferred / ACDL_* fallback until
P5): default "plan" runs `run_platform.sh --plan-only` (fast, no AWS
mutation); "full" runs the real `--apply`/`--destroy` against live AWS.
These tests verify the dispatch logic offline by inspecting script content
(running the scripts end-to-end requires AWS credentials in full mode).
@@ -32,12 +33,14 @@ class TestLifecycleModeFlag:
@pytest.mark.parametrize("script", SCRIPTS)
def test_script_reads_acdl_lifecycle_mode(self, script):
"""Every lifecycle script reads ACDL_LIFECYCLE_MODE with a 'plan' default."""
"""Every lifecycle script reads NOVA_LIFECYCLE_MODE (dual-read with
ACDL_LIFECYCLE_MODE fallback) with a 'plan' default."""
src = _read(script)
# The default must be 'plan' (the speed-up default). The scripts use
# an intermediate LIFECYCLE_MODE var sourced from ACDL_LIFECYCLE_MODE.
assert "${ACDL_LIFECYCLE_MODE:-plan}" in src, \
f"{script} must read ACDL_LIFECYCLE_MODE defaulting to 'plan'"
# P2 (REQ-159): dual-read NOVA_* preferred, ACDL_* fallback. The
# ACDL_LIFECYCLE_MODE:-plan substring is still present inside the
# nested dual-read expression (removed in P5).
assert "${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}" in src, \
f"{script} must dual-read NOVA_/ACDL_LIFECYCLE_MODE defaulting to 'plan'"
assert "LIFECYCLE_MODE=" in src, \
f"{script} must assign LIFECYCLE_MODE from the env var"
+6 -6
View File
@@ -179,11 +179,11 @@ def test_local_lambda_stub_rejects_missing_field(tmp_path):
def test_run_local_e2e_microservice():
"""Headline E2E: contract -> resolver -> adapter -> local S3 backend
-> local ECS (HTTP 200) -> flat-file outbox -> local Lambda. No AWS."""
os.environ["ACDL_LOCAL_TIER"] = "1"
os.environ["NOVA_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/microservice.yml")
finally:
os.environ.pop("ACDL_LOCAL_TIER", None)
os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None)
assert result["tier"] == "local-emulator"
assert result["backend"] == "local"
assert result["ecs"] is not None
@@ -196,11 +196,11 @@ def test_run_local_e2e_microservice():
def test_run_local_e2e_static_assets():
"""Static-assets stack has no ECS service; the local E2E must still
complete (ecs=None) and the outbox chain + Lambda stub must pass."""
os.environ["ACDL_LOCAL_TIER"] = "1"
os.environ["NOVA_LOCAL_TIER"] = "1"
try:
result = le.run_local_e2e("contracts/static-assets.yml")
finally:
os.environ.pop("ACDL_LOCAL_TIER", None)
os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None)
assert result["tier"] == "local-emulator"
assert result["ecs"] is None # no ECS service in this stack
assert result["outbox_chain_verified"] is True
@@ -209,9 +209,9 @@ def test_run_local_e2e_static_assets():
def test_is_local_tier_flag():
assert le.is_local_tier() is False
os.environ["ACDL_LOCAL_TIER"] = "1"
os.environ["NOVA_LOCAL_TIER"] = "1"
try:
assert le.is_local_tier() is True
finally:
os.environ.pop("ACDL_LOCAL_TIER", None)
os.environ.pop("NOVA_LOCAL_TIER", None); os.environ.pop("ACDL_LOCAL_TIER", None)
assert le.is_local_tier() is False
+11 -5
View File
@@ -376,7 +376,9 @@ class TestCli:
# ---------------------------------------------------------------------------
class TestKmsFailLoud:
"""P1-3: SSM publisher must fail loud when ACDL_KMS_KEY_ID is unset."""
"""P1-3: SSM publisher must fail loud when NOVA_KMS_KEY_ID is unset
(P2 renamed from ACDL_KMS_KEY_ID; dual-read NOVA_* preferred,
ACDL_* fallback until P5)."""
def test_kms_unset_raises(self, monkeypatch):
from moto import mock_aws
@@ -385,11 +387,14 @@ class TestKmsFailLoud:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
# Both NOVA_* and ACDL_* unset → helper returns default (None) → fail loud.
monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False)
monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False)
monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", raising=False)
monkeypatch.delenv("ACDL_ALLOW_DEFAULT_KMS", raising=False)
with mock_aws():
with pytest.raises(RuntimeError, match="ACDL_KMS_KEY_ID is not set"):
with pytest.raises(RuntimeError, match="NOVA_KMS_KEY_ID is not set"):
publish_to_ssm({"vpc_id": "vpc-1"}, "dev", "c-1")
def test_kms_unset_allow_default_kms_escape_hatch(self, monkeypatch):
@@ -399,8 +404,9 @@ class TestKmsFailLoud:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.delenv("NOVA_KMS_KEY_ID", raising=False)
monkeypatch.delenv("ACDL_KMS_KEY_ID", raising=False)
monkeypatch.setenv("ACDL_ALLOW_DEFAULT_KMS", "1")
monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1")
with mock_aws():
ssm = boto3.client("ssm", region_name="us-east-1")
@@ -415,8 +421,8 @@ class TestKmsFailLoud:
monkeypatch.setenv("AWS_DEFAULT_REGION", "us-east-1")
monkeypatch.setenv("AWS_ACCESS_KEY_ID", "testing")
monkeypatch.setenv("AWS_SECRET_ACCESS_KEY", "testing")
monkeypatch.setenv("ACDL_KMS_KEY_ID", "arn:aws:kms:us-east-1:123:key/abc")
monkeypatch.setenv("ACDL_ALLOW_DEFAULT_KMS", "1")
monkeypatch.setenv("NOVA_KMS_KEY_ID", "arn:aws:kms:us-east-1:123:key/abc")
monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1")
from core.output_publisher import _kms_key_id
assert _kms_key_id() == "arn:aws:kms:us-east-1:123:key/abc"
+7 -7
View File
@@ -310,7 +310,7 @@ class TestDeployWorkflowConformance:
wf = _load_workflow(".gitea/workflows/deploy.yml")
inputs = wf["on"]["workflow_call"]["inputs"]
assert "contract" in inputs
assert inputs["contract"]["default"] == ".acdl/contract.yml"
assert inputs["contract"]["default"] == ".nova/contract.yml"
def test_deploy_workflow_has_mode_input(self):
wf = _load_workflow(".gitea/workflows/deploy.yml")
@@ -646,20 +646,20 @@ class TestModulesLifecyclePipeline:
assert set(inputs["lifecycle_mode"].get("options", [])) == {"plan", "full"}
def test_lifecycle_job_passes_mode_env_to_steps(self):
"""The lifecycle job sets ACDL_LIFECYCLE_MODE env so scripts dispatch
"""The lifecycle job sets NOVA_LIFECYCLE_MODE env so scripts dispatch
to plan-only by default, full on override."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
env = wf["jobs"]["lifecycle"].get("env", {})
assert "ACDL_LIFECYCLE_MODE" in env
assert "NOVA_LIFECYCLE_MODE" in env
# The expression must resolve to 'plan' when no input/var is set.
assert "plan" in env["ACDL_LIFECYCLE_MODE"]
assert "plan" in env["NOVA_LIFECYCLE_MODE"]
def test_l2_lifecycle_job_passes_mode_env_to_steps(self):
"""The L2 lifecycle job also sets ACDL_LIFECYCLE_MODE env."""
"""The L2 lifecycle job also sets NOVA_LIFECYCLE_MODE env."""
wf = _load_workflow(".gitea/workflows/modules-lifecycle.yml")
env = wf["jobs"]["l2-lifecycle"].get("env", {})
assert "ACDL_LIFECYCLE_MODE" in env
assert "plan" in env["ACDL_LIFECYCLE_MODE"]
assert "NOVA_LIFECYCLE_MODE" in env
assert "plan" in env["NOVA_LIFECYCLE_MODE"]
def test_ci_vpc_apply_skipped_in_plan_mode(self):
"""The CI VPC apply job is skipped in plan mode (nothing is applied)."""
+2
View File
@@ -29,6 +29,7 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch):
def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):
"""Without ACDL_SOD_HALT_TOPIC_ARN, a stderr emission occurs."""
monkeypatch.delenv("NOVA_SOD_HALT_TOPIC_ARN", raising=False)
monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False)
# Mock outbox_writer.write_event to avoid AWS calls.
with mock.patch("core.outbox_writer.write_event", return_value=None):
@@ -40,6 +41,7 @@ def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):
def test_route_halt_outbox_fallback_writes_event(monkeypatch):
"""Without the SNS ARN, the outbox fallback writes a SEPARATION_OF_DUTIES_VIOLATION event."""
monkeypatch.delenv("NOVA_SOD_HALT_TOPIC_ARN", raising=False)
monkeypatch.delenv("ACDL_SOD_HALT_TOPIC_ARN", raising=False)
with mock.patch("core.outbox_writer.write_event") as mock_write:
route_halt_artifact("contract-789", "sod violation", oncall_client=None)
+5 -3
View File
@@ -106,9 +106,11 @@ class TestCreateStateBackend:
"""terraform/bootstrap/create_state_backend.py — mock boto3."""
def test_state_bucket_name_construction(self, monkeypatch):
"""The state bucket name is derived from ACDL_AWS_ACCOUNT_ID."""
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "123456789012")
account_id = os.environ.get("ACDL_AWS_ACCOUNT_ID", "581513795199")
"""The state bucket name is derived from NOVA_AWS_ACCOUNT_ID
(P2 renamed from ACDL_AWS_ACCOUNT_ID; the bucket name acdl-tfstate-*
stays until P4, REQ-163)."""
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "123456789012")
account_id = os.environ.get("NOVA_AWS_ACCOUNT_ID", "581513795199")
state_bucket = f"acdl-tfstate-{account_id}-us-east-1"
assert state_bucket == "acdl-tfstate-123456789012-us-east-1"