feat(P2): env-transition detect-and-destroy — REQ-282..287
acdl-ci / Lint (push) Successful in 10s
acdl-ci / Test (push) Failing after 24s
acdl-ci / Platform check-only (offline) (push) Successful in 24s
Nova Slides Render / render (push) Failing after 24s

- core/env_transition.py: detect_prior_env() + record_applied_env() via DynamoDB nova-contracts table (REQ-282,283)
- scripts/run_platform.sh Step 0b: detect env change, destroy prior env (deletion_protection=false, terraform init -reconfigure + destroy), emit ENV_DESTROYED evidence event, fail closed on destroy failure (REQ-284)
- scripts/run_platform.sh: record applied env after successful apply (REQ-285)
- .github/workflows/deploy.yml: pass NOVA_CONSUMER_REPO to run_platform.sh (REQ-286)
- adapters/terraform/adapter.py: doc comment on env-scoped state key (REQ-287)

No orphan path: if destroy fails, pipeline exits non-zero (no apply runs).

---ci---
project: acdl
phase: 2
milestone: v1.24
status: execute
requirements: [REQ-282,REQ-283,REQ-284,REQ-285,REQ-286,REQ-287]
---/ci---
This commit is contained in:
Jon Chery
2026-08-12 14:30:24 +00:00
parent 25427250ad
commit d069654367
4 changed files with 250 additions and 0 deletions
+86
View File
@@ -215,6 +215,7 @@ stream() {
}
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}" # spike UUID (override via NOVA_CONTRACT_ID)
CONSUMER_REPO="${NOVA_CONSUMER_REPO:-${GITHUB_REPOSITORY:-unknown}}" # v1.24 (REQ-284/285): for env-transition detect/record
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
TF_DIR="$WORK/tf"
rm -rf "$WORK"; mkdir -p "$TF_DIR"
@@ -238,6 +239,82 @@ else
}
fi
# v1.24 (REQ-284): Step 0b — environment-transition check.
# Detect if the contract's environment changed on a known contract.id
# (Shape A promotion). If so, destroy the prior env's resources before
# building the new env. No orphan path — fail closed if destroy fails.
# Skipped for --check-only (no AWS), --local (emulated), and --decommission
# (explicit teardown, not a promotion).
if [ "$CHECK_ONLY" = "0" ] && [ "$LOCAL_TIER" = "0" ] && [ "$DECOMMISSION" = "0" ]; then
RESOLVED_ENV_FOR_DETECT=$(python3 -c "import yaml; print(yaml.safe_load(open('$CONTRACT')).get('environment','dev'))" 2>/dev/null || echo "dev")
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
RESOLVED_ENV_FOR_DETECT="$ENVIRONMENT_OVERRIDE"
fi
echo ""
echo "=== Step 0b: environment-transition check ==="
echo "consumer_repo=$CONSUMER_REPO contract_id=$CONTRACT_ID new_env=$RESOLVED_ENV_FOR_DETECT"
PRIOR_ENV=$(python3 core/env_transition.py detect \
--contract-id "$CONTRACT_ID" \
--consumer-repo "$CONSUMER_REPO" \
--new-env "$RESOLVED_ENV_FOR_DETECT" 2>/dev/null | python3 -c "import json,sys; print(json.load(sys.stdin).get('prior_env') or '')" 2>/dev/null || echo "")
if [ -n "$PRIOR_ENV" ]; then
echo "ENV TRANSITION DETECTED: $PRIOR_ENV -> $RESOLVED_ENV_FOR_DETECT"
echo "Destroying prior env '$PRIOR_ENV' resources before building new env (no orphan path)..."
# Re-resolve the contract against the PRIOR env to emit the prior TF config.
# Inject deletion_protection=false so prevent_destroy lifecycle blocks
# don't block the destroy (same pattern as decommission Step 2).
python3 -c "
import json, sys, yaml, copy
sys.path.insert(0, '$ROOT')
from core.contract_resolver import resolve
contract = yaml.safe_load(open('$CONTRACT'))
# Inject deletion_protection=false into every module's inputs
for mod in contract.get('infrastructure', {}).values():
mod.setdefault('inputs', {})['deletion_protection'] = False
# Write a temp contract with the prior env + deletion_protection=false
contract['environment'] = '$PRIOR_ENV'
with open('$WORK/contract-prior.yml', 'w') as f:
yaml.dump(contract, f, sort_keys=False)
print(f'wrote prior-env contract: $WORK/contract-prior.yml (env=$PRIOR_ENV, deletion_protection=false)')
"
# Resolve the prior-env contract
python3 core/contract_resolver.py "$WORK/contract-prior.yml" "$WORK/stack-prior.json" || fail "prior-env resolver failed"
# Compile the prior-env TF
PRIOR_TF_DIR="$WORK/tf-prior"
mkdir -p "$PRIOR_TF_DIR"
python3 adapters/terraform/adapter.py "$WORK/stack-prior.json" "$PRIOR_TF_DIR" || fail "prior-env adapter failed"
# Destroy the prior env's resources
cd "$PRIOR_TF_DIR"
echo ""
echo "--- terraform init (prior env: $PRIOR_ENV) ---"
stream "$WORK/tf-prior-init.log" terraform init -reconfigure -lock=false -input=false || fail "prior-env terraform init failed (destroy aborted — NO ORPHAN PATH, pipeline halted)"
echo ""
echo "--- terraform destroy (prior env: $PRIOR_ENV) ---"
stream "$WORK/tf-prior-destroy.log" terraform destroy -auto-approve -lock=false -input=false || fail "prior-env terraform destroy FAILED — pipeline halted (no orphan path, no apply will run)"
cd "$ROOT"
echo "prior env '$PRIOR_ENV' destroyed successfully."
# Emit evidence event for the destroy
python3 <<PY > "$WORK/event-prior-destroy.json" 2>/dev/null || true
import json, datetime
event = {
"contractId": "$CONTRACT_ID",
"eventType": "ENV_DESTROYED",
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"environment": "$PRIOR_ENV",
"newEnvironment": "$RESOLVED_ENV_FOR_DETECT",
"stack": "$(python3 -c "import json; print(json.load(open('$WORK/stack-prior.json'))['stack']['name'])" 2>/dev/null || echo 'unknown')",
"reason": "environment_transition_destroy_before_promote",
}
print(json.dumps(event, indent=2))
PY
if [ -f "$WORK/event-prior-destroy.json" ]; then
python3 core/outbox_writer.py "$WORK/event-prior-destroy.json" > "$WORK/outbox-prior-destroy.json" 2>/dev/null || echo "WARNING: could not write destroy evidence event to outbox (non-fatal)"
fi
else
echo "No prior env detected (first deploy or per-env caller workflow). Proceeding normally."
fi
fi
echo "=== Step 1: validate contract against contract.schema.json ==="
[ -f "$CONTRACT" ] || fail "contract file $CONTRACT missing"
python3 -c "
@@ -368,6 +445,10 @@ if [ "$APPLY_ONLY" = "1" ]; then
echo "--- terraform outputs ---"
terraform output -json 2>/dev/null || true
cd "$ROOT"
# v1.24 (REQ-285): record the applied env so future runs can detect transitions.
if [ -n "$RESOLVED_ENV" ]; then
python3 core/env_transition.py record --contract-id "$CONTRACT_ID" --consumer-repo "$CONSUMER_REPO" --env "$RESOLVED_ENV" 2>/dev/null || true
fi
echo ""
echo "=== PLATFORM APPLY OK ==="
exit 0
@@ -523,6 +604,11 @@ echo ""
# G-112: sourced (shared env) — the block references CONTRACT/WORK/DEPLOY_UPTIME.
source "$ROOT/scripts/run_uptime.sh"
# v1.24 (REQ-285): record the applied env so future runs can detect transitions.
if [ -n "$RESOLVED_ENV" ]; then
python3 core/env_transition.py record --contract-id "$CONTRACT_ID" --consumer-repo "$CONSUMER_REPO" --env "$RESOLVED_ENV" 2>/dev/null || true
fi
echo ""
echo "=== PLATFORM E2E OK ==="
echo "contract -> resolver -> stack -> Checkov(static) -> terraform plan -> Wiz-or-Checkov(plan) -> confidence ($BAND) -> outbox -> outputs"