1- # Build the sovereign `lem` binary for every target and ship the zips.
2- # One binary named `lem` per platform (the GPU backend — metal/rocm/cuda — loads
3- # its kernel sidecar at runtime); targets are separated by build folder, not by
4- # binary name. Zips flow to a rolling `dev` prerelease.
1+ # Build the sovereign `lem` binary for every hosted-buildable cell and ship the zips.
2+ #
3+ # lem links DuckDB (go-store/chathistory) via cgo, so there is NO pure-Go build and
4+ # NO cheap cross-compile: each cell is a NATIVE cgo build on a matching runner.
5+ # GitHub owns the portable cpu cells it can build natively; the ROCm/CUDA cells
6+ # (amd, cuda) stay on GitLab's homelab runner (it has the toolchain); the metal
7+ # cell needs a macOS 26 / Metal 4 box to compile the metallibs, so it rides a
8+ # self-hosted Mac lane (dormant until ENABLE_MACOS_METAL is set).
9+ #
10+ # Every build ships in two packagings of the same binary (see docs/release-artifacts.md):
11+ # Native {os}-{arch}-lem-{backend}-{version}.zip -> binary `lem`
12+ # Driver {os}-{arch}-lem-driver-{backend}-{version}.zip -> binary `lem-{backend}`
13+ # Zips flow to a rolling `dev` prerelease; version is `dev-<short-sha>`.
514name : Build
615
716on :
1221permissions :
1322 contents : read
1423
24+ env :
25+ GO_VERSION : " 1.26"
26+
1527jobs :
16- build :
17- name : lem · ${{ matrix.goos }}/${{ matrix.goarch }}
18- runs-on : ${{ matrix.os }}
28+ # ---- cpu cells: native cgo, one per hosted os/arch (duckdb ships prebuilt libs
29+ # for darwin/{amd64,arm64}, linux/{amd64,arm64}, windows/amd64 — NOT
30+ # windows/arm64, which is therefore not a cell). The workspace (go.work)
31+ # wires cli -> local go/; GOWORK=off cannot build the binary because cli
32+ # pins a published go/inference tag that lags the local tree. ----------------
33+ cpu :
34+ name : lem cpu · ${{ matrix.os_name }}/${{ matrix.arch }}
35+ runs-on : ${{ matrix.runner }}
1936 strategy :
2037 fail-fast : false
2138 matrix :
2239 include :
23- - { os: macos-latest, goos: darwin, goarch: arm64, metal: true }
24- - { os: ubuntu-latest, goos: linux, goarch: amd64 }
25- - { os: ubuntu-24.04-arm, goos: linux, goarch: arm64 }
26- - { os: windows-latest, goos: windows, goarch: amd64 }
27- env :
28- CGO_ENABLED : " 1"
29- GOWORK : " off"
40+ # Dropped cells, proven by live runs 2026-07-22 (docs/release-artifacts.md):
41+ # - macos/x86_64: the macos-13 runner label is retired (queued forever) —
42+ # no hosted Intel Mac remains; revisit only on real demand.
43+ # - windows/x86_64: lem's code is not Windows-portable yet (unix-only
44+ # syscall.Kill + raw-fd reads outside build constraints) — a code
45+ # campaign, not a CI matrix entry; restore the cell when it compiles.
46+ - { runner: macos-latest, os_name: macos, arch: aarch64, exe: "" }
47+ - { runner: ubuntu-latest, os_name: linux, arch: x86_64, exe: "" }
48+ - { runner: ubuntu-24.04-arm, os_name: linux, arch: aarch64, exe: "" }
3049 steps :
3150 - uses : actions/checkout@v4
51+ # cpu build needs neither external/mlx (metal) nor the ROCm sources.
52+
53+ - uses : actions/setup-go@v5
54+ with :
55+ go-version : ${{ env.GO_VERSION }}
56+ # go.sum lives in the modules, not the repo root; list both so setup-go's
57+ # cache key is honest (build touches cli, cli requires local go/).
58+ cache-dependency-path : |
59+ go/go.sum
60+ cli/go.sum
61+
62+ - name : Build lem (native cgo; workspace wires cli -> local go/)
63+ shell : bash
64+ env :
65+ CGO_ENABLED : " 1"
66+ EXE : ${{ matrix.exe }}
67+ run : |
68+ set -euo pipefail
69+ VERSION="dev-${GITHUB_SHA:0:12}"
70+ mkdir -p bin
71+ go build -trimpath -ldflags "-X main.version=${VERSION}" -o "bin/lem${EXE}" ./cli
72+ test -s "bin/lem${EXE}"
73+
74+ - uses : actions/upload-artifact@v4
3275 with :
33- submodules : recursive # metal needs external/mlx
76+ name : lem-cpu-${{ matrix.os_name }}-${{ matrix.arch }}
77+ path : bin/lem${{ matrix.exe }}
78+ if-no-files-found : error
79+
80+ # ---- metal cell: macos/aarch64, self-contained (metallibs embedded). Hosted
81+ # macOS runners are < macOS 26 and cannot compile the Metal 4 metallibs, so
82+ # a plain build would ship a metallib-less, non-runnable binary. This lane is
83+ # DORMANT on normal pushes (the `if` is false when the variable is unset, so
84+ # it never queues or fails): register a self-hosted macOS 26 arm64 runner and
85+ # set repo variable ENABLE_MACOS_METAL=true to activate it. Interim: the metal
86+ # artifact is built manually on the owner's Mac (task metallib && task build:embed).
87+ metal :
88+ name : lem metal · macos/aarch64 (self-hosted macOS 26)
89+ if : ${{ vars.ENABLE_MACOS_METAL == 'true' }}
90+ runs-on : [self-hosted, macOS, ARM64]
91+ steps :
92+ - uses : actions/checkout@v4
93+ with :
94+ submodules : recursive # external/mlx for `task metallib`
3495
3596 - uses : actions/setup-go@v5
3697 with :
37- go-version : " 1.26"
98+ go-version : ${{ env.GO_VERSION }}
99+ cache-dependency-path : |
100+ go/go.sum
101+ cli/go.sum
38102
39103 - name : Install Task
40104 run : go install github.com/go-task/task/v3/cmd/task@latest
41105
42- - name : Build lem
106+ - name : Build self-contained metal lem (metallibs baked in)
43107 shell : bash
44- env :
45- METAL : ${{ matrix.metal }}
46108 run : |
47- set -e
48- if [ "$METAL" = "true" ]; then
49- task metallib && task build:embed # self-contained metal lem
50- else
51- task build:native # plain lem (GPU sidecar loads at runtime)
52- fi
53-
54- - name : Assemble build folder (build/dist/<target>/lem)
55- shell : bash
56- env :
57- TARGET : ${{ matrix.goos }}-${{ matrix.goarch }}
58- run : |
59- set -e
60- mkdir -p "build/dist/$TARGET"
61- cp bin/lem* "build/dist/$TARGET/"
109+ set -euo pipefail
110+ VERSION="dev-${GITHUB_SHA:0:12}"
111+ task metallib
112+ task build:embed VERSION="${VERSION}"
113+ test -s bin/lem
62114
63115 - uses : actions/upload-artifact@v4
64116 with :
65- name : lem-${{ matrix.goos }}-${{ matrix.goarch }}
66- path : build/dist/${{ matrix.goos }}-${{ matrix.goarch }}/
117+ name : lem-metal-macos-aarch64
118+ path : bin/lem
119+ if-no-files-found : error
67120
121+ # ---- release: package each built binary under BOTH names (build once, package
122+ # twice) and publish to the rolling `dev` prerelease. Runs only for dev
123+ # pushes; requires cpu to have succeeded and tolerates metal being skipped. ----
68124 release :
69- name : Zip + rolling dev prerelease
70- needs : build
71- if : github.event_name == 'push' && github.ref == 'refs/heads/dev'
125+ name : Zip (Native + Driver) + rolling dev prerelease
126+ needs : [cpu, metal]
127+ if : ${{ !cancelled() && needs.cpu.result == 'success' && github.event_name == 'push' && github.ref == 'refs/heads/dev' }}
72128 runs-on : ubuntu-latest
73129 permissions :
74130 contents : write
75131 steps :
76132 - uses : actions/download-artifact@v4
77133 with :
78- path : dist # one subfolder per target ( lem-<goos >-<goarch> /lem)
134+ path : dist # one dir per cell: dist/ lem-<backend >-<os>-<arch> /lem[.exe]
79135
80- - name : Zip each target
136+ - name : Package Native + Driver zips
137+ shell : bash
81138 run : |
82- set -e
139+ set -euo pipefail
140+ VERSION="dev-${GITHUB_SHA:0:12}"
141+ mkdir -p out
83142 cd dist
84- for d in */; do (cd "$d" && zip -r "../${d%/}.zip" .); done
85- ls -la *.zip
143+ for d in lem-*/; do
144+ name="${d%/}" # e.g. lem-cpu-macos-aarch64
145+ IFS=- read -r _ backend os arch <<< "$name" # backend/os/arch from the name
146+ bin="$(ls "$d")" # lem or lem.exe
147+ ext=""; case "$bin" in *.exe) ext=".exe";; esac
148+ # Native — the binary a user runs locally, named `lem`.
149+ cp "$d/$bin" "lem${ext}"; chmod 0755 "lem${ext}"
150+ zip -jq "../out/${os}-${arch}-lem-${backend}-${VERSION}.zip" "lem${ext}"
151+ rm -f "lem${ext}"
152+ # Driver — the binary a compute box serves over the API, named `lem-<backend>`.
153+ cp "$d/$bin" "lem-${backend}${ext}"; chmod 0755 "lem-${backend}${ext}"
154+ zip -jq "../out/${os}-${arch}-lem-driver-${backend}-${VERSION}.zip" "lem-${backend}${ext}"
155+ rm -f "lem-${backend}${ext}"
156+ done
157+ ls -la ../out
86158
87159 - name : Publish rolling dev prerelease
88160 uses : softprops/action-gh-release@v2
89161 with :
90162 tag_name : dev
91163 name : dev (rolling)
92164 prerelease : true
93- files : dist /*.zip
165+ files : out /*.zip
94166 env :
95167 GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }}
96168
@@ -101,7 +173,10 @@ jobs:
101173 - uses : actions/checkout@v4
102174 - uses : actions/setup-go@v5
103175 with :
104- go-version : " 1.26"
176+ go-version : ${{ env.GO_VERSION }}
177+ cache-dependency-path : |
178+ go/go.sum
179+ cli/go.sum
105180 - uses : actions/setup-node@v4
106181 with :
107182 node-version : " 20"
@@ -111,7 +186,7 @@ jobs:
111186 java-version : " 21"
112187 - name : Install generators
113188 run : |
114- npm install -g @openapitools/openapi-generator-cli@7.22.0
189+ npm install -g @openapitools/openapi-generator-cli@2.40.0 # npm wrapper is 2.x; it manages the 7.x Java generator
115190 go install github.com/go-task/task/v3/cmd/task@latest
116191 - name : Generate SDKs (lem spec -> OpenAPI 3.1 -> typed clients)
117192 run : task sdk
0 commit comments