From 75c227429a4a8892cc12107808ec3bd872070f6a Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Wed, 22 Jul 2026 17:23:00 +0000 Subject: [PATCH] fix(P20): resolve consumer contract path against caller CWD (P0 review fix) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reusable deploy workflow invokes run_platform.sh from the CONSUMER repo's workspace root with a relative contract path (e.g. .acdl/contract.yaml). run_platform.sh does `cd "$ROOT"` (the platform repo) early in its lifecycle, which caused the relative contract path to resolve against the platform repo (acdl-platform/) instead of the consumer repo — the `[ -f "$CONTRACT" ]` check then failed with "contract file missing" and the pipeline could never run. Fix: capture CALLER_CWD before `cd "$ROOT"` and resolve a caller-supplied relative contract path against CALLER_CWD. The default contract (contracts/static-asset.yaml, used only when no contract is supplied) remains relative to ROOT, preserving platform-local CI behavior. Reproduced pre-fix: bash acdl-platform/scripts/run_platform.sh --check-only .acdl/contract.yaml (from a consumer workspace) -> "contract file missing". Verified post-fix: same invocation reads the consumer contract correctly. verify(P0): code review — correctness ---ci--- phase: 20 milestone: v1.5 status: verify lessons: - P0 fix applied: run_platform.sh now resolves relative contract path against caller CWD (deploy workflow contract path was broken) ---/ci--- --- scripts/run_platform.sh | 96 ++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 25 deletions(-) diff --git a/scripts/run_platform.sh b/scripts/run_platform.sh index 2b0e16b..a3c9724 100755 --- a/scripts/run_platform.sh +++ b/scripts/run_platform.sh @@ -1,42 +1,77 @@ #!/usr/bin/env bash # scripts/run_platform.sh - the ACDL platform pipeline. # +# Usage: +# run_platform.sh (full e2e with AWS) +# run_platform.sh --check-only [contract.yaml] (offline, no AWS/Checkov/DynamoDB) +# run_platform.sh --plan-only (AWS plan only, no Checkov/outbox) +# # Modes: # --check-only (offline, no AWS/Checkov/DynamoDB — for CI) -# load IR -> adapter -> stream emitted TF -> validate structure -> exit 0 +# contract -> resolver -> stack -> adapter -> stream TF -> validate -> exit 0 # --plan-only (requires AWS creds, no Checkov/outbox) -# load IR -> adapter -> terraform init/validate/plan (streamed) -> exit 0 +# contract -> resolver -> stack -> adapter -> terraform init/validate/plan -> exit 0 # (default) (requires AWS creds + Checkov + DynamoDB) -# load IR -> adapter -> terraform plan (streamed) -> Checkov (streamed) -> +# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov -> # confidence -> outbox # # Flags: # --quiet suppress terraform/checkov streaming (output to log only) -# default: stream to stdout so the user sees what is happening # -# NOTE: contract resolution (contract_resolver.py) was removed when the -# thin-composition layer was taken out. The pipeline now starts from a -# pre-existing IR instance (modules-ir/l1/l1-s3/spike_instance.json). A -# new contract-resolution mechanism will be designed in a later phase. +# The contract file is a YAML file validated against schemas/contract.schema.json. +# The resolver (acdl_platform/contract_resolver.py) resolves it to a Target Stack +# instance, which the adapter (adapters/terraform/adapter.py) compiles to Terraform. # # Uses the rotated spike key (D-039/D-047) from gitignored .env.secrets. # Plan-only (no apply); -lock=false per D-P09-1. set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +# Capture the caller's CWD before we cd to ROOT. The reusable deploy workflow +# invokes this script from the CONSUMER repo's workspace root with a relative +# contract path (e.g. .acdl/contract.yaml); the contract must resolve against +# the consumer repo, not the platform repo (acdl-platform/). Without this, the +# `[ -f "$CONTRACT" ]` check below looks for the contract inside the platform +# repo and fails (P0 fix — see docs/CONSUMER_GUIDE.md Step 4). +CALLER_CWD="$(pwd)" cd "$ROOT" CHECK_ONLY=0 PLAN_ONLY=0 QUIET=0 +CONTRACT="" + for arg in "$@"; do case "$arg" in --check-only) CHECK_ONLY=1 ;; --plan-only) PLAN_ONLY=1 ;; --quiet) QUIET=1 ;; - *) echo "FAIL: unknown argument: $arg" >&2; exit 1 ;; + --*) echo "FAIL: unknown flag: $arg" >&2; exit 1 ;; + *) CONTRACT="$arg" ;; esac done +# Resolve a caller-supplied contract path to an absolute path against the +# caller's CWD (captured before `cd "$ROOT"`). The default contract below is +# intentionally left relative to ROOT (it is set only when no contract was +# supplied and resolves against ROOT, which is correct for platform-local CI). +if [ -n "$CONTRACT" ]; then + case "$CONTRACT" in + /*) ;; + *) CONTRACT="$CALLER_CWD/$CONTRACT" ;; + esac +fi + +# Default contract for --check-only (CI uses this) +if [ -z "$CONTRACT" ]; then + if [ "$CHECK_ONLY" = "1" ]; then + CONTRACT="contracts/static-asset.yaml" + else + echo "FAIL: contract file required (usage: run_platform.sh )" >&2 + exit 1 + fi +fi + fail() { echo "FAIL: $*" >&2; exit 1; } # stream: pipe a command's stdout+stderr to both a log file and the @@ -51,17 +86,27 @@ stream() { } CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID -WORK="/tmp/spike_e2e" +WORK="/tmp/acdl_platform" rm -rf "$WORK"; mkdir -p "$WORK" -echo "=== Step 1+2: load pre-existing IR instance (contract resolution deferred) ===" -IR_INSTANCE="modules-ir/l1/l1-s3/spike_instance.json" -[ -f "$IR_INSTANCE" ] || fail "IR instance $IR_INSTANCE missing (contract resolution is deferred; load a pre-existing IR)" -python3 -c "import json; d=json.load(open('$IR_INSTANCE')); print(f\"IR: {d['stack']['name']} {d['stack']['kind']} {len(d['resources'])} resource(s)\")" -cp "$IR_INSTANCE" "$WORK/spike_ir.json" +echo "=== Step 1: validate contract against contract.schema.json ===" +[ -f "$CONTRACT" ] || fail "contract file $CONTRACT missing" +python3 -c " +import json, yaml, jsonschema +schema = json.load(open('schemas/contract.schema.json')) +contract = yaml.safe_load(open('$CONTRACT')) +jsonschema.validate(contract, schema) +print(f'contract: module={contract[\"module\"]} env={contract[\"environment\"]} inputs={list(contract.get(\"inputs\",{}).keys())}') +" -echo "=== Step 3: adapter compiles IR -> terraform/spike/*.tf (regenerate) ===" -python3 adapters/terraform/adapter.py "$WORK/spike_ir.json" terraform/spike || fail "adapter failed" +echo "" +echo "=== Step 2: resolve contract -> Target Stack instance ===" +python3 acdl_platform/contract_resolver.py "$CONTRACT" "$WORK/stack.json" || fail "resolver failed" +python3 -c "import json; d=json.load(open('$WORK/stack.json')); print(f'stack: {d[\"stack\"][\"name\"]} {d[\"stack\"][\"kind\"]} {len(d[\"resources\"])} resource(s)')" + +echo "" +echo "=== Step 3: adapter compiles stack -> terraform/spike/*.tf ===" +python3 adapters/terraform/adapter.py "$WORK/stack.json" terraform/spike || fail "adapter failed" echo "adapter: emitted terraform/spike/{main.tf,terraform.tf,providers.tf}" if [ "$QUIET" = "0" ]; then @@ -76,9 +121,9 @@ if [ "$CHECK_ONLY" = "1" ]; then echo "=== Step 3b: validate adapter output structure (offline) ===" python3 -c " import json, os -d = json.load(open('$WORK/spike_ir.json')) -assert d['stack']['name'] == 'l1-s3' -assert len(d['resources']) == 1 +d = json.load(open('$WORK/stack.json')) +assert d['stack']['name'] == 'static-asset', f\"expected static-asset, got {d['stack']['name']}\" +assert len(d['resources']) >= 1 tf_dir = 'terraform/spike' for f in ('main.tf', 'terraform.tf', 'providers.tf'): assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing' @@ -95,7 +140,7 @@ print('adapter output: OK') " echo "" echo "=== PLATFORM CHECK OK ===" - echo "IR instance -> adapter -> structure validated (offline, no AWS)" + echo "contract -> resolver -> stack -> adapter -> structure validated (offline, no AWS)" exit 0 fi @@ -170,9 +215,9 @@ import acdl_platform.confidence_signal as c pcr = json.load(open("$WORK/pcr.json")) inputs = { "policy": pcr, - "validation": {"schema": True, "ir_resolved": True, "tf_validated": True, "tf_planned": True}, + "validation": {"schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True}, "freshness": {"age_days": 0, "max_age_days": 7}, - "source": {"submitter": "spike", "commit_sha": "spike-sha", "signed": False}, + "source": {"submitter": "consumer", "commit_sha": "consumer-sha", "signed": False}, "history": {"prior_rollbacks": 0, "prior_policy_fails": 0}, "nfrs": {"conformance": None}, } @@ -186,6 +231,7 @@ echo "confidence: score=$SCORE band=$BAND" 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 < "$WORK/event.json" || fail "event build failed" import json, datetime sig = json.load(open("$WORK/signal.json")) @@ -194,7 +240,7 @@ event = { "eventType": "CONFIDENCE_COMPUTED", "ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), "environment": "dev", - "stack": "l2-static-asset", + "stack": "$STACK_NAME", "score": sig["score"], "band": sig["band"], "prev_event_hash": "GENESIS", @@ -206,5 +252,5 @@ echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.jso echo "" echo "=== PLATFORM E2E OK ===" -echo "IR instance -> terraform plan -> Checkov -> confidence ($BAND) -> outbox" +echo "contract -> resolver -> stack -> terraform plan -> Checkov -> confidence ($BAND) -> outbox" exit 0 \ No newline at end of file