ci: migrate all workflows to GKE self-hosted runners

Swap all `runs-on: ubuntu-latest` to `runs-on: arc-runner-set` across
18 workflow files (35 job definitions). The ARM docker build job in
docker.yml uses `${{ matrix.runner }}` and is left untouched since
the GKE runner pool is x86_64 only.

Runners are backed by ARC (Actions Runner Controller) on a GKE cluster
with a spot preemptible node pool that scales 0→20 based on job demand.
Cost when idle: ~$25/mo (single e2-standard-2 for the controller).
This commit is contained in:
ethernet 2026-07-17 16:40:28 -04:00
parent 93c97073d8
commit 282d026cf8
17 changed files with 31 additions and 31 deletions

View file

@ -34,7 +34,7 @@ jobs:
# ─────────────────────────────────────────────────────────────────────
detect:
name: Detect affected areas
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
outputs:
python: ${{ steps.classify.outputs.python }}
@ -280,7 +280,7 @@ jobs:
# we don't require docker to pass rn because it's so slow lol
# - docker
if: always()
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
outputs:
needs-json: ${{ steps.evaluate.outputs.needs-json }}
@ -322,7 +322,7 @@ jobs:
name: CI timing report
needs: [all-checks-pass, docker]
if: always()
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
steps:
- name: Checkout code

View file

@ -12,7 +12,7 @@ permissions:
jobs:
check-attribution:
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
outputs:
review_status: ${{ steps.check-emails.outputs.review_status }}

View file

@ -40,7 +40,7 @@ jobs:
# a skills-index PR that doesn't touch website/** paths and so
# doesn't auto-deploy via the deploy-docs path.
if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 30
steps:
- name: Trigger Vercel Deploy
@ -48,7 +48,7 @@ jobs:
deploy-docs:
if: github.repository == 'NousResearch/hermes-agent'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 30
environment:
name: github-pages

View file

@ -23,7 +23,7 @@ concurrency:
jobs:
hadolint:
name: Lint Dockerfile (hadolint)
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
steps:
- name: Checkout code
@ -38,7 +38,7 @@ jobs:
shellcheck:
name: Lint docker/ shell scripts (shellcheck)
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
steps:
- name: Checkout code

View file

@ -157,7 +157,7 @@ jobs:
# ---------------------------------------------------------------------------
merge:
if: github.repository == 'NousResearch/hermes-agent' && (github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'release')
runs-on: ubuntu-latest
runs-on: arc-runner-set
needs: [build]
timeout-minutes: 10
steps:

View file

@ -8,7 +8,7 @@ permissions:
jobs:
docs-site-checks:
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 20
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

View file

@ -25,7 +25,7 @@ permissions:
jobs:
check-common-ancestor:
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
outputs:
review_status: ${{ steps.merge-base-check.outputs.review_status }}

View file

@ -57,7 +57,7 @@ concurrency:
jobs:
generate-patch:
name: Generate eslint --fix patch
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
outputs:
has-fixes: ${{ steps.produce-patch.outputs.has-fixes }}
@ -120,7 +120,7 @@ jobs:
# Skip entirely when generate-patch found no fixes — saves a runner,
# avoids a redundant checkout/download, and keeps the job graph honest.
if: needs.generate-patch.outputs.has-fixes == 'true'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
permissions:
contents: write # needed to push to bot/js-autofix

View file

@ -7,7 +7,7 @@ on:
jobs:
workspaces:
name: List npm workspaces
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 20
outputs:
packages: ${{ steps.set-matrix.outputs.packages }}
@ -32,7 +32,7 @@ jobs:
check:
name: Typecheck & Test
needs: workspaces
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 20
strategy:
matrix:

View file

@ -30,7 +30,7 @@ jobs:
lint-diff:
name: ruff + ty diff
if: inputs.event_name == 'pull_request'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
steps:
- name: Checkout code
@ -121,7 +121,7 @@ jobs:
# ``lint-diff`` job above runs independently so reviewers still get
# the diff comment even when enforcement fails.
name: ruff enforcement (blocking)
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
steps:
- name: Checkout code
@ -147,7 +147,7 @@ jobs:
# shebang scripts via subprocess, bare open() without encoding=, etc.
# See scripts/check-windows-footguns.py for the full rule list.
name: Windows footguns (blocking)
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
steps:
- name: Checkout code

View file

@ -34,7 +34,7 @@ concurrency:
jobs:
diff:
name: package-lock.json semantic diff
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
outputs:
changed: ${{ steps.diff.outputs.changed }}

View file

@ -55,7 +55,7 @@ jobs:
emit-status:
name: Emit review status
runs-on: ubuntu-latest
runs-on: arc-runner-set
needs: scan
if: always()
outputs:

View file

@ -19,7 +19,7 @@ permissions:
jobs:
check-freshness:
if: github.repository == 'NousResearch/hermes-agent'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
steps:
- name: Probe live index

View file

@ -19,7 +19,7 @@ jobs:
build-index:
# Only run on the upstream repository, not on forks
if: github.repository == 'NousResearch/hermes-agent'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@ -58,7 +58,7 @@ jobs:
trigger-deploy:
needs: build-index
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
steps:
- name: Get GitHub App token

View file

@ -54,7 +54,7 @@ jobs:
scan:
name: Scan PR for critical supply chain risks
if: inputs.scan
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
outputs:
review_status: ${{ steps.emit-status.outputs.review_status }}
@ -187,7 +187,7 @@ jobs:
dep-bounds:
name: Check PyPI dependency upper bounds
if: inputs.deps
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
outputs:
review_status: ${{ steps.emit-status.outputs.review_status }}
@ -266,7 +266,7 @@ jobs:
name: Aggregate review statuses
needs: [scan, dep-bounds]
if: always()
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
outputs:
review_status: ${{ steps.merge.outputs.review_status }}

View file

@ -19,7 +19,7 @@ concurrency:
jobs:
generate:
name: "Generate slices"
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
outputs:
matrix: ${{ steps.matrix.outputs.matrix }}
@ -48,7 +48,7 @@ jobs:
test:
name: Run tests slice ${{ matrix.slice.index }}/${{ inputs.slice_count }}
needs: generate
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 30
strategy:
fail-fast: false
@ -135,7 +135,7 @@ jobs:
save-durations:
needs: test
if: needs.test.result == 'success' && github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
steps:
- name: Download all slice durations
@ -165,7 +165,7 @@ jobs:
key: test-durations-${{ github.run_id }}
e2e:
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 15
steps:
- name: Checkout code

View file

@ -60,7 +60,7 @@ concurrency:
jobs:
check:
name: uv lock --check
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 5
outputs:
review_status: ${{ steps.verify.outputs.review_status }}