Skip to content

Commit df1729a

Browse files
author
merge-queue-bot
committed
Merge PR #220: Speed up demo recording: reusable workflow + cached static binaries
2 parents 713322e + e01c9e8 commit df1729a

4 files changed

Lines changed: 200 additions & 138 deletions

File tree

.github/workflows/ci.yml

Lines changed: 5 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -56,77 +56,11 @@ jobs:
5656
- run: ./mdsmith check .
5757

5858
demo:
59-
runs-on: ubuntu-latest
60-
steps:
61-
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
62-
with:
63-
persist-credentials: false
64-
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
65-
with:
66-
go-version-file: go.mod
67-
- name: Build mdsmith
68-
run: go build -o mdsmith ./cmd/mdsmith
69-
- name: Cache VHS + ttyd binaries
70-
id: vhs-cache
71-
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
72-
with:
73-
path: |
74-
~/vhs-bin
75-
key: vhs-0.10.0-ttyd-1.7.7-${{ runner.os }}-${{ runner.arch }}
76-
- name: Install VHS runtime dependencies
77-
run: |
78-
# Google Chrome is pre-installed on GitHub runners and
79-
# go-rod (used by VHS) finds it via launcher.LookPath().
80-
# Installing chromium via apt on Ubuntu 24.04 triggers a
81-
# slow snap install (~500 MB of dependencies), so we skip
82-
# it and only install ffmpeg.
83-
sudo apt-get update -qq
84-
sudo apt-get install -y -qq ffmpeg
85-
if [ -f ~/vhs-bin/ttyd ]; then
86-
sudo install ~/vhs-bin/ttyd /usr/local/bin/ttyd
87-
else
88-
curl -fsSL -o /tmp/ttyd \
89-
"https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64"
90-
sudo install /tmp/ttyd /usr/local/bin/ttyd
91-
fi
92-
- name: Install VHS
93-
run: |
94-
if [ -f ~/vhs-bin/vhs ]; then
95-
sudo install ~/vhs-bin/vhs /usr/local/bin/vhs
96-
else
97-
go install github.com/charmbracelet/vhs@v0.10.0
98-
fi
99-
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
100-
- name: Populate VHS cache
101-
if: steps.vhs-cache.outputs.cache-hit != 'true'
102-
run: |
103-
mkdir -p ~/vhs-bin
104-
cp /usr/local/bin/ttyd ~/vhs-bin/
105-
cp "$(go env GOPATH)/bin/vhs" ~/vhs-bin/ 2>/dev/null || cp /usr/local/bin/vhs ~/vhs-bin/ 2>/dev/null || true
106-
- name: Record demo
107-
run: vhs demo.tape
108-
- name: Validate GIF
109-
run: |
110-
file="assets/demo.gif"
111-
if [ ! -f "$file" ]; then
112-
echo "ERROR: $file not produced" >&2
113-
exit 1
114-
fi
115-
header=$(head -c 6 "$file")
116-
if [ "$header" != "GIF89a" ] && [ "$header" != "GIF87a" ]; then
117-
echo "ERROR: $file is not a valid GIF (header: $header)" >&2
118-
exit 1
119-
fi
120-
size=$(stat -c%s "$file")
121-
if [ "$size" -lt 10240 ]; then
122-
echo "ERROR: $file too small (${size} bytes < 10 KB)" >&2
123-
exit 1
124-
fi
125-
if [ "$size" -gt 5242880 ]; then
126-
echo "ERROR: $file too large (${size} bytes > 5 MB)" >&2
127-
exit 1
128-
fi
129-
echo "assets/demo.gif OK — ${size} bytes, header: $header"
59+
uses: ./.github/workflows/record-demo.yml
60+
with:
61+
# MP4 skips the slow GIF palette-quantization encoding pass.
62+
# The published GIF is produced separately by demo.yml.
63+
output_format: mp4
13064

13165
test:
13266
runs-on: ubuntu-latest

.github/workflows/demo.yml

Lines changed: 12 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,77 +5,30 @@ on:
55
branches: [main]
66

77
permissions:
8-
contents: write
8+
contents: read
99

1010
jobs:
1111
record:
1212
# Skip if the push was made by the CI bot to avoid loops.
1313
if: github.actor != 'github-actions[bot]'
14+
uses: ./.github/workflows/record-demo.yml
15+
16+
publish:
17+
needs: record
18+
if: github.actor != 'github-actions[bot]'
1419
runs-on: ubuntu-latest
20+
permissions:
21+
contents: write
1522
steps:
1623
# Credentials intentionally persisted — needed for git push.
1724
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
1825
with:
1926
fetch-depth: 0
2027
persist-credentials: true
21-
22-
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
28+
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
2329
with:
24-
go-version-file: go.mod
25-
26-
- name: Build mdsmith
27-
run: go build -o mdsmith ./cmd/mdsmith
28-
29-
- name: Cache VHS + ttyd binaries
30-
id: vhs-cache
31-
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
32-
with:
33-
path: |
34-
~/vhs-bin
35-
key: vhs-0.10.0-ttyd-1.7.7-${{ runner.os }}-${{ runner.arch }}
36-
- name: Install VHS runtime dependencies
37-
run: |
38-
# Google Chrome is pre-installed on GitHub runners and
39-
# go-rod (used by VHS) finds it via launcher.LookPath().
40-
# Installing chromium via apt on Ubuntu 24.04 triggers a
41-
# slow snap install (~500 MB of dependencies), so we skip
42-
# it and only install ffmpeg.
43-
sudo apt-get update -qq
44-
sudo apt-get install -y -qq ffmpeg
45-
if [ -f ~/vhs-bin/ttyd ]; then
46-
sudo install ~/vhs-bin/ttyd /usr/local/bin/ttyd
47-
else
48-
curl -fsSL -o /tmp/ttyd \
49-
"https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64"
50-
sudo install /tmp/ttyd /usr/local/bin/ttyd
51-
fi
52-
- name: Install VHS
53-
run: |
54-
if [ -f ~/vhs-bin/vhs ]; then
55-
sudo install ~/vhs-bin/vhs /usr/local/bin/vhs
56-
else
57-
go install github.com/charmbracelet/vhs@v0.10.0
58-
fi
59-
echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
60-
- name: Populate VHS cache
61-
if: steps.vhs-cache.outputs.cache-hit != 'true'
62-
run: |
63-
mkdir -p ~/vhs-bin
64-
cp /usr/local/bin/ttyd ~/vhs-bin/
65-
cp "$(go env GOPATH)/bin/vhs" ~/vhs-bin/ 2>/dev/null || cp /usr/local/bin/vhs ~/vhs-bin/ 2>/dev/null || true
66-
67-
- name: Record demo in isolated directory
68-
run: |
69-
# Run VHS in a temp directory so demo.tape commands
70-
# cannot modify the repo working tree.
71-
iso="$(mktemp -d)"
72-
tar --exclude='.git' -cf - . | (cd "$iso" && tar -xf -)
73-
# No Output-path rewrite needed — VHS runs inside $iso,
74-
# so relative Output assets/demo.gif writes to $iso/assets/.
75-
(cd "$iso" && vhs demo.tape)
76-
cp "$iso/assets/demo.gif" /tmp/demo.gif
77-
rm -rf "$iso"
78-
30+
name: ${{ needs.record.outputs.artifact_name }}
31+
path: /tmp/demo
7932
- name: Push GIF to assets branch
8033
run: |
8134
# Create or update the orphan assets branch with only
@@ -84,9 +37,6 @@ jobs:
8437
git config user.name "github-actions[bot]"
8538
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
8639
87-
# Remove untracked build artifacts so branch switch succeeds.
88-
rm -f mdsmith
89-
9040
# Check if assets branch exists on remote
9141
if git ls-remote --exit-code origin refs/heads/assets >/dev/null 2>&1; then
9242
git fetch origin assets
@@ -100,7 +50,7 @@ jobs:
10050
fi
10151
10252
mkdir -p assets
103-
cp /tmp/demo.gif assets/demo.gif
53+
cp /tmp/demo/demo.gif assets/demo.gif
10454
10555
# Only push if the GIF actually changed
10656
git add assets/demo.gif

.github/workflows/record-demo.yml

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
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

demo.tape

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@
1313
# - demo/sample.md is ignored by .mdsmith.yml; the
1414
# hidden setup copies it to a temp dir and runs
1515
# check/fix there as sample.md.
16-
# - Recording speed: TypingSpeed, Sleep durations,
17-
# and Framerate are tuned for ~10x faster recording.
18-
# PlaybackSpeed 0.1 restores normal apparent speed
19-
# in the output GIF (~30 s recording → ~5 min GIF).
16+
# - Recording speed: TypingSpeed and Sleep durations
17+
# are tuned so the scripted wall time is ~25 s and
18+
# PlaybackSpeed 0.1 stretches it to a ~4 min GIF.
19+
# Framerate 60 × PlaybackSpeed 0.1 → 6 fps effective
20+
# in the output, smooth enough for terminal text.
21+
# Bumping Framerate quadratically inflates capture +
22+
# GIF-encode cost without making the output noticeably
23+
# smoother above ~6-12 fps.
2024
# - Hidden blocks use TypingSpeed 0 since that typing
2125
# is never visible in the output.
2226
# - Sleep durations: 50ms for hidden setup steps;
@@ -33,7 +37,7 @@ Set Shell "bash"
3337
Set FontSize 14
3438
Set Width 960
3539
Set Height 540
36-
Set Framerate 120
40+
Set Framerate 60
3741
Set TypingSpeed 0.005
3842
Set PlaybackSpeed 0.1
3943

0 commit comments

Comments
 (0)