A horde of disposable workers for Forgejo.
  • Python 98.6%
  • Jinja 1%
  • Shell 0.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Hermes Agent 91ba1708bb fix(ansible): real install-source default; install git on target
Two deployment failures back to back on a fresh host:
- default formikaro_install_source was the git.example.com placeholder
  -> pip clone 'Could not resolve host'. The repo's public home is the
    correct default (anonymous clone verified rc=0); examples now carry
    a do-not-embed-tokens warning since the URL lands in ansible output.
- pip git+https needs a git binary; the role never installed one
  (same latent gap class as the python3-debian miss). Added to the
  python3-venv apt task.
2026-09-18 10:55:52 +00:00
ansible fix(ansible): real install-source default; install git on target 2026-09-18 10:55:52 +00:00
docs docs: final smoke runbook — everything verified except two user inputs 2026-09-18 09:13:13 +00:00
examples docs: add example dispatcher config matching the design contract 2026-09-18 04:29:41 +00:00
openspec docs: pin DELETE runner endpoint + colon label grammar in design.md 2026-09-18 09:11:40 +00:00
scripts openspec: add-ephemeral-runner-dispatcher change + repo scaffolding (hygiene, ruff, strict spec validation) 2026-09-18 03:59:03 +00:00
src/formikaro feat: delete orphaned runner records when a job attempt fails post-create 2026-09-18 09:05:50 +00:00
tests feat: delete orphaned runner records when a job attempt fails post-create 2026-09-18 09:05:50 +00:00
.gitignore feat: TDD one-job docker executor (argv, 0600 token file, exit capture) 2026-09-18 04:57:23 +00:00
.pre-commit-config.yaml openspec: add-ephemeral-runner-dispatcher change + repo scaffolding (hygiene, ruff, strict spec validation) 2026-09-18 03:59:03 +00:00
AGENTS.md docs: pin verified one-job CLI + API endpoint families; add AGENTS.md + api-notes 2026-09-18 04:09:59 +00:00
ansible.cfg feat(deploy): ansible role, playbook, and example inventory for the dispatcher 2026-09-18 04:28:48 +00:00
pyproject.toml openspec: mark tasks 5-6 done (API clients, TDD); register pytest network mark 2026-09-18 04:55:41 +00:00
README.md docs: rewrite README — what/why, quickstarts, token scopes, config reference, security 2026-09-18 04:29:05 +00:00

formikaro

Esperanto: formikaro — ant colony. Each worker is born, does one job, and is gone.

formikaro is an ephemeral Forgejo Actions runner dispatcher. Every job gets a pristine, disposable runner: the dispatcher watches Forgejo's waiting-jobs API, and when a job is available it creates an ephemeral runner record, runs forgejo-runner one-job in a --rm container for exactly that job, then destroys everything. One job at a time per host — nothing persists between jobs: no shared work dirs, no caches, no long-lived runner daemon.

The dispatcher never executes jobs itself; it owns the poll loop and the registration lifecycle. Job execution is the stock runner binary in a throwaway container, creating sibling job containers via the host's docker socket. No docker-in-docker.

Quickstart (Ansible)

Requirements on the control node: Ansible (ansible-core ≥ 2.15) and SSH access with sudo to the target hosts.

  1. Copy the example inventory and site vars:

    cp -r ansible/examples my-inventory   # then edit hosts + vars
    
  2. Put your secrets in group_vars/formikaro_hosts/vault.yml (see ansible/examples/group_vars/formikaro_hosts/vault.yml.example) and encrypt it:

    ansible-vault encrypt my-inventory/group_vars/formikaro_hosts/vault.yml
    
  3. Run the playbook:

    ansible-playbook -i my-inventory/inventory.ini ansible/playbooks/formikaro.yml --ask-vault-pass
    

The role installs docker-ce (flag-gated), creates a formikaro service user, installs the dispatcher into /opt/formikaro/venv, renders /etc/formikaro/config.yml and the secret env file, pre-pulls the runner and job images, and enables + starts the formikaro systemd service.

To refresh images after a tag bump:

ansible-playbook -i my-inventory/inventory.ini ansible/playbooks/formikaro.yml --tags images

Quickstart (manual)

On a host with docker already installed:

python3 -m venv /opt/formikaro/venv
/opt/formikaro/venv/bin/pip install .
sudo useradd --system --home-dir /var/lib/formikaro --create-home formikaro
sudo usermod -aG docker formikaro
sudo cp examples/config.yml /etc/formikaro/config.yml   # adjust to your site
sudo cp examples/env /etc/formikaro/env                  # 0600, your token
sudo cp examples/formikaro.service /etc/systemd/system/
sudo systemctl enable --now formikaro

Token scope requirements

The dispatcher talks to Forgejo's runner-management REST API; creating runners requires a token with runner-management write scope (read-only tokens get 403). The scope you dispatch for dictates who must own the token:

Config scope Token must belong to
user the user themselves (personal token)
orgs/<name> an organization owner
repos/<owner>/<repo> a repository admin

There is no instance-wide scope outside admin-only endpoints, which this tool does not use. See docs/api-notes.md for the verified endpoint families.

The playbook never mints tokens — provision one per site and pass it via the (vault-encrypted) formikaro_api_token variable; it is rendered only into /etc/formikaro/env (0600 root:root).

Configuration reference

/etc/formikaro/config.yml — rendered by the role from group_vars:

Key Default (role) Meaning
instance_url https://git.example.com Forgejo instance to poll
api_token_env FORMIKARO_API_TOKEN Env var name holding the API token (value lives in the env file)
poll_interval_seconds 15 Delay between waiting-jobs polls
backoff_max_seconds 300 Cap for the linear backoff after errors
scopes [user, orgs/example] Scope list: user, orgs/<name>, repos/<owner>/<repo>
labels ubuntu-latest: docker://registry… Runner label → job image (fully qualified)
runner.image registry.example.com/ns/forgejo-runner:12.13.2 Runner image executing one-job
runner.socket /var/run/docker.sock Host docker socket mounted into the runner container
runner.network_mode host Docker network mode for the runner container

Concurrency is fixed at one job per host (locked; see design.md) — scale by adding hosts, each running its own dispatcher.

/etc/formikaro/env — the only secret-bearing file:

Variable Meaning
FORMIKARO_API_TOKEN Forgejo API token (runner-management write)
DOCKER_CONFIG /var/lib/formikaro/.docker (isolated registry credentials)

Host assumptions

  • A plain Linux VM (role's docker install targets Ubuntu; anything with an existing docker works with formikaro_manage_docker: false).
  • systemd, python3 + venv support, and network access to your Forgejo instance and container registry.
  • Host docker socket available at /var/run/docker.sock; the formikaro user gets SupplementaryGroups=docker — it can start sibling containers by design (that is the job-execution mechanism), so treat the host as dedicated to this purpose.

Security notes

  • The API token lives only in /etc/formikaro/env (0600 root:root, loaded via systemd EnvironmentFile) and in dispatcher memory. The registry login task runs with no_log: true; no secret ever appears in a template, the unit file, or journald via the playbook.
  • Per-job registration tokens exist only in dispatcher memory and the short-lived one-job process argv; the ephemeral runner record is auto-deleted server-side when the job finishes.
  • No docker-in-docker: the runner container mounts the host socket (SupplementaryGroups=docker). Job containers are siblings, created fresh per job and removed on exit.
  • Each job executes in a never-before-used runner container (--rm) with a fresh registration — the repo's core invariant.

Development

pip install pre-commit   # or brew/pipx
pre-commit install       # hygiene + ruff + openspec validate --strict
pre-commit run --all-files
pytest                   # dispatcher unit tests

Spec-driven repo: work flows through openspec/changes/<id>/ — read the active change's proposal/design/tasks before touching code, and keep openspec validate <change-id> --strict green. Architecture and verified API facts: openspec/changes/add-ephemeral-runner-dispatcher/design.md and docs/api-notes.md.