feat(ci): migrate all workflows to GKE self-hosted runners

Swap all `runs-on: ubuntu-latest` to `runs-on: arc-runner-set` all jobs.
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 based on job demand.

Use the baked Electron dependencies for the desktop E2E job.
This commit is contained in:
ethernet 2026-07-17 16:40:28 -04:00
parent 6ecd335aa8
commit e4fe99987d
22 changed files with 48 additions and 55 deletions

View file

@ -38,7 +38,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 }}
@ -181,7 +181,7 @@ jobs:
name: CI review comment (live)
needs: [detect, review-labels, lockfile-diff, supply-chain, osv-scanner, uv-lockfile, history-check, contributor-check, e2e-desktop]
if: always() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork != true
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 40
steps:
- name: Checkout code
@ -274,7 +274,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 }}
@ -316,7 +316,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

@ -37,12 +37,12 @@ jobs:
matrix:
include:
- arch: amd64
runner: ubuntu-latest
runner: arc-runner-set
platform: linux/amd64
cache-from: type=gha,scope=docker-amd64
cache-to: type=gha,mode=max,scope=docker-amd64
- arch: arm64
runner: ubuntu-24.04-arm
runner: arc-runner-arm64
platform: linux/arm64
cache-from: type=gha,scope=docker-arm64
cache-to: type=gha,mode=max,scope=docker-arm64
@ -143,12 +143,12 @@ jobs:
matrix:
include:
- arch: amd64
runner: ubuntu-latest
runner: arc-runner-set
platform: linux/amd64
cache-from: type=gha,scope=docker-amd64
cache-to: type=gha,mode=max,scope=docker-amd64
- arch: arm64
runner: ubuntu-24.04-arm
runner: arc-runner-arm64
platform: linux/arm64
cache-from: type=gha,scope=docker-arm64
cache-to: type=gha,mode=max,scope=docker-arm64
@ -216,7 +216,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: [publish]
timeout-minutes: 10
environment: container-publish

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
@ -32,7 +32,7 @@ jobs:
- name: Install ascii-guard
uses: ./.github/actions/retry
with:
command: python -m pip install ascii-guard==2.3.0 pyyaml==6.0.3
command: python3 -m pip install ascii-guard==2.3.0 pyyaml==6.0.3
- name: Extract skill metadata for dashboard
run: python3 website/scripts/extract-skills.py

View file

@ -17,7 +17,7 @@ concurrency:
jobs:
e2e:
name: Playwright E2E (Linux)
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 20
outputs:
review_status: ${{ steps.review-status.outputs.review_status }}
@ -25,16 +25,9 @@ jobs:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# ── System deps for Electron on headless Ubuntu ───────────────────
# Electron needs GTK, NSS,atk, etc. even under xvfb. Playwright's
# install-deps covers browsers; for Electron we install the apt
# packages directly.
- name: Install system dependencies for Electron
run: |
sudo apt-get update -qq
sudo apt-get install -y -qq \
xvfb \
libgtk-3-0 libnotify4 libnss3 libxss1 libxtst6 \
xdg-utils libatspi2.0-0 libdrm2 libgbm1 libasound2t64
# xvfb and Electron's GTK/NSS/audio libraries are baked into
# nousresearch/nous-gke-runner so this works in the ARC runner pod
# without requiring passwordless sudo.
# ── Node ───────────────────────────────────────────────────────────
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4

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

@ -28,7 +28,7 @@ permissions:
jobs:
check-no-committed-infographics:
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
outputs:
review_status: ${{ steps.infographic-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
environment: trusted-automation
permissions:

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:
checks: ${{ steps.set-matrix.outputs.checks }}
@ -51,7 +51,7 @@ jobs:
check:
name: ${{ matrix.package }} / ${{ matrix.script }}
needs: workspaces
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 20
strategy:
matrix:

View file

@ -26,7 +26,7 @@ jobs:
rerun-review-labels:
name: Rerun review-labels job
if: github.event.label.name == 'ci-reviewed'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 40
steps:
- name: Wait for CI run to finish, then rerun failed jobs

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
@ -108,7 +108,7 @@ jobs:
env:
HEAD_REF: ${{ inputs.event_name == 'pull_request' && github.head_ref || github.ref_name }}
run: |
python scripts/lint_diff.py \
python3 scripts/lint_diff.py \
--base-ruff .lint-reports/base/ruff.json \
--head-ruff .lint-reports/head/ruff.json \
--base-ty .lint-reports/base/ty.json \
@ -126,7 +126,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
@ -157,7 +157,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
@ -169,4 +169,4 @@ jobs:
python-version: "3.11"
- name: Run footgun checker
run: python scripts/check-windows-footguns.py --all
run: python3 scripts/check-windows-footguns.py --all

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

@ -21,7 +21,7 @@ jobs:
publish:
name: Publish inline E2E evidence
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 10
environment: gh-image
steps:

View file

@ -51,7 +51,7 @@ jobs:
check:
name: Review label gate
if: inputs.ci_review || inputs.mcp_catalog || inputs.supply_chain
runs-on: ubuntu-latest
runs-on: arc-runner-set
timeout-minutes: 2
outputs:
ci_reviewed: ${{ steps.label-check.outputs.ci_reviewed }}

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
environment: trusted-automation
steps:

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
environment: trusted-automation
steps:
@ -44,7 +44,7 @@ jobs:
- name: Build skills index
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: python scripts/build_skills_index.py
run: python3 scripts/build_skills_index.py
- name: Upload index artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
@ -59,7 +59,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
environment: trusted-automation
steps:

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 }}
@ -180,7 +180,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 }}
@ -259,7 +259,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
@ -149,7 +149,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
@ -179,7 +179,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 }}