Forgejo Actions Runner Management
Purpose
This document is a Standard Operating Procedure for adding, removing, and managing Forgejo Actions runners on Fedora Forge.
Scope
This SOP applies to Fedora Infrastructure team members who manage Forgejo Actions runners on forge.fedoraproject.org and forge.stg.fedoraproject.org.
Overview
Forgejo Actions runners execute lightweight workflows on Fedora Forge. Runners are defined in the
ansible repo,
registered in the Forgejo application via its CLI, and run as podman
containers on a KubeVirt VM (forgejo-runnerhost-vm) in the forgejo
OpenShift namespace.
| This document covers Fedora-specific runner management. For detailed runner configuration options, refer to the official Forgejo Actions documentation. |
Architecture
Ansible repo Private repo (batcave01)
============ ========================
runners/<env>/<name>.yml forgejo_runner_secrets:
(name, scope, labels, image) <name>: "<40-char hex>"
\ /
\ /
v v
load-forgejo-runners.yaml ──> forgejo_runners[]
|
┌─────────┼──────────────────────────┐
v v v
K8s Secret Control host Forgejo pod
forgejo- forgejo-runners- forgejo-cli
runner-config vars.yaml actions register
| |
v v
Runner host VM Runner appears in
(watcher syncs secret Forgejo admin UI
to local file) with correct labels
|
v
ansible-role-forgejo-runner
(ansible-pull on boot, then
starts runner containers)
Key components
| Component | Location | Purpose |
|---|---|---|
Runner definitions |
|
Public config (name, scope, labels, image) |
Runner secrets |
|
Registration secrets (40-char hex) |
Forgejo playbook |
|
Orchestrates everything |
Runner host VM |
KubeVirt VM |
Runs all runners as unprivileged systemd user services (rootless podman containers) |
External runner role |
Configures runner daemons on the VM |
Adding a new runner
Step 1: Create the runner definition file
Create a YAML file in roles/openshift-apps/forgejo/runners/<env>/ where
<env> is production or staging. The filename must match the name
field.
Simple label example (runners/production/roadrunner-1.yml):
---
name: roadrunner-1
scope: roadrunner
labels:
- meep-meep
image: code.forgejo.org/forgejo/runner:12
Custom image mapping example (runners/production/roadrunner-2.yml):
---
name: roadrunner-2
scope: roadrunner
labels:
- "docker:docker://node:22-bookworm"
- "docker-slim:docker://node:22-alpine"
image: code.forgejo.org/forgejo/runner:12
With capacity (optional, limits concurrent jobs):
---
name: roadrunner-3
scope: roadrunner
labels:
- meep-meep
image: code.forgejo.org/forgejo/runner:12
capacity: 4
Field reference
| Field | Required | Description |
|---|---|---|
|
Yes |
Unique runner name. Must match the filename (without |
|
Yes |
Forgejo organization name for org-scoped runners, or |
|
Yes |
List of workflow |
|
Yes |
Runner container image (e.g., |
|
No |
Maximum concurrent jobs for this runner. |
See also the Forgejo Runner Configuration example for details on label types and runner options.
Label format
Labels can be simple names or include an image mapping:
-
Simple:
podman,docker,fedora— the runner uses its default container image. -
With image:
"docker:docker://node:22-bookworm"— the part before:is the label name (what workflows use inruns-on), the part after is the container image the runner uses for that label.
During registration in the Forgejo application, only the label name (before
:) is used. The full label:image mapping is passed to the runner daemon
configuration.
Step 2: Add the registration secret to private vars
On batcave01, edit /srv/private/ansible/vars.yml and add the runner’s
secret to the appropriate dict.
Generate a new secret:
openssl rand -hex 20
Add it to the private vars:
# For production
forgejo_runner_secrets:
roadrunner-1: "a1b2c3d4e5f6..." # 40-char hex
# For staging
forgejo_stg_runner_secrets:
roadrunner-1: "f6e5d4c3b2a1..."
Step 3: Commit and deploy
# Commit the runner definition (public repo)
cd /srv/web/infra/ansible
git add roles/openshift-apps/forgejo/runners/<env>/<name>.yml
git commit -m "forgejo: add <name> runner for <env>"
git push
# Run the playbook
sudo rbac-playbook openshift-apps/forgejo.yml -l <limit>
Where <limit> is:
-
stagingfor staging runners -
productionfor production runners
What the playbook does automatically
-
Loads runner definitions — scans
runners/<env>/*.yml, merges each with its secret from private vars. -
Renders runner config — produces
forgejo-runners-vars.yamlcontaining the Forgejo instance URL, full runner list (with secrets andlabel:imagemappings), and Zabbix agent config. -
Updates K8s Secret — deploys the rendered config as
forgejo-runner-configSecret in theforgejonamespace. -
Registers runners in Forgejo — execs
forgejo-cli actions registerinside the Forgejo application pod for each runner (idempotent with--secret). Label names are extracted (stripping:imagesuffixes). -
Watcher syncs to VM — the watcher service on
forgejo-runnerhost-vmdetects the Secret update and writes the config to/home/<user>/forgejo-runners-vars.yaml. -
Runner daemon picks up config — the
ansible-role-forgejo-runneron the VM reads the updated config and starts/restarts runner containers.
Removing a runner
Step 1: Delete the runner definition file
git rm roles/openshift-apps/forgejo/runners/<env>/<name>.yml
git commit -m "forgejo: remove <name> runner from <env>"
git push
Step 2: Run the playbook
sudo rbac-playbook openshift-apps/forgejo.yml -l <limit>
This updates the K8s Secret (the runner will no longer be in the config), and the watcher syncs the change to the VM. The runner daemon stops the removed runner’s container.
Modifying a runner
To change labels, image, scope, or capacity:
-
Edit the runner definition file in
runners/<env>/<name>.yml. -
Commit and push.
-
Run the playbook:
sudo rbac-playbook openshift-apps/forgejo.yml -l <limit> -
If the watcher doesn’t pick up the change, restart it on the VM (see Troubleshooting).
If you change a runner’s labels, the forgejo-cli actions register
command with the same --secret will update the existing registration.
Workflows using the old label will stop matching; workflows using the new
label will start matching.
|
Runner host VM operations
Checking runner status on the VM
# Check if runner containers are running
podman ps
# Check the watcher service
systemctl --user list-units | grep -iE 'watch|secret|sync|forgejo'
# Check ansible-pull log (from initial boot)
cat ~/ansible-pull.log
# Check the synced config
cat ~/forgejo-runners-vars.yaml
Restarting the VM
# Restart (preserves data volume, cloud-init does NOT re-run)
virtctl restart forgejo-runnerhost-vm -n forgejo
# Full rebuild (deletes data volume, cloud-init runs fresh)
oc delete vm forgejo-runnerhost-vm -n forgejo
# Then re-run the playbook to recreate
sudo rbac-playbook openshift-apps/forgejo.yml -l <limit>
Troubleshooting
Runner not appearing in Forgejo UI
-
Check if the playbook ran the registration task successfully:
sudo rbac-playbook openshift-apps/forgejo.yml -l <limit> -v -
Verify the runner’s secret matches between the definition and private vars.
-
Check if the Forgejo pod is running:
oc get pods -n forgejo -l app.kubernetes.io/name=forgejo
Runner registered but not picking up jobs
-
Check if the runner daemon is running on the VM:
virtctl console forgejo-runnerhost-vm -n forgejo podman ps -
Verify the config was synced to the VM:
cat ~/forgejo-runners-vars.yaml | grep -A5 <runner-name> -
Verify labels match between the runner definition, Forgejo UI, and the workflow
runs-on.
K8s Secret updated but VM config is stale
The watcher service may have lost its watch connection. Restart it:
# On the VM
systemctl --user restart forgejo-runner-config-watcher.service
# Verify the file was updated
cat ~/forgejo-runners-vars.yaml | grep -A5 <runner-name>
Cloud-init failures on VM boot
Cloud-init only runs on first boot. Common issues:
-
Home directory owned by root: Fixed by
defer: trueonwrite_filesandchown -Rinruncmd. -
ansible-pull fails: Check
~/ansible-pull.logon the VM. -
Packages fail to install: Check
/var/log/cloud-init-output.logon the VM.
Non-breaking space characters in runner secrets
If runner definitions were copy-pasted from web pages, they may contain
invisible \xa0 (non-breaking space) characters. Detect with:
grep -rP '\xc2\xa0' /srv/private/ansible/vars.yml
Fix by retyping the affected lines or:
sed -i 's/\xc2\xa0/ /g' /srv/private/ansible/vars.yml
Reference
Playbook command
# Staging
sudo rbac-playbook openshift-apps/forgejo.yml -l staging
# Production
sudo rbac-playbook openshift-apps/forgejo.yml -l production
File locations on batcave01
| Path | Purpose |
|---|---|
|
Runner definitions |
|
Runner secrets |
|
Playbook |
File locations on runner host VM
| Path | Purpose |
|---|---|
|
Synced runner config (from K8s Secret) |
|
ansible-pull output from boot |
|
Mounted SA token for K8s API access |
Kubernetes resources in forgejo namespace
| Resource | Name | Purpose |
|---|---|---|
Secret |
|
Runner config (synced to VM) |
Secret |
|
SA token for VM K8s API access |
ServiceAccount |
|
VM identity |
Role |
|
Read secrets permission |
RoleBinding |
|
Binds SA to Role |
VirtualMachine |
|
Runner host VM |
Service |
|
Headless service for VM DNS |
Want to help? Learn how to contribute to Fedora Docs ›