← all scripts

demo/record_demo.sh

186 of 186 statements covered (100.00%).

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

123#!/usr/bin/env bash
2# ─────────────────────────────────────────────────────────────────────────────
3# Record the GCO live feature demo as an animated GIF
4# ─────────────────────────────────────────────────────────────────────────────
5# Live mode executes demo/live_demo.sh against an existing deployment using the
6# repository CLI. It mutates Kubernetes jobs and an inference endpoint. Offline
7# render mode only verifies and re-renders the existing tracked cast.
8#
9# Live mode snapshots only the authorized current kubectl context into a
10# mode-0600 file beneath the private staging directory. Every recorder child
11# inherits that single KUBECONFIG, so CLI refreshes cannot alter the operator's
12# kubeconfig. The sensitive snapshot is removed by the recorder cleanup trap.
13#
14# Output files:
15# demo/live_demo.cast
16# demo/live_demo.gif
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_demo.sh
23# RENDER_EXISTING=1 bash demo/record_demo.sh # no AWS/Kubernetes calls
24#
25# Options:
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=3 GIF playback speed (default: 3)
33# DEMO_THEME=monokai agg color theme
34# DEMO_FONT_FAMILY agg font chain (default: see lib_demo.sh)
35# SKIP_GIF=1 Publish only the cast and remove any stale GIF
36# SKIP_EMOJI_STRIP=1 Skip known unsupported-glyph substitutions
37#
38# Publishable recordings are always sanitized and independently verified.
39# SKIP_SANITIZE is deliberately rejected by this script.
40# ─────────────────────────────────────────────────────────────────────────────
41
4223set -euo pipefail
43
4492SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
45# The checkout being recorded: normally the one this script lives in. The BATS
46# suite points GCO_RECORDING_REPO_ROOT at a disposable fixture repository so
47# the tracked recorder runs in place against it; left unset, every path below
48# is the same as before the override existed.
4969REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)"
5023DEMO_DIR="${REPO_ROOT}/demo"
51
52# shellcheck source=demo/lib_demo.sh
5323source "${SCRIPT_DIR}/lib_demo.sh"
5423setup_colors
55
5623CAST_FILE="${DEMO_DIR}/live_demo.cast"
5723GIF_FILE="${DEMO_DIR}/live_demo.gif"
5823COLS="${DEMO_COLS:-116}"
5923ROWS="${DEMO_ROWS:-36}"
6023SPEED="${DEMO_SPEED:-3}"
6123THEME="${DEMO_THEME:-monokai}"
6223RENDER_EXISTING="${RENDER_EXISTING:-0}"
63
6423RECORDING_TMP_DIR=""
6523RECORDING_KUBECONFIG=""
66cleanup_recording_temps() {
6723 local exit_code="$1"
6823 local rollback_succeeded=1
6923 trap - EXIT
7023 trap '' HUP INT TERM
71
7223 if [ -n "$RECORDING_KUBECONFIG" ] && \
73 ! rm -f -- "$RECORDING_KUBECONFIG" "${RECORDING_KUBECONFIG}.tmp"; then
741 echo "Unable to remove the staged credential-bearing kubeconfig." >&2
751 exit_code=1
76 fi
7723 if ! rollback_recording_publication; then
781 echo "Recording publication rollback failed; preserving staging at ${RECORDING_TMP_DIR}." >&2
791 rollback_succeeded=0
801 exit_code=1
81 fi
8237 if [ -n "$RECORDING_TMP_DIR" ] && [ "$rollback_succeeded" -eq 1 ]; then
8313 if ! rm -rf -- "${RECORDING_TMP_DIR:?}"; then
841 exit_code=1
85 fi
86 fi
8723 if ! release_legacy_recording_lock; then
881 exit_code=1
89 fi
9023 exit "$exit_code"
91}
9223trap 'cleanup_recording_temps "$?"' EXIT
9323trap 'exit 129' HUP
9423trap 'exit 130' INT
9523trap 'exit 143' TERM
96
9723PREFLIGHT_PASS=0
9823PREFLIGHT_FAIL=0
9923PREFLIGHT_WARN=0
100
101preflight_pass() {
102216 echo " ${GREEN}${BOLD}✓${RESET} $1"
103216 PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1))
104}
105
106preflight_fail() {
10716 echo " ${RED}${BOLD}✗${RESET} $1"
10816 echo " ${DIM}Fix: $2${RESET}"
10916 PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1))
110}
111
112preflight_warn() {
1132 echo " ${YELLOW}${BOLD}!${RESET} $1"
1142 echo " ${DIM}$2${RESET}"
1152 PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1))
116}
117
11823echo "=== GCO Live Demo Recorder ==="
11923echo ""
12023echo " ${BOLD}Preflight Check${RESET}"
12123echo ""
122
12323if [ "${SKIP_GIF:-}" != "1" ]; then
12422 if command -v agg &>/dev/null; then
12560 preflight_pass "agg installed ($(agg --version 2>&1 | head -1))"
126 else
1272 if [ "$RENDER_EXISTING" = "1" ]; then
1281 preflight_fail "agg is required for RENDER_EXISTING=1" \
129 "Install agg; the existing live-demo GIF will be preserved"
130 else
1311 preflight_warn "agg not installed — will produce .cast only" \
132 "brew install agg (macOS) or cargo install agg"
1331 SKIP_GIF=1
134 fi
135 fi
136fi
137
13823if [ "${SKIP_SANITIZE:-}" = "1" ]; then
1391 preflight_fail "SKIP_SANITIZE is not allowed for publishable recordings" \
140 "Unset SKIP_SANITIZE so verification remains fail-closed"
141fi
142
14323case "$RENDER_EXISTING" in
144 0)
14517 if command -v asciinema &>/dev/null; then
14648 preflight_pass "asciinema installed ($(asciinema --version 2>&1 | head -1))"
147 else
1481 preflight_fail "asciinema not installed" \
149 "brew install asciinema (macOS) or pip install asciinema"
150 fi
15134 for required_file in live_demo.sh lib_demo.sh; do
15234 if [ -f "${DEMO_DIR}/${required_file}" ]; then
15333 preflight_pass "${required_file} found"
154 else
1551 preflight_fail "${required_file} not found" "Restore demo/${required_file}"
156 fi
157 done
15817 if [ -f "${REPO_ROOT}/cdk.json" ]; then
15916 preflight_pass "cdk.json found"
160 else
1611 preflight_fail "cdk.json not found" "Run from a GCO checkout"
162 fi
16317 override_status=0
16419 verify_enablement_overrides "$REPO_ROOT" || override_status=$?
16517 case "$override_status" in
166 0)
16715 if [ -n "${GCO_DEMO_ENABLE:-}" ]; then
1681 preflight_pass "Run-scoped enablement overrides valid (${GCO_DEMO_ENABLE})"
169 else
17014 preflight_pass "No run-scoped overrides (cdk.json defaults apply)"
171 fi
172 ;;
173 2)
1741 preflight_fail "Cannot validate GCO_DEMO_ENABLE" \
175 "python3 must be available to check the requested names"
176 ;;
177 *)
1781 preflight_fail "GCO_DEMO_ENABLE names an unknown feature or chart" \
179 "Use names from gco/enablement_overrides.py (see gco stacks deploy-all --help)"
180 ;;
181 esac
18217 if command -v jq &>/dev/null; then
18332 preflight_pass "jq installed ($(jq --version 2>&1))"
184 else
1851 preflight_fail "jq not installed" "brew install jq or apt install jq"
186 fi
18717 if command -v kubectl &>/dev/null; then
18816 preflight_pass "kubectl installed"
189 else
1901 preflight_fail "kubectl not installed" "Install kubectl before recording"
191 fi
19234 if (cd "$REPO_ROOT" && python3 -c 'from cli.main import main; assert callable(main)'); then
19316 preflight_pass "Repository GCO CLI module importable"
194 else
1951 preflight_fail "Repository GCO CLI module is not importable" \
196 "Install this checkout's Python dependencies"
197 fi
19817 authorization_verified=0
19917 if verify_legacy_live_recording_authorization "$REPO_ROOT"; then
20015 preflight_pass "Live consent, Git SHA, and AWS account guards verified"
20115 authorization_verified=1
202 else
2032 preflight_fail "Live recording authorization failed" \
204 "Set GCO_RECORDING_LIVE, GCO_EXPECTED_GIT_SHA, and GCO_EXPECTED_ACCOUNT_ID"
205 fi
206
20717 kube_context_verified=0
20832 if [ "$authorization_verified" -eq 1 ] && [ -f "${REPO_ROOT}/cdk.json" ] && \
20930 command -v jq &>/dev/null && command -v kubectl &>/dev/null; then
21030 recording_project=$(jq -r '.context.project_name // "gco"' "${REPO_ROOT}/cdk.json")
21115 detect_region "${REPO_ROOT}/cdk.json"
21215 recording_region="$REGION"
21315 if verify_recording_kube_context \
214 "${recording_project}-${recording_region}" "$recording_region"; then
21514 preflight_pass "kubectl context matches the authorized GCO EKS cluster"
21614 kube_context_verified=1
217 else
2181 preflight_fail "kubectl context does not match the authorized cluster" \
219 "Select ${recording_project}-${recording_region} before recording"
220 fi
221 fi
22217 if [ "$kube_context_verified" -eq 1 ]; then
22314 if kubectl get nodes --request-timeout=5s &>/dev/null; then
22413 preflight_pass "kubectl connected to cluster"
225 else
2261 preflight_fail "kubectl cannot reach the cluster" \
227 "Run scripts/setup-cluster-access.sh before recording"
228 fi
229 fi
230 ;;
231 1)
2325 if [ -f "$CAST_FILE" ]; then
2334 preflight_pass "Existing live-demo cast found for offline rendering"
234 else
2351 preflight_fail "Existing live-demo cast not found" \
236 "Record once with guarded live mode before using RENDER_EXISTING=1"
237 fi
238 ;;
239 *)
2401 preflight_fail "RENDER_EXISTING must be 0 or 1" \
241 "Use RENDER_EXISTING=1 only for offline re-rendering"
242 ;;
243esac
244
24569AVAILABLE_MB=$(df -m "${DEMO_DIR}" 2>/dev/null | awk 'NR==2{print $4}' || echo "0")
24623if [ "$AVAILABLE_MB" -gt 100 ]; then
24722 preflight_pass "Disk space: ${AVAILABLE_MB} MB available"
248else
2491 preflight_warn "Low disk space: ${AVAILABLE_MB} MB" "Free up space before rendering"
250fi
251
25223echo ""
25323echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
25423echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}"
25523echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}"
256
25723if [ "$PREFLIGHT_FAIL" -gt 0 ]; then
2589 echo ""
2599 echo " ${RED}${BOLD}Fix the issues above before recording.${RESET}"
2609 exit 1
261fi
262
26314acquire_legacy_recording_lock "$REPO_ROOT"
264
26528RECORDING_TMP_DIR=$(mktemp -d "${DEMO_DIR}/.live-demo-recording.XXXXXX")
26614chmod 700 "$RECORDING_TMP_DIR"
26714RAW_CAST_FILE="${RECORDING_TMP_DIR}/live_demo.cast"
26814RAW_GIF_FILE="${RECORDING_TMP_DIR}/live_demo.gif"
26914WRAPPER="${RECORDING_TMP_DIR}/run.sh"
27014RECORDING_KUBECONFIG="${RECORDING_TMP_DIR}/kubeconfig"
271
27214if [ "$RENDER_EXISTING" = "1" ]; then
2732 echo "Re-rendering verified live-demo cast (${COLS}x${ROWS}, speed=${SPEED}x)..."
2742 cp -p "$CAST_FILE" "$RAW_CAST_FILE"
275else
27612 KUBECONFIG_TMP="${RECORDING_KUBECONFIG}.tmp"
27724 if ! (umask 077; kubectl config view --raw --minify --flatten > "$KUBECONFIG_TMP"); then
2781 echo "Unable to snapshot the authorized kubectl context for recording." >&2
2791 exit 1
280 fi
28111 if [ ! -s "$KUBECONFIG_TMP" ]; then
2821 echo "The authorized kubectl context snapshot is empty." >&2
2831 exit 1
284 fi
28510 chmod 600 "$KUBECONFIG_TMP"
28610 mv -f -- "$KUBECONFIG_TMP" "$RECORDING_KUBECONFIG"
28720 export KUBECONFIG="$RECORDING_KUBECONFIG"
288
28920 recording_project=$(jq -r '.context.project_name // "gco"' "${REPO_ROOT}/cdk.json")
29010 detect_region "${REPO_ROOT}/cdk.json"
29110 recording_region="$REGION"
29210 if ! verify_recording_kube_context \
293 "${recording_project}-${recording_region}" "$recording_region"; then
2941 echo "The isolated kubeconfig does not match the authorized cluster." >&2
2951 exit 1
296 fi
2979 if ! kubectl get nodes --request-timeout=5s &>/dev/null; then
2981 echo "The isolated kubeconfig cannot reach the authorized cluster." >&2
2991 exit 1
300 fi
3018 echo "✓ Private kubeconfig snapshot verified; operator kubeconfig remains untouched"
302
3038 cat > "$WRAPPER" <<'WRAPPER_SCRIPT'
304#!/usr/bin/env bash
305set -euo pipefail
306cd "$REPO_ROOT"
307export COLUMNS="$GCO_RECORDING_COLUMNS"
308export GCO_DEMO_FAST=1
309export GCO_DEMO_NONINTERACTIVE=1
310export GCO_DEMO_GUARDED_RECORDING=1
311gco() { python3 -m cli.main "$@"; }
312# shellcheck source=demo/live_demo.sh
313source "${REPO_ROOT}/demo/live_demo.sh"
314WRAPPER_SCRIPT
3158 chmod +x "$WRAPPER"
316
3178 echo "Recording live demo (${COLS}x${ROWS})..."
3188 echo "Output: ${CAST_FILE}"
3198 export REPO_ROOT
320 # Inherited by the wrapper so detect_features narrates exactly the features
321 # the paired deploy recording provisioned with the same value.
32216 export GCO_DEMO_ENABLE="${GCO_DEMO_ENABLE:-}"
32316 export GCO_RECORDING_COLUMNS="$COLS"
32416 export GCO_RECORDING_WRAPPER="$WRAPPER"
3258 asciinema rec \
326 --return \
327 --cols "$COLS" \
328 --rows "$ROWS" \
329 --overwrite \
330 --command "bash --norc --noprofile \"\$GCO_RECORDING_WRAPPER\"" \
331 "$RAW_CAST_FILE"
3327 echo "✓ Raw recording complete; sanitizing before publication"
333fi
334
3359sanitize_cast "$RAW_CAST_FILE"
3369verify_cast_sanitized "$RAW_CAST_FILE"
3379echo "✓ Cast sanitized and independently verified"
338
3399strip_emoji_from_cast "$RAW_CAST_FILE"
3409echo "✓ Unsupported glyphs normalized for agg"
341
3429if [ "${SKIP_GIF:-}" != "1" ]; then
3437 echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..."
3447 render_gif "$RAW_CAST_FILE" "$RAW_GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS"
345fi
346
3479PUBLISH_GIF_FILE=""
3489if [ "${SKIP_GIF:-}" != "1" ]; then
3497 PUBLISH_GIF_FILE="$RAW_GIF_FILE"
350fi
3519publish_recording_artifacts \
352 "$RAW_CAST_FILE" "$PUBLISH_GIF_FILE" "$CAST_FILE" "$GIF_FILE"
353
3548echo "✓ Recording pair published: ${CAST_FILE}"
35524echo " Size: $(du -h "$CAST_FILE" | cut -f1)"
3568if [ "${SKIP_GIF:-}" != "1" ]; then
3576 echo "✓ GIF published: ${GIF_FILE}"
35818 echo " Size: $(du -h "$GIF_FILE" | cut -f1)"
359fi
360
3618echo ""
3628echo "=== Done ==="
3638echo "To replay: asciinema play ${CAST_FILE}"
3648echo "To re-render: RENDER_EXISTING=1 DEMO_SPEED=${SPEED} bash $0"
3658echo "Embed in README: ![GCO Live Demo](demo/live_demo.gif)"