novitalabs/github-actions-runner-controller
Go
Captured source
source ↗novitalabs/github-actions-runner-controller
Description: Run GitHub Actions jobs in isolated Novita Sandboxes
Language: Go
License: MIT
Stars: 0
Forks: 0
Open issues: 0
Created: 2026-10-28T15:17:30Z
Pushed: 2026-10-29T08:44:53Z
Default branch: main
Fork: no
Archived: no
README:
This controller uses GitHub's official `github.com/actions/scaleset` client to receive assigned jobs and desired runner capacity. A long-running controller creates one fresh Novita runner Sandbox for each job and kills it when the job finishes.
> v0.1 uses the Scale Set API, which GitHub currently labels Public Preview. > The client API is stable, but its Go interfaces may evolve. This repository > pins github.com/actions/scaleset v0.5.0.
Architecture
flowchart LR G["GitHub Actions assigned job and JIT config"] --> C["Controller Sandbox github-actions-controller"] C --> T["Runner Template github-actions-runner"] T --> R["Ephemeral Runner Sandbox one per job"] R --> K["Job complete kill Sandbox"]
The same controller implementation supports two control-plane placements. Novita Hosted is the recommended default because the controller remains online without depending on a user's machine:
- Novita Hosted: run that controller image in a long-running Control
Sandbox, which creates separate ephemeral runner Sandboxes.
- Local Docker: run the same controller image on a user's machine with
credentials supplied through environment variables.
The runner JIT configuration travels over the adapter's stdin and the Sandbox filesystem API. The adapter atomically publishes the completed file; the runner entrypoint reads and unlinks it, then exports it only to the runner child process as ACTIONS_RUNNER_INPUT_JITCONFIG. It is never included in Sandbox creation environment values, metadata, process arguments, or controller logs.
Recommended: Novita Hosted
Hosted mode uses these public Novita Templates directly; users do not build either Template:
| Role | Public Template | |---|---| | Control Sandbox | github-actions-controller | | Ephemeral CI runner | github-actions-runner |
Before deployment
Prepare the following on the machine where you run the deployment script:
- Bash and Python 3.
- Node.js 20+ and npm if the
novitaCLI is not already installed. - The latest
novitaCLI (version 2.1.0 or newer). TheNOVITA_API_KEYbelow selects the Novita team
that will own the Control Sandbox and brokered Secrets; a separate browser login is not required.
- A GitHub PAT that can manage self-hosted runners. For a classic PAT, use
repo for a repository target or admin:org for an organization target. For a fine-grained PAT, grant repository Administration: read and write or organization Administration: read plus Self-hosted runners: read and write, respectively.
- The exact repository or organization URL to register, such as
https://github.com/novitalabs/github-actions-runner-controller.
Install and verify the CLI if needed:
npm install --global novita-sandbox-cli@latest novita --version
Export the three required values in the current shell. Do not put them in the repository:
export GITHUB_CONFIG_URL=https://github.com/OWNER/REPOSITORY export GITHUB_TOKEN=YOUR_GITHUB_PAT export NOVITA_API_KEY=YOUR_NOVITA_API_KEY
Verify that the CLI sees the intended team before deployment:
novita auth info
GITHUB_CONFIG_URL is mandatory. The script rejects the example OWNER/REPO and OWNER/REPOSITORY placeholders so it cannot silently deploy against an unintended target.
Deploy
Clone this repository and run the Hosted deployment:
git clone https://github.com/novitalabs/github-actions-runner-controller.git cd github-actions-runner-controller bash scripts/deploy-hosted.sh
After cd, run the deployment script from the cloned github-actions-runner-controller/ repository root. The script runs on your machine and creates the long-running Controller Sandbox in Novita.
Success returns the stable target identity, Control Sandbox ID, and health state:
{"targetId":"novitalabs-github-action-a814d8991e","controllerId":"novita-hosted-novitalabs-github-action-a814d8991e","sandboxId":"...","healthy":true}The default Scale Set label is novita-scale-set. In the target repository, create or update .github/workflows/ci.yml (or another workflow file under .github/workflows/) and point each job that should use Novita at this label:
# .github/workflows/ci.yml jobs: test: runs-on: novita-scale-set
The controller creates or reuses the named Scale Set. It intentionally does not delete it on shutdown, allowing safe controller restarts.
The deployment script creates or updates two brokered Secrets and starts a long-running Control Sandbox with a 60-year timeout and pause + auto-resume. It uses the public controller Template and creates job Sandboxes from the public runner Template. The Secrets must remain active for the full life of the resumable Control Sandbox. Kill that Sandbox before deleting them. After Sandbox creation, the script starts the controller through the Sandbox command API and waits for /healthz; this is required because a process saved in a template build snapshot does not inherit creation-time environment values.
Multiple repositories or organizations
Run the deployment once for each GitHub repository or organization. By default, the script derives a stable target ID from the normalized GITHUB_CONFIG_URL. Each target receives its own controller identity, brokered Secrets, and long-running Control Sandbox, so deploying another target cannot reuse the first target's controller or overwrite its GitHub credential.
GITHUB_CONFIG_URL=https://github.com/novitalabs/github-actions-runner-controller \ bash scripts/deploy-hosted.sh
To add another project, replace GITHUB_CONFIG_URL with that repository or organization URL and run the same command again.
Save the JSON output for operations. The resume script accepts either the exact Sandbox ID or the same GitHub target URL. Target-based lookup checks both running and paused Sandboxes and fails on duplicates instead of choosing one arbitrarily. Control Sandboxes created before target metadata was introduced must still be resumed by their Sandbox ID.
When resuming manually, always preserve the intended lifetime:
scripts/resume-hosted.sh SANDBOX_ID...
Excerpt shown — open the source for the full document.