Release pipeline
Design fragment. The "what" and "why" of how Lattice produces downloadable release artefacts. Slice sequencing lives in the slice plan (
docs/dev/operations/slice-plans/release-pipeline.md) once carved.
Goal
A tag-driven GitHub Actions pipeline that, on every v* tag, builds and publishes downloadable release artefacts for Linux, macOS, and Windows (x86_64 + aarch64) plus a source archive, with checksums and supply-chain provenance. The pipeline is testable without cutting a release (preview mode).
This is build/release infrastructure, not editor runtime. The four paramount goals do not directly govern it; the relevant standing rules are no silent failures (a dropped artefact must be logged, never silently absent) and UX follows convention (artefact shapes match what users expect from Helix / Neovim / Zed).
Reference survey (substrate-weighted)
Per the editor-references rule, Helix is weighted heaviest here — same substrate (Rust), same class of tool, no signing infrastructure assumed. Live release assets at time of writing:
| Platform | Helix (Rust) | Zed (Rust, GUI) | Neovim (C) |
|---|---|---|---|
| Linux | tar.xz + AppImage + deb | tar.gz | tar.gz + AppImage |
| macOS | tar.xz | .dmg (signed+notarised) | tar.gz |
| Windows | .zip | .exe installer (signed) | .zip + .msi |
| Source | source.tar.xz | — | — |
| Arch | x86_64 + aarch64 | x86_64 + aarch64 | x86_64 + arm64 |
Conclusions that shaped this design:
- Archives (tar/zip) are the universal baseline — every editor ships them on every platform. Non-negotiable core.
- Linux native bundles are cheap and unsigned-friendly — AppImage (Helix + Neovim) and .deb (Helix) need no certificates and are a real UX win.
- macOS .dmg and Windows .msi are gated on code-signing. Only Zed ships them, because Zed holds an Apple Developer cert (+ notarisation) and a Windows Authenticode cert. An unsigned .dmg/.msi is a worse UX than an archive — Gatekeeper reports "damaged, can't be opened"; SmartScreen blocks the installer. Helix and Neovim deliberately ship plain archives on macOS; Helix ships only a zip on Windows. Lattice follows suit: no macOS/Windows installers until signing certs exist.
- Helix uses native runners per arch (no cross-compilation) and free build-provenance attestation. Both adopted here.
Decisions
| Decision | Choice |
|---|---|
| Trigger | Push of v* tag → real release. workflow_dispatch + PR touching the workflow → preview mode (artefacts only, no release). |
| Architectures | x86_64 + aarch64 on all three OSes (6 build legs). |
| Binary flavors | TUI (lattice, default features) and GUI (lattice built --features gui) shipped as separate archives. |
| Packaging | Helix model: archives everywhere + Linux AppImage + .deb + SHA256SUMS + source archive + provenance attestation. No .dmg / .msi / .rpm in v1. |
| Linux bundles | Wrap the GUI binary only (desktop-install formats). TUI ships as a plain archive (headless/SSH/server audience). |
| Build profile | --release — already dist-grade (panic=abort, thin LTO, codegen-units=1). No separate dist profile. |
Build matrix
Native runner per arch — free for this public repo, avoids cross-compilation:
| build | runner | target |
|---|---|---|
| x86_64-linux | ubuntu-22.04 | x86_64-unknown-linux-gnu |
| aarch64-linux | ubuntu-22.04-arm | aarch64-unknown-linux-gnu |
| x86_64-macos | macos-15-intel | x86_64-apple-darwin |
| aarch64-macos | macos-latest | aarch64-apple-darwin |
| x86_64-windows | windows-latest | x86_64-pc-windows-msvc |
| aarch64-windows | windows-11-arm | aarch64-pc-windows-msvc |
ubuntu-22.04 is pinned (not -latest) for an older glibc → binaries that run on a wider range of distros, matching Helix's portability note.
The rust-toolchain.toml is removed at the start of each build leg (it pins 1.94.0 with profile=minimal and would otherwise override the action's toolchain + target installation), mirroring Helix.
TUI vs GUI coverage
TUI binary builds on all 6 legs (pure Rust, no display-lib deps, low risk).
GUI binary (
--features gui) needs GPUI to link. Current CI (gpui-window-build) only proves linkage on x86_64-linux, aarch64-macos (macos-latest is Apple Silicon), and x86_64-windows. aarch64-linux and aarch64-windows GUI linkage is unproven.Decision (a): build GUI on all 6 legs, but mark
aarch64-linuxandaarch64-windowsGUI stepscontinue-on-error. A GPUI link failure on those two legs therefore does not sink the release: their GUI artefacts are simply absent and the leg logs a warning (no silent drop — the publish job's manifest names every expected artefact and flags any missing). All TUI artefacts (6 legs) and the four proven GUI artefacts always publish. When a preview run proves the two ARM GUI legs link, drop thecontinue-on-error.Linux GUI legs install the same display libs the CI
gpui-window-buildjob uses (libxcb1-dev libxkbcommon-dev libxkbcommon-x11-dev libwayland-dev libfontconfig1-dev).
Artefacts
<ver> = tag with the leading v stripped (e.g. tag v0.2.0 → 0.2.0).
| Artefact | Flavor | Legs |
|---|---|---|
lattice-<ver>-<build>.tar.xz / .zip | TUI | all 6 |
lattice-gui-<ver>-<build>.tar.xz / .zip | GUI | all 6 (2 best-effort) |
lattice-gui-<ver>-<arch>.AppImage + .zsync | GUI | linux (x86_64, aarch64) |
lattice-gui-<ver>-<arch>.deb | GUI | linux (x86_64, aarch64) |
lattice-<ver>-source.tar.xz | — | publish job |
SHA256SUMS | — | publish job |
- Each archive contains the binary +
LICENSE+README.md. - The source archive is a clean
git archiveexport (deterministic, excludestarget/and untracked files). GitHub additionally auto-attaches its own "Source code (zip/tar.gz)" to every release; the namedsource.tar.xzis the convention-matching, reproducible one. - Unix archives use
.tar.xz; Windows uses.zip(7-Zip).
Icons (native bundles)
The only signed-or-unsigned native apps shipped in v1 are the Linux AppImage + .deb. Their icon must match the existing desktop entry assets/linux/com.lattice-editor.lattice.desktop, which declares Icon=com.lattice-editor.lattice and StartupWMClass=lattice.
- Icon source:
assets/lattice.iconset/icon_NxN.png— clean square PNGs at 16/32/64/128/256/512/1024. These are the same sources that build the macOS.icns. - Trap:
assets/lattice-mark-512.pngis 512×614 (non-square) and must not be used for freedesktop icons — it would be stretched/rejected. The workflow and[package.metadata.deb]reference theiconsetPNGs only. .deb: a new[package.metadata.deb]section incrates/lattice-cli/Cargo.tomlinstalls:- the binary →
/usr/bin/lattice com.lattice-editor.lattice.desktop→/usr/share/applications/- each square PNG →
/usr/share/icons/hicolor/<size>x<size>/apps/com.lattice-editor.lattice.png(renamed fromicon_NxN.pngto theIcon=key).
- the binary →
- AppImage: built with
linuxdeploy, passing-d assets/linux/com.lattice-editor.lattice.desktopand-i <icon_512x512.png renamed to com.lattice-editor.lattice.png>. The icon basename must equal the desktopIcon=key or linuxdeploy rejects it. The.zsynccompanion enables AppImage delta-updates (gh-releases-zsync|dhruvasagar|lattice|latest|lattice-gui-*-<arch>.AppImage.zsync). - macOS
.icns: ready (assets/lattice.icns) but unused in v1 (no shipped.dmg). Wired in when signing is added. - Windows: the zipped
.execarries no embedded icon (a bare release binary has the default Windows icon). Embedding one requires awinresourcebuild script — out of scope for v1; documented as a known gap.
Jobs
dist(matrix, 6 legs) — checkout; removerust-toolchain.toml; install toolchain + target; (Linux) install GPUI display libs; build TUI binary; build GUI binary (continue-on-erroron the two unproven ARM legs); assemble TUI + GUI archives; (Linux) build AppImage + .deb for the GUI binary;upload-artifactper leg.publish(ubuntu-latest,needs: dist) — download all leg artefacts; create the clean source archive viagit archive; generateSHA256SUMSover every artefact; verify the expected-artefact manifest and log any missing (the best-effort ARM GUI legs); runactions/attest-build-provenance(free, supply-chain signing — not code signing); in tag mode,softprops/action-gh-release@v2creates the release with auto-generated notes and uploads everything; in preview mode, skip release creation andupload-artifactthe assembleddist/instead.
Permissions: dist needs only contents: read; publish needs contents: write (release upload) + id-token: write + attestations: write (provenance).
Concurrency group keyed on the workflow + ref, cancel-in-progress: false (don't cancel a half-published release).
Version safety
A step in publish asserts the tag matches the workspace version: v<X> from GITHUB_REF_NAME must equal workspace.package.version (grep-extracted from the root Cargo.toml). Mismatch fails the job loudly — prevents tagging v0.3.0 while Cargo.toml still says 0.2.0. Skipped in preview mode (no tag).
scripts/release.sh is the other half of that guard: it is the only supported way to move the version, and it reads Cargo.toml with the same grep expression prepare uses, so the two cannot drift into disagreeing. It stops at the local tag and prints the push line — firing a pipeline that publishes public artefacts stays a deliberate, separate act. See ../operations/releasing.md.
Repo additions
| File | Purpose |
|---|---|
.github/workflows/release.yml | the pipeline |
crates/lattice-cli/Cargo.toml [package.metadata.deb] | .deb file mapping + icon install |
docs/dev/operations/releasing.md | how to cut a release (tag, bump, preview) |
scripts/release.sh | bump + changelog gate + commit + tag |
Out of scope (v1)
- macOS
.dmg/.appand Windows.msi(need signing certs; unsigned is worse UX — see survey). .rpm(Helix doesn't ship it; add viacargo-generate-rpmlater if asked).- Windows
.exeembedded icon (winresourcebuild script). - Homebrew tap / AUR / winget / Flatpak / crates.io publish (distribution channels — separate follow-on work).
- Bench-regression gating and code signing/notarisation.
Failure modes & handling
- ARM GUI link failure — non-blocking (
continue-on-error); artefact absent, logged in the publish manifest. Never silent. - Tag/version mismatch — hard fail in
publishbefore any upload. - Partial leg failure (TUI) —
fail-fast: falseso one leg's failure does not cancel the others; the release publishes what succeeded and the failed leg is visible in the Actions UI and the missing-artefact log.
Amendment (2026-09-18) — archives are prefix-relocatable
The layout above packaged a bare binary, which shipped every artefact with zero core plugins: the legs never ran cargo xtask build-core-plugins, and a flat archive puts nothing on any of discovery's four search paths, so default_core_plugins_dir() returned None — a skip the code documents as benign because in the dev path it is. Verified against a real artefact from the 2026-09-18 preview run: lattice-dev-4b605f4-aarch64-macos.tar.xz contained exactly LICENSE, README.md and lattice.
Archives now carry bin/lattice plus share/lattice/plugins/<name>/{<name>.wasm,plugin.toml,.source}, and a core-plugins job builds the three components once for every leg to download. WASM components are platform-independent, and the dist legs install only their own target triple — so a per-leg build would need a wasm32-wasip2 toolchain on all six runners including ARM Windows, which makes build-once the cheap option rather than merely the tidy one.
The .deb installs the same tree under /usr; the AppImage stages it under AppDir/usr. Artefact filenames are unchanged, so publish's manifest and its best-effort classification are unaffected.
Two things guard this, because a green build proves nothing here: cargo xtask build-core-plugins fails hard when the wasm target is absent (unlike lattice-plugin-host's build script, which warns and continues), and each dist leg asserts the contents of the archives it just built.
Rationale, and the rejected alternatives — a baked LATTICE_INSTALL_PREFIX, a LATTICE_RUNTIME wrapper script, per-leg builds — are in launch-0.9.md §3.