From d225e15f0fb6e4babdd955f33b5de17d58aad53e Mon Sep 17 00:00:00 2001 From: "mudler's LocalAI [bot]" <139863280+localai-bot@users.noreply.github.com> Date: Thu, 30 Jul 2026 11:22:44 +0200 Subject: [PATCH] fix(ci): skip the image and Go PR workflows on content they cannot see (#11218) backend_pr.yml and test-extra.yml already filter themselves, so a gallery-only or docs-only PR costs them about one job each. The image and Go workflows had no filter of any kind, so a one-line gallery/index.yaml edit queued 20 jobs: 7 container image builds, 3 GoReleaser/darwin launcher builds, 3 unit test jobs, 2 golangci-lint, 1 e2e, 1 yamllint, plus the 3 that correctly stop after their detect step. A docs-only PR queued the same. This matters more than the job count suggests. Measured over the week to 2026-07-30, 97% of CI wall-clock is queueing and 3% is execution: a median 5-hour queue against a 4-20 minute median job. Cutting job count is the only lever that shortens feedback time. The volume is there to cut, too: 13 gallery-only PRs merged that week with 10 open at once, and 78 of the 137 PRs opened were bot-generated. Add paths-ignore for gallery/**, docs/**, examples/** and **/*.md to the pull_request trigger of image-pr.yml, build-test.yaml and tests-e2e.yml, and add gallery/** to lint.yml, which already excluded the rest. That drops 13 of the 20 jobs. None of the four can observe such a diff: gallery metadata is parsed at runtime and never copied into an image, docs and markdown never enter one at all, GoReleaser and the launcher take no such input, the e2e suite drives backends over gRPC directly, and golangci-lint runs new-from-merge-base so a diff with no touched Go lines is a no-op. The build-test exclusion also frees macOS capacity, which is the scarcest runner class. The two checks that do validate the gallery are deliberately left alone. test.yml still runs core/gallery/variants_lint_test.go, which reads the real gallery/index.yaml and asserts the index invariants, and yaml-check.yml still lints the syntax. paths-ignore skips a run only when every changed file matches, so a PR touching the gallery and Go code still runs everything. master carries no branch protection and no rulesets, so a skipped workflow reports no status and nothing waits on it; .agents/ci-caching.md records that constraint for whenever required status checks are introduced. image.yml on master push is left unfiltered on purpose: skipping it would stop the master and latest tags being republished for a gallery commit, which is a publishing decision rather than a cost one. Assisted-by: Claude:opus-5 [claude-code] Signed-off-by: Ettore Di Giacinto Co-authored-by: Ettore Di Giacinto --- .agents/ci-caching.md | 30 ++++++++++++++++++++++++++++++ .github/workflows/build-test.yaml | 9 +++++++++ .github/workflows/image-pr.yml | 13 ++++++++++++- .github/workflows/lint.yml | 3 +++ .github/workflows/tests-e2e.yml | 8 ++++++++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/.agents/ci-caching.md b/.agents/ci-caching.md index 26aba82e6..77b87467e 100644 --- a/.agents/ci-caching.md +++ b/.agents/ci-caching.md @@ -145,6 +145,36 @@ Checked against every proto commit in the preceding six months, all nine resolva The Sunday 06:00 UTC cron on `backend.yml` exists specifically because path filtering can leave Python backends frozen on stale wheels. `DEPS_REFRESH` (below) only fires when the build actually runs, so an untouched Python backend would never re-resolve its unpinned deps. The weekly cron is the safety net. +## Content-blind PRs skip the workflows that cannot see them + +`backend_pr.yml` and `test-extra.yml` filter themselves (matrix generation and a `detect-changes` job), so a gallery-only or docs-only PR costs them about one job each. The Go and image workflows had no filter of any kind, so a one-line `gallery/index.yaml` edit queued 20 jobs, and a docs-only PR queued the same. + +This is worth more than it looks. Measured over the week to 2026-07-30, **97% of CI wall-clock is queueing, 3% is execution** (median queue ~5h against a 4-20min median job). Cutting job count is therefore the only lever that shortens feedback time; making individual jobs faster moves 3%. + +The volume is real: 13 gallery-only PRs merged that week with 10 open at once, and 78 of the 137 PRs opened were bot-generated. + +`paths-ignore` on the PR trigger of `image-pr.yml` (7 jobs), `build-test.yaml` (3), `lint.yml` (2) and `tests-e2e.yml` (1) drops 13 of those 20. The excluded set: + +| Path | Why no image or Go build can see it | +|---|---| +| `gallery/**` | Model-gallery metadata, parsed at runtime, never copied into an image | +| `docs/**`, `examples/**`, `**/*.md` | Never enter an image or a binary. `lint.yml` already excluded these before gallery was added | + +What still runs, and why it has to: + +| Workflow | Why it keeps running | +|---|---| +| `test.yml` (`tests`) | `core/gallery/variants_lint_test.go` reads the real `gallery/index.yaml` and asserts the index invariants (no duplicate entry names, no build claimed by two parents). This is the only schema-level check the gallery has. | +| `yaml-check.yml` (`Yamllint`) | Lints `gallery/` for syntax. | +| `backend_pr.yml`, `test-extra.yml` | Already self-filtering; they stop after the detect step. | + +Two properties this relies on: + +- `paths-ignore` skips a run only when **every** changed file matches, so a PR touching the gallery *and* Go code still runs everything. That is what makes the exclusion safe rather than a hole. +- `master` carries no branch protection and no rulesets, so a skipped workflow reports no status and nothing waits on it. If required status checks are ever introduced, these four entries must be excluded from the required set or PRs will hang on "Expected — Waiting for status to be reported". + +Deliberately **not** filtered: `image.yml` on master push still rebuilds all 18 container images for a gallery-only commit. Skipping it would mean the `master` and `latest` tags are not republished for that commit, which is a publishing-semantics decision rather than a pure cost one. + ## The `DEPS_REFRESH` cache-buster (Python backends) Every Python backend goes through the shared `backend/Dockerfile.python`, which ends with: diff --git a/.github/workflows/build-test.yaml b/.github/workflows/build-test.yaml index 0758980b6..23d61d0f5 100644 --- a/.github/workflows/build-test.yaml +++ b/.github/workflows/build-test.yaml @@ -5,6 +5,15 @@ on: branches: - master pull_request: + # GoReleaser and the darwin launcher take no gallery, docs or markdown + # input, so a diff confined to these paths cannot change either binary. + # The darwin job matters here: macOS is the scarcest runner class. + # See .agents/ci-caching.md. + paths-ignore: + - 'gallery/**' + - 'docs/**' + - 'examples/**' + - '**/*.md' # Supersede an in-flight run when a PR gets a new push. Keyed on the PR number # so every push to the same PR shares a group; on a master push the key falls diff --git a/.github/workflows/image-pr.yml b/.github/workflows/image-pr.yml index 146ad22cf..fbf077383 100644 --- a/.github/workflows/image-pr.yml +++ b/.github/workflows/image-pr.yml @@ -3,7 +3,18 @@ on: pull_request: - + # None of these seven image builds can observe a diff confined to these + # paths. Gallery metadata is parsed at runtime and never copied into an + # image; docs and markdown never enter one at all. Gallery content is + # still checked by yaml-check.yml and by + # core/gallery/variants_lint_test.go under 'tests'. + # See .agents/ci-caching.md. + paths-ignore: + - 'gallery/**' + - 'docs/**' + - 'examples/**' + - '**/*.md' + concurrency: group: ci-${{ github.event.pull_request.number || github.sha }}-${{ github.repository }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 572764aea..4cab2d11b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -8,6 +8,9 @@ on: - 'examples/**' - 'README.md' - '**/*.md' + # golangci-lint runs new-from-merge-base, so a diff with no touched Go + # lines can only ever be a no-op. See .agents/ci-caching.md. + - 'gallery/**' push: branches: - master diff --git a/.github/workflows/tests-e2e.yml b/.github/workflows/tests-e2e.yml index eef0a1a1c..3c1cb711c 100644 --- a/.github/workflows/tests-e2e.yml +++ b/.github/workflows/tests-e2e.yml @@ -3,6 +3,14 @@ name: 'E2E Backend Tests' on: pull_request: + # The e2e suite drives backends over gRPC directly and reads none of these + # paths, so a diff confined to them cannot move it. + # See .agents/ci-caching.md. + paths-ignore: + - 'gallery/**' + - 'docs/**' + - 'examples/**' + - '**/*.md' push: branches: - master