Signup-to-release recording on the landing page and Quickstart !465

merged merged by cmc on 2026-09-23 23:26 UTC · krz/gitbay:demo-recording into main

20 files changed, +186 −18

Layout: unified · split

.gitbay/wiki/Quickstart.org +4
@@ -17,3 +17,7 @@
174. =gitbay issue create --title "first"= from inside the clone 174. =gitbay issue create --title "first"= from inside the clone
18 18
19Everything works from stock OpenSSH too: =ssh git@gitbay.org help=. 19Everything works from stock OpenSSH too: =ssh git@gitbay.org help=.
20
21#+CAPTION: Signup to release from a terminal. The tapes are in =docs/demo/=.
22#+ATTR_HTML: :alt A terminal registering an SSH key, verifying the mailed code, creating a repository, pushing, filing an issue, merging a request that closes it, and publishing a release.
23[[https://gitbay.org/static/img/demo.gif]]
docs/demo/.gitignore added +1
@@ -0,0 +1 @@
1out/
docs/demo/common.tape added +14
@@ -0,0 +1,14 @@
1Set Shell bash
2Set FontFamily "Atkinson Hyperlegible Mono"
3Set FontSize 20
4Set Width 1280
5Set Height 720
6Set Padding 32
7Set TypingSpeed 35ms
8Set WaitPattern /\$$/
9Set Theme { "name": "gitbay", "black": "#000000", "red": "#ff6b6b", "green": "#5fd38d", "yellow": "#ffb454", "blue": "#6cb6ff", "magenta": "#d2a8ff", "cyan": "#76e3ea", "white": "#e6e6e6", "brightBlack": "#6e7681", "brightRed": "#ff8e8e", "brightGreen": "#7ee2a8", "brightYellow": "#ffc777", "brightBlue": "#8cc8ff", "brightMagenta": "#e2c5ff", "brightCyan": "#a5f0f5", "brightWhite": "#ffffff", "background": "#0b0b0b", "foreground": "#e6e6e6", "selection": "#333333", "cursor": "#ff8c1a" }
10
11Hide
12Type "source docs/demo/env.sh && clear" Enter
13Sleep 1s
14Show
docs/demo/env.sh added +25
@@ -0,0 +1,25 @@
1# Sourced by the tapes. Gives the recording a home of its own so the
2# demo account's key, known_hosts, git identity and CLI profile never
3# touch the operator's. OpenSSH reads ~ from the password database, not
4# $HOME, so ssh is wrapped to pass -F explicitly. The home stays short:
5# the CLI's ControlPath lives under it and a Unix socket path caps at 104.
6DEMO_HOME=${DEMO_HOME:-$HOME/.gitbay-demo}
7mkdir -p "$DEMO_HOME/.ssh" "$DEMO_HOME/bin" "$DEMO_HOME/.config/gitbay" "$DEMO_HOME/work"
8chmod 700 "$DEMO_HOME/.ssh"
9[ -f "$DEMO_HOME/.ssh/id_ed25519" ] ||
10 ssh-keygen -q -t ed25519 -f "$DEMO_HOME/.ssh/id_ed25519" -N '' -C zerocool
11[ -s "$DEMO_HOME/.ssh/known_hosts" ] ||
12 ssh-keygen -F gitbay.org | grep -v '^#' > "$DEMO_HOME/.ssh/known_hosts"
13printf 'Host *\n IdentityFile %s\n IdentitiesOnly yes\n IdentityAgent none\n UserKnownHostsFile %s\n' \
14 "$DEMO_HOME/.ssh/id_ed25519" "$DEMO_HOME/.ssh/known_hosts" > "$DEMO_HOME/.ssh/config"
15printf '#!/bin/sh\nexec /usr/bin/ssh -F "%s" "$@"\n' "$DEMO_HOME/.ssh/config" > "$DEMO_HOME/bin/ssh"
16chmod +x "$DEMO_HOME/bin/ssh"
17printf '[user]\n\tname = Zero Cool\n\temail = zerocool@gitbay.org\n[init]\n\tdefaultBranch = main\n' \
18 > "$DEMO_HOME/.gitconfig"
19printf 'default = "gitbay"\n\n[instances.gitbay]\nhost = "gitbay.org"\n' \
20 > "$DEMO_HOME/.config/gitbay/config.toml"
21
22export HOME=$DEMO_HOME PATH=$DEMO_HOME/bin:$PATH EDITOR=true
23unset SSH_AUTH_SOCK XDG_CONFIG_HOME
24PS1='\[\e[38;5;208m\]\W\[\e[0m\] $ '
25cd "$DEMO_HOME/work"
docs/demo/render.sh added +30
@@ -0,0 +1,30 @@
1#!/bin/sh
2# Render tapes to frames with vhs and encode them here: vhs 0.12's own
3# ffmpeg step writes nothing under ffmpeg 9. Several tapes are joined in
4# order. A tape already recorded is reused: the tapes write to gitbay.org,
5# so delete docs/demo/out/<name> to record one again. Writes
6# docs/demo/out/demo.{mp4,webm,gif}.
7#
8# DEMO_CODE=<mailed code> docs/demo/render.sh signup verify ship
9set -eu
10cd "$(dirname "$0")/../.."
11out=docs/demo/out
12bg=0b0b0b
13list=$out/concat.txt
14mkdir -p "$out"
15: > "$list"
16for name in "$@"; do
17 [ -d "$out/$name" ] || vhs "docs/demo/$name.tape"
18 ffmpeg -loglevel error -y \
19 -framerate 50 -i "$out/$name/frame-text-%05d.png" \
20 -framerate 50 -i "$out/$name/frame-cursor-%05d.png" \
21 -filter_complex "[0][1]overlay,pad=1248:688:32:32:color=$bg" \
22 -c:v libx264 -pix_fmt yuv420p -r 25 "$out/$name.mp4"
23 echo "file '$name.mp4'" >> "$list"
24done
25ffmpeg -loglevel error -y -f concat -safe 0 -i "$list" -c copy "$out/demo.mp4"
26ffmpeg -loglevel error -y -i "$out/demo.mp4" -c:v libvpx-vp9 -b:v 0 -crf 40 "$out/demo.webm"
27ffmpeg -loglevel error -y -i "$out/demo.mp4" \
28 -vf "fps=10,scale=960:-1:flags=lanczos,split[a][b];[a]palettegen=max_colors=32[p];[b][p]paletteuse=dither=none" \
29 "$out/demo.gif"
30ls -l "$out"/demo.*
docs/demo/ship.tape added +52
@@ -0,0 +1,52 @@
1# Part three: repository, issue, branch, merge request, merge, release,
2# as the account verify.tape activated.
3Output docs/demo/out/ship/
4Source docs/demo/common.tape
5
6Type "gitbay repo create zerocool/hello" Enter
7Wait
8Sleep 1.2s
9Type "git clone -q ssh://git@gitbay.org/zerocool/hello.git && cd hello" Enter
10Wait
11Sleep 1.2s
12Type "echo 'hello, world' > README && git add README && git commit -qm 'Say hello'" Enter
13Wait
14Sleep 1.2s
15Type "git push -q origin main" Enter
16Wait
17Sleep 1.2s
18Ctrl+L
19Sleep 400ms
20
21Type "gitbay issue create --title 'Say hello in French' --body 'Bonjour.'" Enter
22Wait
23Sleep 1.2s
24Type "git switch -qc fr && echo 'bonjour, le monde' >> README" Enter
25Wait
26Sleep 1.2s
27Type "git commit -qam 'Say bonjour' && git push -q origin fr" Enter
28Wait
29Sleep 1.2s
30Type "gitbay mr create --source fr --target main --title Bonjour --body 'Closes #1'" Enter
31Wait
32Sleep 1.2s
33Type "gitbay mr merge 1 --strategy squash" Enter
34Wait
35Sleep 1.2s
36Type "gitbay issue list --state closed" Enter
37Wait
38Sleep 1.2s
39Ctrl+L
40Sleep 400ms
41
42Type "# ship it" Enter
43Sleep 600ms
44Type "git switch -q main && git pull -q && git tag v0.1.0 && git push -q origin v0.1.0" Enter
45Wait
46Sleep 1.2s
47Type "gitbay release create v0.1.0 --notes 'Hello in two languages.'" Enter
48Wait
49Sleep 1.2s
50Type "gitbay release list" Enter
51Wait
52Sleep 1.2s
docs/demo/signup.tape added +11
@@ -0,0 +1,11 @@
1# Part one: an SSH key is the account. Registration mails a code, so the
2# recording stops here; ship.tape picks up with the code.
3Output docs/demo/out/signup/
4Source docs/demo/common.tape
5
6Type "# no account yet, just an SSH key" Enter
7Sleep 800ms
8Type "ssh git@gitbay.org whoami" Enter
9Sleep 2.5s
10Type "ssh git@gitbay.org register --username zerocool --email zerocool@gitbay.org" Enter
11Sleep 4s
docs/demo/verify.tape added +13
@@ -0,0 +1,13 @@
1# Part two: the code signup.tape's register mailed, as DEMO_CODE. A code
2# works once, so a rerun needs a fresh account.
3Output docs/demo/out/verify/
4Source docs/demo/common.tape
5
6Hide
7Type "code=$DEMO_CODE && clear" Enter
8Sleep 500ms
9Show
10Type "# the code from the mail" Enter
11Sleep 600ms
12Type "ssh git@gitbay.org email verify $code" Enter
13Sleep 2s
e2e/design_test.go +2 −2
@@ -189,8 +189,8 @@ func TestLandingRoutes(t *testing.T) {
189 `class="button primary" href="/explore">Explore repositories</a>`, 189 `class="button primary" href="/explore">Explore repositories</a>`,
190 `class="button btn" href="/register">Create an account</a>`, 190 `class="button btn" href="/register">Create an account</a>`,
191 "web login</code>", 191 "web login</code>",
192 "/static/img/mr-dark.png", 192 "/static/img/demo.webm",
193 "<picture>", 193 "<video autoplay muted loop playsinline controls",
194 } { 194 } {
195 if !strings.Contains(body, want) { 195 if !strings.Contains(body, want) {
196 t.Errorf("landing lacks %q", want) 196 t.Errorf("landing lacks %q", want)
internal/httpd/fonts_test.go +13 −3
@@ -47,7 +47,7 @@ func TestStylesheetFontsAreServed(t *testing.T) {
47} 47}
48 48
49// TestLandingImagesAreServed: every file under static/img has a route 49// TestLandingImagesAreServed: every file under static/img has a route
50// that answers 200 with an image type and the stylesheet's cache policy. 50// that answers 200 with an image or video type, and a video answers Range.
51func TestLandingImagesAreServed(t *testing.T) { 51func TestLandingImagesAreServed(t *testing.T) {
52 s := New(config.Default(), nil) 52 s := New(config.Default(), nil)
53 byPattern := map[string]http.HandlerFunc{} 53 byPattern := map[string]http.HandlerFunc{}
@@ -72,8 +72,18 @@ func TestLandingImagesAreServed(t *testing.T) {
72 } 72 }
73 rec := httptest.NewRecorder() 73 rec := httptest.NewRecorder()
74 h(rec, httptest.NewRequest("GET", u, nil)) 74 h(rec, httptest.NewRequest("GET", u, nil))
75 if rec.Code != http.StatusOK || !strings.HasPrefix(rec.Header().Get("Content-Type"), "image/") { 75 ct := rec.Header().Get("Content-Type")
76 t.Errorf("%s: %d %s", e.Name(), rec.Code, rec.Header().Get("Content-Type")) 76 if rec.Code != http.StatusOK || !(strings.HasPrefix(ct, "image/") || strings.HasPrefix(ct, "video/")) {
77 t.Errorf("%s: %d %q", e.Name(), rec.Code, ct)
78 }
79 if strings.HasPrefix(ct, "video/") {
80 rec := httptest.NewRecorder()
81 req := httptest.NewRequest("GET", u, nil)
82 req.Header.Set("Range", "bytes=0-1")
83 h(rec, req)
84 if rec.Code != http.StatusPartialContent || rec.Body.Len() != 2 {
85 t.Errorf("%s: Range answered %d with %d bytes", e.Name(), rec.Code, rec.Body.Len())
86 }
77 } 87 }
78 } 88 }
79} 89}
internal/httpd/web.go +12 −4
@@ -117,16 +117,24 @@ func (s *Server) font(w http.ResponseWriter, r *http.Request) {
117 w.Write(data) 117 w.Write(data)
118} 118}
119 119
120// image serves the embedded landing pictures with the font cache policy. 120var staticTypes = map[string]string{
121 ".gif": "image/gif",
122 ".webm": "video/webm",
123 ".mp4": "video/mp4",
124}
125
126// image serves the embedded landing recording with the font cache policy.
127// ServeContent answers Range, which Safari needs to play video.
121func (s *Server) image(w http.ResponseWriter, r *http.Request) { 128func (s *Server) image(w http.ResponseWriter, r *http.Request) {
122 data, err := web.ImageFS.ReadFile("static" + r.URL.Path[len("/static"):]) 129 name := "static" + r.URL.Path[len("/static"):]
130 data, err := web.ImageFS.ReadFile(name)
123 if err != nil { 131 if err != nil {
124 http.NotFound(w, r) 132 http.NotFound(w, r)
125 return 133 return
126 } 134 }
127 w.Header().Set("Content-Type", "image/png") 135 w.Header().Set("Content-Type", staticTypes[path.Ext(name)])
128 w.Header().Set("Cache-Control", "public, max-age=604800, immutable") 136 w.Header().Set("Cache-Control", "public, max-age=604800, immutable")
129 w.Write(data) 137 http.ServeContent(w, r, name, time.Time{}, bytes.NewReader(data))
130} 138}
131 139
132// notFound renders the designed 404 page with a 404 status. Falls back to 140// notFound renders the designed 404 page with a 404 status. Falls back to
internal/web/static/img/demo.gif added

Binary file not shown.

internal/web/static/img/demo.mp4 added

Binary file not shown.

internal/web/static/img/demo.webm added

Binary file not shown.

internal/web/static/img/mr-dark.png deleted

Binary file not shown.

internal/web/static/img/mr-light.png deleted

Binary file not shown.

internal/web/static/style.css +1 −1
@@ -1703,7 +1703,7 @@ details.refmenu .refdrop a.allrefs {
1703.lede { font-size: var(--fs-4); color: var(--muted); line-height: 1.5; max-width: 40rem; margin: 0 0 var(--sp-5); } 1703.lede { font-size: var(--fs-4); color: var(--muted); line-height: 1.5; max-width: 40rem; margin: 0 0 var(--sp-5); }
1704pre.quickstart { margin: 0 0 var(--sp-4); } 1704pre.quickstart { margin: 0 0 var(--sp-4); }
1705.shot { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; margin: var(--sp-5) 0; } 1705.shot { border: 1px solid var(--line); border-radius: var(--r-card); overflow: hidden; margin: var(--sp-5) 0; }
1706.shot img { display: block; width: 100%; height: auto; max-height: 420px; object-fit: cover; object-position: top; } 1706.shot video { display: block; width: 100%; height: auto; }
1707.shot figcaption { 1707.shot figcaption {
1708 border-top: 1px solid var(--line); 1708 border-top: 1px solid var(--line);
1709 background: var(--surface); 1709 background: var(--surface);
internal/web/templates/landing.html +6 −6
@@ -4,14 +4,14 @@
4<div class="landing"> 4<div class="landing">
5<h1>{{template "mark"}}{{.Site}}</h1> 5<h1>{{template "mark"}}{{.Site}}</h1>
6<p class="lede">A git forge you drive from the terminal. Repositories, issues, merge requests and CI over SSH, with a fast, readable web view of the same state.</p> 6<p class="lede">A git forge you drive from the terminal. Repositories, issues, merge requests and CI over SSH, with a fast, readable web view of the same state.</p>
7<pre class="quickstart" tabindex="0">ssh git@{{.Host}} help # every command, no client to install 7<pre class="quickstart" tabindex="0">ssh git@{{.Host}} help
8git clone ssh://git@{{.Host}}/owner/repo.git</pre> 8git clone ssh://git@{{.Host}}/owner/repo.git</pre>
9<div class="routes"><a class="button primary" href="/explore">Explore repositories</a>{{if .Signup}}<a class="button btn" href="/register">Create an account</a>{{end}}</div> 9<div class="routes"><a class="button primary" href="/explore">Explore repositories</a>{{if .Signup}}<a class="button btn" href="/register">Create an account</a>{{end}}</div>
10<figure class="shot"><picture> 10<figure class="shot"><video autoplay muted loop playsinline controls width="1248" height="688" aria-describedby="demo-caption">
11 <source srcset="/static/img/mr-dark.png" media="(prefers-color-scheme: dark)"> 11 <source src="/static/img/demo.webm" type="video/webm">
12 <img src="/static/img/mr-light.png" width="1280" height="900" alt="A merged merge request: the description on the left, and on the right two CI checks reporting success with how long each ran, an approval, and the source and target branches."> 12 <source src="/static/img/demo.mp4" type="video/mp4">
13</picture> 13</video>
14<figcaption>A merged merge request on this instance. The description is on the left; the column beside it carries the CI checks, the approval that let it merge, and the branches it went between.</figcaption> 14<figcaption id="demo-caption">Signup to release on gitbay.org from a terminal: register an SSH key, verify the mailed code, create a repository and push, file an issue, open and merge a request that closes it, then tag and publish a release.</figcaption>
15</figure> 15</figure>
16<div class="facets"> 16<div class="facets">
17 <section><h2>Read</h2><p>Browse and clone any public repository over HTTPS or <code>git://</code>, no account. Every commit shows whether its signature verified.</p></section> 17 <section><h2>Read</h2><p>Browse and clone any public repository over HTTPS or <code>git://</code>, no account. Every commit shows whether its signature verified.</p></section>
internal/web/templates/layout.html +1 −1
@@ -84,7 +84,7 @@
84</main> 84</main>
85 85
86<footer> 86<footer>
87 <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="https://gitbay.org/krz/gitbay-ios">iOS app</a> · <a href="/privacy">Privacy</a></p> 87 <p>Powered by <a href="https://gitbay.org/krz/gitbay">gitbay</a>{{with gitbayVersion}} · <code><a href="https://gitbay.org/krz/gitbay/commit/{{gitbayCommit}}">{{.}}</a></code>{{end}} · <a href="https://apps.apple.com/us/app/gitbay/id6806399095">iOS app</a> · <a href="/privacy">Privacy</a></p>
88</footer> 88</footer>
89</div> 89</div>
90</div> 90</div>
internal/web/web.go +1 −1
@@ -29,7 +29,7 @@ var FaviconSVG []byte
29//go:embed static/fonts/*.woff2 29//go:embed static/fonts/*.woff2
30var FontFS embed.FS 30var FontFS embed.FS
31 31
32//go:embed static/img/*.png 32//go:embed static/img/*.gif static/img/*.webm static/img/*.mp4
33var ImageFS embed.FS 33var ImageFS embed.FS
34 34
35// version returns the short VCS revision baked into the binary, or "" when 35// version returns the short VCS revision baked into the binary, or "" when