← all scripts

demo/record_deploy.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.

116#!/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
5516set -euo pipefail
56
57# ── Configuration ────────────────────────────────────────────────────────────
58
5964SCRIPT_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.
6448REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)"
6516DEMO_DIR="${REPO_ROOT}/demo"
66
67# shellcheck source=demo/lib_demo.sh
6816source "${SCRIPT_DIR}/lib_demo.sh"
6916setup_colors
70
7116CAST_FILE="${DEMO_DIR}/deploy.cast"
7216GIF_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.
7816RECORDING_TMP_DIR=""
79cleanup_recording_temps() {
8016 local exit_code="$1"
8116 local rollback_succeeded=1
8216 trap - EXIT
8316 trap '' HUP INT TERM
84
8516 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
9026 if [ -n "$RECORDING_TMP_DIR" ] && [ "$rollback_succeeded" -eq 1 ]; then
919 if ! rm -rf -- "${RECORDING_TMP_DIR:?}"; then
921 exit_code=1
93 fi
94 fi
9516 if ! release_legacy_recording_lock; then
961 exit_code=1
97 fi
9816 exit "$exit_code"
99}
10016trap 'cleanup_recording_temps "$?"' EXIT
10116trap 'exit 129' HUP
10216trap 'exit 130' INT
10316trap 'exit 143' TERM
104
105# A 140x37 canvas keeps CloudFormation output readable while leaving room
106# under the reviewed 1360x803/1000-frame GIF policy.
10716COLS="${DEMO_COLS:-140}"
10816ROWS="${DEMO_ROWS:-37}"
109
110# Compress long CloudFormation waits enough to preserve frame-count headroom.
11116SPEED="${DEMO_SPEED:-15}"
11216THEME="${DEMO_THEME:-monokai}"
11316RENDER_EXISTING="${RENDER_EXISTING:-0}"
114
115# ── Preflight ────────────────────────────────────────────────────────────────
116
11716PREFLIGHT_PASS=0
11816PREFLIGHT_FAIL=0
11916PREFLIGHT_WARN=0
120
121preflight_pass() {
12271 echo " ${GREEN}${BOLD}✓${RESET} $1"
12371 PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1))
124}
125
126preflight_fail() {
12712 echo " ${RED}${BOLD}✗${RESET} $1"
12812 echo " ${DIM}Fix: $2${RESET}"
12912 PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1))
130}
131
132preflight_warn() {
1333 echo " ${YELLOW}${BOLD}!${RESET} $1"
1343 echo " ${DIM}$2${RESET}"
1353 PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1))
136}
137
13816echo "=== GCO Deploy Recorder ==="
13916echo ""
14016echo " ${BOLD}Preflight Check${RESET}"
14116echo ""
142
143# GIF rendering is required unless explicitly producing a cast only.
14416if [ "${SKIP_GIF:-}" != "1" ]; then
14513 if command -v agg &>/dev/null; then
14610 preflight_pass "agg installed"
147 else
1483 if [ "$RENDER_EXISTING" = "1" ]; then
1491 preflight_fail "agg is required for RENDER_EXISTING=1" \
150 "Install agg; the existing deploy GIF will be preserved"
151 else
1522 preflight_warn "agg not installed — will produce .cast only" \
153 "brew install agg"
1542 SKIP_GIF=1
155 fi
156 fi
157fi
158
15916if [ "${SKIP_SANITIZE:-}" = "1" ]; then
1601 preflight_fail "SKIP_SANITIZE is not allowed for publishable recordings" \
161 "Unset SKIP_SANITIZE so verification remains fail-closed"
162fi
163
16416case "$RENDER_EXISTING" in
165 0)
16610 if command -v asciinema &>/dev/null; then
1679 preflight_pass "asciinema installed"
168 else
1691 preflight_fail "asciinema not installed" "brew install asciinema"
170 fi
17120 if (cd "$REPO_ROOT" && python3 -c 'from cli.main import main; assert callable(main)'); then
1728 preflight_pass "Repository GCO CLI module importable"
173 else
1742 preflight_fail "Repository GCO CLI module is not importable" \
175 "Install this checkout's Python dependencies before recording"
176 fi
17710 if [ -f "${REPO_ROOT}/cdk.json" ]; then
1789 preflight_pass "cdk.json found"
179 else
1801 preflight_fail "cdk.json not found" "Run from repo root"
181 fi
18210 override_status=0
18312 verify_enablement_overrides "$REPO_ROOT" || override_status=$?
18410 case "$override_status" in
185 0)
1868 if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
1871 preflight_pass "Run-scoped enablement overrides valid (${GCO_DEMO_ENABLE})"
188 else
1897 preflight_pass "No run-scoped overrides (cdk.json defaults apply)"
190 fi
191 ;;
192 2)
1931 preflight_fail "Cannot validate GCO_DEMO_ENABLE" \
194 "python3 must be available to check the requested names"
195 ;;
196 *)
1971 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
20110 if verify_legacy_live_recording_authorization "$REPO_ROOT"; then
2028 preflight_pass "Live consent, Git SHA, and AWS account guards verified"
203 else
2042 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)
2095 if [ -f "$CAST_FILE" ]; then
2104 preflight_pass "Existing deploy cast found for offline rendering"
211 else
2121 preflight_fail "Existing deploy cast not found" \
213 "Record once with guarded live mode before using RENDER_EXISTING=1"
214 fi
215 ;;
216 *)
2171 preflight_fail "RENDER_EXISTING must be 0 or 1" \
218 "Use RENDER_EXISTING=1 only for offline re-rendering"
219 ;;
220esac
221
222# Check disk space
22348AVAILABLE_MB=$(df -m "${DEMO_DIR}" 2>/dev/null | awk 'NR==2{print $4}' || echo "0")
22416if [ "$AVAILABLE_MB" -gt 100 ]; then
22515 preflight_pass "Disk space: ${AVAILABLE_MB} MB available"
226else
2271 preflight_warn "Low disk space: ${AVAILABLE_MB} MB" "Free up space"
228fi
229
23016echo ""
23116echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
23216echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}"
23316echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
234
23516if [ "$PREFLIGHT_FAIL" -gt 0 ]; then
2366 echo ""
2376 echo " ${RED}${BOLD}Fix the issues above before recording.${RESET}"
2386 exit 1
239fi
240
24110acquire_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.
24820RECORDING_TMP_DIR=$(mktemp -d "${DEMO_DIR}/.deploy-recording.XXXXXX")
24910RAW_CAST_FILE="${RECORDING_TMP_DIR}/deploy.cast"
25010RAW_GIF_FILE="${RECORDING_TMP_DIR}/deploy.gif"
25110WRAPPER="${RECORDING_TMP_DIR}/run.sh"
252
25310if [ "$RENDER_EXISTING" = "1" ]; then
2542 echo "Re-rendering verified deploy cast (${COLS}x${ROWS}, speed=${SPEED}x)..."
2552 cp -p "$CAST_FILE" "$RAW_CAST_FILE"
256else
2578 echo ""
2588 echo "Recording deploy (${COLS}x${ROWS})..."
2598 echo "Output: ${CAST_FILE}"
2608 echo ""
2618 if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
2621 echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks deploy-all -y --enable ${GCO_DEMO_ENABLE}${RESET}"
263 else
2647 echo " ${YELLOW}${BOLD}This will run python3 -m cli.main stacks deploy-all -y${RESET}"
265 fi
2668 echo " ${DIM}The deploy can take up to an hour. The recording captures everything.${RESET}"
2678 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.
2798 cat > "$WRAPPER" <<'WRAPPER_SCRIPT'
280#!/usr/bin/env bash
281set -euo pipefail
282cd "$REPO_ROOT"
283export COLUMNS="$GCO_RECORDING_COLUMNS"
284if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
285 python3 -m cli.main stacks deploy-all -y --enable "$GCO_DEMO_ENABLE"
286else
287 python3 -m cli.main stacks deploy-all -y
288fi
289WRAPPER_SCRIPT
2908 chmod +x "$WRAPPER"
291
2928 export REPO_ROOT
29316 export GCO_DEMO_ENABLE="${GCO_DEMO_ENABLE:-}"
29416 export GCO_RECORDING_COLUMNS="$COLS"
29516 export GCO_RECORDING_WRAPPER="$WRAPPER"
2968 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
3047 echo ""
3057 echo "✓ Raw recording complete; sanitizing before publication"
306fi
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
3129sanitize_cast "$RAW_CAST_FILE"
3139verify_cast_sanitized "$RAW_CAST_FILE"
3149echo "✓ 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
3219strip_emoji_from_cast "$RAW_CAST_FILE"
3229echo "✓ 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.
3269if [ "${SKIP_GIF:-}" != "1" ]; then
3276 echo ""
3286 echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..."
3296 render_gif "$RAW_CAST_FILE" "$RAW_GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS"
330fi
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.
3349PUBLISH_GIF_FILE=""
3359if [ "${SKIP_GIF:-}" != "1" ]; then
3366 PUBLISH_GIF_FILE="$RAW_GIF_FILE"
337fi
3389publish_recording_artifacts \
339 "$RAW_CAST_FILE" "$PUBLISH_GIF_FILE" "$CAST_FILE" "$GIF_FILE"
340
3418echo "✓ Recording pair published: ${CAST_FILE}"
34224echo " Size: $(du -h "$CAST_FILE" | cut -f1)"
3438if [ "${SKIP_GIF:-}" != "1" ]; then
3445 echo "✓ GIF published: ${GIF_FILE}"
34515 echo " Size: $(du -h "$GIF_FILE" | cut -f1)"
346fi
347
348# ── Summary ──────────────────────────────────────────────────────────────────
349
3508echo ""
3518echo "=== Done ==="
3528echo ""
3538echo "Files:"
3548echo " ${CAST_FILE}"
35513[ "${SKIP_GIF:-}" != "1" ] && echo " ${GIF_FILE}"
3568echo ""
3578echo "To replay: asciinema play ${CAST_FILE}"
3588echo "To record again: re-run $0 from the exact guarded checkout"
3598echo ""
3608echo "Embed in README:"
3618echo ' ![GCO Deploy](demo/deploy.gif)'