-
Notifications
You must be signed in to change notification settings - Fork 0
339 lines (317 loc) · 16.9 KB
/
Copy pathpackage.yml
File metadata and controls
339 lines (317 loc) · 16.9 KB
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
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# SPDX-FileCopyrightText: Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0
#
# Slice 001 lines 9 and 10. This workflow builds the packaged binary on three operating
# systems and smokes it. What it proves and what it cannot are written out in
# docs/packaging.md and repeated by each job in its own summary, because an artifact from a
# runner is easy to mistake for evidence of something it is not.
#
# A runner has no desktop session. No job here opens a window, and none claims to.
name: package
# Corrected 2026-09-06, before this workflow had ever run. The push trigger was
# `branches: [main]`, so the one branch whose evidence depends on it, the slice branch, was
# the one branch it ignored, and pushing slice 001 produced no package run at all. Slice
# branches are where a packaging change is proven; main is where it has already been proven.
#
# Narrowed 2026-09-19. Between 2026-09-05 and 2026-09-07 this matrix ran 31 times on every
# push, 1 600 runner-minutes, most of them Windows and macOS, for a packaging path that
# slice 001 had already proven. It now runs when a slice or release tag is pushed, on a
# change to something packaging actually depends on, or by hand. A slice that touches
# packaging asks for the run with `workflow_dispatch` and cites the run id in its proof.
on:
push:
tags: ['slice/**', 'v*']
paths:
- mix.exs
- mix.lock
- config/**
- src-tauri/**
- rust-toolchain.toml
- .tool-versions
- .github/workflows/package.yml
workflow_dispatch:
jobs:
package:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: linux x86_64
os: ubuntu-latest
target: linux_x86_64
artifact: desktop_linux_x86_64
- name: macOS aarch64
os: macos-latest
target: macos_aarch64
artifact: desktop_macos_aarch64
- name: windows x86_64
os: windows-latest
target: windows_x86_64
artifact: desktop_windows_x86_64.exe
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1
with:
version-file: .tool-versions
version-type: strict
# The Zig version is read from .tool-versions rather than written here. Burrito 1.6.0
# compares Zig for equality, not a range, so a second copy of the number in this file
# would be a second thing to keep in step, and the one that drifts is always the copy.
- name: Read the pinned Zig version from .tool-versions
id: zig
shell: bash
run: echo "version=$(awk '$1=="zig"{print $2}' .tool-versions)" >> "$GITHUB_OUTPUT"
- uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
with:
version: ${{ steps.zig.outputs.version }}
# The Rust version is not written here either: rustup reads rust-toolchain.toml, which
# is the pin (see NOTES.md D1: asdf has no rust plugin and ignores a rust line).
- name: Rust toolchain from rust-toolchain.toml
shell: bash
run: rustup show active-toolchain
# Tauri v2 on Linux needs these; macOS and Windows runners carry their own webviews.
- name: Tauri system libraries (Linux only)
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev patchelf
- name: Confirm the toolchain is the pinned one
shell: bash
run: |
set -euo pipefail
zig version
test "$(zig version)" = "${{ steps.zig.outputs.version }}"
elixir --version
- run: mix deps.get
# config/prod.exs sets cache_static_manifest; the endpoint raises at boot without it.
- run: mix assets.deploy
# --overwrite is not optional. Without it, and with a release directory already
# present, mix release prompts, gets no stdin, and exits 0 having built nothing.
# Slice 013: on Linux, mdex's NIF is built from source for Burrito's musl ERTS, linked
# through Zig (scripts/zig-cc-musl; config/config.exs names the three settings and
# NOTES.md finding 14 the measurement). rustup installed the musl standard library from
# rust-toolchain.toml above. macOS and Windows load the precompiled artifact for their
# native ERTS; the smoke step below is what says whether it loaded.
- name: Build the Burrito binary
shell: bash
env:
BURRITO_TARGET: ${{ matrix.target }}
MIX_ENV: prod
MDEX_NATIVE_BUILD: ${{ runner.os == 'Linux' && '1' || '' }}
TRINITY_NIF_TARGET: ${{ runner.os == 'Linux' && 'x86_64-unknown-linux-musl' || '' }}
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER: ${{ runner.os == 'Linux' && format('{0}/scripts/zig-cc-musl', github.workspace) || '' }}
run: |
set -euo pipefail
# `mix deps.get` above runs in the default env. On the Windows runner the prod build
# then refused with
#
# Unchecked dependencies for environment prod:
# * daisyui (https://github.com/saadeghi/daisyui.git - v5.5.20)
# lock mismatch: the dependency is out of date
#
# A second `mix deps.get` under MIX_ENV=prod did NOT fix it: it reported "All
# dependencies have been fetched" and the release refused anyway, so the checkout on
# disk is what Mix disagrees with, not the environment. Both offenders are git
# `sparse` deps carrying `app: false, compile: false`; they exist for the asset build
# and nothing in the release links them. Cleaning them forces a fresh checkout.
# Windows only, because neither other runner has ever refused.
if [ "${{ runner.os }}" = "Windows" ]; then
mix deps.clean daisyui heroicons
fi
mix deps.get
mix release desktop --overwrite
- name: The artifact exists and is not empty
shell: bash
run: |
set -euo pipefail
ls -l burrito_out/
test -s "burrito_out/${{ matrix.artifact }}"
# Tauri resolves `externalBin: ["../burrito_out/desktop"]` to
# `desktop-<target triple>`; Burrito writes `desktop_<os>_<arch>`. Without this the
# shell's build script stops with `resource path ... doesn't exist`, which is exactly
# how the first run of this step failed. `ExTauri.run/1` does the same rename for
# production builds; the triple comes from rustc rather than a table in this file.
- name: Name the sidecar the way Tauri expects
shell: bash
run: |
set -euo pipefail
triple=$(rustc -vV | awk '/^host:/{print $2}')
if [ "${{ runner.os }}" = "Windows" ]; then dst="burrito_out/desktop-$triple.exe"
else dst="burrito_out/desktop-$triple"; fi
cp "burrito_out/${{ matrix.artifact }}" "$dst"
ls -l burrito_out/
# Item 2 of the G4 decision. `src-tauri/` exists from slice 001's ex_tauri.install, so
# the shell is buildable in CI. This compiles the Rust window; it does not run it, and
# no job here claims a window opened: a runner has no desktop session.
- name: Build the Tauri shell
shell: bash
run: cargo build --manifest-path src-tauri/Cargo.toml --locked
- name: The shell binary exists
shell: bash
run: |
set -euo pipefail
if [ "${{ runner.os }}" = "Windows" ]; then bin=src-tauri/target/debug/trinity.exe
else bin=src-tauri/target/debug/trinity; fi
ls -l "$bin"
test -s "$bin"
echo "SHELL_BIN=$bin" >> "$GITHUB_ENV"
# --no-halt is what makes the exit mean anything. Burrito launches the release as
# `-s elixir start_cli`, and the Elixir CLI halts when its command list is empty, so
# without --no-halt the binary exits 0 on its own and `--smoke` proves nothing.
- name: "Smoke test: boots, serves, exits by itself, leaves nothing behind"
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
# Match OUR processes, not the machine's. The first version of this step diffed the
# whole `ps -eo pid,ppid,comm` table and failed on macOS because the runner's own
# daemons churn between the two samples: mdworker_shared exiting, CloudTelemetry
# starting, and the `ps` process itself differing. The artifact had launched, served
# and exited cleanly; the assertion was wrong, not the binary. "No process of ours
# remains" is the claim AC7 actually makes.
ours() { ps -eo pid,ppid,comm | grep -E 'burrito|beam\.smp|erl_child_setup|desktop_' | grep -v grep || true; }
ours > ps-before.txt
chmod +x "burrito_out/${{ matrix.artifact }}"
"./burrito_out/${{ matrix.artifact }}" --no-halt --smoke | tee smoke.log
sleep 3
ours > ps-after.txt
grep -q '^TRINITY_SMOKE_PORT=[0-9][0-9]*$' smoke.log
# Slice 013: the renderer's NIF loaded and rendered in this binary (exit 3 otherwise).
grep -q '^TRINITY_SMOKE_MARKDOWN=ok$' smoke.log
echo "--- ours, before ---"; cat ps-before.txt
echo "--- ours, after ----"; cat ps-after.txt
diff ps-before.txt ps-after.txt
- name: Smoke test on Windows
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
& ".\burrito_out\${{ matrix.artifact }}" --no-halt --smoke | Tee-Object smoke.log
if ($LASTEXITCODE -ne 0) { throw "smoke exited $LASTEXITCODE" }
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_PORT=\d+$')) {
throw "no port line"
}
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_MARKDOWN=ok$')) {
throw "the markdown renderer did not render in this binary (slice 013)"
}
# AC1's property on a runner, and AC6's per-OS cold start, which used to be skipped.
# `bash` on every OS, Windows included: the job that never curled was the one gap in
# slice 001's CI evidence, and it was a hole in this file rather than in the artifact.
- name: Serves HTTP 200, timed from launch
shell: bash
run: |
set -euo pipefail
# `date +%s%3N` is a GNU extension: BSD date on the macOS runner emitted
# "17887458973N" and the arithmetic died with "value too great for base". python3 is
# present on all three runner images and gives the same number everywhere.
now_ms() { python3 -c 'import time;print(int(time.time()*1000))'; }
start=$(now_ms)
PHX_SERVER=true "./burrito_out/${{ matrix.artifact }}" --no-halt > serve.log 2>&1 &
PID=$!
# Match the ENDPOINT line, not any loopback address. `grep -o '127.0.0.1:[0-9]*' |
# head -1` picked the wrong port on Windows and only on Windows: there,
# ExTauri.ShutdownManager's heartbeat listens on a **TCP** socket because the BEAM
# cannot listen on a Unix domain socket on that OS, so serve.log reads
#
# [ExTauri.ShutdownManager] Started - heartbeat monitoring active on 127.0.0.1:58911
# Running TrinityWeb.Endpoint with Bandit 1.12.5 at 127.0.0.1:58912 (http)
#
# and the first match is the heartbeat. macOS and Linux use a Unix socket, print no
# loopback line before the endpoint, and were right by luck.
for _ in $(seq 1 240); do
PORT=$(grep -oE 'Running TrinityWeb\.Endpoint with Bandit [0-9.]+ at 127\.0\.0\.1:[0-9]+' serve.log \
| head -1 | grep -oE '[0-9]+$' || true)
[ -n "${PORT:-}" ] && break
sleep 0.5
done
# No endpoint line in two minutes says nothing on its own either: runs 35512430836
# (slice/011), 35515556177 (slice/012) and 35521749862 all failed here on macOS and
# Windows with an empty step log. The log the binary wrote goes to stderr first.
if [ -z "${PORT:-}" ]; then
echo "no 'Running TrinityWeb.Endpoint' line in serve.log after 120 s" >&2
echo "--- serve.log ---" >&2; tail -60 serve.log >&2
exit 1
fi
# Bounded. The first version of this loop had no limit and the Windows runner sat in
# it until the run was cancelled by hand: a step that hangs reports nothing and
# fails nothing, which is the same shape as a workflow that never fires.
ok=0
for _ in $(seq 1 600); do
if curl -sS -o /dev/null "http://127.0.0.1:$PORT/" 2>/dev/null; then ok=1; break; fi
sleep 0.1
done
if [ "$ok" != "1" ]; then
echo "no HTTP response on port $PORT after 60 s" >&2
echo "--- serve.log ---" >&2; tail -40 serve.log >&2
exit 1
fi
elapsed=$(( $(now_ms) - start ))
code=$(curl -sS -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT/")
echo "HTTP $code on port $PORT"
echo "COLD_START_MS=$elapsed" | tee -a "$GITHUB_ENV"
echo "### ${{ matrix.name }}: cold start to first HTTP 200: **${elapsed} ms**" >> "$GITHUB_STEP_SUMMARY"
# A non-200 says nothing on its own: run 35518054546 (slice 013) printed "HTTP 500"
# and nothing else, and the log that would have named the exception was never
# read. The body and the tail of serve.log go to stderr before the step fails.
if [ "$code" != "200" ]; then
echo "--- body of GET / ---" >&2; curl -sS "http://127.0.0.1:$PORT/" | head -c 2000 >&2; echo >&2
echo "--- serve.log ---" >&2; tail -60 serve.log >&2
fi
test "$code" = "200"
# Stopping it, and Windows needs its own verb. `kill` from Git-bash does not stop a
# native Windows process, so the backgrounded .exe outlived the step and the job hung
# long past the bounded loops above: run 34078281292, cancelled by hand. `taskkill
# /T` takes the wrapper and its BEAM together, which on Windows is also the only
# thing that clears finding F1's orphan.
if [ "${{ runner.os }}" = "Windows" ]; then
taskkill //PID "$PID" //T //F >/dev/null 2>&1 || true
taskkill //IM desktop_windows_x86_64.exe //T //F >/dev/null 2>&1 || true
else
CHILD=$(pgrep -P "$PID" 2>/dev/null || true)
kill "$PID" || true
# The wrapper does not forward termination to the BEAM it launched: finding F1,
# re-measured at G4 as an orphan still serving 200 past 7.6 s. Kill the child
# explicitly or the runner leaves it behind.
[ -n "$CHILD" ] && kill "$CHILD" 2>/dev/null || true
fi
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ matrix.artifact }}
path: burrito_out/${{ matrix.artifact }}
if-no-files-found: error
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: launch-log-${{ matrix.target }}
path: |
smoke.log
serve.log
if-no-files-found: warn
# Line 10. Each job states, in its own summary, which of the two things it did, and
# every job here did the second one.
- name: State what this job did not prove
if: always()
shell: bash
run: |
{
echo "## ${{ matrix.name }}: what this run does and does not establish"
echo
echo "**Established:** the artifact builds on ${{ matrix.os }}; it launches and"
echo "reaches serving; under \`--no-halt --smoke\` it exits by itself and the"
echo "process list is unchanged either side; the launch log is attached."
echo
echo "**Also established:** the Tauri **shell compiles** on this OS"
echo "(\`cargo build --manifest-path src-tauri/Cargo.toml --locked\`), and cold"
echo "start to first HTTP 200 was measured at **${COLD_START_MS:-not measured} ms**."
echo
echo "**Not established, and not claimed:** the shell was **built, never run.**"
echo "No native window was opened. This job smoked the **sidecar alone, with no"
echo "display**: it did not run the shell under \`xvfb-run\` and it did not use a"
echo "desktop session, because this runner has none. No screenshot exists here;"
echo "the only screenshot of a real window in this project is the owner's, on"
echo "Linux, at slices/001-packaging-spike/proof/. First **paint** was not"
echo "measured. Nothing here speaks to signing or notarisation."
} >> "$GITHUB_STEP_SUMMARY"