From 0cf67211b5c42f8b4d72fa2abe706a17fa14cb86 Mon Sep 17 00:00:00 2001 From: Snider Date: Wed, 22 Jul 2026 16:27:13 +0100 Subject: [PATCH] ci: three-OS PR test gate + tag-on-main versioned releases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI becomes the PR go/no-go: the model-free suite (engine/metal skips without a metallib, hip contract tests host-side) runs on linux+macos as REQUIRED lanes and windows as EXPERIMENTAL (continue-on-error) until the cgo-on windows graph is proven — its logs name the real offenders either way. Each lane: go/ module suite (GOWORK=off, the published dep surface), cli tui suite (workspace), and the examples module compiled + vetted (the acceptance surface — every public feature ships with its example, so compiling all of them catches API breakage without a model). Build gains tags: v* — a version tag publishes the REAL release, guarded by a merge-base ancestry check (tag not on main = loud red, no release); dev pushes keep the rolling dev prerelease. Binary main.version and the zip names carry the tag on tag builds. Co-Authored-By: Virgil --- .github/workflows/build.yml | 63 ++++++++++++++++++++++++++++++------- .github/workflows/ci.yml | 61 ++++++++++++++++++++++++++--------- 2 files changed, 99 insertions(+), 25 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 132c6c75..c31c4f4c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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: @@ -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}" @@ -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 @@ -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---/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 @@ -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 }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 455c5e27..f79a6af1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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