Feature Specification: Component forgejo-runner

Created: 2026-10-07

Status: Implemented

Input: Reconstructed from the code of core/components/dev/forgejo-runner/ and the dev group at 14504f80d, pull requests #1383, #1430, #1452, #1453, #1454, #1466, #1470, #1484, #1524, #1528, #1534, #1547, #1623, #1693, #1708, #1735, #1746, #1822, #1852, #1956, #1967, #1989 and #2017.

This spec describes one component of the dev group. The family spec ../spec.md describes what all components share.

User Scenarios & Testing (mandatory)

User Story 1 - CI jobs run on a memory tier (Priority: P1)

A workflow asks for a runner by label, for example a memory tier. Each CI host runs a pool of runner instances. Each instance belongs to one tier and runs under the memory cap of that tier. Jobs run natively, with no Docker. A job can ask not to run on a named pool member.

Why this priority: All CI of the repository runs on this pool.

Independent Test: A unit test evaluates the pool from the roster and checks the instances, caps and labels of each tier. No check deploys the pool. The CI configuration reads the tier labels to route checks.

Acceptance Scenarios:

  1. Given a CI host, When it deploys, Then it runs a fixed number of instances per tier, with more small instances than large ones.
  2. Given an instance, When it starts, Then it declares the native label, its tier, the nspawn label and one "not on this peer" label per other pool member, and takes one job at a time.

User Story 2 - A deploy does not cancel running jobs (Priority: P1)

A host switch does not stop the runners. A restarter then drains and restarts them one at a time.

Why this priority: A cancelled job wastes hours of CI time and gives a false red result.

Independent Test: No check covers the drain.

Acceptance Scenarios:

  1. Given a changed runner configuration or a rotated token, When the host switches, Then the restarter brings the pool onto it.
  2. Given a busy runner, When the restarter drains it, Then the job can run to the job timeout before the runner stops.

User Story 3 - The host frees store space without blocking builds (Priority: P2)

CI fills the Nix store. A garbage collection frees space when the free space falls below a trigger, and builds do not wait for it.

Why this priority: A full disk fails every build on the host.

Independent Test: No check covers the garbage collection.

Acceptance Scenarios:

  1. Given free space below the trigger, When the check runs, Then it removes old build roots and system generations, and collects garbage up to a target above the trigger. Else it does nothing.

Edge Cases

  • A host that is not in the pool fails the evaluation.
  • The evaluation fails unless the stop timeout is below the job timeout, the job timeout is at most the shutdown timeout, and the shutdown timeout is below the drain cap, because a drain could cut a job.
  • A roster entry without CD settings fails the evaluation with the host name.
  • Job workspaces are removed after one day.
  • Nix does not collect garbage on its own, so a disk that fills before the collection runs fails builds.

Requirements (mandatory)

Functional Requirements

  • FR-001: The component MUST declare the runner package and MUST run with its own Nix store.
  • FR-002: The component MUST NOT request a domain or TLS, because the host has no DNS entry.
  • FR-003: The component MUST request the runner token from secrets, readable only by root.
  • FR-004: The component MUST import its NixOS module through nixos-module.
  • FR-005: The module MUST know every pool member and the roster name of its own host, and MUST take the forge URL with no default.
  • FR-006: The dev group MUST derive the pool from the roster machines with this component, and MUST give each host its name and the HTTPS URL of the forge.
  • FR-007: Each instance MUST have a name from its tier and index, and MUST run under the memory cap of its tier.
  • FR-008: Each instance MUST derive its forge connection from its registration before it starts, and MUST poll for jobs at a short interval.
  • FR-009: A host switch MUST NOT restart a runner. A change to a runner configuration or to the token MUST trigger the restarter.
  • FR-010: The Nix daemon MUST run under a memory cap that leaves room for the runners.
  • FR-011: The module MUST turn off the automatic garbage collection of Nix, and MUST run its own on a short timer, also soon after boot.
  • FR-012: The garbage collection MUST run at the lowest CPU and IO priority.

Key Entities

  • Memory tier: A runner label with an instance count and a memory cap.
  • Pool: The CI hosts that run this component.
  • Restarter: The service that drains and restarts the runners after a switch.

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: The dev group evaluates with all assertions of the runner module true.
  • SC-002: A deploy of a changed runner configuration cancels no running job.
  • SC-003: After a garbage collection, the free space is above the target.

Assumptions

  • The pool hosts are reached by IPv4 literals and get their host name from the network resource.
  • A unit test of the secrets resource covers the routing of the token, not the trigger on its rotation.

Known Deviations

None.