7.8 KiB
forgejo-nsc-dispatcher
This service exposes a simple HTTP API that tells Namespace Cloud to start ephemeral Forgejo Actions runners on demand. It glues together three pieces:
- Forgejo Actions – the service requests a scoped registration token for the repository/organization/instance where you want to run jobs.
- Namespace (
nsc) – the dispatcher shells out to thenscCLI to create a short‑lived environment, runs theforgejo-runnercontainer inside it, and exits after a single job (forgejo-runner one-job). The Namespace TTL is the hard cap, not the typical lifetime. - Your automation – you call the service via HTTP (directly, through Caddy, via Forgejo webhooks, etc.) whenever a new runner is needed.
Directory layout
.
├── cmd/forgejo-nsc-dispatcher # main entry point
├── internal/ # service packages (config, forgejo client, nsc dispatcher, HTTP server)
├── config.example.yaml # starter config referenced by README
├── flake.nix / flake.lock # reproducible builds (Go binary + container image)
└── .forgejo/workflows # CI that runs go test/build and publishes manifests
Configuration
Copy config.example.yaml and update it for your Forgejo instance and Namespace
profile. The important knobs are:
forgejo.base_url– HTTPS endpoint of your Forgejo server. A PAT withactions:runnerscope is required inforgejo.token.forgejo.instance_url– URL that spawned runners use to register back to Forgejo. This must be reachable from the runner (typically the public URL likehttps://git.burrow.net). On the forge host it commonly differs frombase_url(which may behttp://127.0.0.1:3000).forgejo.default_scope– where new runners register (instance,organization, orrepository).forgejo.default_labels– labels applied to every spawned runner. GateForge workflows viaruns-on: ["namespace-profile-linux-medium"](or othernamespace-profile-linux-*labels).namespace.nsc_binary– path to thenscbinary (the Nix container ships one compiled fromnamespacelabs/foundationso/app/bin/nscworks out of the box).namespace.image– OCI image containingforgejo-runner.namespace.machine_type/namespace.duration– shape + TTL for the ephemeral Namespace environment. The dispatcher destroys the instance after a job so the TTL acts as a hard cap, not an idle timeout.namespace.linux_cache_*/namespace.macos_cache_*– persistent cache volumes mounted into runners so Linux can keep/nixplus build caches warm and macOS can reuse Rust toolchains, Xcode package caches, and derived data.
Running locally
# Ensure nsc is available (e.g. `go build ./foundation/cmd/nsc`)
cp config.example.yaml config.yaml
nix develop # optional dev shell with Go toolchain
go run ./cmd/forgejo-nsc-dispatcher --config config.yaml
API example:
curl -X POST http://localhost:8080/api/v1/dispatch \
-H 'Content-Type: application/json' \
-d '{
"count": 1,
"ttl": "20m",
"labels": ["namespace-profile-linux-medium"],
"scope": {"level": "repository", "owner": "example", "name": "app"}
}'
Deploying with Nix + GHCR
nix build .#packages.x86_64-linux.container-amd64produces a deterministic tarball containing the service, thenscbinary, BusyBox, andforgejo-runner.- The included
Build Containerworkflow builds bothamd64andarm64images on Namespace runners and pushes them toghcr.io/<owner>/<repo>. No Fly.io manifests are emitted – the multi‑arch manifest points only at GHCR.
How this fits behind Caddy (last-mile networking)
The dispatcher is just an HTTP server. You can:
-
Run it anywhere that can reach Forgejo and Namespace: bare metal, Namespace cluster, Kubernetes, Fly, etc.
-
Put Caddy (or any reverse proxy) in front to terminate TLS, do auth, or rewrite URLs. For example:
forgejo-dispatcher.example.com { reverse_proxy 127.0.0.1:8080 basicauth /api/* { user JDJhJDE... } }
The service doesn’t assume Caddy, nor does it manipulate HTTP clients
directly – it simply waits for POST requests. As long as the dispatcher can
reach Forgejo’s REST API and run the nsc binary, you can drop it anywhere.
Autoscaling (webhook + poller)
If you don’t want to call /api/v1/dispatch manually, there’s a companion
autoscaler (cmd/forgejo-nsc-autoscaler) that watches Forgejo job queues and
triggers the dispatcher for you. It operates in two modes simultaneously:
- Polling – every instance polls
GET /api/v1/.../actions/runnersto keep a minimum number of idle Namespace runners per label. This continues until a webhook is successfully processed, so the system is self-bootstrapping. - Webhooks – once Forgejo reaches the autoscaler via the
/webhook/{name}endpoint, the autoscaler stops polling and reacts toworkflow_jobevents in real time. Each payload is mapped to a target label set and results in a dispatch call.
You can manage multiple Forgejo instances by listing them under instances in
autoscaler.example.yaml:
listen: ":8090"
dispatcher:
url: "http://dispatcher:8080"
instances:
- name: burrow
forgejo:
base_url: "https://git.burrow.net"
token: "PENDING-FORGEJO-PAT"
scope:
level: "repository"
owner: "hackclub"
name: "burrow"
disable_polling: true # webhook-only mode
poll_interval: "30s"
webhook_secret: "supersecret"
webhook:
url: "https://nsc-autoscaler.burrow.net/webhook/burrow"
content_type: "json"
events: ["workflow_job"]
active: true
targets:
- labels: ["namespace-profile-linux-medium"]
min_idle: 0 # set to 0 to scale-to-zero between jobs
ttl: "20m"
- labels: ["namespace-profile-macos-large"]
min_idle: 0
ttl: "90m"
machine_type: "6x14"
- labels: ["namespace-profile-windows-large"]
min_idle: 0
ttl: "45m"
machine_type: "windows/amd64:8x16"
For Burrow, use Scripts/provision-forgejo-nsc.sh to mint the Forgejo PAT,
generate a Namespace token from the logged-in namespace account, and render
bootstrap artifacts into intake/forgejo_nsc_{dispatcher,autoscaler}.yaml plus
intake/forgejo_nsc_token.txt. The token file is emitted as JSON with a
bearer_token field so both the Compute API path and the nsc CLI fallback can
consume the same secret material.
Long-lived runtime state is now sourced from age-encrypted files:
secrets/forgejo/admin-password.agesecrets/forgejo/agent-ssh-key.agesecrets/forgejo/nsc-token.agesecrets/forgejo/nsc-dispatcher-config.agesecrets/forgejo/nsc-autoscaler-config.age
After refreshing the intake files, re-encrypt them into secrets/forgejo/*.age
and deploy the forge host so config.age.secrets.* updates the live paths for
services.burrow.forge, services.burrow.forgeRunner, and
services.burrow.forgejoNsc.
Run it next to the dispatcher:
go run ./cmd/forgejo-nsc-autoscaler --config autoscaler.yaml
# or build the binary/container via `nix build .#forgejo-nsc-autoscaler`
If your Forgejo build doesn’t expose the runner listing API, set
disable_polling: true and rely on webhook entries. The autoscaler will
auto-create/update the webhook (using the PAT) so that new workflow_job events
immediately call the dispatcher even if the service isn’t publicly reachable yet.
In Forgejo add a webhook pointing to https://nsc-autoscaler.burrow.net/webhook/burrow
with the shared secret (or let the autoscaler create it by specifying webhook.url
in config). The autoscaler continues polling until it receives the first valid
webhook (unless disabled), so you get capacity immediately even if outbound
webhooks from Forgejo aren’t yet configured.