#!/usr/bin/env bash # scripts/run_codegen.sh — pre-Terraform codegen for the Nova platform pipeline. # # Performs steps 0–3b of run_platform.sh: # 0. Environment onboarding check # 1. Validate contract against contract.schema.json # 2. Resolve contract → Target Stack instance (contract_resolver.py) # 3. Adapter compiles stack → Terraform (adapter.py) # 3b. Structural validation of emitted TF (offline) # # Emits Terraform files to $NOVA_WORK_DIR/tf/{main.tf,terraform.tf,providers.tf} # and prints the work dir path for the caller (workflow or run_platform.sh) # to use for native terraform init/validate/plan/apply steps. # # Usage: # run_codegen.sh [--environment ] [--check-only] # # When --check-only is passed, exits 0 after structural validation (no AWS). # Otherwise, loads AWS credentials from .env.secrets if not already set, # and exits 0 with the work dir ready for terraform. # set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" CALLER_CWD="$(pwd)" cd "$ROOT" CHECK_ONLY=0 ENVIRONMENT_OVERRIDE="" CONTRACT="" while [ $# -gt 0 ]; do case "$1" in --check-only) CHECK_ONLY=1; shift ;; --environment) shift; ENVIRONMENT_OVERRIDE="$1"; shift ;; --environment=*) ENVIRONMENT_OVERRIDE="${1#--environment=}"; shift ;; -h|--help) echo "Usage: run_codegen.sh [--environment ] [--check-only]" exit 0 ;; *) CONTRACT="$1"; shift ;; esac done [ -n "$CONTRACT" ] || { echo "FAIL: no contract file specified" >&2; exit 1; } # Resolve relative contract path against caller's CWD 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" rm -rf "$WORK"; mkdir -p "$TF_DIR" echo "=== Step 0: environment onboarding check ===" if [ -n "$ENVIRONMENT_OVERRIDE" ]; then python3 core/environment_check.py --env="$ENVIRONMENT_OVERRIDE" || { echo "FAIL: environment onboarding check failed for $ENVIRONMENT_OVERRIDE" >&2 exit 1 } else python3 core/environment_check.py || true fi echo "" echo "=== Step 1: validate contract against contract.schema.json ===" python3 -c " import json, sys, yaml from jsonschema import validate schema = json.load(open('schemas/contract.schema.json')) doc = yaml.safe_load(open('$CONTRACT')) validate(instance=doc, schema=schema) print(f'contract valid: {doc.get(\"name\", \"unnamed\")} (env={doc.get(\"environment\",\"dev\")})') " echo "" echo "=== Step 2: resolve contract → Target Stack instance ===" python3 -c " import json, sys sys.path.insert(0, '$ROOT') from core.contract_resolver import resolve stack = resolve('$CONTRACT', environment_override='${ENVIRONMENT_OVERRIDE}' or None) json.dump(stack, open('$WORK/stack.json', 'w'), indent=2) print(f'stack resolved: {stack[\"stack\"][\"name\"]} ({len(stack[\"resources\"])} resource(s))') " echo "" echo "=== Step 3: adapter compiles stack → $TF_DIR/*.tf ===" python3 -c " import json, sys sys.path.insert(0, '$ROOT') from adapters.terraform.adapter import TerraformAdapter stack = json.load(open('$WORK/stack.json')) adapter = TerraformAdapter() adapter.compile(stack, '$TF_DIR') print('adapter: main.tf + terraform.tf + providers.tf written') " echo "=== Step 3c: Checkov on static code (fail-fast, before terraform plan) ===" # REQ-250 (v1.21): Checkov runs on the authored Terraform code BEFORE # terraform plan so developers get immediate policy feedback, not a # delayed plan-stage failure. The runtime plan scan (Wiz-or-Checkov) # runs after the plan in run_postapply.sh Step 5. if [ "$QUIET" = "0" ]; then checkov -d "$TF_DIR" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ 2>&1 | tee "$WORK/checkov-static.json" else checkov -d "$TF_DIR" --framework terraform -o json --soft-fail --external-checks-dir adapters/terraform/policy/custom_rules/ > "$WORK/checkov-static.json" 2> "$WORK/checkov-static.err" fi [ -s "$WORK/checkov-static.json" ] || { echo "FAIL: checkov (static) produced no output" >&2; exit 1; } echo "" echo "checkov (static) summary: $(python3 -c "import json; d=json.load(open('$WORK/checkov-static.json')); print(len(d.get('results',{}).get('failed_checks',[])), 'failed,', len(d.get('results',{}).get('passed_checks',[])), 'passed')")" if [ "$CHECK_ONLY" = "1" ]; then echo "" echo "=== Step 3b: validate adapter output structure (offline) ===" python3 -c " import json, os d = json.load(open('$WORK/stack.json')) assert d['stack']['name'], 'stack name missing' assert len(d['resources']) >= 1, 'expected at least 1 resource' tf_dir = '$TF_DIR' for f in ('main.tf', 'terraform.tf', 'providers.tf'): assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing' main = open(os.path.join(tf_dir, 'main.tf')).read() assert len(main) > 0, 'main.tf is empty' tf = open(os.path.join(tf_dir, 'terraform.tf')).read() assert 'backend' in tf assert 'required_version' in tf prov = open(os.path.join(tf_dir, 'providers.tf')).read() assert 'provider \"aws\"' in prov print(f\"adapter output: OK ({d['stack']['name']}, {len(d['resources'])} resource(s))\") " echo "" echo "=== CODEGEN CHECK OK ===" echo "contract → resolver → stack → adapter → structure validated (offline, no AWS)" echo "WORK_DIR=$WORK" exit 0 fi # Load AWS credentials if not already set (for non-check-only modes) if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then ENV_FILE="$ROOT/.env.secrets" if [ -f "$ENV_FILE" ]; then set -a . "$ENV_FILE" set +a 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 fi echo "" echo "=== CODEGEN OK ===" echo "Terraform files ready in: $TF_DIR" echo "WORK_DIR=$WORK"