1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
name: Demo recording
# Self-maintaining demo: the cast is GENERATED from the current source, so it
# can never drift from what the binary actually does. A push that changes the
# CLI surface (src/), the docs the demo quotes, or the generator itself
# re-records demo/demo.cast in CI and opens a PR; the weekly cron keeps it
# fresh against dependency and toolchain drift.
#
# Recording in CI (--ci-record) covers everything the runner can do:
# - the offline/dry-run segments always run (no credentials);
# - `grr ask` runs for real when the TYPESAFE_API_KEY secret is present;
# - the opencode agent segment is attempted live (opencode's free default
# models, or credentials passed via env secrets); if that fails, the
# committed cast's agent segment is CARRIED FORWARD so a CI regeneration
# can never strip the agent demo from the site;
# - `grr transport` needs a Google token and stays a --local-only segment
# (the committed cast keeps it via the same carry-forward, so it is only
# refreshed by explicit local runs).
on:
push:
branches:
paths:
- demo/**
- scripts/generate-demo.ts
- src/**
- Cargo.toml
- .github/workflows/demo.yml
schedule:
# 03:41 UTC Sundays — off the hour, off the weekday peak.
- cron: "41 3 * * 0"
release:
# The cast quotes `grr --version` and closes with the version, so a
# published release is exactly when the recording must be redone — the
# push on the version bump covers the bump itself, this covers the tag.
types:
workflow_dispatch:
inputs:
tag:
description: "Release tag to attach the GIF to (defaults to the latest release)"
required: false
type: string
permissions:
contents: read
concurrency:
group: demo-recording
cancel-in-progress: true
jobs:
record:
name: re-record the demo
runs-on: ubuntu-24.04
timeout-minutes: 45
permissions:
contents: write
pull-requests: write
# gh workflow run below — waking the merge poller.
actions: write
steps:
- uses: actions/checkout@v7
timeout-minutes: 2
# The demo must show the CURRENT build, so the binary is built from
# source — the same nightly + build-std setup the release uses.
- uses: dtolnay/rust-toolchain@nightly
timeout-minutes: 10
with:
components: rust-src
- uses: Swatinem/rust-cache@v2
timeout-minutes: 10
- name: Build grr from source
run: cargo build --release
timeout-minutes: 30
- uses: actions/setup-node@v7
timeout-minutes: 5
with:
node-version: "26"
# opencode is the agentic harness the agent segment drives grr with.
# Installed best-effort: the segment also has a carry-forward path.
- name: Install opencode (best effort)
continue-on-error: true
timeout-minutes: 5
run: npm install -g opencode-ai@latest || npm install -g opencode || true
- name: Re-record the cast
id: record
timeout-minutes: 15
env:
GRR_BIN: ${{ github.workspace }}/target/release/grr
# The System One key lets the natural-language segment record for
# real. Never logged; consumed by the `grr ask` child process.
TYPESAFE_API_KEY: ${{ secrets.TYPESAFE_API_KEY }}
run: |
node scripts/generate-demo.ts --ci-record
- name: Validate the new cast
timeout-minutes: 5
run: node scripts/generate-demo.ts --validate
# The GIF is the shareable form of the demo: Reddit and HN posts show
# inline media, and a link alone gets scrolled past. Rendered here so
# it can never drift from the cast — and attached to the latest
# release, so there is always a stable URL and it never grows the git
# history. Rendering params are the size/readability point: the raw
# recording is 19 MB at native fps; 10 fps at 1.3x is 8 MB.
- name: Install agg (cast → gif)
# asciinema's agg is NOT on crates.io (a `cargo install agg` resolves a
# wrong/old crate and exits 101), so install the static binary from its
# GitHub release instead. `gh release download` selects the asset by
# glob, so there is no API JSON to parse and no interpreter in the loop.
timeout-minutes: 5
env:
GH_TOKEN: ${{ github.token }}
run: |
dir="$RUNNER_TEMP/agg"
mkdir -p "$dir"
gh release download -R asciinema/agg -p '*x86_64-unknown-linux-gnu' -D "$dir"
chmod +x "$dir"/agg-*
sudo mv "$dir"/agg-* /usr/local/bin/agg
agg --version
- name: Render and attach the demo GIF
timeout-minutes: 10
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
agg -q --fps-cap 10 --speed 1.3 --idle-time-limit 0.8 --font-size 13 demo/demo.cast grr-demo.gif
ls -lh grr-demo.gif
# The dispatching release names itself so a delayed run after a newer
# release cannot attach this cast to the wrong one; falling back to
# `gh release view` keeps a manual/weekly run working.
tag="${{ inputs.tag }}"
if [ -z "$tag" ]; then tag="$(gh release view --json tagName --jq .tagName)"; fi
gh release upload "$tag" grr-demo.gif --clobber
echo "attached grr-demo.gif to $tag"
- name: Open a PR when the recording changed
id: pr
uses: peter-evans/create-pull-request@v8
timeout-minutes: 5
with:
author: "debanjanbasu <10209115+debanjanbasu@users.noreply.github.com>"
committer: "debanjanbasu <10209115+debanjanbasu@users.noreply.github.com>"
branch: chore/demo-recording
title: "chore(demo): re-record the terminal demo from source"
commit-message: "chore(demo): re-record the terminal demo from source"
labels: dependencies
add-paths: |
demo/demo.cast
body: |
Automated re-recording of `demo/demo.cast` — run by
`.github/workflows/demo.yml` against the current source.
Every segment is real output from the just-built binary; the
agent segment is either a fresh live run (when opencode worked
on the runner) or carried forward from the previous cast. Review
the diff for surprising removals — a vanished segment means the
binary changed behaviour and the site copy may need a follow-up.
- name: Wake the merge poller
# CI on a token-authored PR waits in action_required until the poller
# approves it (GitHub's 2026-06 bot-event gate). Waking the poller now
# instead of waiting for the next cron tick removes the measured 5-7
# minutes of approval latency before any CI compute starts. The poller
# is idempotent, and the cron remains the safety net.
if: steps.pr.outputs.pull-request-number != ''
timeout-minutes: 2
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: gh workflow run automation-merge.yml --ref main || echo "::warning::poller dispatch failed"