#!/usr/bin/env bash # mock_executor.sh — REQ-06 / D-022 # # Reads a contract.yaml, resolves the L2 composition, invokes each L1's # mock_apply.sh in order, and writes state.json to the current working # directory. # # Input: argv[1] = path to a contract.yaml file. # Output: # - stdout: per-L1 progress (echoed from each mock_apply.sh) # - state.json in cwd: {"l2": "...", "l1s": [...], "contract": {...}} # Exit: # 0 if all L1s exit 0; 1 if any L1 exited non-zero (state.json is still # written with the recorded exit codes). set -euo pipefail if [[ $# -lt 1 ]]; then echo "usage: mock_executor.sh " >&2 exit 1 fi CONTRACT_PATH="$1" if [[ ! -f "$CONTRACT_PATH" ]]; then echo "contract not found: $CONTRACT_PATH" >&2 exit 1 fi # --- Parse the contract (stack + full contract dict) via python3 + yaml. --- # Emit stack on line 1 and the full contract JSON on line 2, then read both # lines into separate bash variables (so the JSON's internal spaces survive). CONTRACT_PARSED=$(python3 - "$CONTRACT_PATH" <<'PY' import sys, json, yaml path = sys.argv[1] with open(path, "r", encoding="utf-8") as fh: contract = yaml.safe_load(fh) if not isinstance(contract, dict): sys.stderr.write("contract is not a mapping\n") sys.exit(2) stack = contract.get("stack", "") # Use a compact JSON (no spaces) so the single-line contract survives bash # variable capture cleanly. print(stack) print(json.dumps(contract, sort_keys=True, separators=(",", ":"))) PY ) STACK=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '1p') CONTRACT_JSON=$(printf '%s\n' "$CONTRACT_PARSED" | sed -n '2p') if [[ -z "$STACK" ]]; then echo "contract missing 'stack' key" >&2 exit 1 fi # --- Resolve the L2 manifest. --- L2_MANIFEST="modules/l2/${STACK}/manifest.yaml" if [[ ! -f "$L2_MANIFEST" ]]; then echo "L2_NOT_FOUND: ${STACK}" >&2 exit 1 fi # --- Read the L2's l1s: list (ordered names) via python. --- L1_NAMES_JSON=$(python3 - "$L2_MANIFEST" <<'PY' import sys, json, yaml path = sys.argv[1] with open(path, "r", encoding="utf-8") as fh: manifest = yaml.safe_load(fh) l1s = manifest.get("l1s", []) if isinstance(manifest, dict) else [] names = [entry.get("name", "") for entry in l1s if isinstance(entry, dict)] print(json.dumps(names)) PY ) # --- Invoke each L1's mock_apply.sh in order, recording exit codes. --- # Build the l1s results array in JSON via python, appending as we go. RESULTS_JSON="[]" ALL_OK=0 while IFS= read -r L1_NAME; do L1_SCRIPT="modules/l1/${L1_NAME}/mock_apply.sh" if [[ ! -f "$L1_SCRIPT" ]]; then echo "L1_NOT_FOUND: ${L1_NAME}" >&2 exit 1 fi # Capture stdout + exit code. stderr passes through. L1_OUT=$(bash "$L1_SCRIPT") L1_RC=$? # Echo the L1's stdout so the pipeline sees the progress lines. printf '%s\n' "$L1_OUT" # Record {"name": ..., "applied": true, "exit_code": ...}. RESULTS_JSON=$(python3 - "$RESULTS_JSON" "$L1_NAME" "$L1_RC" <<'PY' import sys, json results = json.loads(sys.argv[1]) name = sys.argv[2] rc = int(sys.argv[3]) results.append({"name": name, "applied": True, "exit_code": rc}) print(json.dumps(results)) PY ) if [[ $L1_RC -ne 0 ]]; then ALL_OK=1 fi done < <(python3 -c "import sys, json; print('\n'.join(json.loads(sys.argv[1])))" "$L1_NAMES_JSON") # --- Write state.json to the current working directory (D-022). --- python3 - "$RESULTS_JSON" "$STACK" "$CONTRACT_JSON" <<'PY' import sys, json results = json.loads(sys.argv[1]) stack = sys.argv[2] contract = json.loads(sys.argv[3]) state = { "l2": stack, "l1s": results, "contract": contract, } with open("state.json", "w", encoding="utf-8") as fh: json.dump(state, fh, indent=2) fh.write("\n") PY exit "$ALL_OK"