Skip to content

Merge pull request #68 from ScriptKittyOS/chore/public-repo-posture #86

Merge pull request #68 from ScriptKittyOS/chore/public-repo-posture

Merge pull request #68 from ScriptKittyOS/chore/public-repo-posture #86

Workflow file for this run

# 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.
# Corrected again after slice 062. The tags-only trigger meant a change to a packaging input
# (a dependency, the toolchain, the Tauri shell, config) was first packaged at the tag, which is
# after the owner has approved the slice: slice 062 added `jose` and the first packaging run of
# it was the tag's, which failed. Branches are added beside tags, with the same path filter, so
# a change to those inputs is packaged on the branch that makes it. This is not the guard for a
# prod-only code defect: the filter below does not match `lib/`, and `scripts/prod_check.sh` in
# `mix gate` is what catches that class on every push.
on:
push:
branches: ['**']
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 }}"
# Slice 032: the variable, not `--smoke`: Kernel.CLI read the argument as a file
# ("No file named --smoke", exit 1) and won the race against the halting Task on
# macOS in run 35608084951.
TRINITY_SMOKE=1 "./burrito_out/${{ matrix.artifact }}" --no-halt | 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
# Slice 032, AC7: a fake-vector search ran inside this binary through the vector
# store in force (exit 4 otherwise). The EXLA and SEMANTIC lines are recorded, not
# asserted: whether the XLA library loads in Burrito's ERTS is what they measure.
grep -q '^TRINITY_SMOKE_VEC=ok:' smoke.log
grep -E '^TRINITY_SMOKE_(EXLA|SEMANTIC)=' 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"
$env:TRINITY_SMOKE = "1"
& ".\burrito_out\${{ matrix.artifact }}" --no-halt | 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)"
}
# Slice 032, AC7, as on the other two: the vector search binds, the other two lines are recorded.
if (-not (Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_VEC=ok:')) {
throw "the vector search did not run inside this binary (slice 032)"
}
Select-String -Path smoke.log -Pattern '^TRINITY_SMOKE_(EXLA|SEMANTIC)='
# 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"