www/content/blog/ansible/forgejo-runner.md
Julien Dessaux c6ecfe0d34
All checks were successful
/ all (push) Successful in 18s
feat(blog): add forgejo runner article
2025-04-08 21:24:57 +02:00

5.7 KiB

title description date tags
Deploying a forgejo runner with ansible Some ansible code and a golden rule 2025-04-08
ansible
forgejo

Introduction

After [migrating my git.adyxax.org to forgejo]({{< ref "forgejo.md" >}}) a few weeks back, I started experimenting with their CI offering: forgejo actions. It is mostly a clone of GitHub actions, which means it is a mixed bag.

I am still relying on eventline for all the important stuff I manage, but I wanted to get the actions integration for some simple and non consequential CI jobs.

The good and the bad

The good part is obviously the tight integration with forgejo's UI. Forgejo (or gitea which forgejo forked from) developers also made the great decision of adding runners dedicated to individual users, which allows me to open my forgejo instance to other people without offering them access to a runner.

The bad part all has to do with trying to be a GitHub actions clone:

  • The reliance on nodejs actions for everything.
  • The non case sensitivity in weird places (the most damning example in my eyes being if: ${{ github.ref == 'refs/heads/main' }} which looks like an equality check, but it will match any case variation of MaIn).
  • It is YAML soup, and workflow reuse is clunky at best.

The golden rule

I find that the following golden rule applies to all CIs: avoid building any actual logic into your workflows outside of the simple orchestration of the tasks. Here is an example workflow that illustrates this rule.

As you can see, I use simple (and often one liner) build, test or deploy commands that call a Makefile to do the heavy lifting. This guarantees that I am always able to build or deploy locally as well as debug more easily my workflows.

Making do without the runner containers

I greatly dislike the default forgejo runner containers: They package everything and the kitchen sink, which is necessary given how clunky the whole nodejs ecosystem is (which the actions rely on).

Fortunately I can do without these runner containers. The documentation will warn about its dangers and I caution you too if you plan to follow in my footsteps: You need to manage the proper isolation yourself and take care of not making a mess of the host operating system!

Managing the proper isolation is not hard: instead of letting forgejo runner spawn its own containers, I myself run it constrained inside either a container or a jail.

Not making a mess of the host operating system requires discipline though because the runner environment does not get cleaned on each run. Since I write my workflows and actions myself and follow the previously mentioned golden rule, with some discipline and experience I make it work.

Ansible role

Tasks

Here is an example tasks.yaml that deploys the forgejo runner on a Debian system. It does not configure the runner itself, that I do manually once after the first deployment.

---
- name: 'Install forgejo-runner dependencies'
  package:
    name:
      - 'git-crypt'
      - 'hugo'
      - 'nodejs'

- name: 'Download forgejo runner {{ versions.forgejo_runner.tag }}'
  get_url:
    url: 'https://code.forgejo.org/forgejo/runner/releases/download/v{{ versions.forgejo_runner.tag }}/forgejo-runner-{{ versions.forgejo_runner.tag }}-linux-amd64'
    dest: '/usr/local/bin/forgejo-runner'
    mode: '0555'
  notify: 'restart forgejo runner'

- name: 'Create forgejo-runner group on server'
  group:
    name: 'forgejo-runner'
    system: 'yes'

- name: 'Create forgejo-runner user on server'
  user:
    name: 'forgejo-runner'
    group: 'forgejo-runner'
    shell: '/bin/sh'
    home: '/srv/forgejo-runner'
    createhome: 'yes'
    system: 'yes'
    password: '*'

- name: 'Deploy forgejo systemd service unit'
  copy:
    src: 'forgejo-runner.service'
    dest: '/etc/systemd/system/'
    owner: 'root'
    group: 'root'
    mode: '0440'
  notify: 'systemctl daemon-reload'

- name: 'Start forgejo-runner and activate it on boot'
  service:
    name: 'forgejo-runner'
    enabled: true
    state: 'started'

Handlers

This role relies on two handlers:

---
- name: 'restart forgejo runner'
  service:
    name: 'forgejo-runner'
    state: 'restarted'

- name: 'systemctl daemon-reload'
  shell:
    cmd: 'systemctl daemon-reload'

Files

Here is my forgejo-runner.service systemd unit file:

###############################################################################
#     \_o<     WARNING : This file is being managed by ansible!      >o_/     #
#     ~~~~                                                           ~~~~     #
###############################################################################
[Unit]
Description=Forgejo Runner
Documentation=https://forgejo.org/docs/latest/admin/actions/
After=network.target

[Service]
ExecStart=forgejo-runner daemon
ExecReload=/bin/kill -s HUP $MAINPID
User=forgejo-runner
WorkingDirectory=/srv/forgejo-runner
Restart=on-failure
TimeoutSec=0
RestartSec=10

[Install]
WantedBy=multi-user.target

Manual runner registration

There is an offline registration mechanism, but I did not attempt to use it. Since this is not an action I will redo often, I am content with running once:

su - forgejo-runner
forgejo-runner register
               --instance https://git.adyxax.org \
               --labels self-hosted:host://-self-hosted \
               --name myth.adyxax.org \
               --token XXXXXXXXXXX \
               --no-interactive
exit
systemctl restart forgejo-runner

The registration token comes from my user settings page at https://git.adyxax.org/user/settings/actions/runners/.

Conclusion

It works well, for now I am happy with it.