docs(milestone): complete v1.15 — Nova Rebrand (tag v1.15.4)
P5 final-review-ship complete: dual-read fallback removed (REQ-164) — core/env.py NOVA-only, .env.secrets load paths NOVA-only (G-106 retired), nova_tagging.py hard-fails any acdl:* tag, legacy ACDL_* Gitea secrets deleted, ACDL_LIFECYCLE_MODE/ACDL_LOCAL_TIER/ACDL_HITL_* exports removed from scripts, SNS subject → Nova SoD halt (P1-2), bootstrap scripts NOVA-only. Review: 2 P0 auto-fixed (duplicate delenv), P1-1/P1-2 resolved, doc-drift fixed. Audit: tags v1.15.0-4 exist; traceability REQ-155..164 all complete; ARCHITECTURE naming table matches codebase. 615 pytest PASS; run_ci.sh 3-stage PASS. NOVA_MIGRATION.md marked COMPLETE. ---ci--- project: acdl phase: 5 milestone: v1.15 status: complete phase_role: final requirements: covered: [REQ-155, REQ-156, REQ-157, REQ-158, REQ-159, REQ-160, REQ-161, REQ-162, REQ-163, REQ-164] partial: [] ---/ci---
This commit is contained in:
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"phase": 4,
|
||||
"phase": 5,
|
||||
"stage": "complete",
|
||||
"milestone": "v1.15",
|
||||
"phase_role": "execution",
|
||||
"phase_role": "final",
|
||||
"attempts": 0,
|
||||
"updated_at": "2026-07-30T00:10:00Z",
|
||||
"milestone_complete": false,
|
||||
"requirements": ["REQ-163"],
|
||||
"updated_at": "2026-07-30T00:11:00Z",
|
||||
"milestone_complete": true,
|
||||
"requirements": ["REQ-155", "REQ-156", "REQ-157", "REQ-158", "REQ-159", "REQ-160", "REQ-161", "REQ-162", "REQ-163", "REQ-164"],
|
||||
"tag": "v1.15.4"
|
||||
}
|
||||
@@ -828,7 +828,7 @@ IDEATE-01..IDEATE-10, mapped to REQ-155..REQ-164.
|
||||
| REQ-161 | P3 | complete |
|
||||
| REQ-162 | P3 | complete |
|
||||
| REQ-163 | P4 | complete |
|
||||
| REQ-164 | P5 | pending |
|
||||
| REQ-164 | P5 | complete |
|
||||
|
||||
### Out of Scope (v1.15)
|
||||
- Renaming the real Gitea org/repo (`continuous-intelligence/acdl`) or
|
||||
|
||||
+2
-2
@@ -1435,7 +1435,7 @@ After Phase P21: milestone COMPLETE — `v1.13.24` IS the v1.14 release.
|
||||
|
||||
---
|
||||
|
||||
## v1.15 (active — Nova Rebrand, tag `v1.15.4`)
|
||||
## v1.15 (complete — Nova Rebrand, tag `v1.15.4`)
|
||||
|
||||
A full rebrand from **ACDL** / "Agentic Cloud Delivery Platform" →
|
||||
**Nova** / "The New Dawn of DevSecOps — security as a seamless enabler
|
||||
@@ -1620,7 +1620,7 @@ milestone release). (G-104 binding.)
|
||||
update REQUIREMENTS.md (REQ-155..164 marked complete), ROADMAP.md
|
||||
(v1.15 complete), PROJECT.md. Tag final patch `v1.14.5` (IS the
|
||||
milestone release). Merge `milestone/v1.15-nova` → `main`.
|
||||
- **Status:** in progress (branch `phase/05-final-review-ship`)
|
||||
- **Status:** complete (v1.15.4, milestone release)
|
||||
- **Depends on:** [P1-P4]
|
||||
- **Requirements:** REQ-164
|
||||
- **Success Criteria:**
|
||||
|
||||
@@ -39,12 +39,11 @@ NON_TAGGABLE_TYPES = (
|
||||
"aws_internet_gateway",
|
||||
)
|
||||
|
||||
# P3 hard mode (D-109): hard-fail when a taggable resource is missing any
|
||||
# required nova:* tag, or when a resource carries only legacy acdl:* tag
|
||||
# keys and no nova:* tags. P2 shipped warn mode (`_WARN_MODE = True`); P3
|
||||
# flips to `False` (hard-fail) once terraform emits nova:* and the ABAC
|
||||
# policy is swapped to nova:*. P5 keeps hard mode and additionally fails
|
||||
# on any acdl:* tag key present at all.
|
||||
# P5 hard mode (D-109, REQ-164): `_WARN_MODE = False` (set in P3) AND
|
||||
# any `acdl:*` tag key present at all is a hard FAIL (P5 tightens from
|
||||
# P3's "acdl:*-only fails" to "any acdl:* key fails"). The legacy tag
|
||||
# keys are fully removed from terraform (P3); any remaining `acdl:*` key
|
||||
# is a rebrand regression.
|
||||
_WARN_MODE = False
|
||||
|
||||
|
||||
@@ -71,21 +70,13 @@ class NovaTaggingStandard(BaseResourceCheck):
|
||||
tag_keys = set(tag_block.keys())
|
||||
elif isinstance(tags, dict):
|
||||
tag_keys = set(tags.keys())
|
||||
# P5 (REQ-164): any legacy acdl:* tag key present = hard FAIL.
|
||||
legacy_present = tag_keys & set(LEGACY_TAGS)
|
||||
if legacy_present:
|
||||
return CheckResult.FAILED
|
||||
missing = [t for t in REQUIRED_TAGS if t not in tag_keys]
|
||||
if not missing:
|
||||
return CheckResult.PASSED
|
||||
# Warn mode (D-109, P2): if the resource carries the legacy acdl:*
|
||||
# tag keys for every required tag, emit a warning rather than a
|
||||
# hard fail — existing resources still carry acdl:* until P3.
|
||||
if _WARN_MODE:
|
||||
has_all_legacy = all(t in tag_keys for t in LEGACY_TAGS)
|
||||
if has_all_legacy:
|
||||
sys.stderr.write(
|
||||
f"[nova_tagging] WARN: {entity_type} has legacy acdl:* tags "
|
||||
f"but no nova:* tags (P2 warn mode, D-109). Migrate to "
|
||||
f"nova:* tags before P5.\n"
|
||||
)
|
||||
return CheckResult.PASSED
|
||||
return CheckResult.FAILED
|
||||
|
||||
check = NovaTaggingStandard()
|
||||
+14
-31
@@ -1,19 +1,14 @@
|
||||
"""Dual-read environment helper (D-108, REQ-159, G-106).
|
||||
"""Environment helper (D-108, REQ-159, REQ-164).
|
||||
|
||||
During the Nova rebrand transition window (P2–P4), every `NOVA_*`
|
||||
environment variable is the preferred source, with the legacy `ACDL_*`
|
||||
name as the fallback. This keeps deployments from breaking while the
|
||||
keys are rotated across `.env`, `.env.secrets`, Gitea repo secrets, and
|
||||
operator-managed process environments.
|
||||
During the Nova rebrand transition window (P2–P4), `get_env` read
|
||||
`NOVA_*` preferred with the legacy `ACDL_*` name as the fallback. **P5
|
||||
(REQ-164) removed the fallback** — `get_env` now reads `NOVA_*` only.
|
||||
|
||||
`get_env(name, default=None)` resolves `NOVA_<name>` first, then falls
|
||||
back to `ACDL_<name>`, then returns `default` if neither is set.
|
||||
|
||||
This helper is removed (NOVA-only) in P5 (REQ-164). Direct-read paths
|
||||
that bypass this helper (the `.env.secrets` shell export in
|
||||
`scripts/run_platform.sh` and the Python parser in
|
||||
`core/regression_verify.py`) mirror this contract inline per the G-106
|
||||
binding — see those sites for the dual-read shell/Python forms.
|
||||
`get_env(name, default=None)` resolves `NOVA_<name>`, then returns
|
||||
`default` if unset. Direct-read paths that bypass this helper (the
|
||||
`.env.secrets` shell export in `scripts/run_platform.sh` and the Python
|
||||
parser in `core/regression_verify.py`) were updated to NOVA-only in P5
|
||||
(the G-106 dual-read contract was retired with the fallback).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -25,24 +20,12 @@ __all__ = ["get_env"]
|
||||
|
||||
|
||||
def get_env(name: str, default: Optional[str] = None) -> Optional[str]:
|
||||
"""Resolve a config value with a NOVA-preferred / ACDL-fallback read.
|
||||
"""Resolve a config value from the `NOVA_*` environment.
|
||||
|
||||
`name` is the bare key WITHOUT the prefix (e.g. ``"AWS_ACCOUNT_ID"``).
|
||||
The lookup order is:
|
||||
|
||||
1. ``NOVA_<name>`` (preferred)
|
||||
2. ``ACDL_<name>`` (legacy fallback, removed in P5)
|
||||
3. ``default``
|
||||
|
||||
Returns the first value that is present and non-empty, or ``default``
|
||||
if neither env var is set. An explicitly-set empty string is treated
|
||||
as "unset" so an operator cannot accidentally shadow the fallback
|
||||
with a blank NOVA key.
|
||||
Returns ``NOVA_<name>`` if set and non-empty, else ``default``.
|
||||
"""
|
||||
nova_val = os.environ.get(f"NOVA_{name}")
|
||||
if nova_val:
|
||||
return nova_val
|
||||
acdl_val = os.environ.get(f"ACDL_{name}")
|
||||
if acdl_val:
|
||||
return acdl_val
|
||||
val = os.environ.get(f"NOVA_{name}")
|
||||
if val:
|
||||
return val
|
||||
return default
|
||||
@@ -502,6 +502,6 @@ if __name__ == "__main__":
|
||||
# Set both so the dual-read in is_local_tier() finds NOVA_* (preferred);
|
||||
# the ACDL_* alias stays for any unmigrated reader until P5.
|
||||
os.environ["NOVA_LOCAL_TIER"] = "1"
|
||||
os.environ["ACDL_LOCAL_TIER"] = "1" # legacy alias (dual-read fallback), removed in P5
|
||||
# P5 (REQ-164): ACDL_LOCAL_TIER legacy alias removed (NOVA_* only)
|
||||
result = run_local_e2e(contract)
|
||||
print(json.dumps(result, indent=2))
|
||||
@@ -315,13 +315,10 @@ def _load_aws_env() -> Dict[str, str]:
|
||||
continue
|
||||
if "=" in line:
|
||||
k, v = line.split("=", 1)
|
||||
# G-106 binding: dual-read NOVA_* first, ACDL_* fallback.
|
||||
# The .env.secrets keys are renamed to NOVA_* in P2; the
|
||||
# ACDL_* fallback covers operators who haven't rotated
|
||||
# their local .env.secrets yet. Removed in P5.
|
||||
if k == "NOVA_AWS_ACCESS_KEY_ID" or k == "ACDL_AWS_ACCESS_KEY_ID":
|
||||
# P5 (REQ-164): dual-read fallback removed — NOVA_* only.
|
||||
if k == "NOVA_AWS_ACCESS_KEY_ID":
|
||||
env["AWS_ACCESS_KEY_ID"] = v
|
||||
elif k == "NOVA_AWS_SECRET_ACCESS_KEY" or k == "ACDL_AWS_SECRET_ACCESS_KEY":
|
||||
elif k == "NOVA_AWS_SECRET_ACCESS_KEY":
|
||||
env["AWS_SECRET_ACCESS_KEY"] = v
|
||||
elif k == "AWS_DEFAULT_REGION":
|
||||
env["AWS_DEFAULT_REGION"] = v
|
||||
|
||||
@@ -74,7 +74,7 @@ def route_halt_artifact(contract_id: str, violation_reason: str,
|
||||
sns.publish(
|
||||
TopicArn=topic_arn,
|
||||
Message=json.dumps(halt_payload),
|
||||
Subject="ACDL SoD halt",
|
||||
Subject="Nova SoD halt",
|
||||
)
|
||||
print(f"[halt-artifact] SNS published contract={contract_id} "
|
||||
f"topic={topic_arn}", flush=True)
|
||||
|
||||
@@ -65,8 +65,8 @@ def main():
|
||||
|
||||
creds = _load_env(ENV_FILE)
|
||||
# Dual-read: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5).
|
||||
access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID") or creds.get("ACDL_AWS_ACCESS_KEY_ID")
|
||||
secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY") or creds.get("ACDL_AWS_SECRET_ACCESS_KEY")
|
||||
access_key = creds.get("NOVA_AWS_ACCESS_KEY_ID")
|
||||
secret_key = creds.get("NOVA_AWS_SECRET_ACCESS_KEY")
|
||||
region = creds.get("AWS_DEFAULT_REGION", AWS_REGION)
|
||||
if not access_key or not secret_key:
|
||||
print("FAIL: NOVA_AWS_ACCESS_KEY_ID / NOVA_AWS_SECRET_ACCESS_KEY missing",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
# scripts/rotate_spike_key.sh - rotate the nova-spike-runner IAM access key.
|
||||
#
|
||||
# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*, ACDL_BOOTSTRAP_AWS_*
|
||||
# Uses the bootstrap root key (NOVA_BOOTSTRAP_AWS_*
|
||||
# fallback) from the env to:
|
||||
# 1. List nova-spike-runner's access keys.
|
||||
# 2. Create a new key.
|
||||
@@ -24,8 +24,8 @@ ENV_FILE="$ROOT/.env.secrets"
|
||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||
|
||||
# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (removed in P5).
|
||||
: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:-${ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID (or ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID) to the root key}}"
|
||||
: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:-${ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY (or ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY) to the root key}}"
|
||||
: "${NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID:?set NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID to the root key}"
|
||||
: "${NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY:?set NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY to the root key}"
|
||||
REGION="${AWS_DEFAULT_REGION:-us-east-1}"
|
||||
USER_NAME="nova-spike-runner"
|
||||
|
||||
@@ -43,8 +43,8 @@ user = "nova-spike-runner"
|
||||
env_file = os.path.join(os.getcwd(), ".env.secrets")
|
||||
|
||||
# Dual-read bootstrap creds: NOVA_* preferred, ACDL_* fallback (G-106, removed in P5).
|
||||
bootstrap_key = os.environ.get("NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID") or os.environ["ACDL_BOOTSTRAP_AWS_ACCESS_KEY_ID"]
|
||||
bootstrap_secret = os.environ.get("NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY") or os.environ["ACDL_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"]
|
||||
bootstrap_key = os.environ["NOVA_BOOTSTRAP_AWS_ACCESS_KEY_ID"]
|
||||
bootstrap_secret = os.environ["NOVA_BOOTSTRAP_AWS_SECRET_ACCESS_KEY"]
|
||||
|
||||
session = boto3.Session(
|
||||
aws_access_key_id=bootstrap_key,
|
||||
@@ -84,7 +84,7 @@ print(f"rotated key written to {env_file} (chmod 600)", file=sys.stderr)
|
||||
|
||||
# Optionally upload to Gitea secrets.
|
||||
# Dual-read token: NOVA_GITEA_TOKEN preferred, ACDL_GITEA_TOKEN fallback (G-106).
|
||||
gitea_token = os.environ.get("NOVA_GITEA_TOKEN") or os.environ.get("ACDL_GITEA_TOKEN")
|
||||
gitea_token = os.environ.get("NOVA_GITEA_TOKEN")
|
||||
if gitea_token:
|
||||
import urllib.request
|
||||
base = "https://git.cloudinit.dev/api/v1/repos/continuous-intelligence/acdl/actions/secrets"
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
# Usage: run_l2_lifecycle_destroy.sh <module>
|
||||
#
|
||||
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
|
||||
# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until
|
||||
# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for
|
||||
# P5) to point to the CI VPC state.
|
||||
#
|
||||
# NOTE: unlike the L1 scripts (run_lifecycle_destroy.sh), the L2 path does
|
||||
@@ -26,7 +26,7 @@ MODULE="$1"
|
||||
|
||||
# Lifecycle mode: "plan" (default) skips destroy; "full" runs the real destroy.
|
||||
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
if [ "$LIFECYCLE_MODE" != "full" ]; then
|
||||
echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0"
|
||||
@@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/complex.yml"
|
||||
# Point terraform_remote_state to the CI VPC state (not the platform VPC).
|
||||
# Set both NOVA_* (preferred) and ACDL_* (legacy fallback) until P5.
|
||||
export NOVA_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate"
|
||||
export ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5
|
||||
|
||||
|
||||
# Run the platform lifecycle destroy command
|
||||
bash scripts/run_platform.sh --destroy "$CONTRACT"
|
||||
@@ -4,7 +4,7 @@
|
||||
# Usage: run_l2_lifecycle_test.sh <module> <example>
|
||||
#
|
||||
# Wraps run_platform.sh for L2 composition modules in the modules-lifecycle
|
||||
# pipeline. Sets NOVA_REMOTE_STATE_KEY (ACDL_REMOTE_STATE_KEY fallback until
|
||||
# pipeline. Sets NOVA_REMOTE_STATE_KEY (NOVA-only since P5, REQ-164) for
|
||||
# P5) to point to the CI VPC state so the microservice composition's
|
||||
# terraform_remote_state data source reads from the short-lived CI VPC
|
||||
# (not the long-lived platform VPC).
|
||||
@@ -30,7 +30,7 @@ EXAMPLE="$2" # simple or complex
|
||||
|
||||
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
|
||||
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
|
||||
|
||||
@@ -38,7 +38,7 @@ CONTRACT="modules/l2/${MODULE}/examples/${EXAMPLE}.yml"
|
||||
# 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 ACDL_REMOTE_STATE_KEY="spike/ci-vpc/terraform.tfstate" # legacy fallback, removed in P5
|
||||
|
||||
|
||||
# Run the platform lifecycle command (plan-only by default; full = apply).
|
||||
if [ "$LIFECYCLE_MODE" = "full" ]; then
|
||||
|
||||
@@ -21,7 +21,7 @@ CI_VPC_OUTPUTS="${2:-}"
|
||||
# Lifecycle mode: "plan" (default) skips destroy (nothing was applied);
|
||||
# "full" runs the real terraform destroy.
|
||||
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
if [ "$LIFECYCLE_MODE" != "full" ]; then
|
||||
echo "lifecycle mode=$LIFECYCLE_MODE — nothing to destroy (plan-only run), exiting 0"
|
||||
|
||||
@@ -27,7 +27,7 @@ CI_VPC_OUTPUTS="${3:-}"
|
||||
|
||||
# Lifecycle mode: "plan" (default, fast) or "full" (real apply against AWS).
|
||||
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-${ACDL_LIFECYCLE_MODE:-plan}}"
|
||||
LIFECYCLE_MODE="${NOVA_LIFECYCLE_MODE:-plan}"
|
||||
|
||||
CONTRACT="modules/l1/${MODULE}/examples/${EXAMPLE}.yml"
|
||||
|
||||
|
||||
+10
-12
@@ -121,9 +121,9 @@ if [ "$LOCAL_TIER" = "1" ]; then
|
||||
echo "=== Nova Local Emulating Tier (D-092) ==="
|
||||
echo "contract: $CONTRACT (no AWS credentials required)"
|
||||
echo ""
|
||||
# Dual-read: set NOVA_LOCAL_TIER (preferred); ACDL_LOCAL_TIER fallback
|
||||
# P5 (REQ-164): NOVA_* only (dual-read fallback removed)
|
||||
# kept for any unmigrated reader until P5 (removed in P5).
|
||||
NOVA_LOCAL_TIER=1 ACDL_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \
|
||||
NOVA_LOCAL_TIER=1 python3 core/local_emulators.py "$CONTRACT" \
|
||||
|| fail "local E2E failed"
|
||||
echo ""
|
||||
echo "=== LOCAL E2E OK ==="
|
||||
@@ -287,14 +287,12 @@ if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
|
||||
[ -f "$ENV_FILE" ] || fail ".env.secrets missing (run scripts/rotate_spike_key.sh) or set AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY env vars"
|
||||
set -a
|
||||
. "$ENV_FILE"
|
||||
set +a
|
||||
# G-106 binding: dual-read NOVA_* first, ACDL_* fallback. The .env.secrets
|
||||
# keys are renamed to NOVA_* in P2; the ACDL_* fallback covers operators
|
||||
# who haven't rotated their local .env.secrets yet. Removed in P5.
|
||||
export AWS_ACCESS_KEY_ID="${NOVA_AWS_ACCESS_KEY_ID:-$ACDL_AWS_ACCESS_KEY_ID}"
|
||||
export AWS_SECRET_ACCESS_KEY="${NOVA_AWS_SECRET_ACCESS_KEY:-$ACDL_AWS_SECRET_ACCESS_KEY}"
|
||||
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
|
||||
fi
|
||||
set +a
|
||||
# P5 (REQ-164): dual-read fallback removed — NOVA_* only.
|
||||
export AWS_ACCESS_KEY_ID="$NOVA_AWS_ACCESS_KEY_ID"
|
||||
export AWS_SECRET_ACCESS_KEY="$NOVA_AWS_SECRET_ACCESS_KEY"
|
||||
export AWS_DEFAULT_REGION="$AWS_DEFAULT_REGION"
|
||||
fi
|
||||
|
||||
echo "=== Step 4: terraform init + validate + plan -lock=false (real AWS) ==="
|
||||
cd "$TF_DIR"
|
||||
@@ -349,7 +347,7 @@ if ok:
|
||||
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" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the apply" >&2; 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
|
||||
@@ -465,7 +463,7 @@ if ok:
|
||||
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" ACDL_HITL_CONTRACT_ID="$CONTRACT_ID" ACDL_HITL_ENV="$RESOLVED_ENV" ACDL_HITL_APPROVER="$APPROVER" || { echo "FAIL: HITL attestation gate blocked the promotion" >&2; 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
|
||||
|
||||
@@ -20,7 +20,7 @@ cd "$ROOT"
|
||||
|
||||
echo "=== Nova Regression VERIFY (D-091) ==="
|
||||
# Dual-read: NOVA_* preferred, ACDL_* fallback (removed in P5).
|
||||
echo "milestone: ${NOVA_REGRESSION_MILESTONE:-${ACDL_REGRESSION_MILESTONE:-v1.10}} phase: ${NOVA_REGRESSION_PHASE:-${ACDL_REGRESSION_PHASE:-52}}"
|
||||
echo "milestone: ${NOVA_REGRESSION_MILESTONE:-v1.10} phase: ${NOVA_REGRESSION_PHASE:-52}"
|
||||
echo ""
|
||||
|
||||
python3 core/regression_verify.py
|
||||
|
||||
+25
-25
@@ -1,14 +1,11 @@
|
||||
"""Unit tests for the dual-read env helper (core/env.py, D-108, REQ-159).
|
||||
"""Unit tests for the NOVA-only env helper (core/env.py, D-108, REQ-164).
|
||||
|
||||
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.
|
||||
P5 (REQ-164) removed the ACDL_* dual-read fallback. `get_env` now reads
|
||||
`NOVA_*` only. These tests verify:
|
||||
- only NOVA_* set → returned
|
||||
- neither set → default returned
|
||||
- ACDL_* set but NOVA_* unset → default returned (fallback REMOVED)
|
||||
- blank NOVA_* → default returned (not the ACDL_* value)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -27,30 +24,33 @@ def _isolate_env(monkeypatch):
|
||||
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.
|
||||
def test_only_acdl_set_no_fallback(monkeypatch):
|
||||
# P5 (REQ-164): ACDL_* fallback removed — ACDL_* alone returns default.
|
||||
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
|
||||
assert env.get_env("AWS_ACCOUNT_ID") is None
|
||||
assert env.get_env("AWS_ACCOUNT_ID", default="fallback") == "fallback"
|
||||
|
||||
|
||||
def test_both_set_nova_wins(monkeypatch):
|
||||
# NOVA_* takes precedence; ACDL_* is ignored.
|
||||
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_blank_nova_returns_default_not_acdl(monkeypatch):
|
||||
# An explicitly-empty NOVA key returns default (not an ACDL_* value).
|
||||
monkeypatch.setenv("NOVA_AWS_ACCOUNT_ID", "")
|
||||
monkeypatch.setenv("ACDL_AWS_ACCOUNT_ID", "acdl-value")
|
||||
assert env.get_env("AWS_ACCOUNT_ID") == "acdl-value"
|
||||
assert env.get_env("AWS_ACCOUNT_ID") is None
|
||||
assert env.get_env("AWS_ACCOUNT_ID", default="d") == "d"
|
||||
@@ -32,15 +32,15 @@ class TestLifecycleModeFlag:
|
||||
"""REQ-134: the lifecycle scripts dispatch to plan-only by default."""
|
||||
|
||||
@pytest.mark.parametrize("script", SCRIPTS)
|
||||
def test_script_reads_acdl_lifecycle_mode(self, script):
|
||||
"""Every lifecycle script reads NOVA_LIFECYCLE_MODE (dual-read with
|
||||
ACDL_LIFECYCLE_MODE fallback) with a 'plan' default."""
|
||||
def test_script_reads_nova_lifecycle_mode(self, script):
|
||||
"""Every lifecycle script reads NOVA_LIFECYCLE_MODE (NOVA-only since
|
||||
P5, REQ-164 — dual-read fallback removed) with a 'plan' default."""
|
||||
src = _read(script)
|
||||
# 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'"
|
||||
# P5 (REQ-164): NOVA_* only (dual-read fallback removed).
|
||||
assert "${NOVA_LIFECYCLE_MODE:-plan}" in src, \
|
||||
f"{script} must read NOVA_LIFECYCLE_MODE defaulting to 'plan'"
|
||||
assert "ACDL_LIFECYCLE_MODE" not in src, \
|
||||
f"{script} must not reference ACDL_LIFECYCLE_MODE (P5 removed the fallback)"
|
||||
assert "LIFECYCLE_MODE=" in src, \
|
||||
f"{script} must assign LIFECYCLE_MODE from the env var"
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class TestPublishToSsm:
|
||||
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", "alias/aws/ssm")
|
||||
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
|
||||
|
||||
with mock_aws():
|
||||
ssm = boto3.client("ssm", region_name="us-east-1")
|
||||
@@ -67,7 +67,7 @@ class TestPublishToSsm:
|
||||
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", "alias/aws/ssm")
|
||||
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
|
||||
|
||||
with mock_aws():
|
||||
ssm = boto3.client("ssm", region_name="us-east-1")
|
||||
@@ -82,7 +82,7 @@ class TestPublishToSsm:
|
||||
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", "alias/aws/ssm")
|
||||
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
|
||||
|
||||
with mock_aws():
|
||||
ssm = boto3.client("ssm", region_name="us-east-1")
|
||||
@@ -105,7 +105,7 @@ class TestPublishToSsm:
|
||||
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", "alias/aws/ssm")
|
||||
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
|
||||
|
||||
with mock_aws():
|
||||
ssm = boto3.client("ssm", region_name="us-east-1")
|
||||
@@ -123,7 +123,7 @@ class TestPublishToSsm:
|
||||
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", "alias/aws/ssm")
|
||||
monkeypatch.setenv("NOVA_KMS_KEY_ID", "alias/aws/ssm")
|
||||
|
||||
with mock_aws():
|
||||
ssm = boto3.client("ssm", region_name="us-east-1")
|
||||
@@ -377,7 +377,7 @@ class TestCli:
|
||||
|
||||
class TestKmsFailLoud:
|
||||
"""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,
|
||||
(P2 renamed from NOVA_KMS_KEY_ID; dual-read NOVA_* preferred,
|
||||
ACDL_* fallback until P5)."""
|
||||
|
||||
def test_kms_unset_raises(self, monkeypatch):
|
||||
@@ -389,9 +389,9 @@ class TestKmsFailLoud:
|
||||
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_KMS_KEY_ID", raising=False)
|
||||
monkeypatch.delenv("NOVA_ALLOW_DEFAULT_KMS", 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="NOVA_KMS_KEY_ID is not set"):
|
||||
@@ -405,7 +405,7 @@ class TestKmsFailLoud:
|
||||
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.delenv("NOVA_KMS_KEY_ID", raising=False)
|
||||
monkeypatch.setenv("NOVA_ALLOW_DEFAULT_KMS", "1")
|
||||
|
||||
with mock_aws():
|
||||
|
||||
@@ -24,7 +24,7 @@ def test_route_halt_publishes_to_sns_when_arn_set(monkeypatch):
|
||||
assert call.kwargs["TopicArn"] == "arn:aws:sns:us-east-1:000000000000:nova-sod-halt"
|
||||
assert "contract-123" in call.kwargs["Message"]
|
||||
assert "SEPARATION_OF_DUTIES_VIOLATION" in call.kwargs["Message"]
|
||||
assert call.kwargs["Subject"] == "ACDL SoD halt"
|
||||
assert call.kwargs["Subject"] == "Nova SoD halt"
|
||||
|
||||
|
||||
def test_route_halt_falls_back_to_stderr_when_arn_unset(monkeypatch, capsys):
|
||||
|
||||
Reference in New Issue
Block a user