No description
  • Rust 86.1%
  • Nix 13.9%
Find a file
2026-06-23 23:27:51 +01:00
docs fix: ci 2026-06-23 23:27:51 +01:00
examples/forgejo fix: ci 2026-06-23 23:27:51 +01:00
proto/runner/v1 plan 2026-06-23 14:13:38 +01:00
src fix: ci 2026-06-23 23:27:51 +01:00
tests plan 2026-06-23 14:13:38 +01:00
.gitignore plan 2026-06-23 14:13:38 +01:00
build.rs plan 2026-06-23 14:13:38 +01:00
Cargo.lock plan 2026-06-23 14:13:38 +01:00
Cargo.toml plan 2026-06-23 14:13:38 +01:00
flake.lock plan 2026-06-23 14:13:38 +01:00
flake.nix fix: ci 2026-06-23 23:27:51 +01:00
README.md fix: ci 2026-06-23 23:27:51 +01:00
unified-ci-compute-platform.md plan 2026-06-23 14:13:38 +01:00

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_process execution 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, nix with flakes enabled, and unified-ci-guest-agent.
  • A Docker-enabled Firecracker guest variant for workflows that need act to talk to Docker inside the VM.
  • Shell run: steps through act.
  • Offline-mapped composite uses: actions via --act-local-repository.
  • Optional Firecracker egress_only networking for public internet egress with host/private/job-to-job isolation.

Not Supported Yet

  • Full forgejo-runner compatibility.
  • 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 /healthz
  • GET /status
  • GET /jobs/<job-id>
  • GET /usage

More Docs