fix(ci): rebuild backends when shared build inputs change#10975
Merged
Conversation
The backend matrix path filter only matched files under a backend's own directory, so a change to shared build infrastructure rebuilt nothing at all: an empty matrix, every job green, and the change reaching no image. PR #10946 fixed scripts/build/package-gpu-libs.sh shipping a partial 4-of-8 cuDNN library set, which mixed versions with the venv's pip cuDNN and produced CUDNN_STATUS_SUBLIBRARY_VERSION_MISMATCH at inference time. It merged 1h48m after the weekly full-matrix cron had already run, so no backend image ever received the fix and nothing signalled that it had been un-shipped. Add a SHARED_BUILD_INPUTS table mapping each shared path to the narrowest set of matrix entries it can honestly invalidate, plus a generic rule for backend/Dockerfile.<x> (which each entry already names). A full matrix is 417 Linux + 56 Darwin builds, so package-gpu-libs.sh now rebuilds the 176 Python entries rather than everything. Unclassified files under scripts/build/ fall back to a full rebuild deliberately: over-building is recoverable, silently shipping nothing is not. Extract the filtering logic to scripts/lib/backend-filter.mjs so it can be unit-tested without bun, js-yaml or a GitHub API round-trip, and run those tests from the existing lint workflow via `make test-ci-scripts`. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-4-8[1m] [Read] [Edit] [Bash]
This was referenced Jul 20, 2026
mudler
added a commit
that referenced
this pull request
Jul 20, 2026
… coverage-ratchet fixes (#10989) * fix(http): make /readyz reflect startup readiness instead of always 200 /readyz was registered as a static handler returning 200 unconditionally, so it carried no information: it was green whenever it could be reached at all. Readiness could not distinguish "serving" from "still starting", and any future change that started the HTTP listener earlier would silently turn the probe into a lie. Track startup completion on the Application (atomic flag, flipped at the very end of New() on the success path only) and have the readiness handler consult it per request, returning 503 with a small JSON body while startup is in progress. A nil readiness source fails open so embedders keep the historical behaviour. /healthz is deliberately left readiness-independent. Liveness and readiness answer different questions, and failing liveness during a long preload makes an orchestrator restart the pod mid-download so the preload never finishes. This matters because since #10949 the startup preload materializes HuggingFace artifacts for managed backends: tens of GB for a large model (31 GB observed on a live cluster). Both probes stay in quietPaths and stay exempt from auth. Note the listener is still started only after New() returns, so today the not-ready state is not observable over HTTP. Moving the listener earlier is a separate, deliberate decision and is not made here. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] * chore(gitignore): anchor the mock-backend pattern so its source dir is traversable The bare `mock-backend` pattern matched the *directory* tests/e2e/mock-backend/, not just the binary built into it. Git will not descend into an ignored directory even for tracked files, so `git add tests/e2e/mock-backend/main.go` required -f. This was hit while working on #10970. Anchor it to the artifact's full path. The built binary stays ignored (it is also covered by tests/e2e/mock-backend/.gitignore) while the source directory becomes traversable again. Verified with `git check-ignore -v`: a new source file under tests/e2e/mock-backend/ is no longer ignored, and the binary produced by `make build-mock-backend` still is. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] * chore(coverage): raise the coverage ratchet from 48.5% to 54.2% The committed baseline had drifted well below reality: it still read 48.5% while a full instrumented run measures 54.2%. A stale-low baseline makes the gate meaningless — coverage could regress by more than 5 percentage points and still pass. Raising a ratchet is a deliberate act, not something to fold into an unrelated fix, so it gets its own commit. The headroom was earned by tests landed in #10946, #10947, #10948, #10949, #10956, #10967, #10968, #10970 and #10975. Measured with `make test-coverage` on this branch (the same instrumented run `make test-coverage-baseline` uses: ginkgo over ./pkg and ./core plus the in-process tests/e2e suite, --covermode=atomic, --coverpkg over core/... and pkg/..., generated protobuf excluded). The run completed with exit 0 and zero spec failures; the total was then written with the exact command the test-coverage-baseline target uses: go tool cover -func=coverage/coverage.out \ | awk '/^total:/{gsub(/%/,"",$NF); print $NF}' > coverage-baseline.txt Verified afterwards with scripts/coverage-check.sh, which reports OK. Note the measured figure includes the readiness specs added earlier on this branch, so it is a demonstrated floor rather than an estimate. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
mudler
added a commit
that referenced
this pull request
Jul 20, 2026
…ry edits (#10988) PR #10975 taught the backend matrix filter about shared build inputs, but left two paths that still rebuild nothing. Go backends link code from the main tree. `go list -deps ./backend/go/...` resolves to exactly six pkg subtrees (audio, grpc incl. base/grpcerrors/proto, httpclient, sound, store, utils), identical for GOOS/GOARCH in {linux,darwin} x {amd64,arm64}. Editing any of them changes the shipped binary, but they sit outside every backend directory so the prefix match never saw them. Enumerating those six rather than taking all of pkg/ is the point: all of pkg/ changes in ~8.6% of commits, these six in 2.0% — the same order as the already accepted scripts/build/ rule (1.9%). Blast radius 199/417 Linux, 26/56 Darwin; the ~21 core-server-only pkg subtrees still rebuild nothing, and neither do _test.go files. .github/backend-matrix.yml was excluded wholesale because matching its path would rebuild all 417 entries on every new-backend PR. That hid a real hole: editing an existing entry's base-image, build-type or cuda version changes the image it produces while touching no file the filter can see. Since the change is within a structured file, compare it against the base revision and rebuild only the entries whose fields actually differ — 1 entry for a base-image edit, 0 for a comment or whitespace edit, and all 417 only when the previous revision cannot be resolved. This also closes a third hole: a new matrix entry for an existing backend (a new CUDA variant, say) touches nothing under that backend's directory and previously rebuilt nothing. changed-backends.js fetches the base revision via the contents API, and only when the changed-file list actually names the matrix file, so the common path costs no extra request. Assisted-by: Claude Code:claude-opus-4-8[1m] [Read] [Edit] [Bash] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The incident
PR #10946 fixed a real bug: backend images shipped a partial 4-of-8 cuDNN library set that silently mixed versions with the venv's pip cuDNN, producing
CUDNN_STATUS_SUBLIBRARY_VERSION_MISMATCHat inference time on ~10 of 11 backends on an L4T fleet.It merged 2026-07-19 07:48 UTC. The weekly full-matrix cron (
0 6 * * 0) had fired at 06:00 the same day, 1h48m earlier. Every build since was path-filtered.No backend image contains that fix, and none would until the 07-26 cron. CI was green throughout. The fix looked merged and shipped; it was neither. It was only caught because someone went looking for the rebuilt image to deploy it.
Cause
emitFilteredMatrixfiltered withchangedFiles.some(f => f.startsWith(backendPath)), wherebackendPathis the backend's own directory. A file only triggered a rebuild if it lived under that backend's tree.scripts/build/package-gpu-libs.shis COPY'd and executed bybackend/Dockerfile.pythonfor every Python backend image, but lives underscripts/— so it matched nothing and produced an empty matrix.The failure mode is invisible: an empty matrix is not an error.
The same hole applied to
backend/Dockerfile.*itself. A change tobackend/Dockerfile.llama-cpptriggered nothing, because the Dockerfile does not live underbackend/cpp/llama-cpp/. That one was not in the original report and was found while deriving the path set.Fix
A
SHARED_BUILD_INPUTStable maps each shared path to the narrowest set of matrix entries it can honestly invalidate, plus a generic rule forbackend/Dockerfile.<x>— exact rather than heuristic, since every matrix entry already names itsdockerfile:.A full matrix is 417 Linux + 56 Darwin builds, so granularity matters. Measured against the real matrix:
backend/backend.protobackend/Dockerfile.<x>dockerfile:names itbackend/python/common/scripts/build/package-gpu-libs.shscripts/build/<x>-darwin.sh.github/workflows/backend_build[_darwin].ymlscripts/build/(except*_test.sh)Each inclusion was verified against the Dockerfiles, the reusable workflows and the Makefile targets rather than assumed — e.g.
package-gpu-libs.shis Linux-only becausepython-darwin.shnever invokes it.That last row is deliberate: a new shared script silently shipping to nothing is the exact failure being fixed here. Over-building is recoverable; under-building is invisible.
Deliberately excluded, to keep CI spend honest
backend/index.yaml— grepped: zero references in any Dockerfile or inbackend_build.yml. Gallery metadata read at runtime; never enters an image..github/backend-matrix.yml— would fire all 417 builds on every new-backend PR, and those already trigger via their own directory. Residual gap: editing an existing entry'sbase-imagewill not rebuild it.backend/Dockerfile.base-grpc-builder— owned bybase-images.yml, which has its own path filter; backend builds consume the result by mutable tag.Makefile— ~11% of recent commits; its backend-relevant edits arrive alongside the backend directory anyway.Known remaining gap
Go backends genuinely link
pkg/(e.g.backend/go/ced/main.goimportspkg/grpc), so a core change can alter a Go backend binary without rebuilding it. Includingpkg/would fire a Go matrix on nearly every commit, so it is left for a follow-up rather than absorbed here.Tests
The filtering logic moved to
scripts/lib/backend-filter.mjsso it can be unit-tested without bun, js-yaml, or a GitHub API round-trip. 15 tests vianode --test(zero dependencies), run from the existingbuild-scriptsjob inlint.yml.Verified red-first against the pre-fix logic, extracted unchanged — 11 failed, 4 passed:
The negative pins passed before the fix as well as after — which is what makes them meaningful, since they would have caught an over-broad fix that simply rebuilt everything.
Final: 15/15 pass.
make test-build-scriptsunchanged and still green (16 PASS lines, no regression).make lint:0 issues.Note for reviewers
.agents/adding-backends.mdand.github/backend-matrix.ymlboth point contributors atscripts/changed-backends.js:inferBackendPath, which this change moves. Those docs describe backend registration as "the single most common omission", so a stale pointer there has real cost — hence the doc updates in this diff. Separable if you would rather keep it narrow.🤖 Generated with Claude Code