Skip to content

Commit f42f7b4

Browse files
authored
Merge pull request #13 from dAppCore/ci/artifact-matrix
ci: stop GitHub Actions failure spam + Driver/Native artifact grid
2 parents 33b907c + 591e139 commit f42f7b4

8 files changed

Lines changed: 288 additions & 66 deletions

File tree

.github/workflows/build.yml

Lines changed: 122 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
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>`.
514
name: Build
615

716
on:
@@ -12,85 +21,148 @@ on:
1221
permissions:
1322
contents: read
1423

24+
env:
25+
GO_VERSION: "1.26"
26+
1527
jobs:
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

.github/workflows/ci.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ jobs:
2626
- uses: actions/setup-go@v5
2727
with:
2828
go-version: "1.26"
29+
# The module root is go/ (go.sum lives there, not at the repo root); without
30+
# this, setup-go's cache aborts with "Dependencies file is not found ...
31+
# Supported file pattern: go.sum". This lane only compiles the go/ module.
32+
cache-dependency-path: go/go.sum
2933

3034
- name: Portable tests + coverage (default tags — engine/metal excluded)
3135
working-directory: go

Taskfile.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,13 @@ vars:
1818
VERSION: dev
1919
GO_VERSION_LDFLAGS: '-X main.version={{.VERSION}}'
2020
GO_DARWIN_LDFLAGS: '-extldflags=-mmacosx-version-min=26.0'
21+
# Core count for cmake --parallel (metallib:mlx only). go-task evaluates global
22+
# sh-vars eagerly on every invocation, so this must succeed on EVERY platform:
23+
# getconf/nproc cover Linux, sysctl -n hw.ncpu is the macOS path, and the `echo 4`
24+
# floor keeps it green where none of the three exist (Windows, task's internal
25+
# POSIX shell has no such binaries) — a bare `sysctl -n hw.ncpu` was the CI failure.
2126
NCPU:
22-
sh: getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || sysctl -n hw.ncpu
27+
sh: getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4
2328
env:
2429
MLX_METALLIB_PATH: '{{.ROOT_DIR}}/build/dist/lib/mlx.metallib'
2530

docs/release-artifacts.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Release artifacts — the `lem` Driver/Native grid
2+
3+
`lem` is one binary. The GPU backend is registered at **build** time, and `lem` also
4+
serves its own HTTP API — so a compute box can run a backend-specific `lem` as a remote
5+
GPU **driver**, and a user's local `lem` consumes it over that API. The release surface
6+
therefore ships every per-`{os,arch,backend}` build in **two packagings of the same binary**:
7+
8+
| Packaging | Zip name | Contains (one binary, nothing else) | Role |
9+
|-----------|----------|-------------------------------------|------|
10+
| **Native** | `{os}-{arch}-lem-{backend}-{version}.zip` | `lem` (`lem.exe` on Windows) | Run locally on your machine. |
11+
| **Driver** | `{os}-{arch}-lem-driver-{backend}-{version}.zip` | `lem-{backend}` (`.exe` on Windows) | Deploy on a compute box; serves the GPU over the API. |
12+
13+
Worked example (the maintainer's own setup):
14+
15+
- On his Mac: `macos-aarch64-lem-metal-v0.15.0.zip` → a `lem` binary for mac + metal.
16+
- On his AMD Linux box: `linux-x86_64-lem-driver-amd-v0.15.0.zip` → a `lem-amd` binary
17+
serving the GPU over the API, which the Mac's `lem` then drives remotely.
18+
19+
**Build once, package twice.** Each cell is built a single time; the one binary is copied
20+
into the Native zip as `lem` and into the Driver zip as `lem-{backend}`. No double build.
21+
22+
## Dimensions
23+
24+
- `os` ∈ {`macos`, `linux`, `windows`} (GOOS `darwin``macos`)
25+
- `arch` ∈ {`x86_64`, `aarch64`} (GOARCH `amd64``x86_64`, `arm64``aarch64`)
26+
- `backend` ∈ {`metal`, `amd`, `cuda`, `cpu`}
27+
- `version` = the git tag on a release build; the rolling `dev` prerelease uses `dev-<short-sha>`.
28+
29+
## The grid — valid cells only
30+
31+
| os | arch | backend | Built by | How |
32+
|----|------|---------|----------|-----|
33+
| macos | aarch64 | metal | self-hosted macOS 26 lane (dormant) / manual | `task metallib && task build:embed` |
34+
| macos | aarch64 | cpu | **GitHub** `macos-latest` | native cgo `go build ./cli` |
35+
| linux | x86_64 | amd | **GitLab** homelab | `make lem-amd` |
36+
| linux | x86_64 | cuda | **GitLab** homelab | `make lem-cuda` |
37+
| linux | x86_64 | cpu | **GitHub** `ubuntu-latest` (rolling) · **GitLab** `make lem-cpu-x86` (tag) | native cgo |
38+
| linux | aarch64 | cpu | **GitHub** `ubuntu-24.04-arm` (rolling) · **GitLab** `make lem-cpu-aarch64` (tag) | native cgo |
39+
40+
### Excluded cells (and why)
41+
42+
- **cpu · macos/x86_64** — the `macos-13` runner label is retired (live-proven 2026-07-22:
43+
queued forever); GitHub hosts no Intel Mac any more. Revisit only on real demand.
44+
- **cpu · windows/x86_64** — lem's code is not Windows-portable yet: unix-only
45+
`syscall.Kill` + raw-fd reads sit outside build constraints (live-proven 2026-07-22,
46+
three compile errors). Restoring the cell is a code-portability campaign, not a
47+
matrix entry.
48+
- **cpu · windows/aarch64**`duckdb-go-bindings` ships no `windows-arm64` prebuilt lib, so
49+
the binary cannot link. Not a cell.
50+
- **metal · anything but macos/aarch64** — Metal is Apple-GPU only.
51+
- **amd, cuda · anything but linux/x86_64** — need the ROCm / CUDA toolchain and its Linux libs.
52+
- **any backend with `CGO_ENABLED=0`** — impossible; see below.
53+
54+
## Why every cell is a *native* cgo build (no cross-compile)
55+
56+
`lem` links **DuckDB** (the `go-store` / chathistory driver) through cgo. `duckdb-go-bindings`
57+
guards its prebuilt-lib packages with `//go:build cgo`, so `CGO_ENABLED=0` fails at compile
58+
(`build constraints exclude all Go files ...`). There is **no pure-Go `lem`** and therefore
59+
**no free `GOOS/GOARCH` cross-compile** — cross-building needs a target C toolchain. Each cell
60+
is built natively on a runner of that `os/arch` (GitLab's `lem-cpu-aarch64` is the one
61+
cross-cgo build, via `aarch64-linux-gnu-gcc`). Probed on 2026-07-22 against
62+
`duckdb-go-bindings v0.10504.0`: cgo-off builds fail for every target; native cgo builds pass.
63+
64+
DuckDB's supported set (the prebuilt libs that exist): `darwin/{amd64,arm64}`,
65+
`linux/{amd64,arm64}`, `windows/amd64`. That set defines the valid `cpu` cells.
66+
67+
## Which CI owns which cells
68+
69+
- **GitHub Actions** (`.github/workflows/build.yml`) — the portable **cpu** cells it can build
70+
natively on hosted runners, on every push to `dev`, published to the rolling `dev`
71+
prerelease as `dev-<short-sha>` zips (Native + Driver). Hosted runners have no ROCm/CUDA
72+
toolchain and are older than macOS 26, so they build **only** cpu.
73+
- **GitLab CI** (`.gitlab-ci.yml`, homelab AMD box) — the toolchain-gated **amd** and **cuda**
74+
cells, plus its own **cpu-x86 / cpu-aarch64**, on a git **tag** via the `release` gate and
75+
the `Makefile` targets. This side is unchanged (it is green and has the toolchain).
76+
- **Self-hosted Mac lane** — the **metal** cell (see below).
77+
78+
The two systems do not fight: GitHub is the continuous `dev` channel; GitLab is the tagged
79+
release for the Linux/GPU cells.
80+
81+
## The metal cell decision
82+
83+
Metal's Go side (engine/metal, the objc bridge) is no-cgo and cross-compiles, **but** a usable
84+
metal `lem` embeds the MLX + lthn `.metallib` kernels (`-tags embed_metallib`), and those are
85+
compiled by `task metallib` with **Metal 4 / macOS 26** (`-std=metal4.0`,
86+
`CMAKE_OSX_DEPLOYMENT_TARGET=26.0`) and are gitignored (generated, never committed).
87+
88+
- A GitHub **hosted** macOS runner is macOS 14/15 → it **cannot** compile the metallibs. A plain
89+
(non-embed) build would resolve them at runtime via `MLX_METALLIB_PATH`, but the user has no
90+
metallib on their machine — so that artifact is **non-runnable**. We do **not** ship a
91+
fabricated/broken metal binary from hosted CI.
92+
- **Decision:** the metal cell ships from a **self-hosted macOS 26 arm64 runner** running the
93+
house build path (`task metallib && task build:embed`), producing the self-contained binary.
94+
The `metal` job in `build.yml` encodes this recipe but is **dormant** — its `if` is false
95+
unless repo variable `ENABLE_MACOS_METAL=true` is set — so it never queues or fails on a
96+
normal push. **Interim** (until a self-hosted runner is registered): the maintainer builds
97+
the metal zip manually on his Mac (the primary dev box already runs `task build:embed` daily).
98+
- **Trade-off:** correctness (a self-contained, runnable metal binary) over convenience (a
99+
hosted job). The cost is one self-hosted runner or a manual step for the metal cell only.
100+
101+
## Zip layout rule
102+
103+
Each zip contains **exactly one binary and nothing else** — matching what `build.yml` shipped
104+
before (binary only, no licence file). Self-contained cells honour this directly:
105+
106+
- **cpu** — native-cgo `lem`, no GPU engine, no sidecar.
107+
- **metal**`build:embed` bakes the metallibs in.
108+
109+
**Exception — amd / cuda:** these load a HIP/CUDA kernel **sidecar** (`.hsaco` / `.o`) at
110+
runtime that is not embedded, so those cells ship the binary **plus** its sidecar (the
111+
`Makefile` already tars them together). This is the single documented departure from
112+
binary-only, driven by runtime necessity.
113+
114+
## Not yet wired (follow-ups)
115+
116+
- **Tagged GitHub release.** `build.yml` publishes only the rolling `dev` prerelease (dev
117+
pushes). Producing tag-versioned (`v*`) zips for the GitHub cells (macos/windows cpu, metal)
118+
is a small extension: add `push.tags` to `on:` and version from the tag. GitLab already
119+
handles tagged Linux/GPU cells.
120+
- **GitLab grid-named zips.** GitLab currently uploads the raw `lem-{backend}` binaries (already
121+
Driver-named) via `build/bin/`. Emitting the `{os}-{arch}-lem-...-{version}.zip` names (with
122+
the amd/cuda sidecars) is a `Makefile`/`.gitlab-ci.yml` packaging follow-up that must be
123+
validated on the ROCm box — it cannot be exercised from a macOS lane, so it was left untouched
124+
here rather than changed blind.

go/agent/ai/ai_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func TestReadEvents_Good_SkipsMissingDays(t *testing.T) {
120120
if len(events) != 2 {
121121
t.Fatalf("expected 2 events, got %d", len(events))
122122
}
123-
if events[0].Timestamp != dayOne || events[1].Timestamp != dayThree {
123+
if !events[0].Timestamp.Equal(dayOne) || !events[1].Timestamp.Equal(dayThree) {
124124
t.Fatalf("events not returned in chronological order: %+v", events)
125125
}
126126
}

0 commit comments

Comments
 (0)