feat(P4): transparent terraform + feature flags + run_platform.sh split (REQ-233..238)

Create run_codegen.sh (pre-TF: env check, validate, resolve, adapt).
Create run_postapply.sh (post-TF: Checkov, confidence, HITL, outbox, SSM, uptime).
Add variable 'enabled' (bool, default true) + count=var.enabled?1:0 to all 12
L1 modules (alb, cloudfront, ecr, ecs-cluster, ecs-service, iam-role, kms-key,
rds, s3, uptime, vpc, waf). Fix all cross-resource references with [0] indexing.
Update interface.json for all modules to declare 'enabled' input.
Fix stale artifact path /tmp/acdl_platform_run_v18 → /tmp/nova_platform_run (REQ-238).
run_platform.sh remains as backward-compat shim for local-dev usage.

---ci---
project: acdl
phase: 4
milestone: v1.20
status: execute
requirements: [REQ-233, REQ-234, REQ-235, REQ-236, REQ-237, REQ-238]
---/ci---
This commit is contained in:
Jon Chery
2026-08-07 18:49:56 +00:00
parent ed5ea90654
commit 0ca383dae6
52 changed files with 667 additions and 79 deletions
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env bash
# scripts/run_postapply.sh — post-Terraform steps for the Nova platform pipeline.
#
# Performs steps 59 of run_platform.sh (after terraform apply/destroy):
# 5. Checkov policy scan on the emitted main.tf
# 6. Checkov adapter → PolicyCheckResult (compliance details)
# 7. Confidence signal compute
# 7b. HITL attestation gate (qa/prod/dr only)
# 8. Write evidence event to DynamoDB outbox
# 9. Publish outputs to SSM + GitHub PR comment
# 9b. Uptime monitoring sub-deploy (sourced from run_uptime.sh)
#
# Expects the work dir ($NOVA_WORK_DIR) to already contain:
# - tf/*.tf (from run_codegen.sh)
# - stack.json (from run_codegen.sh)
# And terraform to have already run (init/validate/plan/apply) in $WORK/tf/.
#
# Usage:
# run_postapply.sh <contract.yml> [--environment <name>] [--quiet] [--deploy-uptime]
#
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
CALLER_CWD="$(pwd)"
cd "$ROOT"
QUIET=0
DEPLOY_UPTIME=0
ENVIRONMENT_OVERRIDE=""
CONTRACT=""
while [ $# -gt 0 ]; do
case "$1" in
--quiet) QUIET=1; shift ;;
--deploy-uptime) DEPLOY_UPTIME=1; shift ;;
--environment) shift; ENVIRONMENT_OVERRIDE="$1"; shift ;;
--environment=*) ENVIRONMENT_OVERRIDE="${1#--environment=}"; shift ;;
-h|--help)
echo "Usage: run_postapply.sh <contract.yml> [--environment <name>] [--quiet] [--deploy-uptime]"
exit 0 ;;
*) CONTRACT="$1"; shift ;;
esac
done
[ -n "$CONTRACT" ] || { echo "FAIL: no contract file specified" >&2; exit 1; }
if ! [[ "$CONTRACT" = /* ]]; then
CONTRACT="$CALLER_CWD/$CONTRACT"
fi
[ -f "$CONTRACT" ] || { echo "FAIL: contract not found: $CONTRACT" >&2; exit 1; }
if [ -n "$ENVIRONMENT_OVERRIDE" ]; then
export NOVA_ENVIRONMENT_OVERRIDE="$ENVIRONMENT_OVERRIDE"
fi
CONTRACT_ID="${NOVA_CONTRACT_ID:-11111111-1111-1111-1111-111111111111}"
WORK="${NOVA_WORK_DIR:-/tmp/nova_platform_run}"
TF_DIR="$WORK/tf"
[ -d "$TF_DIR" ] || { echo "FAIL: work dir $TF_DIR not found (run run_codegen.sh first)" >&2; exit 1; }
stream() {
local log="$1"; shift
if [ "$QUIET" = "1" ]; then
"$@" > "$log" 2>&1
else
"$@" 2>&1 | tee "$log"
fi
}
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:-${FORGE_ACTOR:-}}"
if [ -z "$_approver" ]; then
echo "WARNING: no approver identity (GITHUB_ACTOR/FORGE_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
ok, reason = attest('$_cid', '$_env', '$_approver')
if ok:
print(f'HITL: {reason}')
else:
print(f'HITL BLOCK: {reason}', file=sys.stderr)
sys.exit(1)
" || { echo "FAIL: HITL gate blocked" >&2; return 1; }
}
echo "=== Step 5: run Checkov on $TF_DIR/main.tf ==="
if [ "$QUIET" = "0" ]; then
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov.json"
else
checkov -f "$TF_DIR/main.tf" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov.json" 2> "$WORK/checkov.err"
fi
[ -s "$WORK/checkov.json" ] || { echo "FAIL: checkov produced no output" >&2; exit 1; }
echo ""
echo "checkov summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")"
echo ""
echo "=== Step 6: Checkov adapter → PolicyCheckResult (compliance details) ==="
python3 adapters/terraform/policy/checkov_adapter.py "$WORK/checkov.json" "$CONTRACT_ID" > "$WORK/pcr.json" || { echo "FAIL: checkov adapter failed" >&2; exit 1; }
python3 -c "
import json
pcrs = json.load(open('$WORK/pcr.json'))
print(f'PolicyCheckResult: {len(pcrs)} record(s)')
print()
for pcr in pcrs:
sev = pcr.get('severity', 'info')
res = pcr.get('result', 'unknown')
rule = pcr.get('ruleId', 'unknown')
msg = pcr.get('message', '')
marker = 'PASS' if res == 'pass' else 'FAIL' if res == 'fail' else 'SKIP' if res == 'skipped' else res.upper()
print(f' [{marker}] {sev:8s} {rule:30s} {msg}')
"
echo ""
echo "=== Step 7: confidence signal compute ==="
python3 <<PY > "$WORK/signal.json" || { echo "FAIL: confidence signal failed" >&2; exit 1; }
import json
import core.confidence_signal as c
pcr = json.load(open("$WORK/pcr.json"))
inputs = {
"policy": pcr,
"validation": {"schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True},
"freshness": {"age_days": 0, "max_age_days": 7},
"source": {"submitter": "consumer", "commit_sha": "consumer-sha", "signed": False},
"history": {"prior_rollbacks": 0, "prior_policy_fails": 0},
"nfrs": {"conformance": None},
}
sig = c.compute("$CONTRACT_ID", "dev", inputs)
print(json.dumps({"score": sig.score, "band": sig.band, "perInput": sig.perInput, "reasonCodes": sig.reasonCodes}, indent=2))
PY
BAND=$(python3 -c "import json; print(json.load(open('$WORK/signal.json'))['band'])")
SCORE=$(python3 -c "import json; print(round(json.load(open('$WORK/signal.json'))['score'],3))")
echo "confidence: score=$SCORE band=$BAND"
[ "$BAND" = "pass" ] || { echo "FAIL: confidence band is $BAND, expected pass for dev" >&2; exit 1; }
echo ""
echo "=== Step 7b: HITL attestation gate (qa/prod/dr only) ==="
RESOLVED_ENV=$(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="$ENVIRONMENT_OVERRIDE"
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 ==="
STACK_NAME=$(python3 -c "import json; print(json.load(open('$WORK/stack.json'))['stack']['name'])")
python3 <<PY > "$WORK/event.json" || { echo "FAIL: event build failed" >&2; exit 1; }
import json, datetime
sig = json.load(open("$WORK/signal.json"))
event = {
"contractId": "$CONTRACT_ID",
"eventType": "CONFIDENCE_COMPUTED",
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
"environment": "dev",
"stack": "$STACK_NAME",
"score": sig["score"],
"band": sig["band"],
"prev_event_hash": "GENESIS",
}
print(json.dumps(event, indent=2))
PY
python3 core/outbox_writer.py "$WORK/event.json" > "$WORK/outbox_item.json" || { echo "FAIL: outbox write failed" >&2; exit 1; }
echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.json')); print('contractId=', d['contractId'], 'hash=', d['hash'][:16]+'...')")"
echo ""
echo "=== Step 9: publish outputs to SSM + GitHub PR comment ==="
cd "$TF_DIR"
TF_OUTPUTS=$(terraform output -json 2>/dev/null || echo "{}")
cd "$ROOT"
python3 <<PY > "$WORK/outputs_step.json" 2>/dev/null || true
import json, sys
sys.path.insert(0, "$ROOT")
from core.output_publisher import publish_to_ssm, format_comment, post_github_comment
tf_raw = json.loads('''$TF_OUTPUTS''')
outputs = {k: v.get("value") if isinstance(v, dict) else v for k, v in tf_raw.items()}
ssm_results = publish_to_ssm(outputs, "dev", "$CONTRACT_ID")
comment = format_comment(outputs, "dev", "$CONTRACT_ID", ssm_results)
posted = post_github_comment(comment)
print(json.dumps({"ssm": ssm_results, "posted": posted, "comment": comment}))
PY
if [ -f "$WORK/outputs_step.json" ]; then
echo "outputs published to SSM: $(python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(len([v for v in d.get('ssm',{}).values() if v]), 'parameters')" 2>/dev/null || echo "done")"
if [ "$QUIET" = "0" ]; then
python3 -c "import json; d=json.load(open('$WORK/outputs_step.json')); print(d.get('comment',''))" 2>/dev/null || true
fi
fi
echo ""
# Uptime monitoring: sourced from run_uptime.sh
source "$ROOT/scripts/run_uptime.sh"
echo ""
echo "=== POST-APPLY OK ==="
echo "Checkov → confidence ($BAND) → outbox → outputs → uptime"