- Rust 86.1%
- Nix 13.9%
| docs | ||
| examples/forgejo | ||
| proto/runner/v1 | ||
| src | ||
| tests | ||
| .gitignore | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| flake.lock | ||
| flake.nix | ||
| README.md | ||
| unified-ci-compute-platform.md | ||
unified-ci
unified-ci is a Forgejo-first CI runner service with a source-neutral
scheduler and execution path. The current implementation can register as a
Forgejo runner, poll for jobs, execute supported workflows through act, stream
logs, report final status, and record usage.
The project is still a scaffold/MVP, not a production-hardened runner. It is useful today for basic Forgejo Actions workflows and for validating the Firecracker guest execution model.
What Works Today
- Forgejo runner registration or loading existing runner credentials.
- Forgejo label declaration and polling.
- Internal development job submission over HTTP.
- Source-neutral scheduling with global and per-template capacity.
- JSON state for jobs, results, templates, Forgejo account balances, and usage.
guest_processexecution for local validation without KVM.- Firecracker execution with one VM per job on Linux hosts with KVM.
- Packaged NixOS Firecracker guest images containing
act,git, shell tools,nixwith flakes enabled, andunified-ci-guest-agent. - A Docker-enabled Firecracker guest variant for workflows that need
actto talk to Docker inside the VM. - Shell
run:steps throughact. - Offline-mapped composite
uses:actions via--act-local-repository. - Optional Firecracker
egress_onlynetworking for public internet egress with host/private/job-to-job isolation.
Not Supported Yet
- Full
forgejo-runnercompatibility. - Full Docker action parity.
- Service containers.
- Caches and artifacts.
- Arbitrary marketplace or network-fetched actions inside the VM.
- Production-grade state storage and admin tooling.
- Nostr/Cashu ingress and settlement.
See examples/forgejo for workflows matching the currently tested subset.
Quick Checks
Run the normal local checks:
cargo fmt -- --check
cargo test --offline
nix flake check
Validate the live Forgejo protocol path without Firecracker:
nix run .#forgejo-live-smoke
Validate Firecracker execution on a Linux host with readable/writable
/dev/kvm:
nix run .#firecracker-smoke
Validate Forgejo plus Firecracker together:
nix run .#forgejo-firecracker-smoke
Validate Firecracker public egress with host/private/job isolation. This needs
root or equivalent CAP_NET_ADMIN privileges in addition to KVM:
sudo nix run .#firecracker-egress-smoke
Smoke apps exit with code 77 and a SKIP message when the host does not have
the required KVM or network privileges.
NixOS Module
The flake exposes a NixOS module as nixosModules.default. It creates the
unified-ci user/group, writes /etc/unified-ci/config.toml, and installs a
systemd service. By default it runs unified-ci --check-config before service
start.
Guest-Process Setup
This is the easiest way to verify registration, polling, scheduling, log streaming, and status reporting before enabling Firecracker.
{
inputs.unified-ci.url = "path:/path/to/unified_ci";
outputs = { self, nixpkgs, unified-ci, ... }: {
nixosConfigurations.runner = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
unified-ci.nixosModules.default
({ pkgs, ... }: {
services.unified-ci = {
enable = true;
executorBackend = "guest_process";
executorPackages = [
pkgs.act
pkgs.git
];
commandExecutor = {
command = "${unified-ci.packages.x86_64-linux.default}/bin/unified-ci-guest-agent";
args = [
"--act-command" "${pkgs.act}/bin/act"
"--act"
];
workingDir = "/var/lib/unified-ci/work";
};
forgejo = {
enable = true;
instanceUrl = "https://forgejo.example.com";
runnerName = "unified-ci";
runnerLabels = [ "ubuntu-latest" ];
registrationTokenPath = "/run/secrets/forgejo-runner-registration-token";
accountId = "forgejo-default";
};
billing.forgejoAccounts.forgejo-default.balance_seconds = 3600;
};
})
];
};
};
}
After the service starts, credentials are written to
/var/lib/unified-ci/forgejo-runner.json. Future starts can use that credentials
file without the registration token.
Firecracker Setup
Build one of the packaged guest images:
nix build .#firecracker-guest-minimal
nix build .#firecracker-guest-docker
nix build .#firecracker-guest
firecracker-guest currently aliases firecracker-guest-docker. Use the
minimal image for shell/composite workflows with a smaller rootfs. Both guest
variants include nix, nix-build, nix-shell, and flakes support. Use the
Docker image when workflows need Docker actions or Docker CLI access inside the
VM. Then configure the Firecracker executor with one or more templates. The
scheduler selects a template by matching job labels.
{ pkgs, unified-ci, ... }:
{
services.unified-ci = {
enable = true;
executorBackend = "firecracker";
commandExecutor = {
command = "${unified-ci.packages.x86_64-linux.default}/bin/unified-ci-firecracker-launcher";
args = [
"--firecracker" "${pkgs.firecracker}/bin/firecracker"
];
runtimeDir = "/var/lib/unified-ci/firecracker";
# Required only for templates using network = "egress_only".
networkHelper = "${unified-ci.packages.x86_64-linux.default}/bin/unified-ci-firecracker-netns-helper";
# Optional. If unset, the helper uses the host default route interface.
# networkEgressInterface = "eth0";
};
vmTemplates.minimal = {
name = "nixos-act-minimal";
labels = [ "ubuntu-latest" "nix" "x86_64-linux" ];
default = true;
kernel = "/nix/store/...-unified-ci-firecracker-guest-minimal/vmlinux";
initrd = "/nix/store/...-unified-ci-firecracker-guest-minimal/initrd";
rootfs = "/nix/store/...-unified-ci-firecracker-guest-minimal/rootfs.ext4";
vcpu_count = 2;
memory_mib = 4096;
max_concurrent_jobs = 1;
network = "egress_only";
};
vmTemplates.docker = {
name = "nixos-act-docker";
labels = [ "ubuntu-latest" "nix" "x86_64-linux" "docker" ];
kernel = "/nix/store/...-unified-ci-firecracker-guest-docker/vmlinux";
initrd = "/nix/store/...-unified-ci-firecracker-guest-docker/initrd";
rootfs = "/nix/store/...-unified-ci-firecracker-guest-docker/rootfs.ext4";
vcpu_count = 2;
memory_mib = 6144;
max_concurrent_jobs = 1;
network = "egress_only";
};
forgejo = {
enable = true;
instanceUrl = "https://forgejo.example.com";
runnerName = "unified-ci-firecracker";
runnerLabels = [ "ubuntu-latest" "nix" "x86_64-linux" "docker" ];
registrationTokenPath = "/run/secrets/forgejo-runner-registration-token";
accountId = "forgejo-default";
};
billing.forgejoAccounts.forgejo-default.balance_seconds = 3600;
};
}
The main unified-ci service runs as the unified-ci user. Firecracker itself
needs usable /dev/kvm. The egress_only network helper needs root or
equivalent CAP_NET_ADMIN rights to create network namespaces, TAP/veth
devices, nftables rules, and NAT.
Multiple VM templates can be configured at the same time; use distinct labels
and per-template capacity to control which jobs land on the minimal or
Docker-enabled image. forgejo.runnerLabels is what the runner advertises to
Forgejo; vmTemplates.<name>.labels is what unified-ci uses to choose a VM
template after a task is fetched. A Forgejo workflow with runs-on: docker
therefore requires docker in both forgejo.runnerLabels and the Docker
template's label list.
Operational Commands
Validate a rendered config without contacting Forgejo or booting a VM:
unified-ci --config /etc/unified-ci/config.toml --check-config
Run one Forgejo poll cycle and exit:
unified-ci --config /etc/unified-ci/config.toml --forgejo-once
Start the daemon:
unified-ci --config /etc/unified-ci/config.toml --serve
Status endpoints:
GET /healthzGET /statusGET /jobs/<job-id>GET /usage