fix(P20): resolve consumer contract path against caller CWD (P0 review fix)
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---
This commit is contained in:
+71
-25
@@ -1,42 +1,77 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# scripts/run_platform.sh - the ACDL platform pipeline.
|
# scripts/run_platform.sh - the ACDL platform pipeline.
|
||||||
#
|
#
|
||||||
|
# Usage:
|
||||||
|
# run_platform.sh <contract.yaml> (full e2e with AWS)
|
||||||
|
# run_platform.sh --check-only [contract.yaml] (offline, no AWS/Checkov/DynamoDB)
|
||||||
|
# run_platform.sh --plan-only <contract.yaml> (AWS plan only, no Checkov/outbox)
|
||||||
|
#
|
||||||
# Modes:
|
# Modes:
|
||||||
# --check-only (offline, no AWS/Checkov/DynamoDB — for CI)
|
# --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)
|
# --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)
|
# (default) (requires AWS creds + Checkov + DynamoDB)
|
||||||
# load IR -> adapter -> terraform plan (streamed) -> Checkov (streamed) ->
|
# contract -> resolver -> stack -> adapter -> terraform plan -> Checkov ->
|
||||||
# confidence -> outbox
|
# confidence -> outbox
|
||||||
#
|
#
|
||||||
# Flags:
|
# Flags:
|
||||||
# --quiet suppress terraform/checkov streaming (output to log only)
|
# --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
|
# The contract file is a YAML file validated against schemas/contract.schema.json.
|
||||||
# thin-composition layer was taken out. The pipeline now starts from a
|
# The resolver (acdl_platform/contract_resolver.py) resolves it to a Target Stack
|
||||||
# pre-existing IR instance (modules-ir/l1/l1-s3/spike_instance.json). A
|
# instance, which the adapter (adapters/terraform/adapter.py) compiles to Terraform.
|
||||||
# new contract-resolution mechanism will be designed in a later phase.
|
|
||||||
#
|
#
|
||||||
# Uses the rotated spike key (D-039/D-047) from gitignored .env.secrets.
|
# Uses the rotated spike key (D-039/D-047) from gitignored .env.secrets.
|
||||||
# Plan-only (no apply); -lock=false per D-P09-1.
|
# Plan-only (no apply); -lock=false per D-P09-1.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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"
|
cd "$ROOT"
|
||||||
|
|
||||||
CHECK_ONLY=0
|
CHECK_ONLY=0
|
||||||
PLAN_ONLY=0
|
PLAN_ONLY=0
|
||||||
QUIET=0
|
QUIET=0
|
||||||
|
CONTRACT=""
|
||||||
|
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
--check-only) CHECK_ONLY=1 ;;
|
--check-only) CHECK_ONLY=1 ;;
|
||||||
--plan-only) PLAN_ONLY=1 ;;
|
--plan-only) PLAN_ONLY=1 ;;
|
||||||
--quiet) QUIET=1 ;;
|
--quiet) QUIET=1 ;;
|
||||||
*) echo "FAIL: unknown argument: $arg" >&2; exit 1 ;;
|
--*) echo "FAIL: unknown flag: $arg" >&2; exit 1 ;;
|
||||||
|
*) CONTRACT="$arg" ;;
|
||||||
esac
|
esac
|
||||||
done
|
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 <contract.yaml>)" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
fail() { echo "FAIL: $*" >&2; exit 1; }
|
fail() { echo "FAIL: $*" >&2; exit 1; }
|
||||||
|
|
||||||
# stream: pipe a command's stdout+stderr to both a log file and the
|
# 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
|
CONTRACT_ID="11111111-1111-1111-1111-111111111111" # spike fixed UUID
|
||||||
WORK="/tmp/spike_e2e"
|
WORK="/tmp/acdl_platform"
|
||||||
rm -rf "$WORK"; mkdir -p "$WORK"
|
rm -rf "$WORK"; mkdir -p "$WORK"
|
||||||
|
|
||||||
echo "=== Step 1+2: load pre-existing IR instance (contract resolution deferred) ==="
|
echo "=== Step 1: validate contract against contract.schema.json ==="
|
||||||
IR_INSTANCE="modules-ir/l1/l1-s3/spike_instance.json"
|
[ -f "$CONTRACT" ] || fail "contract file $CONTRACT missing"
|
||||||
[ -f "$IR_INSTANCE" ] || fail "IR instance $IR_INSTANCE missing (contract resolution is deferred; load a pre-existing IR)"
|
python3 -c "
|
||||||
python3 -c "import json; d=json.load(open('$IR_INSTANCE')); print(f\"IR: {d['stack']['name']} {d['stack']['kind']} {len(d['resources'])} resource(s)\")"
|
import json, yaml, jsonschema
|
||||||
cp "$IR_INSTANCE" "$WORK/spike_ir.json"
|
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) ==="
|
echo ""
|
||||||
python3 adapters/terraform/adapter.py "$WORK/spike_ir.json" terraform/spike || fail "adapter failed"
|
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}"
|
echo "adapter: emitted terraform/spike/{main.tf,terraform.tf,providers.tf}"
|
||||||
|
|
||||||
if [ "$QUIET" = "0" ]; then
|
if [ "$QUIET" = "0" ]; then
|
||||||
@@ -76,9 +121,9 @@ if [ "$CHECK_ONLY" = "1" ]; then
|
|||||||
echo "=== Step 3b: validate adapter output structure (offline) ==="
|
echo "=== Step 3b: validate adapter output structure (offline) ==="
|
||||||
python3 -c "
|
python3 -c "
|
||||||
import json, os
|
import json, os
|
||||||
d = json.load(open('$WORK/spike_ir.json'))
|
d = json.load(open('$WORK/stack.json'))
|
||||||
assert d['stack']['name'] == 'l1-s3'
|
assert d['stack']['name'] == 'static-asset', f\"expected static-asset, got {d['stack']['name']}\"
|
||||||
assert len(d['resources']) == 1
|
assert len(d['resources']) >= 1
|
||||||
tf_dir = 'terraform/spike'
|
tf_dir = 'terraform/spike'
|
||||||
for f in ('main.tf', 'terraform.tf', 'providers.tf'):
|
for f in ('main.tf', 'terraform.tf', 'providers.tf'):
|
||||||
assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing'
|
assert os.path.isfile(os.path.join(tf_dir, f)), f'{f} missing'
|
||||||
@@ -95,7 +140,7 @@ print('adapter output: OK')
|
|||||||
"
|
"
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== PLATFORM CHECK OK ==="
|
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
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -170,9 +215,9 @@ import acdl_platform.confidence_signal as c
|
|||||||
pcr = json.load(open("$WORK/pcr.json"))
|
pcr = json.load(open("$WORK/pcr.json"))
|
||||||
inputs = {
|
inputs = {
|
||||||
"policy": pcr,
|
"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},
|
"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},
|
"history": {"prior_rollbacks": 0, "prior_policy_fails": 0},
|
||||||
"nfrs": {"conformance": None},
|
"nfrs": {"conformance": None},
|
||||||
}
|
}
|
||||||
@@ -186,6 +231,7 @@ echo "confidence: score=$SCORE band=$BAND"
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== Step 8: write evidence event to DynamoDB outbox ==="
|
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" || fail "event build failed"
|
python3 <<PY > "$WORK/event.json" || fail "event build failed"
|
||||||
import json, datetime
|
import json, datetime
|
||||||
sig = json.load(open("$WORK/signal.json"))
|
sig = json.load(open("$WORK/signal.json"))
|
||||||
@@ -194,7 +240,7 @@ event = {
|
|||||||
"eventType": "CONFIDENCE_COMPUTED",
|
"eventType": "CONFIDENCE_COMPUTED",
|
||||||
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
"ts": datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||||
"environment": "dev",
|
"environment": "dev",
|
||||||
"stack": "l2-static-asset",
|
"stack": "$STACK_NAME",
|
||||||
"score": sig["score"],
|
"score": sig["score"],
|
||||||
"band": sig["band"],
|
"band": sig["band"],
|
||||||
"prev_event_hash": "GENESIS",
|
"prev_event_hash": "GENESIS",
|
||||||
@@ -206,5 +252,5 @@ echo "outbox: $(python3 -c "import json; d=json.load(open('$WORK/outbox_item.jso
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "=== PLATFORM E2E OK ==="
|
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
|
exit 0
|
||||||
Reference in New Issue
Block a user