diff --git a/.env.example b/.env.example index 99d6cc1..c3d0116 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,14 @@ APP_LOGIN= RUNNER_NAME_PREFIX=runner LABELS=docker RUNNER_GROUP=Default +# One runner can join an extra pool, e.g. a host whose runners are `light` with +# one that also builds: +# RUNNER_3_LABELS=docker,x86,light,large + +# Coordination runners (two, labelled `control`), for the short jobs a merge +# waits on. Off unless the profile is set: +# COMPOSE_PROFILES=control +# CONTROL_LABELS=control # Small hosts without room for per-runner caches (e.g. ARM64 boards): # COMPOSE_FILE=docker-compose.yml:hosts/no-caches.compose.yml diff --git a/README.md b/README.md index 2918c53..562c42d 100644 --- a/README.md +++ b/README.md @@ -45,6 +45,17 @@ general jobs and `heavy` for memory-hungry ones. Workflows then ask for its name. A host can carry several labels, so a large machine labelled `build,heavy` also picks up general jobs when the others are busy. +**One runner in another pool.** `RUNNER__LABELS` (for `runner`, `runner-2`, +`runner-3`) replaces `LABELS` for that runner alone. A host whose runners are +`light` can let one of them also build, with +`RUNNER_3_LABELS=docker,x86,light,large`, without a second host file. + +**Coordination runners.** `COMPOSE_PROFILES=control` adds two runners that +carry only `CONTROL_LABELS` (default `control`). Point the short jobs a merge +waits on at them, such as a picker or a required summary job, so those never +queue behind long builds. They hold no `docker.sock` and no tool cache, so keep +jobs that check out and build code off them. + **Small hosts.** Boards such as a Raspberry Pi have little disk. Layer [`hosts/no-caches.compose.yml`](hosts/no-caches.compose.yml) by setting `COMPOSE_FILE` in `.env`. Those runners skip the per-runner cache volumes. diff --git a/docker-compose.yml b/docker-compose.yml index 12a4542..792cb11 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,10 +4,10 @@ # read-only in every runner, filled by the pruner. `volumes:` lists replace on # merge, so docker.sock is repeated per service. Small hosts can layer # hosts/no-caches.compose.yml on top (see README). -x-runner: &runner - image: ${RUNNER_IMAGE:-ghcr.io/dodi-smart/github-runner} - restart: unless-stopped - environment: +# The environment is its own anchor so a service can change one key (LABELS) +# without restating the rest: a `<<:` merge replaces a whole mapping, so +# `environment:` in a service would otherwise drop every other variable. +x-runner-env: &runner-env APP_ID: ${APP_ID} APP_PRIVATE_KEY: ${APP_PRIVATE_KEY} APP_LOGIN: ${APP_LOGIN} @@ -36,6 +36,11 @@ x-runner: &runner # downloads, and only reads: the `pruner` service fills the shared # runner-action-cache volume (see pruner.sh), mounted read-only below. ACTIONS_RUNNER_ACTION_ARCHIVE_CACHE: /opt/actionarchivecache + +x-runner: &runner + image: ${RUNNER_IMAGE:-ghcr.io/dodi-smart/github-runner} + restart: unless-stopped + environment: *runner-env # Runner.Listener is the long-lived process of a registered runner. Exec form # on purpose: under CMD-SHELL the wrapping `sh -c` carries the pattern in its # own command line and `pgrep -f` would match itself, healthy forever. The @@ -52,9 +57,29 @@ x-runner: &runner security_opt: - label:disable +# Coordination runners carry only CONTROL_LABELS (default `control`), never a +# work label, so no build or test can land on them: they exist so the short +# jobs a merge waits on (a picker, a required summary) never queue behind long +# work. Those jobs read the API and check nothing out, so each service mounts +# no docker.sock and no tool cache, only its work directory and the read-only +# action cache. +x-control: &control + <<: *runner + profiles: [control] + environment: + <<: *runner-env + LABELS: ${CONTROL_LABELS:-control} + RUNNER_NAME_PREFIX: ${RUNNER_NAME_PREFIX:-runner}-control + services: + # Each runner registers with the host's LABELS, or with RUNNER__LABELS when + # that is set: one runner of a host can then join another pool (a `large` + # runner on a host whose others stay `light`) without a second host file. runner: <<: *runner + environment: + <<: *runner-env + LABELS: ${RUNNER_1_LABELS:-${LABELS:-docker}} volumes: - /var/run/docker.sock:/var/run/docker.sock - runner-work:/tmp/runner/work @@ -71,6 +96,9 @@ services: - runner-action-cache:/opt/actionarchivecache:ro runner-2: <<: *runner + environment: + <<: *runner-env + LABELS: ${RUNNER_2_LABELS:-${LABELS:-docker}} volumes: - /var/run/docker.sock:/var/run/docker.sock - runner-work-2:/tmp/runner/work @@ -87,6 +115,9 @@ services: - runner-action-cache:/opt/actionarchivecache:ro runner-3: <<: *runner + environment: + <<: *runner-env + LABELS: ${RUNNER_3_LABELS:-${LABELS:-docker}} volumes: - /var/run/docker.sock:/var/run/docker.sock - runner-work-3:/tmp/runner/work @@ -101,6 +132,17 @@ services: - runner-mise-cache-3:/root/.cache/mise - runner-hk-3:/root/.cache/hk - runner-action-cache:/opt/actionarchivecache:ro + # Coordination runners (see x-control), off unless COMPOSE_PROFILES=control. + control: + <<: *control + volumes: + - runner-work-control:/tmp/runner/work + - runner-action-cache:/opt/actionarchivecache:ro + control-2: + <<: *control + volumes: + - runner-work-control-2:/tmp/runner/work + - runner-action-cache:/opt/actionarchivecache:ro # Maintenance off the job path (pruner.sh): nightly cache caps and docker # prune, hourly action-cache fill. Mounts only what it caps. pruner: @@ -144,6 +186,8 @@ volumes: runner-work: {} runner-work-2: {} runner-work-3: {} + runner-work-control: {} + runner-work-control-2: {} runner-bun: {} runner-bun-2: {} runner-bun-3: {}