Skip to content

Commit 42541dd

Browse files
committed
ci: route site deploy and lint to the self-hosted runner
The GitHub-hosted runner pool is shared per ACCOUNT, not per repo, so a burst in one repo starves every other. On 2026-07-31 it reached zero scheduled jobs for 35 consecutive minutes with 39 jobs queued, while arc-runner-set completed 12 jobs without interruption across the same window. Actions was healthy globally (other public repos were scheduling normally), so this is an account-level throttle we cannot fix from inside the workflows, only route around. Site publishing and lint are small, run on nearly every commit, and gain nothing from waiting behind a saturated hosted queue, so both move to arc-runner-set, the label already proven in generate_intel_image.yaml. lint.yml is routed for PUSH ONLY, and this is the important part: that workflow also triggers on pull_request, and a fork PR executes untrusted contributor code. Running that on a persistent self-hosted runner would be a real compromise vector, so anything that is not a push to mudler/LocalAI stays on the ephemeral hosted pool. gh-pages.yml needs no such clause: it triggers only on push-to-master and workflow_dispatch, so it never runs pull-request code. Both carry a repository guard so forks, which have no such runner label, fall back to hosted instead of queueing forever. Neither workflow uses sudo or apt, and both fetch their own toolchains via setup-go / actions-hugo. A self-hosted image can still be leaner than the hosted one, so each lint job opens with a preflight that names the missing tool (curl/unzip/make for protoc and lint; gcc/ldd/python3 for the packaging-script tests) rather than failing opaquely mid-build. Reverting is one runs-on expression per job. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude Code:claude-opus-5 [ClaudeCode]
1 parent fb54d0f commit 42541dd

3 files changed

Lines changed: 81 additions & 4 deletions

File tree

.agents/ci-caching.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,21 @@ GitHub Actions caches are limited to 10 GB per repo. Steady-state worst case: ~8
359359

360360
One residual self-hosted reference remains in `test-extra.yml` (`tests-vibevoice-cpp-grpc-transcription` uses `bigger-runner` for the 30s JFK-decode timeout headroom). That's a separate concern.
361361

362+
### Small always-on jobs routed to `arc-runner-set`
363+
364+
The hosted pool is shared across the whole *account*, not per repo, so a burst in one repo starves the others. On 2026-07-31 it went to **zero scheduled jobs for 35 consecutive minutes** with 39 jobs queued, while `arc-runner-set` completed 12 jobs without interruption over the same window. Actions was healthy globally at the time (other public repos were scheduling normally), so this is an account-level throttle, not an outage.
365+
366+
Two small, high-frequency workflows are therefore routed to `arc-runner-set`:
367+
368+
| workflow | jobs | routing expression selects self-hosted when |
369+
|---|---|---|
370+
| `gh-pages.yml` | `build`, `deploy` | `github.repository == 'mudler/LocalAI'` |
371+
| `lint.yml` | `golangci-lint`, `build-scripts` | `github.event_name == 'push'` **and** the repo guard |
372+
373+
The `lint.yml` routing is push-only **on purpose**. That workflow also triggers on `pull_request`, and a fork PR runs untrusted contributor code; that must stay on the ephemeral hosted pool and never touch a self-hosted runner. `gh-pages.yml` needs no such clause because it only triggers on push-to-master and `workflow_dispatch`. The repository guard in both keeps forks (which have no such runner label) from queueing forever.
374+
375+
Both workflows fetch their own toolchains (`setup-go`, `actions-hugo`) and use no `sudo`/`apt`. Because a self-hosted image may be leaner than the hosted one, each `lint.yml` job opens with a preflight step that names any missing tool (`curl`/`unzip`/`make` for protoc and lint, `gcc`/`ldd`/`python3` for the packaging-script tests) instead of failing opaquely mid-build. If the runner image turns out to lack them, either extend the image or revert the single `runs-on:` expression per job.
376+
362377
## Touching the cache pipeline
363378

364379
When changing `image_build.yml`, `backend_build.yml`, any of the `backend/Dockerfile.*` files, `Dockerfile.base-grpc-builder`, `.docker/install-base-deps.sh`, `.docker/<backend>-compile.sh`, or `scripts/changed-backends.js`:

.github/workflows/gh-pages.yml

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,20 @@ concurrency:
2525

2626
jobs:
2727
build:
28-
runs-on: ubuntu-latest
28+
# Self-hosted. This workflow is push-to-master + workflow_dispatch only, so
29+
# it never executes pull-request code and a fork cannot reach the runner
30+
# with untrusted changes. The repository guard keeps forks (whose own master
31+
# pushes would otherwise queue forever against a label they do not have) on
32+
# the hosted pool.
33+
#
34+
# Why: the GitHub-hosted pool is shared account-wide and has repeatedly
35+
# starved (2026-07-31: 35 consecutive minutes at zero scheduled jobs, while
36+
# arc-runner-set kept completing work throughout). Publishing the site is
37+
# small, frequent, and must not sit behind a saturated hosted queue.
38+
#
39+
# Needs only git, tar and curl on the runner: setup-go and actions-hugo
40+
# fetch their own toolchains, and no step uses sudo, apt, make or unzip.
41+
runs-on: ${{ github.repository == 'mudler/LocalAI' && 'arc-runner-set' || 'ubuntu-latest' }}
2942
env:
3043
HUGO_VERSION: "0.146.3"
3144
steps:
@@ -86,7 +99,11 @@ jobs:
8699
environment:
87100
name: github-pages
88101
url: ${{ steps.deployment.outputs.page_url }}
89-
runs-on: ubuntu-latest
102+
# Same routing as build: a hosted slot for a ~10s deploy is exactly the kind
103+
# of job that should not block on a starved pool. deploy-pages authenticates
104+
# with the job's OIDC token (id-token: write above), which self-hosted
105+
# runners issue the same way hosted ones do.
106+
runs-on: ${{ github.repository == 'mudler/LocalAI' && 'arc-runner-set' || 'ubuntu-latest' }}
90107
needs: build
91108
steps:
92109
- name: Deploy to GitHub Pages

.github/workflows/lint.yml

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,36 @@ concurrency:
2121

2222
jobs:
2323
golangci-lint:
24-
runs-on: ubuntu-latest
24+
# Self-hosted for PUSH only, and only in the canonical repo.
25+
#
26+
# This workflow also runs on pull_request, which for a fork PR means
27+
# executing untrusted contributor code. That must never land on a
28+
# self-hosted runner, so anything that is not a push to mudler/LocalAI stays
29+
# on the ephemeral hosted pool. Pushes to master are trusted code that has
30+
# already been reviewed and merged.
31+
#
32+
# Why at all: the hosted pool is shared account-wide and starved for 35
33+
# straight minutes on 2026-07-31 while arc-runner-set kept completing jobs.
34+
# Lint is small and runs on every commit, so it is a good candidate to move
35+
# off the contended pool.
36+
runs-on: ${{ (github.event_name == 'push' && github.repository == 'mudler/LocalAI') && 'arc-runner-set' || 'ubuntu-latest' }}
2537
steps:
38+
- name: Preflight - required host tools
39+
# The hosted images ship these; a self-hosted container image may not.
40+
# Check up front so a missing tool reports itself by name instead of
41+
# surfacing as an opaque failure inside `make protogen-go` (which needs
42+
# curl + unzip for protoc) or `make lint`.
43+
run: |
44+
missing=""
45+
for t in git curl unzip make tar; do
46+
command -v "$t" >/dev/null 2>&1 || missing="$missing $t"
47+
done
48+
echo "runner: ${RUNNER_NAME:-unknown} os: $(uname -sm)"
49+
if [ -n "$missing" ]; then
50+
echo "::error::missing required tools on this runner:$missing"
51+
exit 1
52+
fi
53+
echo "all required tools present"
2654
- uses: actions/checkout@v7
2755
with:
2856
# Full history so golangci-lint's new-from-merge-base can reach
@@ -55,8 +83,25 @@ jobs:
5583
# container build (a missing transitive dep, a partial cuDNN family). Their
5684
# shell tests need nothing but bash + gcc + ldd, so run them on every PR
5785
# rather than waiting on a multi-GB cross-arch backend image build.
58-
runs-on: ubuntu-latest
86+
#
87+
# Push-only self-hosted routing, same fork-safety reasoning as
88+
# golangci-lint above.
89+
runs-on: ${{ (github.event_name == 'push' && github.repository == 'mudler/LocalAI') && 'arc-runner-set' || 'ubuntu-latest' }}
5990
steps:
91+
- name: Preflight - required host tools
92+
# This job additionally needs a C toolchain: the packaging-script tests
93+
# compile a throwaway binary and inspect it with ldd.
94+
run: |
95+
missing=""
96+
for t in git make gcc ldd python3; do
97+
command -v "$t" >/dev/null 2>&1 || missing="$missing $t"
98+
done
99+
echo "runner: ${RUNNER_NAME:-unknown} os: $(uname -sm)"
100+
if [ -n "$missing" ]; then
101+
echo "::error::missing required tools on this runner:$missing"
102+
exit 1
103+
fi
104+
echo "all required tools present"
60105
- uses: actions/checkout@v7
61106
- name: run packaging script tests
62107
run: make test-build-scripts

0 commit comments

Comments
 (0)