Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 52 additions & 11 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ name: Build
on:
push:
branches: [dev, main]
tags: ['v*'] # a version tag publishes the REAL release (guarded on-main)
workflow_dispatch:

permissions:
Expand Down Expand Up @@ -66,7 +67,7 @@ jobs:
EXE: ${{ matrix.exe }}
run: |
set -euo pipefail
VERSION="dev-${GITHUB_SHA:0:12}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then VERSION="$GITHUB_REF_NAME"; else VERSION="dev-${GITHUB_SHA:0:12}"; fi
mkdir -p bin
go build -trimpath -ldflags "-X main.version=${VERSION}" -o "bin/lem${EXE}" ./cli
test -s "bin/lem${EXE}"
Expand Down Expand Up @@ -107,7 +108,7 @@ jobs:
shell: bash
run: |
set -euo pipefail
VERSION="dev-${GITHUB_SHA:0:12}"
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then VERSION="$GITHUB_REF_NAME"; else VERSION="dev-${GITHUB_SHA:0:12}"; fi
task metallib
task build:embed VERSION="${VERSION}"
test -s bin/lem
Expand All @@ -119,25 +120,65 @@ jobs:
if-no-files-found: error

# ---- release: package each built binary under BOTH names (build once, package
# twice) and publish to the rolling `dev` prerelease. Runs only for dev
# pushes; requires cpu to have succeeded and tolerates metal being skipped. ----
# twice). Two publish shapes: a dev push feeds the rolling `dev`
# prerelease; a v* TAG (guarded: the tagged commit must be on main)
# publishes the real versioned release. Requires cpu green; tolerates
# metal skipped. ----
release:
name: Zip (Native + Driver) + rolling dev prerelease
name: Zip (Native + Driver) + release
needs: [cpu, metal]
if: ${{ !cancelled() && needs.cpu.result == 'success' && github.event_name == 'push' && github.ref == 'refs/heads/dev' }}
if: ${{ !cancelled() && needs.cpu.result == 'success' && github.event_name == 'push' && (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/v')) }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # the tag-on-main ancestry guard needs history

# A tag release only publishes if the tagged commit is reachable from
# main — "a tag hitting main". A tag pushed off-branch fails LOUDLY
# (the run goes red) rather than silently minting a release.
- name: Guard — tag must be on main
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
run: |
set -euo pipefail
git fetch -q origin main
if git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "tag $GITHUB_REF_NAME is on main — releasing"
else
echo "::error::tag $GITHUB_REF_NAME ($GITHUB_SHA) is NOT on main — refusing to release"
exit 1
fi

- name: Resolve version + release shape
id: shape
shell: bash
run: |
set -euo pipefail
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then
echo "version=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
echo "tag=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
echo "name=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT"
echo "prerelease=false" >> "$GITHUB_OUTPUT"
else
echo "version=dev-${GITHUB_SHA:0:12}" >> "$GITHUB_OUTPUT"
echo "tag=dev" >> "$GITHUB_OUTPUT"
echo "name=dev (rolling)" >> "$GITHUB_OUTPUT"
echo "prerelease=true" >> "$GITHUB_OUTPUT"
fi

- uses: actions/download-artifact@v4
with:
path: dist # one dir per cell: dist/lem-<backend>-<os>-<arch>/lem[.exe]

- name: Package Native + Driver zips
shell: bash
env:
VERSION: ${{ steps.shape.outputs.version }}
run: |
set -euo pipefail
VERSION="dev-${GITHUB_SHA:0:12}"
mkdir -p out
cd dist
# ONLY the binary artifacts (lem-cpu-*, lem-metal-*) — lem-sdks also
Expand All @@ -159,12 +200,12 @@ jobs:
done
ls -la ../out

- name: Publish rolling dev prerelease
- name: Publish release
uses: softprops/action-gh-release@v2
with:
tag_name: dev
name: dev (rolling)
prerelease: true
tag_name: ${{ steps.shape.outputs.tag }}
name: ${{ steps.shape.outputs.name }}
prerelease: ${{ steps.shape.outputs.prerelease }}
files: out/*.zip
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down
61 changes: 47 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# Portable CI for go-inference — the default-tag (no-GPU) test + coverage lane.
# go-inference is a Metal/CGO project; the FULL lem build (engine/metal, the
# metallibs) needs macOS 26 / Metal 4 and lives in build.yml on a self-hosted
# runner. This lane runs the portable suite (Taskfile `test`/`cover`) on any OS.
# PR gate + push CI for go-inference — the model-free test surface on all three
# desktop OSes. The suite is testable without a model loaded (engine/metal skips
# cleanly without MLX_METALLIB_PATH; engine/hip's contract tests are host-side),
# so this lane is the go/no-go for PRs into dev: linux + macos are REQUIRED
# checks; windows is EXPERIMENTAL (continue-on-error) until the cgo-on windows
# graph is proven — its logs name the real portability offenders either way.
# The full artifact build (and the metal lane) lives in build.yml.
name: CI

on:
Expand All @@ -13,29 +16,59 @@ on:
permissions:
contents: read

env:
GO_VERSION: "1.26"

jobs:
test:
name: Portable test + coverage
runs-on: ubuntu-latest
name: tests · ${{ matrix.os_name }}
runs-on: ${{ matrix.runner }}
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- { runner: ubuntu-latest, os_name: linux, experimental: false }
- { runner: macos-latest, os_name: macos, experimental: false }
- { runner: windows-latest, os_name: windows, experimental: true }
env:
GOWORK: "off" # resolve deps from go/go.mod tags, not the workspace
CGO_ENABLED: "1" # go/ links duckdb (go-duckdb/v2 ships prebuilt linux libs)
CGO_ENABLED: "1" # go/ links duckdb (prebuilt libs for all three OSes)
steps:
- uses: actions/checkout@v4

- uses: actions/setup-go@v5
with:
go-version: "1.26"
# The module root is go/ (go.sum lives there, not at the repo root); without
# this, setup-go's cache aborts with "Dependencies file is not found ...
# Supported file pattern: go.sum". This lane only compiles the go/ module.
cache-dependency-path: go/go.sum
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: |
go/go.sum
cli/go.sum

- name: Portable tests + coverage (default tags — engine/metal excluded)
# The module suite resolves from go.mod tags (GOWORK=off) — proves the
# published dependency surface, not the workspace.
- name: Module suite (default tags — engine/metal skips without a metallib)
working-directory: go
env:
GOWORK: "off"
shell: bash
run: go test -count=1 -covermode=atomic -coverprofile=coverage.out ./...

# cli + examples need the workspace (cli requires the LOCAL go/ tree; the
# examples module is the acceptance surface — every public feature ships
# with its example, so compiling them all catches API breakage model-free).
- name: TUI suite (workspace)
working-directory: cli
shell: bash
run: go test -count=1 ./tui/...

- name: Examples compile + vet (workspace — the acceptance surface)
working-directory: examples
shell: bash
run: |
go build ./...
go vet ./...

- name: Upload coverage to Codecov
if: matrix.os_name == 'linux'
uses: codecov/codecov-action@v5
with:
files: go/coverage.out
Expand Down
Loading