Architecture¶
Component and sequence view of the warm pool. See also the ASCII summary in the
root README.md and the operational failure-mode table in docs/DR-OPS.md.
Component diagram¶
flowchart TD
subgraph vault["KeePass vault (master copy)"]
PEM["4x App private key .pem\n(one per PC)"]
end
subgraph pc["Each of 4 home PCs"]
DPAPI["Windows DPAPI blob\n(config.KeyRef, CurrentUser-bound)"]
CFG["config.psd1\n(per-PC, gitignored)"]
MOD["watchdog/GitHubApp.psm1\nkey -> JWT -> install token -> reg token"]
WLIN["Watchdog-Linux.ps1\nkeep-one-alive loop"]
WWIN["Watchdog-Windows.ps1\nkeep-one-alive loop"]
DOCKER["Docker Desktop\nww-runner:linux container(s)"]
HOSTRUNNER["actions/runner\non Windows host"]
end
subgraph gh["GitHub"]
APP["GitHub App\nrepo mode: Administration RW\norg mode: Self-hosted runners RW (org perm)"]
SCOPE["Actions runner pool\nrepo mode: repos/{owner}/{repo}\norg mode: orgs/{org} (+ home-ci group/label)"]
QUEUE["Job queue / scheduler"]
end
PEM -- "Provision-Key.ps1 (one-time)" --> DPAPI
DPAPI --> MOD
CFG -- "Org / MaxRunners / Stale* keys" --> WLIN
CFG --> WWIN
MOD --> WLIN
MOD --> WWIN
WLIN -- "docker run -e RUNNER_TOKEN=..." --> DOCKER
WWIN -- "config.cmd --token ... ; run.cmd" --> HOSTRUNNER
MOD -- "JWT (RS256)" --> APP
APP -- "installation token" --> MOD
MOD -- "registration-token request\n(backoff + jitter on retry)" --> SCOPE
WLIN -- "stale-runner sweep\n(DELETE offline ww-<pc>-* past threshold)" --> SCOPE
DOCKER -- "registers ephemeral runner" --> SCOPE
HOSTRUNNER -- "registers ephemeral runner" --> SCOPE
QUEUE -- "routes queued job" --> SCOPE
Four PCs run this same set of components independently. There is no
coordination service between them - GitHub's own scheduler is the only shared
state (see docs/DR-OPS.md "Model").
Repo mode vs org mode is a per-PC config switch (Org in config.psd1,
see docs/ORG-MIGRATION.md): the auth flow is identical, only the runner API
scope changes (repos/{owner}/{repo} vs orgs/{org}), the runner registers
against https://github.com/<org> instead of the repo URL, and the shared
home-ci label is appended so every fleet repo can target
runs-on: [self-hosted, home-ci, <os>]. The Linux watchdog keeps up to
MaxRunners (default 1, hard cap 4) containers warm and runs the per-PC
stale-runner sweep; the Windows host runner is always exactly one.
Sequence: one reconcile pass launching a fresh runner¶
sequenceDiagram
participant WD as Watchdog (Linux or Windows loop)
participant MOD as GitHubApp.psm1
participant KEY as DPAPI / CredMan (host-local)
participant GH as GitHub API
participant RT as Runner (container or host process)
WD->>WD: reconcile pass - is a runner already alive?
alt runner alive
WD->>WD: steady state, do nothing
else no runner
WD->>MOD: Get-WWRunnerRegistration(AppId, InstallationId, Repo, KeyRef, KeySource)
MOD->>KEY: Get-WWAppPrivateKey (unprotect / cred read)
KEY-->>MOD: RSA private key (in-memory only)
MOD->>MOD: New-WWAppJwt (RS256, ~9 min)
MOD->>GH: POST /app/installations/{id}/access_tokens (Bearer JWT)
GH-->>MOD: installation token (~1h)
alt repo mode (config.Repo)
MOD->>GH: POST /repos/{owner}/{repo}/actions/runners/registration-token (token install)
else org mode (config.Org)
MOD->>GH: POST /orgs/{org}/actions/runners/registration-token (token install)
end
Note over MOD,GH: every call retried with exponential backoff + jitter on transient failure
GH-->>MOD: registration token (~1h)
MOD-->>WD: { RegistrationToken, InstallationToken }
Note over MOD: RSA key disposed immediately after use
WD->>RT: launch with ONLY the registration token (docker run -e / config.cmd --token)
RT->>GH: config.sh/config.cmd --ephemeral --token ...
GH-->>RT: runner registered, picks up next matching job
RT->>RT: serve exactly one job, then unregister + exit
WD->>WD: next poll reaps the exited runner, reconciles again
end