demo/record_deploy.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 | 16 | #!/usr/bin/env bash |
| 2 | # ───────────────────────────────────────────────────────────────────────────── | |
| 3 | # Record a fresh GCO deployment as an animated GIF | |
| 4 | # ───────────────────────────────────────────────────────────────────────────── | |
| 5 | # Records `python3 -m cli.main stacks deploy-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/deploy.cast — asciinema recording | |
| 10 | # demo/deploy.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_deploy.sh | |
| 23 | # RENDER_EXISTING=1 bash demo/record_deploy.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=140 Terminal width (default: 140) | |
| 31 | # DEMO_ROWS=37 Terminal height (default: 37) | |
| 32 | # DEMO_SPEED=15 Playback speed for GIF (default: 15 — deploy is long) | |
| 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 | 16 | set -euo pipefail |
| 56 | ||
| 57 | # ── Configuration ──────────────────────────────────────────────────────────── | |
| 58 | ||
| 59 | 64 | 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 | 48 | REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)" |
| 65 | 16 | DEMO_DIR="${REPO_ROOT}/demo" |
| 66 | ||
| 67 | # shellcheck source=demo/lib_demo.sh | |
| 68 | 16 | source "${SCRIPT_DIR}/lib_demo.sh" |
| 69 | 16 | setup_colors |
| 70 | ||
| 71 | 16 | CAST_FILE="${DEMO_DIR}/deploy.cast" |
| 72 | 16 | GIF_FILE="${DEMO_DIR}/deploy.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 | 16 | RECORDING_TMP_DIR="" |
| 79 | cleanup_recording_temps() { | |
| 80 | 16 | local exit_code="$1" |
| 81 | 16 | local rollback_succeeded=1 |
| 82 | 16 | trap - EXIT |
| 83 | 16 | trap '' HUP INT TERM |
| 84 | ||
| 85 | 16 | 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 | 26 | if [ -n "$RECORDING_TMP_DIR" ] && [ "$rollback_succeeded" -eq 1 ]; then |
| 91 | 9 | if ! rm -rf -- "${RECORDING_TMP_DIR:?}"; then |
| 92 | 1 | exit_code=1 |
| 93 | fi | |
| 94 | fi | |
| 95 | 16 | if ! release_legacy_recording_lock; then |
| 96 | 1 | exit_code=1 |
| 97 | fi | |
| 98 | 16 | exit "$exit_code" |
| 99 | } | |
| 100 | 16 | trap 'cleanup_recording_temps "$?"' EXIT |
| 101 | 16 | trap 'exit 129' HUP |
| 102 | 16 | trap 'exit 130' INT |
| 103 | 16 | trap 'exit 143' TERM |
| 104 | ||
| 105 | # A 140x37 canvas keeps CloudFormation output readable while leaving room | |
| 106 | # under the reviewed 1360x803/1000-frame GIF policy. | |
| 107 | 16 | COLS="${DEMO_COLS:-140}" |
| 108 | 16 | ROWS="${DEMO_ROWS:-37}" |
| 109 | ||
| 110 | # Compress long CloudFormation waits enough to preserve frame-count headroom. | |
| 111 | 16 | SPEED="${DEMO_SPEED:-15}" |
| 112 | 16 | THEME="${DEMO_THEME:-monokai}" |
| 113 | 16 | RENDER_EXISTING="${RENDER_EXISTING:-0}" |
| 114 | ||
| 115 | # ── Preflight ──────────────────────────────────────────────────────────────── | |
| 116 | ||
| 117 | 16 | PREFLIGHT_PASS=0 |
| 118 | 16 | PREFLIGHT_FAIL=0 |
| 119 | 16 | PREFLIGHT_WARN=0 |
| 120 | ||
| 121 | preflight_pass() { | |
| 122 | 71 | echo " ${GREEN}${BOLD}✓${RESET} $1" |
| 123 | 71 | PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1)) |
| 124 | } | |
| 125 | ||
| 126 | preflight_fail() { | |
| 127 | 12 | echo " ${RED}${BOLD}✗${RESET} $1" |
| 128 | 12 | echo " ${DIM}Fix: $2${RESET}" |
| 129 | 12 | PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1)) |
| 130 | } | |
| 131 | ||
| 132 | preflight_warn() { | |
| 133 | 3 | echo " ${YELLOW}${BOLD}!${RESET} $1" |
| 134 | 3 | echo " ${DIM}$2${RESET}" |
| 135 | 3 | PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1)) |
| 136 | } | |
| 137 | ||
| 138 | 16 | echo "=== GCO Deploy Recorder ===" |
| 139 | 16 | echo "" |
| 140 | 16 | echo " ${BOLD}Preflight Check${RESET}" |
| 141 | 16 | echo "" |
| 142 | ||
| 143 | # GIF rendering is required unless explicitly producing a cast only. | |
| 144 | 16 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 145 | 13 | if command -v agg &>/dev/null; then |
| 146 | 10 | preflight_pass "agg installed" |
| 147 | else | |
| 148 | 3 | if [ "$RENDER_EXISTING" = "1" ]; then |
| 149 | 1 | preflight_fail "agg is required for RENDER_EXISTING=1" \ |
| 150 | "Install agg; the existing deploy GIF will be preserved" | |
| 151 | else | |
| 152 | 2 | preflight_warn "agg not installed — will produce .cast only" \ |
| 153 | "brew install agg" | |
| 154 | 2 | SKIP_GIF=1 |
| 155 | fi | |
| 156 | fi | |
| 157 | fi | |
| 158 | ||
| 159 | 16 | if [ "${SKIP_SANITIZE:-}" = "1" ]; then |
| 160 | 1 | preflight_fail "SKIP_SANITIZE is not allowed for publishable recordings" \ |
| 161 | "Unset SKIP_SANITIZE so verification remains fail-closed" | |
| 162 | fi | |
| 163 | ||
| 164 | 16 | case "$RENDER_EXISTING" in |
| 165 | 0) | |
| 166 | 10 | if command -v asciinema &>/dev/null; then |
| 167 | 9 | preflight_pass "asciinema installed" |
| 168 | else | |
| 169 | 1 | preflight_fail "asciinema not installed" "brew install asciinema" |
| 170 | fi | |
| 171 | 20 | if (cd "$REPO_ROOT" && python3 -c 'from cli.main import main; assert callable(main)'); then |
| 172 | 8 | preflight_pass "Repository GCO CLI module importable" |
| 173 | else | |
| 174 | 2 | preflight_fail "Repository GCO CLI module is not importable" \ |
| 175 | "Install this checkout's Python dependencies before recording" | |
| 176 | fi | |
| 177 | 10 | if [ -f "${REPO_ROOT}/cdk.json" ]; then |
| 178 | 9 | preflight_pass "cdk.json found" |
| 179 | else | |
| 180 | 1 | preflight_fail "cdk.json not found" "Run from repo root" |
| 181 | fi | |
| 182 | 10 | override_status=0 |
| 183 | 12 | verify_enablement_overrides "$REPO_ROOT" || override_status=$? |
| 184 | 10 | case "$override_status" in |
| 185 | 0) | |
| 186 | 8 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then |
| 187 | 1 | preflight_pass "Run-scoped enablement overrides valid (${GCO_DEMO_ENABLE})" |
| 188 | else | |
| 189 | 7 | preflight_pass "No run-scoped overrides (cdk.json defaults apply)" |
| 190 | fi | |
| 191 | ;; | |
| 192 | 2) | |
| 193 | 1 | preflight_fail "Cannot validate GCO_DEMO_ENABLE" \ |
| 194 | "python3 must be available to check the requested names" | |
| 195 | ;; | |
| 196 | *) | |
| 197 | 1 | preflight_fail "GCO_DEMO_ENABLE names an unknown feature or chart" \ |
| 198 | "Use names from gco/enablement_overrides.py (see gco stacks deploy-all --help)" | |
| 199 | ;; | |
| 200 | esac | |
| 201 | 10 | if verify_legacy_live_recording_authorization "$REPO_ROOT"; then |
| 202 | 8 | preflight_pass "Live consent, Git SHA, and AWS account guards verified" |
| 203 | else | |
| 204 | 2 | preflight_fail "Live recording authorization failed" \ |
| 205 | "Set GCO_RECORDING_LIVE, GCO_EXPECTED_GIT_SHA, and GCO_EXPECTED_ACCOUNT_ID" | |
| 206 | fi | |
| 207 | ;; | |
| 208 | 1) | |
| 209 | 5 | if [ -f "$CAST_FILE" ]; then |
| 210 | 4 | preflight_pass "Existing deploy cast found for offline rendering" |
| 211 | else | |
| 212 | 1 | preflight_fail "Existing deploy cast not found" \ |
| 213 | "Record once with guarded live mode before using RENDER_EXISTING=1" | |
| 214 | fi | |
| 215 | ;; | |
| 216 | *) | |
| 217 | 1 | preflight_fail "RENDER_EXISTING must be 0 or 1" \ |
| 218 | "Use RENDER_EXISTING=1 only for offline re-rendering" | |
| 219 | ;; | |
| 220 | esac | |
| 221 | ||
| 222 | # Check disk space | |
| 223 | 48 | AVAILABLE_MB=$(df -m "${DEMO_DIR}" 2>/dev/null | awk 'NR==2{print $4}' || echo "0") |
| 224 | 16 | if [ "$AVAILABLE_MB" -gt 100 ]; then |
| 225 | 15 | preflight_pass "Disk space: ${AVAILABLE_MB} MB available" |
| 226 | else | |
| 227 | 1 | preflight_warn "Low disk space: ${AVAILABLE_MB} MB" "Free up space" |
| 228 | fi | |
| 229 | ||
| 230 | 16 | echo "" |
| 231 | 16 | echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}" |
| 232 | 16 | echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}" |
| 233 | 16 | echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}" |
| 234 | ||
| 235 | 16 | if [ "$PREFLIGHT_FAIL" -gt 0 ]; then |
| 236 | 6 | echo "" |
| 237 | 6 | echo " ${RED}${BOLD}Fix the issues above before recording.${RESET}" |
| 238 | 6 | exit 1 |
| 239 | fi | |
| 240 | ||
| 241 | 10 | acquire_legacy_recording_lock "$REPO_ROOT" |
| 242 | ||
| 243 | # ── Record ─────────────────────────────────────────────────────────────────── | |
| 244 | ||
| 245 | # Stage every raw output beside the final files so successful `mv` publication | |
| 246 | # cannot cross filesystems. Existing tracked artifacts remain untouched until | |
| 247 | # verification and GIF rendering succeed. | |
| 248 | 20 | RECORDING_TMP_DIR=$(mktemp -d "${DEMO_DIR}/.deploy-recording.XXXXXX") |
| 249 | 10 | RAW_CAST_FILE="${RECORDING_TMP_DIR}/deploy.cast" |
| 250 | 10 | RAW_GIF_FILE="${RECORDING_TMP_DIR}/deploy.gif" |
| 251 | 10 | WRAPPER="${RECORDING_TMP_DIR}/run.sh" |
| 252 | ||
| 253 | 10 | if [ "$RENDER_EXISTING" = "1" ]; then |
| 254 | 2 | echo "Re-rendering verified deploy cast (${COLS}x${ROWS}, speed=${SPEED}x)..." |
| 255 | 2 | cp -p "$CAST_FILE" "$RAW_CAST_FILE" |
| 256 | else | |
| 257 | 8 | echo "" |
| 258 | 8 | echo "Recording deploy (${COLS}x${ROWS})..." |
| 259 | 8 | echo "Output: ${CAST_FILE}" |
| 260 | 8 | echo "" |
| 261 | 8 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then |
| 262 | 1 | echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks deploy-all -y --enable ${GCO_DEMO_ENABLE}${RESET}" |
| 263 | else | |
| 264 | 7 | echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks deploy-all -y${RESET}" |
| 265 | fi | |
| 266 | 8 | echo " ${DIM}The deploy can take up to an hour. The recording captures everything.${RESET}" |
| 267 | 8 | echo "" |
| 268 | ||
| 269 | # Create a wrapper script so asciinema runs one repository-bound command. | |
| 270 | # | |
| 271 | # GCO_DEMO_ENABLE is threaded through as `--enable` so the deploy and the | |
| 272 | # live demo are driven by one knob: whatever this recording provisions is | |
| 273 | # exactly what the demo recording will narrate. The committed cdk.json is | |
| 274 | # never rewritten, so verify_recording_git_state's clean-worktree rule and | |
| 275 | # the shipped opt-in defaults both survive. | |
| 276 | # | |
| 277 | # The two branches avoid expanding an empty bash array under `set -u`, | |
| 278 | # which is an error on the macOS bash 3.2 the recorder CI job exercises. | |
| 279 | 8 | cat > "$WRAPPER" <<'WRAPPER_SCRIPT' |
| 280 | #!/usr/bin/env bash | |
| 281 | set -euo pipefail | |
| 282 | cd "$REPO_ROOT" | |
| 283 | export COLUMNS="$GCO_RECORDING_COLUMNS" | |
| 284 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then | |
| 285 | python3 -m cli.main stacks deploy-all -y --enable "$GCO_DEMO_ENABLE" | |
| 286 | else | |
| 287 | python3 -m cli.main stacks deploy-all -y | |
| 288 | fi | |
| 289 | WRAPPER_SCRIPT | |
| 290 | 8 | chmod +x "$WRAPPER" |
| 291 | ||
| 292 | 8 | export REPO_ROOT |
| 293 | 16 | export GCO_DEMO_ENABLE="${GCO_DEMO_ENABLE:-}" |
| 294 | 16 | export GCO_RECORDING_COLUMNS="$COLS" |
| 295 | 16 | export GCO_RECORDING_WRAPPER="$WRAPPER" |
| 296 | 8 | asciinema rec \ |
| 297 | --return \ | |
| 298 | --cols "$COLS" \ | |
| 299 | --rows "$ROWS" \ | |
| 300 | --overwrite \ | |
| 301 | --command "bash --norc --noprofile \"\$GCO_RECORDING_WRAPPER\"" \ | |
| 302 | "$RAW_CAST_FILE" | |
| 303 | ||
| 304 | 7 | echo "" |
| 305 | 7 | echo "✓ Raw recording complete; sanitizing before publication" |
| 306 | fi | |
| 307 | ||
| 308 | # ── Sanitize ──────────────────────────────────────────────────────────────── | |
| 309 | # Redact any AWS account/access-key IDs before anyone can view the cast or the | |
| 310 | # GIF derived from it. See sanitize_cast() in lib_demo.sh for details. | |
| 311 | ||
| 312 | 9 | sanitize_cast "$RAW_CAST_FILE" |
| 313 | 9 | verify_cast_sanitized "$RAW_CAST_FILE" |
| 314 | 9 | echo "✓ Cast sanitized and verified (AWS account/access-key IDs redacted)" |
| 315 | ||
| 316 | # ── Strip tofu-triggering codepoints ──────────────────────────────────────── | |
| 317 | # Rewrite the handful of Unicode characters Menlo can't render so agg never | |
| 318 | # falls back to the system's LastResort tofu font. See strip_emoji_from_cast() | |
| 319 | # in lib_demo.sh for the substitution table. | |
| 320 | ||
| 321 | 9 | strip_emoji_from_cast "$RAW_CAST_FILE" |
| 322 | 9 | echo "✓ Tofu-triggering codepoints stripped (ℹ→i, ✅→✓, ✨→*, 📦→[pkg], 🚀→>>)" |
| 323 | ||
| 324 | # Render from the sanitized staging cast before publishing either artifact. If | |
| 325 | # agg fails, the previous tracked cast/GIF pair remains untouched. | |
| 326 | 9 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 327 | 6 | echo "" |
| 328 | 6 | echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..." |
| 329 | 6 | render_gif "$RAW_CAST_FILE" "$RAW_GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS" |
| 330 | fi | |
| 331 | ||
| 332 | # Publish the fully prepared pair through the shared rollback transaction. An | |
| 333 | # empty staged GIF removes any older final GIF as the second transaction step. | |
| 334 | 9 | PUBLISH_GIF_FILE="" |
| 335 | 9 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 336 | 6 | PUBLISH_GIF_FILE="$RAW_GIF_FILE" |
| 337 | fi | |
| 338 | 9 | publish_recording_artifacts \ |
| 339 | "$RAW_CAST_FILE" "$PUBLISH_GIF_FILE" "$CAST_FILE" "$GIF_FILE" | |
| 340 | ||
| 341 | 8 | echo "✓ Recording pair published: ${CAST_FILE}" |
| 342 | 24 | echo " Size: $(du -h "$CAST_FILE" | cut -f1)" |
| 343 | 8 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 344 | 5 | echo "✓ GIF published: ${GIF_FILE}" |
| 345 | 15 | echo " Size: $(du -h "$GIF_FILE" | cut -f1)" |
| 346 | fi | |
| 347 | ||
| 348 | # ── Summary ────────────────────────────────────────────────────────────────── | |
| 349 | ||
| 350 | 8 | echo "" |
| 351 | 8 | echo "=== Done ===" |
| 352 | 8 | echo "" |
| 353 | 8 | echo "Files:" |
| 354 | 8 | echo " ${CAST_FILE}" |
| 355 | 13 | [ "${SKIP_GIF:-}" != "1" ] && echo " ${GIF_FILE}" |
| 356 | 8 | echo "" |
| 357 | 8 | echo "To replay: asciinema play ${CAST_FILE}" |
| 358 | 8 | echo "To record again: re-run $0 from the exact guarded checkout" |
| 359 | 8 | echo "" |
| 360 | 8 | echo "Embed in README:" |
| 361 | 8 | echo ' ' |