fix(svc-runner): declare the node-local swarm workload for the CI gates

The swarm convergence gate derived runner_runner-1 from the services
metadata and polled a stack service the role never creates: the runner
fleet deploys as node-local compose projects on every node because each
runner needs its own node's docker socket and identity. Declare
workload: node-local on the runner services entry and let the gate
context map it to HAS_SWARM_SERVICE=false, so the stack-service gates
skip themselves while the swarm deploy, the registry distribution chain
and the CLI e2e keep running. Roles without the attribute keep the
image/template heuristic; workload: stack forces the gates on. Document
the runner architecture for both deploy modes in the role README.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 4.8 committed 2026-07-15 13:46:22 +02:00
1 parent 236117f6f1
commit 76508c946a
3 files changed
+83 -6

No files matched your search

+65
View File
@@ -146,6 +146,71 @@ The role self-validates via the scripts in `files/test/`, discovered and run by
In short: `local.sh` proves the runner can build and deploy entirely on its own hardware; `external.sh` proves it can receive real jobs from GitHub.
## Architecture
Every runner instance is an ephemeral container that mounts its host's docker
socket (DooD). For each CI job it starts a fresh Infinito DinD container with
a unique subnet and volume, runs the job inside it, and tears it down; the
runner then re-registers with GitHub as a fresh ephemeral runner.
```mermaid
flowchart LR
gh[GitHub Actions] -->|job| r1[runner-1]
gh -->|job| rn[runner-N]
subgraph host [host node]
r1 -->|/var/run/docker.sock| dockerd[(host dockerd)]
rn -->|/var/run/docker.sock| dockerd
dockerd --> dind1[DinD container<br/>job 1]
dockerd --> dindn[DinD container<br/>job N]
end
```
### Compose mode
One host runs the whole fleet: the role builds the `infinito-runner` image
locally, renders one compose project and starts `RUNNER_COUNT` instances.
```mermaid
flowchart TB
subgraph host [single host]
build[build infinito-runner image]
fleet[compose up:<br/>runner-1 .. runner-N]
build --> fleet
end
```
### Swarm mode
The fleet is deployed on **every** cluster node, but not as a `docker stack`
service (`workload: node-local` in `meta/services.yml`): each runner needs
the docker socket and the registered identity of exactly its node. The image
is built once on the manager and distributed through the registry dependency
(`svc-registry-docker`), the only part that runs as a swarm service.
```mermaid
flowchart TB
subgraph cluster [swarm cluster]
subgraph mgr [manager]
build[build infinito-runner image]
reg[(registry<br/>swarm service)]
mfleet[node-local compose:<br/>runner-1 .. runner-N]
end
subgraph wrk [each worker]
wfleet[node-local compose:<br/>runner-1 .. runner-N]
end
end
build -->|push| reg
reg -->|pull| mfleet
reg -->|pull| wfleet
```
Because no stack service exists, the swarm test gates that poll
`docker service ls` (`converge`, `seed`, `drain`, `assert`) skip themselves
via the `workload: node-local` declaration
(`scripts/tests/deploy/swarm/utils/_context.sh` maps `node-local` to
`HAS_SWARM_SERVICE=false`); the deploy itself, the registry distribution
chain and the CLI e2e suite run in full.
## Further Resources
- [GitHub Actions self-hosted runner documentation](https://docs.github.com/en/actions/hosting-your-own-runners)
+1
View File
@@ -2,6 +2,7 @@
# nocheck: dockerfile-custom
runner:
lifecycle: beta
workload: node-local
proxy:
service_key: runner-1
github_owner: infinito-nexus
+17 -6
View File
@@ -46,17 +46,28 @@ fi
HAS_SWARM_SERVICE="$(PYTHONPATH="${_REPO_ROOT}" "${PYTHON}" -c "
import os, yaml
svc = os.path.join('${ROLE_DIR}', 'meta', 'services.yml')
data = {}
if os.path.exists(svc):
data = yaml.safe_load(open(svc)) or {}
if not isinstance(data, dict):
data = {}
entity = data.get('${ENTITY}')
workload = entity.get('workload') if isinstance(entity, dict) else None
if isinstance(workload, str):
workload = workload.strip().lower()
if workload == 'node-local':
print('false')
raise SystemExit
if workload == 'stack':
print('true')
raise SystemExit
try:
from utils.roles.meta_lookup import get_role_skip
swarm_skipped = 'swarm' in get_role_skip('${APP_ID}')
except Exception:
swarm_skipped = False
has_image = False
svc = os.path.join('${ROLE_DIR}', 'meta', 'services.yml')
if os.path.exists(svc):
data = yaml.safe_load(open(svc)) or {}
if isinstance(data, dict):
has_image = any(isinstance(v, dict) and 'image' in v for v in data.values())
has_image = any(isinstance(v, dict) and 'image' in v for v in data.values())
has_tpl = any(os.path.exists(os.path.join('${ROLE_DIR}', 'templates', t)) for t in ('compose.yml.j2', 'service.yml.j2'))
print('false' if (swarm_skipped or not (has_image or has_tpl)) else 'true')
")"