From a Runner Pool to a Registered GitHub Runner
This page follows one runner from its Kubernetes configuration to the point where GitHub can see it. It stops before GitHub assigns a job. That is covered on How a GitHub Job Reaches a Runner.
The starting configuration
An ImpVMRunnerPool says which VM template to use, which CI platform to connect to, and how much runner capacity to keep. For GitHub Actions, the important inputs are:
templateName: the base VM image and class;platform.scope: one organisation or one repository;platform.runnerGroup: an organisation runner group, when one is required;platform.credentialsSecret: the Kubernetes Secret holding the GitHub credential material;platform.tokenSource: github_app: use a GitHub App rather than a long-lived personal token;runnerLayer: the OCI layer that supplies the runner program;scaling: the lower and upper capacity limits.
The API validates the required scaling fields for GitHub pools. When platform.scope is present, its schema allows exactly one of organisation scope or repository scope. The controller also rejects a missing scope.
flowchart TD
pool[ImpVMRunnerPool] --> template[ImpVMTemplate]
pool --> credentials[Operator-owned GitHub App Secret]
pool --> scaling[Scaling policy]
template --> vm[New ImpVM]
credentials --> jwt[Short-lived App JWT]
jwt --> install[Installation token]
install --> jit[One-time GitHub JIT configuration]
jit --> secret[Secret owned by this ImpVM]
secret --> vmPlain explanation: the pool declares the kind of runner. The controller creates one VM record. It then asks GitHub for a one-time setup package and stores that package in a Secret owned by that VM.
What happens, in order
1. Kubernetes stores the pool
The operator reads the runner-pool object and its referenced template. It lists the VMs already owned by that pool. Terminal runner VMs are deleted because each runner is single-use.
The controller chooses a desired VM count from minIdle, queue depth when polling is enabled, or an externally written imp.dev/runner-demand annotation. It then applies maxConcurrent and scaleUpStep before creating any new VM records.
2. The controller creates one ImpVM
For every VM it needs, the controller creates an ImpVM from the template. The new VM is marked as ephemeral and labelled as a member of the pool. If the pool provides a runner layer, that layer is copied to the VM specification.
The VM is created before its setup Secret. That order gives the Secret a concrete Kubernetes owner. If the VM is deleted, Kubernetes can clean up the owned Secret too.
3. The operator proves its identity to GitHub
For a GitHub App pool, the controller reads three fields from the configured Kubernetes Secret: the App private key, App ID, and installation ID. It signs a short-lived JSON Web Token (JWT), then exchanges it with GitHub for an installation token.
The private key is used by the operator. It is not copied into the ImpVM or the runner setup package.
4. GitHub returns a one-time runner setup
Using the installation token, the controller asks GitHub to create a just-in-time, or JIT, runner configuration. GitHub returns an encoded setup value and a generated runner name.
If a runner group is configured, the controller resolves that organisation group to its numeric ID first. Repository-scoped pools cannot use an organisation runner group.
The current source asks GitHub to apply the self-hosted label to the JIT runner. The ImpVMRunnerPool.spec.labels field is not passed to that JIT request in the current implementation. Do not rely on that field to route a GitHub Actions job until the implementation changes.
5. The JIT configuration is tied to one VM
The controller writes the encoded JIT configuration to a Kubernetes Secret and makes the ImpVM its owner. It records the Secret name in ImpVM.spec.runnerConfigSecret.
At this point the runner is prepared, but it is not registered with GitHub yet. The next page starts when Imp selects a node and the local agent boots the VM.
Failure boundaries
| Boundary | What the current controller does |
|---|---|
| Template is missing | Requeues after 30 seconds. |
| GitHub token or JIT mint fails | Reconcile returns an error; no JIT Secret is attached. |
| GitHub rejects or removes the App installation | The token mint reports the installation as unavailable. |
| JIT Secret cannot be written | The VM exists, but it cannot receive the runner setup. |
| A JIT configuration is generated | This proves only that GitHub issued setup data. It does not prove registration or job execution. |
Source map
- Pool fields and validation:
api/v1alpha1/impvmrunnerpool_types.go:5-139 - Pool count, VM creation, and JIT Secret ownership:
internal/controller/impvmrunnerpool_controller.go:74-184,284-391 - GitHub App JWT and installation-token exchange:
internal/runner/githubapp.go:25-189 - GitHub JIT configuration and runner-group lookup:
internal/runner/github.go:121-173
