fix(ci): only rebuild the full backend matrix on breaking backend.proto edits (#11192)

backend/backend.proto is consumed by every language, so its SHARED_BUILD_INPUTS
rule could only ever be always/always: 417 Linux plus 56 Darwin builds. It fires
on ~1.3% of commits (10 of 767 over six months), which made it the single
largest CI cost driver in the repo.

On 2026-07-29 the queue reached 2178 jobs against 8 concurrent runners. Four
runs totalling 935 of those jobs were triggered by nothing but a proto edit. The
largest, 378 jobs on master, came from PR #11158, whose entire proto diff was
six lines adding `bool cache_prompt = 8;` to one message. No backend that does
not read that field behaves any differently for it.

Make the rule content-aware. changed-backends.js resolves backend.proto at the
base revision (the contents-API pattern already used for backend-matrix.yml) and
hands both texts to protoChangeIsAdditive(), which compares them structurally so
a comment reflow, reindent or field reorder does not read as a change. An
additive-only edit (new field with an unused number, new message, new enum
value, new RPC) suppresses the rule and rebuilds nothing; a removed, renumbered,
retyped or renamed field, a dropped RPC or a changed option still rebuilds
everything, as does an unresolvable base revision.

Every other matched rule is untouched, so a PR that edits the proto and
scripts/build/ is still a full rebuild, and the weekly full-matrix cron remains
the backstop for stale wheels.

Verified against all ten proto commits of the preceding six months: the nine
with a resolvable parent all classify as additive, and controls covering a
retyped-and-renumbered field, a deleted RPC, identical revisions and a
reindent-plus-comment-reflow all classify correctly.


Assisted-by: Claude:opus-5 [claude-code]

Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
mudler's LocalAI [bot]andEttore Di Giacinto authored and GitHub committed 2026-07-29 15:58:27 +02:00
1 parent 89ee62b2af
commit 8089b2bf09
4 files changed
+339 -5

No files matched your search

+44 -2
View File
@@ -6,6 +6,7 @@ import {
getAllBackendPaths,
filterMatrix,
BACKEND_MATRIX_FILE,
BACKEND_PROTO_FILE,
} from "./lib/backend-filter.mjs";
// Matrix data lives in a small data-only YAML so both backend.yml (master push)
@@ -116,6 +117,42 @@ async function getPreviousMatrix(event) {
}
}
// backend.proto at the base revision plus the checked-out copy, so filterMatrix
// can tell an additive edit (a new field, message or RPC, which invalidates no
// existing image) from a breaking one. Returning null means "rebuild
// everything", the same posture as an unresolvable matrix diff.
//
// Only called when the changed-file list names the proto, so the common path
// costs no extra API request.
async function getProtoRevisions(event) {
const ref = event.pull_request ? event.pull_request.base.sha : event.before;
if (!ref || /^0+$/.test(ref)) return null;
const owner = event.repository.owner.login;
const repo = event.repository.name;
try {
const res = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
owner,
repo,
path: BACKEND_PROTO_FILE,
ref,
mediaType: { format: 'raw' },
});
const previous = typeof res.data === 'string'
? res.data
: Buffer.from(res.data.content, 'base64').toString('utf8');
return {
previous,
current: fs.readFileSync(BACKEND_PROTO_FILE, "utf8"),
};
} catch (err) {
console.log(
`could not read ${BACKEND_PROTO_FILE} at ${ref}, falling back to run-all:`,
err.message
);
return null;
}
}
// Group matrix entries by tag-suffix and emit a merge-matrix entry per group.
// Both multi-leg groups (per-arch fan-out) and singletons get one entry each:
// the build job pushes by digest only with no tags applied, so every backend
@@ -240,7 +277,7 @@ function emitFullMatrix() {
}
}
function emitFilteredMatrix(changedFiles, previousMatrix) {
function emitFilteredMatrix(changedFiles, previousMatrix, protoRevisions) {
console.log("Changed files:", changedFiles);
const { filtered, filteredDarwin, changedBackends } = filterMatrix({
@@ -248,6 +285,7 @@ function emitFilteredMatrix(changedFiles, previousMatrix) {
includesDarwin,
changedFiles,
previousMatrix,
protoRevisions,
});
console.log("Filtered files:", filtered);
@@ -306,5 +344,9 @@ function emitFilteredMatrix(changedFiles, previousMatrix) {
? await getPreviousMatrix(event)
: null;
emitFilteredMatrix(changedFiles, previousMatrix);
const protoRevisions = changedFiles.includes(BACKEND_PROTO_FILE)
? await getProtoRevisions(event)
: null;
emitFilteredMatrix(changedFiles, previousMatrix, protoRevisions);
})();