forgejo-runner install writes no config, so every rig-installed runner is pinned to one job at a time and no operator can change it durably #150
Labels
No labels
attention
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-ruling
needs-triage
offsite
post-merge
ready
release
scope:bootstrap
scope:coolify
scope:db
scope:docs
scope:drill
scope:installer
scope:labels
scope:platform
scope:runner
scope:users
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/rig#150
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Context
rig forgejo-runner installgives the daemon no configuration file, so everyrig-installed runner executes jobs one at a time, and there is no supported
way for an operator to change that.
Read at
main@cb4de44:commands/forgejo-runner-install.sh:518writes
ExecStart=${BIN} daemon— no--config.grep -rn 'config.yaml' .over the repository returns nothing: rig nevergenerates, converges or references one.
grep -rni 'capacity' .returns nothing either.So the daemon runs on upstream's built-in defaults. Whatever those are, they are
not rig's to choose today, and the effect measured below is a concurrency of one.
Measured
ci-forgejo-box, an 8 GB Incus box on a CI server, 2026-08-25T21:31Z, with40 jobs queued across three repositories:
One container against a 40-deep queue. Forge-side throughput over the same
window: 36 jobs in two hours, ~18/hour, last completion 15 minutes before the
sample. The backlog drains in over two hours and the box is nearly idle doing
it — 85 MB of 8 GB, one job's worth of CPU.
The consumer that produces the load is a repository whose CI fans out to 13 jobs
per commit. At a concurrency of one, a single push occupies the runner for the
better part of an hour and every other repository's checks queue behind it.
Why an operator cannot fix this themselves, durably
The obvious remedies both fail, and the second fails silently:
config.yaml. Nothing reads it. The unit passes no--config,so the file has no effect wherever it is placed. An operator who does this
reasonably concludes the setting does not work.
ExecStartin the unit. rig converges that file cmp-guarded(
:532-538),so the next
forgejo-runner install— a version converge, a re-bake, a boxrebuild — rewrites it and drops the flag. Nothing fails and the runner keeps
working; it simply goes back to serial.
A systemd drop-in under
forgejo-runner.service.d/does survive, because rignever touches that directory, and that is the workaround in use on
ci-forgejo-boxtoday. It is not a fix: it is invisible torig forgejo-runner status, invisible to the next operator, and it pins an absolutebinary path that rig is otherwise free to move.
Spec
Decision 1 —
rig forgejo-runner installgains--capacity N, defaulting to1. The default preserves today's behaviour exactly, so this issue changes no
existing installation's concurrency until an operator asks for it.
Decision 2 — rig writes
$RUNNER_DIR/config.yamland converges itcmp-guarded, the same shape the unit already uses: write to a temp file,
compare, install only on difference, and say so. One more converged file in a
command that already converges two.
Decision 3 — the unit gains
--config $RUNNER_DIR/config.yamlunconditionally, at every capacity including 1. Not only when
--capacityispassed. A config file that exists on some boxes and not others is a second
shape to reason about, and the file is where a future setting will go.
Decision 4 — mode 0644, owned by the runner user. Deliberately unlike
.runner, and the contrast is worth stating in the code:.runneris 0600because it holds the runner's long-lived registration secret.
config.yamlholds no credential, and making it unreadable would only obscure what the box is
running.
Decision 5 —
rig forgejo-runner statusreports the effective capacity.Without it there is no way to answer "how many jobs can this box run" except by
reading two files and knowing which one wins. This is the same gap #143 records
for the
activatingstate: the command's job is to say what is true.Decision 6 — a re-run with an unchanged
--capacitywrites nothing andreloads nothing, matching the unit's existing cmp-guard. Restarting a runner
mid-job to change nothing is a lost job.
Tasks
--capacity Nonforgejo-runner install; validate it is a positiveinteger and refuse otherwise, with the reason.
config.yamlfromforgejo-runner generate-configwhere thebinary offers it, falling back to a minimal document rig owns. Set
runner.capacityfrom the flag.--configto the unit'sExecStart.forgejo-runner status.--helpand in the runner docs,including the sizing note below.
forgejo-runner.service.d/drop-in must remove it once this lands — adrop-in
ExecStart=still overrides the unit and would pin a stale path.test/cli.shand the runner drill leg.changelog.d/<issue>.md.Acceptance criteria
installwith no--capacityproduces aconfig.yamlreadingcapacity: 1, a unit carrying--config, and a runner whose observableconcurrency is unchanged from today.
install --capacity 3yields three concurrentFORGEJO-ACTIONS-TASK-*containers under a queue deeper than three.--capacityreports "already current", rewritesneither file, and does not restart the service.
--capacityrewrites the config, restarts,and says both.
forgejo-runner statusprints the effective capacity, and it matches whatthe daemon is actually running.
--capacity 0and--capacity abcare refused with a reason, and changenothing.
config.yamlis 0644 and owned by the runner user;.runneris still0600 and its mode is untouched by this work.
Sizing, for the docs
Capacity is bounded by memory, not cores, and the boxes have no swap — an
over-subscribed runner OOM-kills jobs rather than queueing them. On the 8 GB box
measured here the daemon itself holds 85 MB and each job is an
ubuntu:act-22.04container running lint and test work, so 3 is a comfortablestarting point and 4 is the ceiling. Worth saying in the docs rather than
leaving each operator to discover it by killing a job.
Dependencies
None. This is additive to
forgejo-runner-install.shand touches no othercommand. It sits beside the other open
scope:runneritems (#141, #142, #143,#148) without overlapping any of them: those are about reporting and
preflight on the install path, this one adds a setting to it.
Evidence
Terminal output above, captured by the operator on
ci-forgejo-box2026-08-25T21:31Z. Forge-side queue depth and throughput read from the Forgejo
API over the same window. Repository facts read at
main@cb4de44.One claim deliberately not made: this issue does not assert what upstream's
default capacity constant is. It asserts that rig supplies no configuration, and
that the measured concurrency of a rig-installed runner is one.
Filed from the heavy-duty fleet's side, where the queue this surfaced on lives.