demo/record_destroy.sh150 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.
| 1 | 15 | #!/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 | ||
| 55 | 15 | set -euo pipefail |
| 56 | ||
| 57 | # ── Configuration ──────────────────────────────────────────────────────────── | |
| 58 | ||
| 59 | 60 | SCRIPT_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. | |
| 64 | 45 | REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)" |
| 65 | 15 | DEMO_DIR="${REPO_ROOT}/demo" |
| 66 | ||
| 67 | # shellcheck source=demo/lib_demo.sh | |
| 68 | 15 | source "${SCRIPT_DIR}/lib_demo.sh" |
| 69 | 15 | setup_colors |
| 70 | ||
| 71 | 15 | CAST_FILE="${DEMO_DIR}/destroy.cast" |
| 72 | 15 | GIF_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. | |
| 78 | 15 | RECORDING_TMP_DIR="" |
| 79 | cleanup_recording_temps() { | |
| 80 | 15 | local exit_code="$1" |
| 81 | 15 | local rollback_succeeded=1 |
| 82 | 15 | trap - EXIT |
| 83 | 15 | trap '' HUP INT TERM |
| 84 | ||
| 85 | 15 | if ! rollback_recording_publication; then |
| 86 | 1 | echo "Recording publication rollback failed; preserving staging at ${RECORDING_TMP_DIR}." >&2 |
| 87 | 1 | rollback_succeeded=0 |
| 88 | 1 | exit_code=1 |
| 89 | fi | |
| 90 | 24 | if [ -n "$RECORDING_TMP_DIR" ] && [ "$rollback_succeeded" -eq 1 ]; then |
| 91 | 8 | if ! rm -rf -- "${RECORDING_TMP_DIR:?}"; then |
| 92 | 1 | exit_code=1 |
| 93 | fi | |
| 94 | fi | |
| 95 | 15 | if ! release_legacy_recording_lock; then |
| 96 | 1 | exit_code=1 |
| 97 | fi | |
| 98 | 15 | exit "$exit_code" |
| 99 | } | |
| 100 | 15 | trap 'cleanup_recording_temps "$?"' EXIT |
| 101 | 15 | trap 'exit 129' HUP |
| 102 | 15 | trap 'exit 130' INT |
| 103 | 15 | trap 'exit 143' TERM |
| 104 | ||
| 105 | # Terminal dimensions (same as record_demo.sh) | |
| 106 | 15 | COLS="${DEMO_COLS:-116}" |
| 107 | 15 | ROWS="${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. | |
| 114 | 15 | SPEED="${DEMO_SPEED:-50}" |
| 115 | 15 | THEME="${DEMO_THEME:-monokai}" |
| 116 | 15 | RENDER_EXISTING="${RENDER_EXISTING:-0}" |
| 117 | ||
| 118 | # ── Preflight ──────────────────────────────────────────────────────────────── | |
| 119 | ||
| 120 | 15 | PREFLIGHT_PASS=0 |
| 121 | 15 | PREFLIGHT_FAIL=0 |
| 122 | 15 | PREFLIGHT_WARN=0 |
| 123 | ||
| 124 | preflight_pass() { | |
| 125 | 68 | echo " ${GREEN}${BOLD}✓${RESET} $1" |
| 126 | 68 | PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1)) |
| 127 | } | |
| 128 | ||
| 129 | preflight_fail() { | |
| 130 | 10 | echo " ${RED}${BOLD}✗${RESET} $1" |
| 131 | 10 | echo " ${DIM}Fix: $2${RESET}" |
| 132 | 10 | PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1)) |
| 133 | } | |
| 134 | ||
| 135 | preflight_warn() { | |
| 136 | 2 | echo " ${YELLOW}${BOLD}!${RESET} $1" |
| 137 | 2 | echo " ${DIM}$2${RESET}" |
| 138 | 2 | PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1)) |
| 139 | } | |
| 140 | ||
| 141 | 15 | echo "=== GCO Destroy Recorder ===" |
| 142 | 15 | echo "" |
| 143 | 15 | echo " ${BOLD}Preflight Check${RESET}" |
| 144 | 15 | echo "" |
| 145 | ||
| 146 | # GIF rendering is required unless explicitly producing a cast only. | |
| 147 | 15 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 148 | 13 | if command -v agg &>/dev/null; then |
| 149 | 11 | preflight_pass "agg installed" |
| 150 | else | |
| 151 | 2 | if [ "$RENDER_EXISTING" = "1" ]; then |
| 152 | 1 | preflight_fail "agg is required for RENDER_EXISTING=1" \ |
| 153 | "Install agg; the existing destroy GIF will be preserved" | |
| 154 | else | |
| 155 | 1 | preflight_warn "agg not installed — will produce .cast only" \ |
| 156 | "brew install agg" | |
| 157 | 1 | SKIP_GIF=1 |
| 158 | fi | |
| 159 | fi | |
| 160 | fi | |
| 161 | ||
| 162 | 15 | if [ "${SKIP_SANITIZE:-}" = "1" ]; then |
| 163 | 1 | preflight_fail "SKIP_SANITIZE is not allowed for publishable recordings" \ |
| 164 | "Unset SKIP_SANITIZE so verification remains fail-closed" | |
| 165 | fi | |
| 166 | ||
| 167 | 15 | case "$RENDER_EXISTING" in |
| 168 | 0) | |
| 169 | 9 | if command -v asciinema &>/dev/null; then |
| 170 | 8 | preflight_pass "asciinema installed" |
| 171 | else | |
| 172 | 1 | preflight_fail "asciinema not installed" "brew install asciinema" |
| 173 | fi | |
| 174 | 18 | if (cd "$REPO_ROOT" && python3 -c 'from cli.main import main; assert callable(main)'); then |
| 175 | 8 | preflight_pass "Repository GCO CLI module importable" |
| 176 | else | |
| 177 | 1 | preflight_fail "Repository GCO CLI module is not importable" \ |
| 178 | "Install this checkout's Python dependencies before recording" | |
| 179 | fi | |
| 180 | 9 | if [ -f "${REPO_ROOT}/cdk.json" ]; then |
| 181 | 8 | preflight_pass "cdk.json found" |
| 182 | else | |
| 183 | 1 | preflight_fail "cdk.json not found" "Run from repo root" |
| 184 | fi | |
| 185 | 9 | override_status=0 |
| 186 | 11 | verify_enablement_overrides "$REPO_ROOT" || override_status=$? |
| 187 | 9 | case "$override_status" in |
| 188 | 0) | |
| 189 | 7 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then |
| 190 | 1 | preflight_pass "Run-scoped enablement overrides valid (${GCO_DEMO_ENABLE})" |
| 191 | else | |
| 192 | 6 | preflight_pass "No run-scoped overrides (cdk.json defaults apply)" |
| 193 | fi | |
| 194 | ;; | |
| 195 | 2) | |
| 196 | 1 | preflight_fail "Cannot validate GCO_DEMO_ENABLE" \ |
| 197 | "python3 must be available to check the requested names" | |
| 198 | ;; | |
| 199 | *) | |
| 200 | 1 | 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 | |
| 204 | 9 | if verify_legacy_live_recording_authorization "$REPO_ROOT"; then |
| 205 | 8 | preflight_pass "Live consent, Git SHA, and AWS account guards verified" |
| 206 | else | |
| 207 | 1 | 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) | |
| 212 | 5 | if [ -f "$CAST_FILE" ]; then |
| 213 | 4 | preflight_pass "Existing destroy cast found for offline rendering" |
| 214 | else | |
| 215 | 1 | preflight_fail "Existing destroy cast not found" \ |
| 216 | "Record once with guarded live mode before using RENDER_EXISTING=1" | |
| 217 | fi | |
| 218 | ;; | |
| 219 | *) | |
| 220 | 1 | preflight_fail "RENDER_EXISTING must be 0 or 1" \ |
| 221 | "Use RENDER_EXISTING=1 only for offline re-rendering" | |
| 222 | ;; | |
| 223 | esac | |
| 224 | ||
| 225 | # Check disk space | |
| 226 | 45 | AVAILABLE_MB=$(df -m "${DEMO_DIR}" 2>/dev/null | awk 'NR==2{print $4}' || echo "0") |
| 227 | 15 | if [ "$AVAILABLE_MB" -gt 100 ]; then |
| 228 | 14 | preflight_pass "Disk space: ${AVAILABLE_MB} MB available" |
| 229 | else | |
| 230 | 1 | preflight_warn "Low disk space: ${AVAILABLE_MB} MB" "Free up space" |
| 231 | fi | |
| 232 | ||
| 233 | 15 | echo "" |
| 234 | 15 | echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}" |
| 235 | 15 | echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}" |
| 236 | 15 | echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}" |
| 237 | ||
| 238 | 15 | if [ "$PREFLIGHT_FAIL" -gt 0 ]; then |
| 239 | 6 | echo "" |
| 240 | 6 | echo " ${RED}${BOLD}Fix the issues above before recording.${RESET}" |
| 241 | 6 | exit 1 |
| 242 | fi | |
| 243 | ||
| 244 | 9 | acquire_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. | |
| 251 | 18 | RECORDING_TMP_DIR=$(mktemp -d "${DEMO_DIR}/.destroy-recording.XXXXXX") |
| 252 | 9 | RAW_CAST_FILE="${RECORDING_TMP_DIR}/destroy.cast" |
| 253 | 9 | RAW_GIF_FILE="${RECORDING_TMP_DIR}/destroy.gif" |
| 254 | 9 | WRAPPER="${RECORDING_TMP_DIR}/run.sh" |
| 255 | ||
| 256 | 9 | if [ "$RENDER_EXISTING" = "1" ]; then |
| 257 | 2 | echo "Re-rendering verified destroy cast (${COLS}x${ROWS}, speed=${SPEED}x)..." |
| 258 | 2 | cp -p "$CAST_FILE" "$RAW_CAST_FILE" |
| 259 | else | |
| 260 | 7 | echo "" |
| 261 | 7 | echo "Recording destroy (${COLS}x${ROWS})..." |
| 262 | 7 | echo "Output: ${CAST_FILE}" |
| 263 | 7 | echo "" |
| 264 | 7 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then |
| 265 | 1 | echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks destroy-all -y --enable ${GCO_DEMO_ENABLE}${RESET}" |
| 266 | else | |
| 267 | 6 | echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks destroy-all -y${RESET}" |
| 268 | fi | |
| 269 | 7 | echo " ${DIM}The destroy takes 10-20 minutes. The recording captures everything.${RESET}" |
| 270 | 7 | 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. | |
| 283 | 7 | cat > "$WRAPPER" <<'WRAPPER_SCRIPT' |
| 284 | #!/usr/bin/env bash | |
| 285 | set -euo pipefail | |
| 286 | cd "$REPO_ROOT" | |
| 287 | export COLUMNS="$GCO_RECORDING_COLUMNS" | |
| 288 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then | |
| 289 | python3 -m cli.main stacks destroy-all -y --enable "$GCO_DEMO_ENABLE" | |
| 290 | else | |
| 291 | python3 -m cli.main stacks destroy-all -y | |
| 292 | fi | |
| 293 | WRAPPER_SCRIPT | |
| 294 | 7 | chmod +x "$WRAPPER" |
| 295 | ||
| 296 | 7 | export REPO_ROOT |
| 297 | 14 | export GCO_DEMO_ENABLE="${GCO_DEMO_ENABLE:-}" |
| 298 | 14 | export GCO_RECORDING_COLUMNS="$COLS" |
| 299 | 14 | export GCO_RECORDING_WRAPPER="$WRAPPER" |
| 300 | 7 | 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 | ||
| 308 | 6 | echo "" |
| 309 | 6 | echo "✓ Raw recording complete; sanitizing before publication" |
| 310 | fi | |
| 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 | ||
| 316 | 8 | sanitize_cast "$RAW_CAST_FILE" |
| 317 | 8 | verify_cast_sanitized "$RAW_CAST_FILE" |
| 318 | 8 | echo "✓ 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 | ||
| 325 | 8 | strip_emoji_from_cast "$RAW_CAST_FILE" |
| 326 | 8 | echo "✓ 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. | |
| 330 | 8 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 331 | 6 | echo "" |
| 332 | 6 | echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..." |
| 333 | 6 | render_gif "$RAW_CAST_FILE" "$RAW_GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS" |
| 334 | fi | |
| 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. | |
| 338 | 8 | PUBLISH_GIF_FILE="" |
| 339 | 8 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 340 | 6 | PUBLISH_GIF_FILE="$RAW_GIF_FILE" |
| 341 | fi | |
| 342 | 8 | publish_recording_artifacts \ |
| 343 | "$RAW_CAST_FILE" "$PUBLISH_GIF_FILE" "$CAST_FILE" "$GIF_FILE" | |
| 344 | ||
| 345 | 7 | echo "✓ Recording pair published: ${CAST_FILE}" |
| 346 | 21 | echo " Size: $(du -h "$CAST_FILE" | cut -f1)" |
| 347 | 7 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 348 | 5 | echo "✓ GIF published: ${GIF_FILE}" |
| 349 | 15 | echo " Size: $(du -h "$GIF_FILE" | cut -f1)" |
| 350 | fi | |
| 351 | ||
| 352 | # ── Summary ────────────────────────────────────────────────────────────────── | |
| 353 | ||
| 354 | 7 | echo "" |
| 355 | 7 | echo "=== Done ===" |
| 356 | 7 | echo "" |
| 357 | 7 | echo "Files:" |
| 358 | 7 | echo " ${CAST_FILE}" |
| 359 | 12 | [ "${SKIP_GIF:-}" != "1" ] && echo " ${GIF_FILE}" |
| 360 | 7 | echo "" |
| 361 | 7 | echo "To replay: asciinema play ${CAST_FILE}" |
| 362 | 7 | echo "To record again: re-run $0 from the exact guarded checkout" |
| 363 | 7 | echo "" |
| 364 | 7 | echo "Embed in README:" |
| 365 | 7 | echo ' ' |