← all scripts

demo/record_destroy.sh

150 of 150 statements covered (100.00%).

coveredmissednever traced by Bash (not counted)A line ending in … continues the statement above it and shares its fate.

115#!/usr/bin/env bash
2# ─────────────────────────────────────────────────────────────────────────────
3# Record a GCO teardown as an animated GIF
4# ─────────────────────────────────────────────────────────────────────────────
5# Records `python3 -m cli.main stacks destroy-all -y` from the guarded checkout
6# using asciinema, then converts to an animated GIF using agg.
7#
8# Output files (deposited in demo/):
9# demo/destroy.cast — asciinema recording
10# demo/destroy.gif — animated GIF for embedding in READMEs
11#
12# Prerequisites:
13# - asciinema: brew install asciinema
14# - agg: brew install agg
15# - Repository Python dependencies installed
16# - AWS credentials configured
17#
18# Usage:
19# GCO_RECORDING_LIVE=1 \
20# GCO_EXPECTED_GIT_SHA=<40-char-sha> \
21# GCO_EXPECTED_ACCOUNT_ID=<12-digit-account> \
22# bash demo/record_destroy.sh
23# RENDER_EXISTING=1 bash demo/record_destroy.sh # no AWS calls
24#
25# Options (via environment variables):
26# GCO_RECORDING_LIVE=1 Required acknowledgement for live recording
27# GCO_EXPECTED_GIT_SHA Required full reviewed SHA for live recording
28# GCO_EXPECTED_ACCOUNT_ID Required authorized account for live recording
29# RENDER_EXISTING=1 Re-render the existing verified cast without AWS
30# DEMO_COLS=116 Terminal width (default: 116)
31# DEMO_ROWS=36 Terminal height (default: 36)
32# DEMO_SPEED=50 Playback speed for GIF (default: 50)
33# DEMO_THEME=monokai agg color theme (default: monokai)
34# DEMO_FONT_FAMILY agg font fallback chain (default: see lib_demo.sh)
35# SKIP_GIF=1 Only produce the .cast file
36# SKIP_SANITIZE=1 Rejected for publishable recordings
37# SKIP_EMOJI_STRIP=1 Skip emoji substitution (debugging only)
38#
39# The raw cast and GIF are written under a same-filesystem temporary directory.
40# The tracked pair is published only after these passes succeed. Because POSIX
41# cannot atomically rename two files as one unit, the previous pair is preserved
42# and restored on command failure or handled HUP/INT/TERM interruption. SIGKILL
43# cannot be trapped; each individual final-path rename remains atomic.
44#
45# The recorded .cast is post-processed in three passes before the GIF is
46# rendered:
47# 1. sanitize_cast — account IDs and AWS access-key IDs are replaced.
48# 2. verify_cast_sanitized — independently rejects any residual pattern.
49# 3. strip_emoji_from_cast — rewrites the five codepoints agg's text
50# engine can't render with Menlo (ℹ ✅ ✨ 📦 🚀) to safe monochrome
51# equivalents. See lib_demo.sh for the full mapping and rationale.
52#
53# ─────────────────────────────────────────────────────────────────────────────
54
5515set -euo pipefail
56
57# ── Configuration ────────────────────────────────────────────────────────────
58
5960SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
60# The checkout being recorded: normally the one this script lives in. The BATS
61# suite points GCO_RECORDING_REPO_ROOT at a disposable fixture repository so
62# the tracked recorder runs in place against it; left unset, every path below
63# is the same as before the override existed.
6445REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)"
6515DEMO_DIR="${REPO_ROOT}/demo"
66
67# shellcheck source=demo/lib_demo.sh
6815source "${SCRIPT_DIR}/lib_demo.sh"
6915setup_colors
70
7115CAST_FILE="${DEMO_DIR}/destroy.cast"
7215GIF_FILE="${DEMO_DIR}/destroy.gif"
73
74# Raw recordings, renders, and prior-artifact backups stay in demo/ so every
75# individual rename is same-filesystem atomic. The shared publication helper
76# tracks whether paired publication is in progress; EXIT cleanup rolls it back
77# before deleting staging. Preserve staging if rollback itself cannot complete.
7815RECORDING_TMP_DIR=""
79cleanup_recording_temps() {
8015 local exit_code="$1"
8115 local rollback_succeeded=1
8215 trap - EXIT
8315 trap '' HUP INT TERM
84
8515 if ! rollback_recording_publication; then
861 echo "Recording publication rollback failed; preserving staging at ${RECORDING_TMP_DIR}." >&2
871 rollback_succeeded=0
881 exit_code=1
89 fi
9024 if [ -n "$RECORDING_TMP_DIR" ] && [ "$rollback_succeeded" -eq 1 ]; then
918 if ! rm -rf -- "${RECORDING_TMP_DIR:?}"; then
921 exit_code=1
93 fi
94 fi
9515 if ! release_legacy_recording_lock; then
961 exit_code=1
97 fi
9815 exit "$exit_code"
99}
10015trap 'cleanup_recording_temps "$?"' EXIT
10115trap 'exit 129' HUP
10215trap 'exit 130' INT
10315trap 'exit 143' TERM
104
105# Terminal dimensions (same as record_demo.sh)
10615COLS="${DEMO_COLS:-116}"
10715ROWS="${DEMO_ROWS:-36}"
108
109# A full-feature teardown deletes FSx, Valkey, Aurora, the vector-store replica
110# and every chart's resources, so it runs well over an hour of wall clock. This
111# speed keeps the rendered GIF near the ~90s of the shorter teardowns it
112# replaces; frame count and byte size are set by how often CloudFormation
113# repaints, so raising the speed costs nothing but shortens playback.
11415SPEED="${DEMO_SPEED:-50}"
11515THEME="${DEMO_THEME:-monokai}"
11615RENDER_EXISTING="${RENDER_EXISTING:-0}"
117
118# ── Preflight ────────────────────────────────────────────────────────────────
119
12015PREFLIGHT_PASS=0
12115PREFLIGHT_FAIL=0
12215PREFLIGHT_WARN=0
123
124preflight_pass() {
12568 echo " ${GREEN}${BOLD}✓${RESET} $1"
12668 PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1))
127}
128
129preflight_fail() {
13010 echo " ${RED}${BOLD}✗${RESET} $1"
13110 echo " ${DIM}Fix: $2${RESET}"
13210 PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1))
133}
134
135preflight_warn() {
1362 echo " ${YELLOW}${BOLD}!${RESET} $1"
1372 echo " ${DIM}$2${RESET}"
1382 PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1))
139}
140
14115echo "=== GCO Destroy Recorder ==="
14215echo ""
14315echo " ${BOLD}Preflight Check${RESET}"
14415echo ""
145
146# GIF rendering is required unless explicitly producing a cast only.
14715if [ "${SKIP_GIF:-}" != "1" ]; then
14813 if command -v agg &>/dev/null; then
14911 preflight_pass "agg installed"
150 else
1512 if [ "$RENDER_EXISTING" = "1" ]; then
1521 preflight_fail "agg is required for RENDER_EXISTING=1" \
153 "Install agg; the existing destroy GIF will be preserved"
154 else
1551 preflight_warn "agg not installed — will produce .cast only" \
156 "brew install agg"
1571 SKIP_GIF=1
158 fi
159 fi
160fi
161
16215if [ "${SKIP_SANITIZE:-}" = "1" ]; then
1631 preflight_fail "SKIP_SANITIZE is not allowed for publishable recordings" \
164 "Unset SKIP_SANITIZE so verification remains fail-closed"
165fi
166
16715case "$RENDER_EXISTING" in
168 0)
1699 if command -v asciinema &>/dev/null; then
1708 preflight_pass "asciinema installed"
171 else
1721 preflight_fail "asciinema not installed" "brew install asciinema"
173 fi
17418 if (cd "$REPO_ROOT" && python3 -c 'from cli.main import main; assert callable(main)'); then
1758 preflight_pass "Repository GCO CLI module importable"
176 else
1771 preflight_fail "Repository GCO CLI module is not importable" \
178 "Install this checkout's Python dependencies before recording"
179 fi
1809 if [ -f "${REPO_ROOT}/cdk.json" ]; then
1818 preflight_pass "cdk.json found"
182 else
1831 preflight_fail "cdk.json not found" "Run from repo root"
184 fi
1859 override_status=0
18611 verify_enablement_overrides "$REPO_ROOT" || override_status=$?
1879 case "$override_status" in
188 0)
1897 if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
1901 preflight_pass "Run-scoped enablement overrides valid (${GCO_DEMO_ENABLE})"
191 else
1926 preflight_pass "No run-scoped overrides (cdk.json defaults apply)"
193 fi
194 ;;
195 2)
1961 preflight_fail "Cannot validate GCO_DEMO_ENABLE" \
197 "python3 must be available to check the requested names"
198 ;;
199 *)
2001 preflight_fail "GCO_DEMO_ENABLE names an unknown feature or chart" \
201 "Use names from gco/enablement_overrides.py (see gco stacks destroy-all --help)"
202 ;;
203 esac
2049 if verify_legacy_live_recording_authorization "$REPO_ROOT"; then
2058 preflight_pass "Live consent, Git SHA, and AWS account guards verified"
206 else
2071 preflight_fail "Live recording authorization failed" \
208 "Set GCO_RECORDING_LIVE, GCO_EXPECTED_GIT_SHA, and GCO_EXPECTED_ACCOUNT_ID"
209 fi
210 ;;
211 1)
2125 if [ -f "$CAST_FILE" ]; then
2134 preflight_pass "Existing destroy cast found for offline rendering"
214 else
2151 preflight_fail "Existing destroy cast not found" \
216 "Record once with guarded live mode before using RENDER_EXISTING=1"
217 fi
218 ;;
219 *)
2201 preflight_fail "RENDER_EXISTING must be 0 or 1" \
221 "Use RENDER_EXISTING=1 only for offline re-rendering"
222 ;;
223esac
224
225# Check disk space
22645AVAILABLE_MB=$(df -m "${DEMO_DIR}" 2>/dev/null | awk 'NR==2{print $4}' || echo "0")
22715if [ "$AVAILABLE_MB" -gt 100 ]; then
22814 preflight_pass "Disk space: ${AVAILABLE_MB} MB available"
229else
2301 preflight_warn "Low disk space: ${AVAILABLE_MB} MB" "Free up space"
231fi
232
23315echo ""
23415echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
23515echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}"
23615echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
237
23815if [ "$PREFLIGHT_FAIL" -gt 0 ]; then
2396 echo ""
2406 echo " ${RED}${BOLD}Fix the issues above before recording.${RESET}"
2416 exit 1
242fi
243
2449acquire_legacy_recording_lock "$REPO_ROOT"
245
246# ── Record ───────────────────────────────────────────────────────────────────
247
248# Stage every raw output beside the final files so successful `mv` publication
249# cannot cross filesystems. Existing tracked artifacts remain untouched until
250# verification and GIF rendering succeed.
25118RECORDING_TMP_DIR=$(mktemp -d "${DEMO_DIR}/.destroy-recording.XXXXXX")
2529RAW_CAST_FILE="${RECORDING_TMP_DIR}/destroy.cast"
2539RAW_GIF_FILE="${RECORDING_TMP_DIR}/destroy.gif"
2549WRAPPER="${RECORDING_TMP_DIR}/run.sh"
255
2569if [ "$RENDER_EXISTING" = "1" ]; then
2572 echo "Re-rendering verified destroy cast (${COLS}x${ROWS}, speed=${SPEED}x)..."
2582 cp -p "$CAST_FILE" "$RAW_CAST_FILE"
259else
2607 echo ""
2617 echo "Recording destroy (${COLS}x${ROWS})..."
2627 echo "Output: ${CAST_FILE}"
2637 echo ""
2647 if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
2651 echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks destroy-all -y --enable ${GCO_DEMO_ENABLE}${RESET}"
266 else
2676 echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks destroy-all -y${RESET}"
268 fi
2697 echo " ${DIM}The destroy takes 10-20 minutes. The recording captures everything.${RESET}"
2707 echo ""
271
272 # Create a wrapper script so asciinema runs one repository-bound command.
273 #
274 # The destroy carries the same GCO_DEMO_ENABLE as the deploy so both
275 # recordings evaluate an identical app. Deletion itself does not depend on
276 # it: `cdk destroy` issues a CloudFormation DeleteStack, which removes
277 # whatever the deployed template contains, and no override name gates a
278 # whole stack. Passing it keeps the recorded teardown an honest counterpart
279 # to the recorded deploy rather than a differently-configured run.
280 #
281 # The two branches avoid expanding an empty bash array under `set -u`,
282 # which is an error on the macOS bash 3.2 the recorder CI job exercises.
2837 cat > "$WRAPPER" <<'WRAPPER_SCRIPT'
284#!/usr/bin/env bash
285set -euo pipefail
286cd "$REPO_ROOT"
287export COLUMNS="$GCO_RECORDING_COLUMNS"
288if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
289 python3 -m cli.main stacks destroy-all -y --enable "$GCO_DEMO_ENABLE"
290else
291 python3 -m cli.main stacks destroy-all -y
292fi
293WRAPPER_SCRIPT
2947 chmod +x "$WRAPPER"
295
2967 export REPO_ROOT
29714 export GCO_DEMO_ENABLE="${GCO_DEMO_ENABLE:-}"
29814 export GCO_RECORDING_COLUMNS="$COLS"
29914 export GCO_RECORDING_WRAPPER="$WRAPPER"
3007 asciinema rec \
301 --return \
302 --cols "$COLS" \
303 --rows "$ROWS" \
304 --overwrite \
305 --command "bash --norc --noprofile \"\$GCO_RECORDING_WRAPPER\"" \
306 "$RAW_CAST_FILE"
307
3086 echo ""
3096 echo "✓ Raw recording complete; sanitizing before publication"
310fi
311
312# ── Sanitize ────────────────────────────────────────────────────────────────
313# Redact any AWS account/access-key IDs before anyone can view the cast or the
314# GIF derived from it. See sanitize_cast() in lib_demo.sh for details.
315
3168sanitize_cast "$RAW_CAST_FILE"
3178verify_cast_sanitized "$RAW_CAST_FILE"
3188echo "✓ Cast sanitized and verified (AWS account/access-key IDs redacted)"
319
320# ── Strip tofu-triggering codepoints ────────────────────────────────────────
321# Rewrite the handful of Unicode characters Menlo can't render so agg never
322# falls back to the system's LastResort tofu font. See strip_emoji_from_cast()
323# in lib_demo.sh for the substitution table.
324
3258strip_emoji_from_cast "$RAW_CAST_FILE"
3268echo "✓ Tofu-triggering codepoints stripped (ℹ→i, ✅→✓, ✨→*, 📦→[pkg], 🚀→>>)"
327
328# Render from the sanitized staging cast before publishing either artifact. If
329# agg fails, the previous tracked cast/GIF pair remains untouched.
3308if [ "${SKIP_GIF:-}" != "1" ]; then
3316 echo ""
3326 echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..."
3336 render_gif "$RAW_CAST_FILE" "$RAW_GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS"
334fi
335
336# Publish the fully prepared pair through the shared rollback transaction. An
337# empty staged GIF removes any older final GIF as the second transaction step.
3388PUBLISH_GIF_FILE=""
3398if [ "${SKIP_GIF:-}" != "1" ]; then
3406 PUBLISH_GIF_FILE="$RAW_GIF_FILE"
341fi
3428publish_recording_artifacts \
343 "$RAW_CAST_FILE" "$PUBLISH_GIF_FILE" "$CAST_FILE" "$GIF_FILE"
344
3457echo "✓ Recording pair published: ${CAST_FILE}"
34621echo " Size: $(du -h "$CAST_FILE" | cut -f1)"
3477if [ "${SKIP_GIF:-}" != "1" ]; then
3485 echo "✓ GIF published: ${GIF_FILE}"
34915 echo " Size: $(du -h "$GIF_FILE" | cut -f1)"
350fi
351
352# ── Summary ──────────────────────────────────────────────────────────────────
353
3547echo ""
3557echo "=== Done ==="
3567echo ""
3577echo "Files:"
3587echo " ${CAST_FILE}"
35912[ "${SKIP_GIF:-}" != "1" ] && echo " ${GIF_FILE}"
3607echo ""
3617echo "To replay: asciinema play ${CAST_FILE}"
3627echo "To record again: re-run $0 from the exact guarded checkout"
3637echo ""
3647echo "Embed in README:"
3657echo ' ![GCO Destroy](demo/destroy.gif)'