Skip to content

Commit 72aad35

Browse files
committed
Merge slice/001-packaging-spike
Slice 001 — packaging spike: Burrito + ex_tauri smoke build. Approved by the owner at G4, 2026-09-07. The scaffold packages as a single Burrito binary and opens in a native Tauri window. Proven on three operating systems for everything short of the window, and on Linux including it: linux x86_64 desktop_linux_x86_64 21293896 B 589 ms to first HTTP 200 macOS aarch64 desktop_macos_aarch64 12435992 B 1556 ms Windows x86_64 desktop_windows_x86_64.exe 25024512 B 929 ms AC2 and AC3 exit unproven and named, not counted: a native window on macOS and on Windows, both needing a desktop that does not exist here. Two findings go to slice 100: F1, the production sidecar orphaning when its parent dies with nothing attached; F2, the Windows heartbeat's unauthenticated loopback TCP port. ADR-0004 stays proposed with three appended corrections. Signed-off-by: Ayla Croft <aylacroft@proton.me>
2 parents de95666 + 2a47357 commit 72aad35

63 files changed

Lines changed: 10629 additions & 75 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.githooks/commit-msg‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,63 @@ grep -v '^Co-Authored-By: Claude' "$msg_file" \
1919
# Drop blank lines the deletions left at the end of the message.
2020
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' "$tmp" > "$msg_file"
2121
rm -f "$tmp"
22+
23+
# ---------------------------------------------------------------------------
24+
# Warning only: does every repo path the message names appear in the staged diff?
25+
#
26+
# Added at slice 001 G4, for a mistake this slice made three times: a commit whose message ran
27+
# ahead of its tree. Twice it was an unread `plan_check exit=1`; once a message and a NOTES
28+
# line said `src-tauri/Cargo.lock` had been added to REUSE.toml when the edit had failed on a
29+
# stale anchor, in the same output as a green gate. `mix gate` does not check that a commit
30+
# message is true, and nothing else did either.
31+
#
32+
# **It warns and never blocks**, deliberately. A message legitimately names paths it does not
33+
# touch — the file a fix refers to, a path being explained, a command being quoted — so a
34+
# blocking version would be wrong far more often than right. This exists to put the claim and
35+
# the diff in front of the author at the moment of writing, not to arbitrate.
36+
#
37+
# Heuristic and stated as one: it looks for tokens that end in a source-ish extension or sit
38+
# under a known top-level directory, and it only reports paths that do not exist in the staged
39+
# diff AND do not exist in the working tree at all.
40+
staged=$(git diff --cached --name-only 2>/dev/null || true)
41+
[ -z "$staged" ] && exit 0
42+
43+
missing=""
44+
unstaged=""
45+
for tok in $(tr -c 'A-Za-z0-9_./-' ' ' < "$msg_file"); do
46+
case "$tok" in
47+
*/*)
48+
case "$tok" in
49+
lib/*|test/*|config/*|scripts/*|docs/*|slices/*|priv/*|assets/*|src-tauri/*|.github/*|.githooks/*|credo_checks/*) ;;
50+
*) continue ;;
51+
esac
52+
;;
53+
mix.exs|mix.lock|README.md|REUSE.toml|ROADMAP.md|VERSIONS.md|coverage.tsv|.gitignore|.tool-versions|rust-toolchain.toml) ;;
54+
*) continue ;;
55+
esac
56+
tok=${tok%%,}; tok=${tok%%.}; tok=${tok%%:}
57+
printf '%s\n' "$staged" | grep -qxF "$tok" && continue
58+
if [ -e "$tok" ]; then
59+
# Exists, but this commit does not touch it. This is the tier that would have caught
60+
# a6085b3, whose message said REUSE.toml had gained a line when the edit had failed:
61+
# REUSE.toml existed, so "does it exist?" alone said nothing.
62+
case " $unstaged " in *" $tok "*) ;; *) unstaged="$unstaged $tok" ;; esac
63+
continue
64+
fi
65+
case " $missing " in *" $tok "*) ;; *) missing="$missing $tok" ;; esac
66+
done
67+
68+
if [ -n "$missing" ]; then
69+
echo "commit-msg: WARNING — the message names paths that exist nowhere in the tree:" >&2
70+
for m in $missing; do echo " $m" >&2; done
71+
fi
72+
73+
if [ -n "$unstaged" ]; then
74+
echo "commit-msg: NOTE — the message names paths this commit does not touch:" >&2
75+
for m in $unstaged; do echo " $m" >&2; done
76+
echo " Fine when the message refers to a file; wrong when it claims to have" >&2
77+
echo " changed one. This is the noisy tier and it is meant to be read, not obeyed." >&2
78+
fi
79+
80+
# Never blocks, in either tier.
81+
exit 0

‎.github/workflows/package.yml‎

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
2+
# SPDX-License-Identifier: Apache-2.0
3+
#
4+
# Slice 001 lines 9 and 10. This workflow builds the packaged binary on three operating
5+
# systems and smokes it. What it proves and what it cannot are written out in
6+
# docs/packaging.md and repeated by each job in its own summary, because an artifact from a
7+
# runner is easy to mistake for evidence of something it is not.
8+
#
9+
# A runner has no desktop session. No job here opens a window, and none claims to.
10+
name: package
11+
12+
# Corrected 2026-09-06, before this workflow had ever run. The push trigger was
13+
# `branches: [main]`, so the one branch whose evidence depends on it — the slice branch — was
14+
# the one branch it ignored, and pushing slice 001 produced no package run at all. Slice
15+
# branches are where a packaging change is proven; main is where it has already been proven.
16+
on:
17+
push:
18+
branches: [main, 'slice/**']
19+
pull_request:
20+
workflow_dispatch:
21+
22+
jobs:
23+
package:
24+
name: ${{ matrix.name }}
25+
runs-on: ${{ matrix.os }}
26+
strategy:
27+
fail-fast: false
28+
matrix:
29+
include:
30+
- name: linux x86_64
31+
os: ubuntu-latest
32+
target: linux_x86_64
33+
artifact: desktop_linux_x86_64
34+
- name: macOS aarch64
35+
os: macos-latest
36+
target: macos_aarch64
37+
artifact: desktop_macos_aarch64
38+
- name: windows x86_64
39+
os: windows-latest
40+
target: windows_x86_64
41+
artifact: desktop_windows_x86_64.exe
42+
43+
steps:
44+
- uses: actions/checkout@v4
45+
46+
- uses: erlef/setup-beam@v1
47+
with:
48+
version-file: .tool-versions
49+
version-type: strict
50+
51+
# The Zig version is read from .tool-versions rather than written here. Burrito 1.6.0
52+
# compares Zig for equality, not a range, so a second copy of the number in this file
53+
# would be a second thing to keep in step, and the one that drifts is always the copy.
54+
- name: Read the pinned Zig version from .tool-versions
55+
id: zig
56+
shell: bash
57+
run: echo "version=$(awk '$1=="zig"{print $2}' .tool-versions)" >> "$GITHUB_OUTPUT"
58+
59+
- uses: mlugg/setup-zig@v2
60+
with:
61+
version: ${{ steps.zig.outputs.version }}
62+
63+
# The Rust version is not written here either: rustup reads rust-toolchain.toml, which
64+
# is the pin (see NOTES.md D1 — asdf has no rust plugin and ignores a rust line).
65+
- name: Rust toolchain from rust-toolchain.toml
66+
shell: bash
67+
run: rustup show active-toolchain
68+
69+
# Tauri v2 on Linux needs these; macOS and Windows runners carry their own webviews.
70+
- name: Tauri system libraries (Linux only)
71+
if: runner.os == 'Linux'
72+
run: |
73+
sudo apt-get update
74+
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \
75+
libayatana-appindicator3-dev librsvg2-dev patchelf
76+
77+
- name: Confirm the toolchain is the pinned one
78+
shell: bash
79+
run: |
80+
set -euo pipefail
81+
zig version
82+
test "$(zig version)" = "${{ steps.zig.outputs.version }}"
83+
elixir --version
84+
85+
- run: mix deps.get
86+
87+
# config/prod.exs sets cache_static_manifest; the endpoint raises at boot without it.
88+
- run: mix assets.deploy
89+
90+
# --overwrite is not optional. Without it, and with a release directory already
91+
# present, mix release prompts, gets no stdin, and exits 0 having built nothing.
92+
- name: Build the Burrito binary
93+
shell: bash
94+
env:
95+
BURRITO_TARGET: ${{ matrix.target }}
96+
MIX_ENV: prod
97+
run: |
98+
set -euo pipefail
99+
# `mix deps.get` above runs in the default env. On the Windows runner the prod build
100+
# then refused with
101+
#
102+
# Unchecked dependencies for environment prod:
103+
# * daisyui (https://github.com/saadeghi/daisyui.git - v5.5.20)
104+
# lock mismatch: the dependency is out of date
105+
#
106+
# A second `mix deps.get` under MIX_ENV=prod did NOT fix it — it reported "All
107+
# dependencies have been fetched" and the release refused anyway, so the checkout on
108+
# disk is what Mix disagrees with, not the environment. Both offenders are git
109+
# `sparse` deps carrying `app: false, compile: false`; they exist for the asset build
110+
# and nothing in the release links them. Cleaning them forces a fresh checkout.
111+
# Windows only, because neither other runner has ever refused.
112+
if [ "${{ runner.os }}" = "Windows" ]; then
113+
mix deps.clean daisyui heroicons
114+
fi
115+
mix deps.get
116+
mix release desktop --overwrite
117+
118+
- name: The artifact exists and is not empty
119+
shell: bash
120+
run: |
121+
set -euo pipefail
122+
ls -l burrito_out/
123+
test -s "burrito_out/${{ matrix.artifact }}"
124+
125+
# Tauri resolves `externalBin: ["../burrito_out/desktop"]` to
126+
# `desktop-<target triple>`; Burrito writes `desktop_<os>_<arch>`. Without this the
127+
# shell's build script stops with `resource path ... doesn't exist`, which is exactly
128+
# how the first run of this step failed. `ExTauri.run/1` does the same rename for
129+
# production builds; the triple comes from rustc rather than a table in this file.
130+
- name: Name the sidecar the way Tauri expects
131+
shell: bash
132+
run: |
133+
set -euo pipefail
134+
triple=$(rustc -vV | awk '/^host:/{print $2}')
135+
if [ "${{ runner.os }}" = "Windows" ]; then dst="burrito_out/desktop-$triple.exe"
136+
else dst="burrito_out/desktop-$triple"; fi
137+
cp "burrito_out/${{ matrix.artifact }}" "$dst"
138+
ls -l burrito_out/
139+
140+
# Item 2 of the G4 decision. `src-tauri/` exists from slice 001's ex_tauri.install, so
141+
# the shell is buildable in CI. This compiles the Rust window; it does not run it, and
142+
# no job here claims a window opened — a runner has no desktop session.
143+
- name: Build the Tauri shell
144+
shell: bash
145+
run: cargo build --manifest-path src-tauri/Cargo.toml --locked
146+
147+
- name: The shell binary exists
148+
shell: bash
149+
run: |
150+
set -euo pipefail
151+
if [ "${{ runner.os }}" = "Windows" ]; then bin=src-tauri/target/debug/trinity.exe
152+
else bin=src-tauri/target/debug/trinity; fi
153+
ls -l "$bin"
154+
test -s "$bin"
155+
echo "SHELL_BIN=$bin" >> "$GITHUB_ENV"
156+
157+
# --no-halt is what makes the exit mean anything. Burrito launches the release as
158+
# `-s elixir start_cli`, and the Elixir CLI halts when its command list is empty, so
159+
# without --no-halt the binary exits 0 on its own and `--smoke` proves nothing.
160+
- name: Smoke — boots, serves, exits by itself, leaves nothing behind
161+
if: runner.os != 'Windows'
162+
shell: bash
163+
run: |
164+
set -euo pipefail
165+
# Match OUR processes, not the machine's. The first version of this step diffed the
166+
# whole `ps -eo pid,ppid,comm` table and failed on macOS because the runner's own
167+
# daemons churn between the two samples — mdworker_shared exiting, CloudTelemetry
168+
# starting, and the `ps` process itself differing. The artifact had launched, served
169+
# and exited cleanly; the assertion was wrong, not the binary. "No process of ours
170+
# remains" is the claim AC7 actually makes.
171+
ours() { ps -eo pid,ppid,comm | grep -E 'burrito|beam\.smp|erl_child_setup|desktop_' | grep -v grep || true; }
172+
ours > ps-before.txt
173+
chmod +x "burrito_out/${{ matrix.artifact }}"
174+
"./burrito_out/${{ matrix.artifact }}" --no-halt --smoke | tee smoke.log
175+
sleep 3
176+
ours > ps-after.txt
177+
grep -q '^TRINITY_SMOKE_PORT=[0-9][0-9]*$' smoke.log
178+
echo "--- ours, before ---"; cat ps-before.txt
179+
echo "--- ours, after ----"; cat ps-after.txt
180+
diff ps-before.txt ps-after.txt
181+
182+
- name: Smoke — Windows
183+
if: runner.os == 'Windows'
184+
shell: pwsh
185+
run: |
186+
$ErrorActionPreference = "Stop"
187+
& ".\burrito_out\${{ matrix.artifact }}" --no-halt --smoke | Tee-Object smoke.log
188+
if ($LASTEXITCODE -ne 0) { throw "smoke exited $LASTEXITCODE" }
189+
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_PORT=\d+$')) {
190+
throw "no port line"
191+
}
192+
193+
# AC1's property on a runner, and AC6's per-OS cold start, which used to be skipped.
194+
# `bash` on every OS, Windows included: the job that never curled was the one gap in
195+
# slice 001's CI evidence, and it was a hole in this file rather than in the artifact.
196+
- name: Serves HTTP 200, timed from launch
197+
shell: bash
198+
run: |
199+
set -euo pipefail
200+
# `date +%s%3N` is a GNU extension: BSD date on the macOS runner emitted
201+
# "17887458973N" and the arithmetic died with "value too great for base". python3 is
202+
# present on all three runner images and gives the same number everywhere.
203+
now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; }
204+
start=$(now_ms)
205+
PHX_SERVER=true "./burrito_out/${{ matrix.artifact }}" --no-halt > serve.log 2>&1 &
206+
PID=$!
207+
# Match the ENDPOINT line, not any loopback address. `grep -o '127.0.0.1:[0-9]*' |
208+
# head -1` picked the wrong port on Windows and only on Windows: there,
209+
# ExTauri.ShutdownManager's heartbeat listens on a **TCP** socket because the BEAM
210+
# cannot listen on a Unix domain socket on that OS, so serve.log reads
211+
#
212+
# [ExTauri.ShutdownManager] Started - heartbeat monitoring active on 127.0.0.1:58911
213+
# Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:58912 (http)
214+
#
215+
# and the first match is the heartbeat. macOS and Linux use a Unix socket, print no
216+
# loopback line before the endpoint, and were right by luck.
217+
for _ in $(seq 1 240); do
218+
PORT=$(grep -oE 'Running TrinityWeb\.Endpoint with Bandit [0-9.]+ at 127\.0\.0\.1:[0-9]+' serve.log \
219+
| head -1 | grep -oE '[0-9]+$' || true)
220+
[ -n "${PORT:-}" ] && break
221+
sleep 0.5
222+
done
223+
test -n "${PORT:-}"
224+
# Bounded. The first version of this loop had no limit and the Windows runner sat in
225+
# it until the run was cancelled by hand — a step that hangs reports nothing and
226+
# fails nothing, which is the same shape as a workflow that never fires.
227+
ok=0
228+
for _ in $(seq 1 600); do
229+
if curl -sS -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then ok=1; break; fi
230+
sleep 0.1
231+
done
232+
if [ "$ok" != "1" ]; then
233+
echo "no HTTP response on port $PORT after 60 s" >&2
234+
echo "--- serve.log ---" >&2; tail -40 serve.log >&2
235+
exit 1
236+
fi
237+
elapsed=$(( $(now_ms) - start ))
238+
code=$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/")
239+
echo "HTTP $code on port $PORT"
240+
echo "COLD_START_MS=$elapsed" | tee -a "$GITHUB_ENV"
241+
echo "### ${{ matrix.name }} — cold start to first HTTP 200: **${elapsed} ms**" >> "$GITHUB_STEP_SUMMARY"
242+
test "$code" = "200"
243+
# Stopping it, and Windows needs its own verb. `kill` from Git-bash does not stop a
244+
# native Windows process, so the backgrounded .exe outlived the step and the job hung
245+
# long past the bounded loops above — run 34078281292, cancelled by hand. `taskkill
246+
# /T` takes the wrapper and its BEAM together, which on Windows is also the only
247+
# thing that clears finding F1's orphan.
248+
if [ "${{ runner.os }}" = "Windows" ]; then
249+
taskkill //PID "$PID" //T //F >/dev/null 2>&1 || true
250+
taskkill //IM desktop_windows_x86_64.exe //T //F >/dev/null 2>&1 || true
251+
else
252+
CHILD=$(pgrep -P "$PID" 2>/dev/null || true)
253+
kill "$PID" || true
254+
# The wrapper does not forward termination to the BEAM it launched — finding F1,
255+
# re-measured at G4 as an orphan still serving 200 past 7.6 s. Kill the child
256+
# explicitly or the runner leaves it behind.
257+
[ -n "$CHILD" ] && kill "$CHILD" 2>/dev/null || true
258+
fi
259+
260+
- uses: actions/upload-artifact@v4
261+
with:
262+
name: ${{ matrix.artifact }}
263+
path: burrito_out/${{ matrix.artifact }}
264+
if-no-files-found: error
265+
266+
- uses: actions/upload-artifact@v4
267+
if: always()
268+
with:
269+
name: launch-log-${{ matrix.target }}
270+
path: |
271+
smoke.log
272+
serve.log
273+
if-no-files-found: warn
274+
275+
# Line 10. Each job states, in its own summary, which of the two things it did — and
276+
# every job here did the second one.
277+
- name: State what this job did not prove
278+
if: always()
279+
shell: bash
280+
run: |
281+
{
282+
echo "## ${{ matrix.name }} — what this run does and does not establish"
283+
echo
284+
echo "**Established:** the artifact builds on ${{ matrix.os }}; it launches and"
285+
echo "reaches serving; under \`--no-halt --smoke\` it exits by itself and the"
286+
echo "process list is unchanged either side; the launch log is attached."
287+
echo
288+
echo "**Also established:** the Tauri **shell compiles** on this OS"
289+
echo "(\`cargo build --manifest-path src-tauri/Cargo.toml --locked\`), and cold"
290+
echo "start to first HTTP 200 was measured at **${COLD_START_MS:-not measured} ms**."
291+
echo
292+
echo "**Not established, and not claimed:** the shell was **built, never run.**"
293+
echo "No native window was opened. This job smoked the **sidecar alone, with no"
294+
echo "display** — it did not run the shell under \`xvfb-run\` and it did not use a"
295+
echo "desktop session, because this runner has none. No screenshot exists here;"
296+
echo "the only screenshot of a real window in this project is the owner's, on"
297+
echo "Linux, at slices/001-packaging-spike/proof/. First **paint** was not"
298+
echo "measured. Nothing here speaks to signing or notarisation."
299+
} >> "$GITHUB_STEP_SUMMARY"

‎.gitignore‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,15 @@ erl_crash.dump
1818
*.db-wal
1919

2020
# Desktop shell (slices 001, 100, 101)
21-
/tauri/target/
21+
# `mix ex_tauri.install` creates `src-tauri/`, not `tauri/`, so the rule this line replaces
22+
# named a directory that has never existed. Corrected at slice 001 G4. Rust build output was
23+
# never actually at risk — the generator writes its own src-tauri/.gitignore carrying
24+
# `/target/` — but a rule that matches nothing is a claim that is not true.
25+
/src-tauri/target/
2226
/priv/static/assets/
27+
# Burrito's output: one packaged binary per target, tens of MB each. Slice 001 measures them
28+
# and records the sizes in docs/packaging.md; the binaries themselves stay out of history.
29+
/burrito_out/
2330

2431
# Editor and OS noise
2532
.elixir_ls/
@@ -31,6 +38,12 @@ erl_crash.dump
3138
/tmp/
3239
trinity-*.tar
3340
/priv/static/cache_manifest.json
41+
# `mix assets.deploy` (needed before MIX_ENV=prod builds, because config/prod.exs sets
42+
# cache_static_manifest) writes a digested copy and a .gz sibling next to every static file.
43+
# Both are derived from tracked originals. Added at slice 001 line 5, after a release build
44+
# staged seven of them.
45+
/priv/static/**/*.gz
46+
/priv/static/**/*-????????????????????????????????.*
3447
npm-debug.log
3548
/assets/node_modules/
3649
*.db-*

‎.tool-versions‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
erlang 28.5.0.5
22
elixir 1.20.4-otp-28
3+
zig 0.16.0

0 commit comments

Comments
 (0)