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:

PlatformHelix (Rust)Zed (Rust, GUI)Neovim (C)
Linuxtar.xz + AppImage + debtar.gztar.gz + AppImage
macOStar.xz.dmg (signed+notarised)tar.gz
Windows.zip.exe installer (signed).zip + .msi
Sourcesource.tar.xz
Archx86_64 + aarch64x86_64 + aarch64x86_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

DecisionChoice
TriggerPush of v* tag → real release. workflow_dispatch + PR touching the workflow → preview mode (artefacts only, no release).
Architecturesx86_64 + aarch64 on all three OSes (6 build legs).
Binary flavorsTUI (lattice, default features) and GUI (lattice built --features gui) shipped as separate archives.
PackagingHelix model: archives everywhere + Linux AppImage + .deb + SHA256SUMS + source archive + provenance attestation. No .dmg / .msi / .rpm in v1.
Linux bundlesWrap 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:

buildrunnertarget
x86_64-linuxubuntu-22.04x86_64-unknown-linux-gnu
aarch64-linuxubuntu-22.04-armaarch64-unknown-linux-gnu
x86_64-macosmacos-15-intelx86_64-apple-darwin
aarch64-macosmacos-latestaarch64-apple-darwin
x86_64-windowswindows-latestx86_64-pc-windows-msvc
aarch64-windowswindows-11-armaarch64-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-linux and aarch64-windows GUI steps continue-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 the continue-on-error.

    Linux GUI legs install the same display libs the CI gpui-window-build job 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.00.2.0).

ArtefactFlavorLegs
lattice-<ver>-<build>.tar.xz / .zipTUIall 6
lattice-gui-<ver>-<build>.tar.xz / .zipGUIall 6 (2 best-effort)
lattice-gui-<ver>-<arch>.AppImage + .zsyncGUIlinux (x86_64, aarch64)
lattice-gui-<ver>-<arch>.debGUIlinux (x86_64, aarch64)
lattice-<ver>-source.tar.xzpublish job
SHA256SUMSpublish job
  • Each archive contains the binary + LICENSE + README.md.
  • The source archive is a clean git archive export (deterministic, excludes target/ and untracked files). GitHub additionally auto-attaches its own "Source code (zip/tar.gz)" to every release; the named source.tar.xz is 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.png is 512×614 (non-square) and must not be used for freedesktop icons — it would be stretched/rejected. The workflow and [package.metadata.deb] reference the iconset PNGs only.
  • .deb: a new [package.metadata.deb] section in crates/lattice-cli/Cargo.toml installs:
    • 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 from icon_NxN.png to the Icon= key).
  • AppImage: built with linuxdeploy, passing -d assets/linux/com.lattice-editor.lattice.desktop and -i <icon_512x512.png renamed to com.lattice-editor.lattice.png>. The icon basename must equal the desktop Icon= key or linuxdeploy rejects it. The .zsync companion 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 .exe carries no embedded icon (a bare release binary has the default Windows icon). Embedding one requires a winresource build script — out of scope for v1; documented as a known gap.

Jobs

  1. dist (matrix, 6 legs) — checkout; remove rust-toolchain.toml; install toolchain + target; (Linux) install GPUI display libs; build TUI binary; build GUI binary (continue-on-error on the two unproven ARM legs); assemble TUI + GUI archives; (Linux) build AppImage + .deb for the GUI binary; upload-artifact per leg.
  2. publish (ubuntu-latest, needs: dist) — download all leg artefacts; create the clean source archive via git archive; generate SHA256SUMS over every artefact; verify the expected-artefact manifest and log any missing (the best-effort ARM GUI legs); run actions/attest-build-provenance (free, supply-chain signing — not code signing); in tag mode, softprops/action-gh-release@v2 creates the release with auto-generated notes and uploads everything; in preview mode, skip release creation and upload-artifact the assembled dist/ 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

FilePurpose
.github/workflows/release.ymlthe pipeline
crates/lattice-cli/Cargo.toml [package.metadata.deb].deb file mapping + icon install
docs/dev/operations/releasing.mdhow to cut a release (tag, bump, preview)
scripts/release.shbump + changelog gate + commit + tag

Out of scope (v1)

  • macOS .dmg / .app and Windows .msi (need signing certs; unsigned is worse UX — see survey).
  • .rpm (Helix doesn't ship it; add via cargo-generate-rpm later if asked).
  • Windows .exe embedded icon (winresource build 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 publish before any upload.
  • Partial leg failure (TUI)fail-fast: false so 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.