demo/record_demo.sh186 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.
| 1 | 23 | #!/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 | ||
| 42 | 23 | set -euo pipefail |
| 43 | ||
| 44 | 92 | SCRIPT_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. | |
| 49 | 69 | REPO_ROOT="$(cd "${GCO_RECORDING_REPO_ROOT:-$SCRIPT_DIR/..}" && pwd)" |
| 50 | 23 | DEMO_DIR="${REPO_ROOT}/demo" |
| 51 | ||
| 52 | # shellcheck source=demo/lib_demo.sh | |
| 53 | 23 | source "${SCRIPT_DIR}/lib_demo.sh" |
| 54 | 23 | setup_colors |
| 55 | ||
| 56 | 23 | CAST_FILE="${DEMO_DIR}/live_demo.cast" |
| 57 | 23 | GIF_FILE="${DEMO_DIR}/live_demo.gif" |
| 58 | 23 | COLS="${DEMO_COLS:-116}" |
| 59 | 23 | ROWS="${DEMO_ROWS:-36}" |
| 60 | 23 | SPEED="${DEMO_SPEED:-3}" |
| 61 | 23 | THEME="${DEMO_THEME:-monokai}" |
| 62 | 23 | RENDER_EXISTING="${RENDER_EXISTING:-0}" |
| 63 | ||
| 64 | 23 | RECORDING_TMP_DIR="" |
| 65 | 23 | RECORDING_KUBECONFIG="" |
| 66 | cleanup_recording_temps() { | |
| 67 | 23 | local exit_code="$1" |
| 68 | 23 | local rollback_succeeded=1 |
| 69 | 23 | trap - EXIT |
| 70 | 23 | trap '' HUP INT TERM |
| 71 | ||
| 72 | 23 | if [ -n "$RECORDING_KUBECONFIG" ] && \ |
| 73 | ! rm -f -- "$RECORDING_KUBECONFIG" "${RECORDING_KUBECONFIG}.tmp"; then | |
| 74 | 1 | echo "Unable to remove the staged credential-bearing kubeconfig." >&2 |
| 75 | 1 | exit_code=1 |
| 76 | fi | |
| 77 | 23 | if ! rollback_recording_publication; then |
| 78 | 1 | echo "Recording publication rollback failed; preserving staging at ${RECORDING_TMP_DIR}." >&2 |
| 79 | 1 | rollback_succeeded=0 |
| 80 | 1 | exit_code=1 |
| 81 | fi | |
| 82 | 37 | if [ -n "$RECORDING_TMP_DIR" ] && [ "$rollback_succeeded" -eq 1 ]; then |
| 83 | 13 | if ! rm -rf -- "${RECORDING_TMP_DIR:?}"; then |
| 84 | 1 | exit_code=1 |
| 85 | fi | |
| 86 | fi | |
| 87 | 23 | if ! release_legacy_recording_lock; then |
| 88 | 1 | exit_code=1 |
| 89 | fi | |
| 90 | 23 | exit "$exit_code" |
| 91 | } | |
| 92 | 23 | trap 'cleanup_recording_temps "$?"' EXIT |
| 93 | 23 | trap 'exit 129' HUP |
| 94 | 23 | trap 'exit 130' INT |
| 95 | 23 | trap 'exit 143' TERM |
| 96 | ||
| 97 | 23 | PREFLIGHT_PASS=0 |
| 98 | 23 | PREFLIGHT_FAIL=0 |
| 99 | 23 | PREFLIGHT_WARN=0 |
| 100 | ||
| 101 | preflight_pass() { | |
| 102 | 216 | echo " ${GREEN}${BOLD}✓${RESET} $1" |
| 103 | 216 | PREFLIGHT_PASS=$((PREFLIGHT_PASS + 1)) |
| 104 | } | |
| 105 | ||
| 106 | preflight_fail() { | |
| 107 | 16 | echo " ${RED}${BOLD}✗${RESET} $1" |
| 108 | 16 | echo " ${DIM}Fix: $2${RESET}" |
| 109 | 16 | PREFLIGHT_FAIL=$((PREFLIGHT_FAIL + 1)) |
| 110 | } | |
| 111 | ||
| 112 | preflight_warn() { | |
| 113 | 2 | echo " ${YELLOW}${BOLD}!${RESET} $1" |
| 114 | 2 | echo " ${DIM}$2${RESET}" |
| 115 | 2 | PREFLIGHT_WARN=$((PREFLIGHT_WARN + 1)) |
| 116 | } | |
| 117 | ||
| 118 | 23 | echo "=== GCO Live Demo Recorder ===" |
| 119 | 23 | echo "" |
| 120 | 23 | echo " ${BOLD}Preflight Check${RESET}" |
| 121 | 23 | echo "" |
| 122 | ||
| 123 | 23 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 124 | 22 | if command -v agg &>/dev/null; then |
| 125 | 60 | preflight_pass "agg installed ($(agg --version 2>&1 | head -1))" |
| 126 | else | |
| 127 | 2 | if [ "$RENDER_EXISTING" = "1" ]; then |
| 128 | 1 | preflight_fail "agg is required for RENDER_EXISTING=1" \ |
| 129 | "Install agg; the existing live-demo GIF will be preserved" | |
| 130 | else | |
| 131 | 1 | preflight_warn "agg not installed — will produce .cast only" \ |
| 132 | "brew install agg (macOS) or cargo install agg" | |
| 133 | 1 | SKIP_GIF=1 |
| 134 | fi | |
| 135 | fi | |
| 136 | fi | |
| 137 | ||
| 138 | 23 | if [ "${SKIP_SANITIZE:-}" = "1" ]; then |
| 139 | 1 | preflight_fail "SKIP_SANITIZE is not allowed for publishable recordings" \ |
| 140 | "Unset SKIP_SANITIZE so verification remains fail-closed" | |
| 141 | fi | |
| 142 | ||
| 143 | 23 | case "$RENDER_EXISTING" in |
| 144 | 0) | |
| 145 | 17 | if command -v asciinema &>/dev/null; then |
| 146 | 48 | preflight_pass "asciinema installed ($(asciinema --version 2>&1 | head -1))" |
| 147 | else | |
| 148 | 1 | preflight_fail "asciinema not installed" \ |
| 149 | "brew install asciinema (macOS) or pip install asciinema" | |
| 150 | fi | |
| 151 | 34 | for required_file in live_demo.sh lib_demo.sh; do |
| 152 | 34 | if [ -f "${DEMO_DIR}/${required_file}" ]; then |
| 153 | 33 | preflight_pass "${required_file} found" |
| 154 | else | |
| 155 | 1 | preflight_fail "${required_file} not found" "Restore demo/${required_file}" |
| 156 | fi | |
| 157 | done | |
| 158 | 17 | if [ -f "${REPO_ROOT}/cdk.json" ]; then |
| 159 | 16 | preflight_pass "cdk.json found" |
| 160 | else | |
| 161 | 1 | preflight_fail "cdk.json not found" "Run from a GCO checkout" |
| 162 | fi | |
| 163 | 17 | override_status=0 |
| 164 | 19 | verify_enablement_overrides "$REPO_ROOT" || override_status=$? |
| 165 | 17 | case "$override_status" in |
| 166 | 0) | |
| 167 | 15 | if [ -n "${GCO_DEMO_ENABLE:-}" ]; then |
| 168 | 1 | preflight_pass "Run-scoped enablement overrides valid (${GCO_DEMO_ENABLE})" |
| 169 | else | |
| 170 | 14 | preflight_pass "No run-scoped overrides (cdk.json defaults apply)" |
| 171 | fi | |
| 172 | ;; | |
| 173 | 2) | |
| 174 | 1 | preflight_fail "Cannot validate GCO_DEMO_ENABLE" \ |
| 175 | "python3 must be available to check the requested names" | |
| 176 | ;; | |
| 177 | *) | |
| 178 | 1 | 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 | |
| 182 | 17 | if command -v jq &>/dev/null; then |
| 183 | 32 | preflight_pass "jq installed ($(jq --version 2>&1))" |
| 184 | else | |
| 185 | 1 | preflight_fail "jq not installed" "brew install jq or apt install jq" |
| 186 | fi | |
| 187 | 17 | if command -v kubectl &>/dev/null; then |
| 188 | 16 | preflight_pass "kubectl installed" |
| 189 | else | |
| 190 | 1 | preflight_fail "kubectl not installed" "Install kubectl before recording" |
| 191 | fi | |
| 192 | 34 | if (cd "$REPO_ROOT" && python3 -c 'from cli.main import main; assert callable(main)'); then |
| 193 | 16 | preflight_pass "Repository GCO CLI module importable" |
| 194 | else | |
| 195 | 1 | preflight_fail "Repository GCO CLI module is not importable" \ |
| 196 | "Install this checkout's Python dependencies" | |
| 197 | fi | |
| 198 | 17 | authorization_verified=0 |
| 199 | 17 | if verify_legacy_live_recording_authorization "$REPO_ROOT"; then |
| 200 | 15 | preflight_pass "Live consent, Git SHA, and AWS account guards verified" |
| 201 | 15 | authorization_verified=1 |
| 202 | else | |
| 203 | 2 | preflight_fail "Live recording authorization failed" \ |
| 204 | "Set GCO_RECORDING_LIVE, GCO_EXPECTED_GIT_SHA, and GCO_EXPECTED_ACCOUNT_ID" | |
| 205 | fi | |
| 206 | ||
| 207 | 17 | kube_context_verified=0 |
| 208 | 32 | if [ "$authorization_verified" -eq 1 ] && [ -f "${REPO_ROOT}/cdk.json" ] && \ |
| 209 | 30 | command -v jq &>/dev/null && command -v kubectl &>/dev/null; then |
| 210 | 30 | recording_project=$(jq -r '.context.project_name // "gco"' "${REPO_ROOT}/cdk.json") |
| 211 | 15 | detect_region "${REPO_ROOT}/cdk.json" |
| 212 | 15 | recording_region="$REGION" |
| 213 | 15 | if verify_recording_kube_context \ |
| 214 | "${recording_project}-${recording_region}" "$recording_region"; then | |
| 215 | 14 | preflight_pass "kubectl context matches the authorized GCO EKS cluster" |
| 216 | 14 | kube_context_verified=1 |
| 217 | else | |
| 218 | 1 | preflight_fail "kubectl context does not match the authorized cluster" \ |
| 219 | "Select ${recording_project}-${recording_region} before recording" | |
| 220 | fi | |
| 221 | fi | |
| 222 | 17 | if [ "$kube_context_verified" -eq 1 ]; then |
| 223 | 14 | if kubectl get nodes --request-timeout=5s &>/dev/null; then |
| 224 | 13 | preflight_pass "kubectl connected to cluster" |
| 225 | else | |
| 226 | 1 | preflight_fail "kubectl cannot reach the cluster" \ |
| 227 | "Run scripts/setup-cluster-access.sh before recording" | |
| 228 | fi | |
| 229 | fi | |
| 230 | ;; | |
| 231 | 1) | |
| 232 | 5 | if [ -f "$CAST_FILE" ]; then |
| 233 | 4 | preflight_pass "Existing live-demo cast found for offline rendering" |
| 234 | else | |
| 235 | 1 | preflight_fail "Existing live-demo cast not found" \ |
| 236 | "Record once with guarded live mode before using RENDER_EXISTING=1" | |
| 237 | fi | |
| 238 | ;; | |
| 239 | *) | |
| 240 | 1 | preflight_fail "RENDER_EXISTING must be 0 or 1" \ |
| 241 | "Use RENDER_EXISTING=1 only for offline re-rendering" | |
| 242 | ;; | |
| 243 | esac | |
| 244 | ||
| 245 | 69 | AVAILABLE_MB=$(df -m "${DEMO_DIR}" 2>/dev/null | awk 'NR==2{print $4}' || echo "0") |
| 246 | 23 | if [ "$AVAILABLE_MB" -gt 100 ]; then |
| 247 | 22 | preflight_pass "Disk space: ${AVAILABLE_MB} MB available" |
| 248 | else | |
| 249 | 1 | preflight_warn "Low disk space: ${AVAILABLE_MB} MB" "Free up space before rendering" |
| 250 | fi | |
| 251 | ||
| 252 | 23 | echo "" |
| 253 | 23 | echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}" |
| 254 | 23 | echo " ${BOLD}Results:${RESET} ${GREEN}${PREFLIGHT_PASS} passed${RESET} ${RED}${PREFLIGHT_FAIL} failed${RESET} ${YELLOW}${PREFLIGHT_WARN} warnings${RESET}" |
| 255 | 23 | echo " ${DIM}──────────────────────────────────────────────────────────────${RESET}" |
| 256 | ||
| 257 | 23 | if [ "$PREFLIGHT_FAIL" -gt 0 ]; then |
| 258 | 9 | echo "" |
| 259 | 9 | echo " ${RED}${BOLD}Fix the issues above before recording.${RESET}" |
| 260 | 9 | exit 1 |
| 261 | fi | |
| 262 | ||
| 263 | 14 | acquire_legacy_recording_lock "$REPO_ROOT" |
| 264 | ||
| 265 | 28 | RECORDING_TMP_DIR=$(mktemp -d "${DEMO_DIR}/.live-demo-recording.XXXXXX") |
| 266 | 14 | chmod 700 "$RECORDING_TMP_DIR" |
| 267 | 14 | RAW_CAST_FILE="${RECORDING_TMP_DIR}/live_demo.cast" |
| 268 | 14 | RAW_GIF_FILE="${RECORDING_TMP_DIR}/live_demo.gif" |
| 269 | 14 | WRAPPER="${RECORDING_TMP_DIR}/run.sh" |
| 270 | 14 | RECORDING_KUBECONFIG="${RECORDING_TMP_DIR}/kubeconfig" |
| 271 | ||
| 272 | 14 | if [ "$RENDER_EXISTING" = "1" ]; then |
| 273 | 2 | echo "Re-rendering verified live-demo cast (${COLS}x${ROWS}, speed=${SPEED}x)..." |
| 274 | 2 | cp -p "$CAST_FILE" "$RAW_CAST_FILE" |
| 275 | else | |
| 276 | 12 | KUBECONFIG_TMP="${RECORDING_KUBECONFIG}.tmp" |
| 277 | 24 | if ! (umask 077; kubectl config view --raw --minify --flatten > "$KUBECONFIG_TMP"); then |
| 278 | 1 | echo "Unable to snapshot the authorized kubectl context for recording." >&2 |
| 279 | 1 | exit 1 |
| 280 | fi | |
| 281 | 11 | if [ ! -s "$KUBECONFIG_TMP" ]; then |
| 282 | 1 | echo "The authorized kubectl context snapshot is empty." >&2 |
| 283 | 1 | exit 1 |
| 284 | fi | |
| 285 | 10 | chmod 600 "$KUBECONFIG_TMP" |
| 286 | 10 | mv -f -- "$KUBECONFIG_TMP" "$RECORDING_KUBECONFIG" |
| 287 | 20 | export KUBECONFIG="$RECORDING_KUBECONFIG" |
| 288 | ||
| 289 | 20 | recording_project=$(jq -r '.context.project_name // "gco"' "${REPO_ROOT}/cdk.json") |
| 290 | 10 | detect_region "${REPO_ROOT}/cdk.json" |
| 291 | 10 | recording_region="$REGION" |
| 292 | 10 | if ! verify_recording_kube_context \ |
| 293 | "${recording_project}-${recording_region}" "$recording_region"; then | |
| 294 | 1 | echo "The isolated kubeconfig does not match the authorized cluster." >&2 |
| 295 | 1 | exit 1 |
| 296 | fi | |
| 297 | 9 | if ! kubectl get nodes --request-timeout=5s &>/dev/null; then |
| 298 | 1 | echo "The isolated kubeconfig cannot reach the authorized cluster." >&2 |
| 299 | 1 | exit 1 |
| 300 | fi | |
| 301 | 8 | echo "✓ Private kubeconfig snapshot verified; operator kubeconfig remains untouched" |
| 302 | ||
| 303 | 8 | cat > "$WRAPPER" <<'WRAPPER_SCRIPT' |
| 304 | #!/usr/bin/env bash | |
| 305 | set -euo pipefail | |
| 306 | cd "$REPO_ROOT" | |
| 307 | export COLUMNS="$GCO_RECORDING_COLUMNS" | |
| 308 | export GCO_DEMO_FAST=1 | |
| 309 | export GCO_DEMO_NONINTERACTIVE=1 | |
| 310 | export GCO_DEMO_GUARDED_RECORDING=1 | |
| 311 | gco() { python3 -m cli.main "$@"; } | |
| 312 | # shellcheck source=demo/live_demo.sh | |
| 313 | source "${REPO_ROOT}/demo/live_demo.sh" | |
| 314 | WRAPPER_SCRIPT | |
| 315 | 8 | chmod +x "$WRAPPER" |
| 316 | ||
| 317 | 8 | echo "Recording live demo (${COLS}x${ROWS})..." |
| 318 | 8 | echo "Output: ${CAST_FILE}" |
| 319 | 8 | 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. | |
| 322 | 16 | export GCO_DEMO_ENABLE="${GCO_DEMO_ENABLE:-}" |
| 323 | 16 | export GCO_RECORDING_COLUMNS="$COLS" |
| 324 | 16 | export GCO_RECORDING_WRAPPER="$WRAPPER" |
| 325 | 8 | asciinema rec \ |
| 326 | --return \ | |
| 327 | --cols "$COLS" \ | |
| 328 | --rows "$ROWS" \ | |
| 329 | --overwrite \ | |
| 330 | --command "bash --norc --noprofile \"\$GCO_RECORDING_WRAPPER\"" \ | |
| 331 | "$RAW_CAST_FILE" | |
| 332 | 7 | echo "✓ Raw recording complete; sanitizing before publication" |
| 333 | fi | |
| 334 | ||
| 335 | 9 | sanitize_cast "$RAW_CAST_FILE" |
| 336 | 9 | verify_cast_sanitized "$RAW_CAST_FILE" |
| 337 | 9 | echo "✓ Cast sanitized and independently verified" |
| 338 | ||
| 339 | 9 | strip_emoji_from_cast "$RAW_CAST_FILE" |
| 340 | 9 | echo "✓ Unsupported glyphs normalized for agg" |
| 341 | ||
| 342 | 9 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 343 | 7 | echo "Converting to GIF (speed=${SPEED}x, theme=${THEME})..." |
| 344 | 7 | render_gif "$RAW_CAST_FILE" "$RAW_GIF_FILE" "$SPEED" "$THEME" "$COLS" "$ROWS" |
| 345 | fi | |
| 346 | ||
| 347 | 9 | PUBLISH_GIF_FILE="" |
| 348 | 9 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 349 | 7 | PUBLISH_GIF_FILE="$RAW_GIF_FILE" |
| 350 | fi | |
| 351 | 9 | publish_recording_artifacts \ |
| 352 | "$RAW_CAST_FILE" "$PUBLISH_GIF_FILE" "$CAST_FILE" "$GIF_FILE" | |
| 353 | ||
| 354 | 8 | echo "✓ Recording pair published: ${CAST_FILE}" |
| 355 | 24 | echo " Size: $(du -h "$CAST_FILE" | cut -f1)" |
| 356 | 8 | if [ "${SKIP_GIF:-}" != "1" ]; then |
| 357 | 6 | echo "✓ GIF published: ${GIF_FILE}" |
| 358 | 18 | echo " Size: $(du -h "$GIF_FILE" | cut -f1)" |
| 359 | fi | |
| 360 | ||
| 361 | 8 | echo "" |
| 362 | 8 | echo "=== Done ===" |
| 363 | 8 | echo "To replay: asciinema play ${CAST_FILE}" |
| 364 | 8 | echo "To re-render: RENDER_EXISTING=1 DEMO_SPEED=${SPEED} bash $0" |
| 365 | 8 | echo "Embed in README: " |