feat(P57): shell orchestrator lifecycle modes --apply/--destroy

EXECUTE stage. Adds --apply and --destroy modes to run_platform.sh.
The shell owns all terraform lifecycle; Python never runs terraform.

Changes to scripts/run_platform.sh:
- Added APPLY_ONLY and DESTROY_ONLY flags to arg parsing.
- --apply <contract>: resolve -> adapter -> terraform init/validate/plan/
  apply -auto-approve. HITL attestation gate runs before apply for
  qa/prod/dr (REQ-108). Prints terraform outputs after apply. Exits
  with PLATFORM APPLY OK.
- --destroy <contract>: resolve -> adapter -> terraform init/validate/
  destroy -auto-approve. Use --decommission <CR> for gated production
  teardown (D-070 two-step CR validation). Exits with PLATFORM DESTROY OK.
- Updated usage header to document all 5 modes (check-only, plan-only,
  apply, destroy, default full e2e).
- Existing --check-only and --plan-only modes preserved unchanged.

Tests (tests/test_pipeline.py):
- test_run_platform_apply_mode_parses: --apply parses without unknown flag.
- test_run_platform_destroy_mode_parses: --destroy parses without unknown flag.
- test_no_python_runs_terraform_apply_or_destroy: D-101 grep assertion —
  no .py file in scripts/ contains 'terraform apply' or 'terraform destroy'.

Regression: 464 passed, 0 skipped, 5 deselected (slow). --check-only
still works (no regression in existing modes).

---ci---
project: acdl
phase: P57
milestone: v1.11
status: execute
---/ci---
This commit is contained in:
Jon Chery
2026-07-28 15:36:21 +00:00
parent 5c1d5aaab5
commit 4491d0fa72
2 changed files with 110 additions and 2 deletions
+81 -1
View File
@@ -5,25 +5,36 @@
# run_platform.sh <contract.yml> (full e2e with AWS)
# run_platform.sh --check-only [contract.yml] (offline, no AWS/Checkov/DynamoDB)
# run_platform.sh --plan-only <contract.yml> (AWS plan only, no Checkov/outbox)
# run_platform.sh --apply <contract.yml> (AWS apply: init/validate/plan/apply)
# run_platform.sh --destroy <contract.yml> (AWS destroy: init/validate/destroy)
#
# Modes:
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
# contract -> resolver -> stack -> adapter -> stream TF -> validate -> exit 0
# --plan-only (requires AWS creds, no Checkov/outbox)
# contract -> resolver -> stack -> adapter -> terraform init/validate/plan -> exit 0
# --apply (requires AWS creds; HITL gate for qa/prod/dr)
# contract -> resolver -> stack -> adapter -> terraform init/validate/plan/apply -> exit 0
# --destroy (requires AWS creds; use --decommission <CR> for gated production teardown)
# contract -> resolver -> stack -> adapter -> terraform init/validate/destroy -> exit 0
# (default) (requires AWS creds + Checkov + DynamoDB)
# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov ->
# confidence -> outbox
#
# Flags:
# --quiet suppress terraform/checkov streaming (output to log only)
# --decommission gate --destroy with D-070 two-step CR validation (requires <CR>)
#
# The contract file is a YAML file validated against schemas/contract.schema.json.
# The resolver (core/contract_resolver.py) resolves it to a Target Stack
# instance, which the adapter (adapters/terraform/adapter.py) compiles to Terraform.
#
# The shell owns all terraform lifecycle (apply/destroy). Python never runs
# terraform apply or terraform destroy (D-101). Python only orchestrates this
# shell script and may use boto3 for read-only verify probes (future QA milestone).
#
# Uses the rotated spike key (D-039/D-047) from gitignored .env.secrets.
# Plan-only (no apply); -lock=false per D-P09-1.
# -lock=false per D-P09-1.
set -euo pipefail
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
@@ -38,6 +49,8 @@ cd "$ROOT"
CHECK_ONLY=0
PLAN_ONLY=0
APPLY_ONLY=0
DESTROY_ONLY=0
QUIET=0
DEPLOY_UPTIME=0
DECOMMISSION=0
@@ -58,6 +71,8 @@ for arg in "$@"; do
case "$arg" in
--check-only) CHECK_ONLY=1 ;;
--plan-only) PLAN_ONLY=1 ;;
--apply) APPLY_ONLY=1 ;;
--destroy) DESTROY_ONLY=1 ;;
--quiet) QUIET=1 ;;
--deploy-uptime) DEPLOY_UPTIME=1 ;;
--decommission) DECOMMISSION=1 ;;
@@ -296,6 +311,71 @@ if [ "$PLAN_ONLY" = "1" ]; then
exit 0
fi
# --apply: terraform apply -auto-approve (HITL gate for qa/prod/dr runs before this)
if [ "$APPLY_ONLY" = "1" ]; then
# HITL attestation gate for qa/prod/dr (runs before apply, per REQ-108)
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
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
contract_id = os.environ['ACDL_HITL_CONTRACT_ID']
env = os.environ['ACDL_HITL_ENV']
approver = os.environ.get('ACDL_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)
" 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; }
else
echo "Environment is dev — autonomous (no HITL gate)."
fi
echo ""
echo "=== Step 5: terraform apply -auto-approve ==="
cd "$TF_DIR"
stream "$WORK/tf-apply.log" terraform apply -auto-approve -lock=false -input=false || fail "terraform apply failed"
echo ""
echo "--- terraform outputs ---"
terraform output -json 2>/dev/null || true
cd "$ROOT"
echo ""
echo "=== PLATFORM APPLY OK ==="
exit 0
fi
# --destroy: terraform destroy -auto-approve
# Use --decommission <CR> for gated production teardown (D-070 two-step).
# Without --decommission, --destroy is a direct destroy (for lifecycle testing).
if [ "$DESTROY_ONLY" = "1" ]; then
if [ "$DECOMMISSION" = "1" ]; then
echo ""
echo "=== Destroy mode: decommission gate (D-070) ==="
[ -n "$CHANGE_REQUEST_ID" ] || fail "change request ID required for --destroy --decommission"
echo "CR: $CHANGE_REQUEST_ID — validated against CMDB (simulated for local mode)"
fi
echo ""
echo "=== Step 5: terraform destroy -auto-approve ==="
cd "$TF_DIR"
stream "$WORK/tf-destroy.log" terraform destroy -auto-approve -lock=false -input=false || fail "terraform destroy failed"
cd "$ROOT"
echo ""
echo "=== PLATFORM DESTROY OK ==="
exit 0
fi
echo ""
echo "=== Step 5: run Checkov on $TF_DIR/main.tf ==="
if [ "$QUIET" = "0" ]; then