From 755b5624d0c206b4cf6612c509ed53b063e885ea Mon Sep 17 00:00:00 2001 From: Leonardo Candiani Date: Wed, 23 Sep 2026 10:31:44 -0300 Subject: [PATCH] feat(runner): every job reads runs-on from CI_RUNS_ON, and the runner setup survives restarts ci.yml and pr-auto-merge.yml had ubuntu-latest hardcoded and ignored the config's runner. {{RUNNER}} now renders to a fromJSON(vars.CI_RUNS_ON ...) expression that falls back to ubuntu-latest, so a repo moves to a self-hosted pool with one gh variable set and back with one delete. setup-self-hosted-runner.sh kept the registration inside the container with a one-hour token, so the runner looped on 'already configured' after its first restart. Registration now lives in a host volume, with limits and a pinned image. --- CHANGELOG.md | 24 ++++++ commands/setup.md | 7 +- scripts/lib/placeholders.ts | 9 ++- .../setup-self-hosted-runner.sh.template | 76 +++++++++++++------ templates/workflows/ci.yml.template | 8 +- .../workflows/pr-auto-merge.yml.template | 2 +- 6 files changed, 94 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3cf6d6..ff21319 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ All notable changes to this project are documented here. Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versioning follows [SemVer](https://semver.org/). +## [Unreleased] + +### Changed + +- **Every workflow job now picks its runner from the repository variable + `CI_RUNS_ON`.** `{{RUNNER}}` renders to + `${{ fromJSON(vars.CI_RUNS_ON || '"ubuntu-latest"') }}`, and `ci.yml` and + `pr-auto-merge.yml`, which had `ubuntu-latest` hardcoded and ignored the + config's `runner`, use it too. Switching a repo to a self-hosted pool is + `gh variable set CI_RUNS_ON --body '["self-hosted","linux","vps"]'`, and + deleting the variable is the kill switch back to GitHub-hosted, no PR needed. + Deploy templates keep `ubuntu-latest` until each one is proven on the pool. + +### Fixed + +- **`setup-self-hosted-runner.sh` no longer produces a runner that dies on its + first restart.** It passed a one-hour registration token and kept the + registration inside the container, so any restart after the token expired + looped on "Cannot configure the runner because it is already configured". + The registration now lives in a host volume + (`CONFIGURED_ACTIONS_RUNNER_FILES_DIR`), automatic deregistration on stop is + off, the image is pinned, the container gets CPU, memory and PID limits, and + the token reaches the host over stdin instead of the command line. + ## [2.3.0] — 2026-08-31 ### Added diff --git a/commands/setup.md b/commands/setup.md index f633ecd..6863019 100644 --- a/commands/setup.md +++ b/commands/setup.md @@ -42,7 +42,12 @@ nowhere. that detection left genuinely ambiguous: - **Stack** — confirm detected or correct it - **Deploy** — vercel | supabase-functions | docker-ghcr | npm-publish | static-pages | none - - **Runner** — self-hosted (zero CI minutes) | github + - **Runner**: self-hosted (zero CI minutes) | github. Either way every + job reads `runs-on` from the repository variable `CI_RUNS_ON` and falls + back to `ubuntu-latest` when it is unset. For self-hosted, bring the runner + up with `scripts/setup-self-hosted-runner.sh` and finish with the + `gh variable set CI_RUNS_ON ...` line it prints; deleting the variable + sends the repo back to GitHub-hosted runners without a PR. - **Auth** — oauth (subscription token, no metered cost) | apikey (pay per use) - **Critical files / custom validators** — optional, multi-select diff --git a/scripts/lib/placeholders.ts b/scripts/lib/placeholders.ts index 0b152c0..19cb5f3 100644 --- a/scripts/lib/placeholders.ts +++ b/scripts/lib/placeholders.ts @@ -109,9 +109,12 @@ export function buildPlaceholderMap( INVARIANT_REFS: (config.layers ?? []).length ? `the invariants for ${(config.layers ?? []).join(", ")}` : "the invariants in this repo", - // GitHub Actions runner. self-hosted only when the config asks for it; - // otherwise the generic GitHub-hosted runner, so workflows run in any repo. - RUNNER: config.runner === "self-hosted" ? "[self-hosted, linux, x64]" : "ubuntu-latest", + // GitHub Actions runner, chosen per repo at run time by the repository + // variable CI_RUNS_ON (a JSON label array such as ["self-hosted","linux"]). + // Unset, every job falls back to the GitHub-hosted runner, so deleting the + // variable is the kill switch when the self-hosted pool is down. The config's + // `runner` only decides whether setup prints the command that sets it. + RUNNER: `\${{ fromJSON(vars.CI_RUNS_ON || '"ubuntu-latest"') }}`, // Issue triage. `github-models` runs the classifier free in Actions over the // GITHUB_TOKEN; `off` makes the triage workflow a no-op via its top-level if. ISSUES_TRIAGE: config.issues?.triage ?? "github-models", diff --git a/templates/scripts/setup-self-hosted-runner.sh.template b/templates/scripts/setup-self-hosted-runner.sh.template index 23abbb4..d87e7f8 100644 --- a/templates/scripts/setup-self-hosted-runner.sh.template +++ b/templates/scripts/setup-self-hosted-runner.sh.template @@ -1,54 +1,84 @@ #!/usr/bin/env bash -# setup-self-hosted-runner.sh — brings up a containerized self-hosted runner +# setup-self-hosted-runner.sh: brings up a containerized self-hosted runner # to zero out GitHub Actions minute usage (a self-hosted runner doesn't count # against the minute limit, for any repo visibility). # # SECURITY: use ONLY on a PRIVATE repo with trusted collaborators. On a public -# repo, a fork PR runs arbitrary code on your host. Don't break this. +# repo, a fork PR runs arbitrary code on your host. Don't break this. The +# container never gets the host's docker.sock. # # Pre: docker on the target host; gh authenticated with admin on the repo. # -# usage: bash setup-self-hosted-runner.sh