|
| 1 | +name: Record demo |
| 2 | + |
| 3 | +# Reusable: builds mdsmith, installs VHS + ttyd + ffmpeg, executes |
| 4 | +# demo.tape in an isolated copy of the repo, and uploads the result |
| 5 | +# as an artifact. Single source of truth for VHS pinning, called by |
| 6 | +# both ci.yml (for tape validation on PRs — uses MP4 to skip the |
| 7 | +# expensive GIF palette quantization pass) and demo.yml (for |
| 8 | +# publishing the GIF on push to main). |
| 9 | + |
| 10 | +on: |
| 11 | + workflow_call: |
| 12 | + inputs: |
| 13 | + output_format: |
| 14 | + description: Output format — `gif` for the published artifact, `mp4` for fast CI validation |
| 15 | + type: string |
| 16 | + default: gif |
| 17 | + outputs: |
| 18 | + artifact_name: |
| 19 | + description: Name of the uploaded artifact |
| 20 | + value: ${{ jobs.record.outputs.artifact_name }} |
| 21 | + |
| 22 | +permissions: |
| 23 | + contents: read |
| 24 | + |
| 25 | +jobs: |
| 26 | + record: |
| 27 | + runs-on: ubuntu-latest |
| 28 | + outputs: |
| 29 | + artifact_name: demo-${{ inputs.output_format }} |
| 30 | + steps: |
| 31 | + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 |
| 32 | + with: |
| 33 | + persist-credentials: false |
| 34 | + - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 |
| 35 | + with: |
| 36 | + go-version-file: go.mod |
| 37 | + - name: Build mdsmith |
| 38 | + run: go build -o mdsmith ./cmd/mdsmith |
| 39 | + - name: Cache VHS + ttyd + ffmpeg binaries |
| 40 | + id: vhs-cache |
| 41 | + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 |
| 42 | + with: |
| 43 | + path: | |
| 44 | + ~/vhs-bin |
| 45 | + key: vhs-0.10.0-ttyd-1.7.7-ffmpeg-7.0.2-${{ runner.os }}-${{ runner.arch }} |
| 46 | + - name: Install VHS runtime dependencies |
| 47 | + run: | |
| 48 | + # Google Chrome is pre-installed on GitHub runners and |
| 49 | + # go-rod (used by VHS) finds it via launcher.LookPath(). |
| 50 | + # ffmpeg, ttyd, and vhs are installed from upstream |
| 51 | + # static/release binaries (verified by SHA-256) and |
| 52 | + # cached in ~/vhs-bin/. We download from upstream rather |
| 53 | + # than `apt-get install` because the apt path requires a |
| 54 | + # `apt-get update` that adds ~25-30s per cache miss. |
| 55 | + # |
| 56 | + # The ffmpeg URL points to johnvansickle.com's "release" |
| 57 | + # (latest) build; when upstream rebuilds the tarball the |
| 58 | + # pinned SHA-256 below will fail-loud, prompting a version |
| 59 | + # bump here and in the cache key. This is intentional. |
| 60 | + if [ -f ~/vhs-bin/ffmpeg ]; then |
| 61 | + sudo install ~/vhs-bin/ffmpeg /usr/local/bin/ffmpeg |
| 62 | + else |
| 63 | + case "${{ runner.arch }}" in |
| 64 | + X64) ffmpeg_arch="amd64"; ffmpeg_sha256="abda8d77ce8309141f83ab8edf0596834087c52467f6badf376a6a2a4c87cf67" ;; |
| 65 | + ARM64) ffmpeg_arch="arm64"; ffmpeg_sha256="f4149bb2b0784e30e99bdda85471c9b5930d3402014e934a5098b41d0f7201b1" ;; |
| 66 | + *) echo "Unsupported arch: ${{ runner.arch }}" >&2; exit 1 ;; |
| 67 | + esac |
| 68 | + curl -fsSL -o /tmp/ffmpeg.tar.xz \ |
| 69 | + "https://johnvansickle.com/ffmpeg/releases/ffmpeg-release-${ffmpeg_arch}-static.tar.xz" |
| 70 | + echo "${ffmpeg_sha256} /tmp/ffmpeg.tar.xz" | sha256sum -c |
| 71 | + tar -xJf /tmp/ffmpeg.tar.xz -C /tmp --strip-components=1 --wildcards '*/ffmpeg' |
| 72 | + sudo install /tmp/ffmpeg /usr/local/bin/ffmpeg |
| 73 | + fi |
| 74 | + if [ -f ~/vhs-bin/ttyd ]; then |
| 75 | + sudo install ~/vhs-bin/ttyd /usr/local/bin/ttyd |
| 76 | + else |
| 77 | + case "${{ runner.arch }}" in |
| 78 | + X64) ttyd_arch="x86_64"; ttyd_sha256="8a217c968aba172e0dbf3f34447218dc015bc4d5e59bf51db2f2cd12b7be4f55" ;; |
| 79 | + ARM64) ttyd_arch="aarch64"; ttyd_sha256="b38acadd89d1d396a0f5649aa52c539edbad07f4bc7348b27b4f4b7219dd4165" ;; |
| 80 | + *) echo "Unsupported arch: ${{ runner.arch }}" >&2; exit 1 ;; |
| 81 | + esac |
| 82 | + curl -fsSL -o /tmp/ttyd \ |
| 83 | + "https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.${ttyd_arch}" |
| 84 | + echo "${ttyd_sha256} /tmp/ttyd" | sha256sum -c |
| 85 | + sudo install /tmp/ttyd /usr/local/bin/ttyd |
| 86 | + fi |
| 87 | + - name: Install VHS |
| 88 | + run: | |
| 89 | + if [ -f ~/vhs-bin/vhs ]; then |
| 90 | + sudo install ~/vhs-bin/vhs /usr/local/bin/vhs |
| 91 | + else |
| 92 | + case "${{ runner.arch }}" in |
| 93 | + X64) vhs_arch="x86_64"; vhs_sha256="b552c3870aca101dcafe533cfef32dceb7b783400ad32642e728775c9f125407" ;; |
| 94 | + ARM64) vhs_arch="arm64"; vhs_sha256="6d7300028d4641b9dc004a05cf411d40f1e12e560cc6fca985dd90504b6652a7" ;; |
| 95 | + *) echo "Unsupported arch: ${{ runner.arch }}" >&2; exit 1 ;; |
| 96 | + esac |
| 97 | + curl -fsSL -o /tmp/vhs.tar.gz \ |
| 98 | + "https://github.com/charmbracelet/vhs/releases/download/v0.10.0/vhs_0.10.0_Linux_${vhs_arch}.tar.gz" |
| 99 | + echo "${vhs_sha256} /tmp/vhs.tar.gz" | sha256sum -c |
| 100 | + tar -xzf /tmp/vhs.tar.gz -C /tmp --strip-components=1 --wildcards '*/vhs' |
| 101 | + sudo install /tmp/vhs /usr/local/bin/vhs |
| 102 | + fi |
| 103 | + - name: Populate VHS cache |
| 104 | + if: steps.vhs-cache.outputs.cache-hit != 'true' |
| 105 | + run: | |
| 106 | + mkdir -p ~/vhs-bin |
| 107 | + cp /usr/local/bin/ttyd ~/vhs-bin/ |
| 108 | + cp /usr/local/bin/vhs ~/vhs-bin/ |
| 109 | + cp /usr/local/bin/ffmpeg ~/vhs-bin/ |
| 110 | + - name: Record demo in isolated directory |
| 111 | + env: |
| 112 | + OUTPUT_FORMAT: ${{ inputs.output_format }} |
| 113 | + run: | |
| 114 | + # Run VHS in a temp directory so demo.tape commands |
| 115 | + # cannot modify the repo working tree. |
| 116 | + case "$OUTPUT_FORMAT" in |
| 117 | + gif|mp4) ;; |
| 118 | + *) echo "Unsupported output_format: $OUTPUT_FORMAT" >&2; exit 1 ;; |
| 119 | + esac |
| 120 | + iso="$(mktemp -d)" |
| 121 | + tar --exclude='.git' -cf - . | (cd "$iso" && tar -xf -) |
| 122 | + # Rewrite the tape's `Output` line to the requested |
| 123 | + # extension. The published demo (gif) and the CI |
| 124 | + # validation (mp4) share one tape. |
| 125 | + sed -i -E "s|^Output assets/demo\\.[a-z0-9]+|Output assets/demo.${OUTPUT_FORMAT}|" "$iso/demo.tape" |
| 126 | + (cd "$iso" && vhs demo.tape) |
| 127 | + mkdir -p artifacts |
| 128 | + cp "$iso/assets/demo.${OUTPUT_FORMAT}" "artifacts/demo.${OUTPUT_FORMAT}" |
| 129 | + rm -rf "$iso" |
| 130 | + - name: Validate output |
| 131 | + env: |
| 132 | + OUTPUT_FORMAT: ${{ inputs.output_format }} |
| 133 | + run: | |
| 134 | + file="artifacts/demo.${OUTPUT_FORMAT}" |
| 135 | + if [ ! -f "$file" ]; then |
| 136 | + echo "ERROR: $file not produced" >&2 |
| 137 | + exit 1 |
| 138 | + fi |
| 139 | + size=$(stat -c%s "$file") |
| 140 | + case "$OUTPUT_FORMAT" in |
| 141 | + gif) |
| 142 | + header=$(head -c 6 "$file") |
| 143 | + if [ "$header" != "GIF89a" ] && [ "$header" != "GIF87a" ]; then |
| 144 | + echo "ERROR: $file is not a valid GIF (header: $header)" >&2 |
| 145 | + exit 1 |
| 146 | + fi |
| 147 | + min=10240; max=5242880 |
| 148 | + ;; |
| 149 | + mp4) |
| 150 | + # MP4 files start with an `ftyp` box; bytes 4-7 are the box type. |
| 151 | + boxtype=$(dd if="$file" bs=1 skip=4 count=4 2>/dev/null) |
| 152 | + if [ "$boxtype" != "ftyp" ]; then |
| 153 | + echo "ERROR: $file is not a valid MP4 (box type: $boxtype)" >&2 |
| 154 | + exit 1 |
| 155 | + fi |
| 156 | + min=10240; max=10485760 |
| 157 | + ;; |
| 158 | + esac |
| 159 | + if [ "$size" -lt "$min" ]; then |
| 160 | + echo "ERROR: $file too small (${size} bytes < ${min})" >&2 |
| 161 | + exit 1 |
| 162 | + fi |
| 163 | + if [ "$size" -gt "$max" ]; then |
| 164 | + echo "ERROR: $file too large (${size} bytes > ${max})" >&2 |
| 165 | + exit 1 |
| 166 | + fi |
| 167 | + echo "demo.${OUTPUT_FORMAT} OK — ${size} bytes" |
| 168 | + - name: Upload artifact |
| 169 | + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 |
| 170 | + with: |
| 171 | + name: demo-${{ inputs.output_format }} |
| 172 | + path: artifacts/demo.${{ inputs.output_format }} |
| 173 | + retention-days: 7 |
| 174 | + if-no-files-found: error |
0 commit comments