Contact us: contact@deburen.uk

Forgejo Runners in FreeBSD Jails

Following a request on Mastodon this is some documentation on how to get Forgejo Runners working in FreeBSD.

Introduction

A Forgejo Runner is a dedicated daemon that executes automated CI/CD workflows (Actions) by fetching tasks from your Forgejo instance, running them, and reporting the results back.

Deploying a Runner in a jail is a little bit involved as it involves some manual work the first time you deploy.

Bastillefile

The following are needed to be added where necessary to the Bastillefile (note I build my jails for idempotency):

OVERLAY usr
# Next line only needed if using forgejo below version 15
OVERLAY var
# Below needed as we copy the registration data once created into the right 
# place in the jail
CMD chown -R act_runner:act_runner /var/db/act_runner
CMD chown root:wheel /usr/local/etc/doas.conf
# To give shell access to the www user
CMD pw user mod www -d <your choice of web root folder> -s /bin/sh
PKG doas forgejo-act_runner
# Next line only needed if using forgejo below version 15
CMD chown -R act_runner:act_runner /var/db/act_runner
SYSRC act_runner_enable=YES
SERVICE act_runner restart

User elevation

act_runner runs as user act_runner, which is why I bring doas into the jail. You can always use your preferred elevation tool!

An example doas.conf for this where I want the runner to make changes to files owned by the www user:

permit nopass act_runner as www

If you're using Bastille jails then this doas.conf should be put in the template as: <template>/usr/local/etc/doas.conf.

act_runner.conf

I deploy my act_runner.conf from my Bastille template, so use bastille rcp to copy /usr/local/etc/act_runner.conf.sample which is now in the jail to <template>/usr/local/etc/act_runner.conf.

The only change I needed to make was to the labels:. The only label I needed to set up was as follows:

labels: ["<jailname>:host"]

Either redeploy the template, or do the same inside the jail.

First time setup (after forgejo version 15)

1 Generate act_runner tokens in forgejo

You need to generate a runner token from forgejo. You can do this site wide (in the site settings), organisation wide (in the organisation settings), or repo only (in the repo settings). In my case I did it organisation-wide as this client has multiple repos that we may want to automate. Organisation -> Settings -> Actions -> Runners -> Create new runner and copy the runner configuration file yaml from forgejo: onwards.

Add this to the end of the <template>/usr/local/etc/act_runner.conf file inside the server: and connections: entries.

2 Deploy and run act_runner service

Deploy the template, which will replace the configuration with the changes above as well as restart the act_runner service. Forgejo should now show the runner as being Active or Idle in the list of runners.

First time setup (before forgejo version 15)

1 Preparation

Deploy your changes to the jail (in my case that means reapplying the Bastille template) and get a root console in the jail.

Since the act_runner account is set to /usr/sbin/nologin and we only need to register this once, I do this as the root user in the jail, which does not put the file in the right place, but that's fine for me as I will deploy the file as part of the Bastille template.

2 Generate act_runner tokens in forgejo

You need to generate a runner token from forgejo. You can do this site wide (in the site settings), organisation wide (in the organisation settings), or repo only (in the repo settings). In my case I did it organisation-wide as this client has multiple repos that we may want to automate. Organisation -> Settings -> Actions -> Runners -> Create new runner and copy the Registration token.

3 Configure act_runner

Now run act_runner register as root in the jail:

act_runner register
INFO Registering runner, arch=amd64, os=freebsd, version=12.5.0.
INFO Enter the Forgejo instance URL (for example, https://next.forgejo.org/):
# Enter your forgejo instance here (and make sure it can be reached from the jail)
INFO Enter the runner token:
# Enter the runner token you copied above here
INFO Enter the runner name (if set empty, use hostname: jailname):
# I used the jail name for this
INFO Enter the runner labels, leave blank to use the default labels (comma-separated, for example, ubuntu-20.04:docker://node:20-bookworm,ubuntu-18.04:docker://node:20-bookworm):
# Use the label you created in act_runner.conf
INFO Registering runner, name=jailname, instance=https://forgejo.tld/, labels=[jailname:host].
DEBU Successfully pinged the Forgejo instance server
INFO Runner registered successfully.

4 Prepare and run act_runner service

This generates a file called .runner in your current directory. Copy this into your template var/db/act_runner/ folder with the bastille rcp command if you're using Bastille, or into the jail path ~act_runner/ to make it work in this jail without a Bastille deployment.

If you're using Bastille, re-apply the template and the runner should now be visible in forgejo.

If you're managing the jail manually then make sure the .runner file is owned by act_runner:act_runner. (re)start the act_runner service and it should now be visible in forgejo.

Create a workflow

In your git repo, create a folder called .forgejo/workflows and create a yaml file, call it whatever you want to be visible in forgejo. Mine is push_to_env.yaml. You can have as many of these as you want

The Forgejo Action reference docs are helpful for this, and include some useful examples for each function. Here is my runner (with some items redacted), which allows the users to deploy to Staging, optionally nominating a branch name, and deploy to Production with no branch choice. It also does some checks to report on the success of the deployments:

name: Deploy theme
on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Environment'
        required: true
        default: 'Staging'
        type: choice
        options:
          - Staging
          - Production
      branch:
        description: 'Deploy branch name (Staging only)'
        required: true
        default: 'main'
        type: string
jobs:
  staging:
    name: Deploy latest changes to Staging and switch branch
    if: inputs.environment == 'Staging'
    runs-on: <runner name>
    steps:
      - name: Display deployment info
        run: echo "Deploying to ${{ inputs.environment }} and switching branch to ${{ inputs.branch }}"
      - name: Switch branch and pull latest changes
        run: doas -u www sh -c "cd /folder/for/staging; git fetch origin && git switch ${{ inputs.branch }} && git pull origin ${{ inputs.branch }}"
      - name: Show git status
        run: doas -u www sh -c "cd /folder/for/staging; git status"

  production:
    name: Deploy latest changes in master to Production
    if: inputs.environment == 'Production'
    runs-on: <runner name>
    steps:
      - name: Deploy latest changes in master to Production
        run: doas -u www sh -c "cd /folder/for/production; git pull origin main"
      - name: Show git status
        run: doas -u www sh -c "cd /folder/for/production; git status"

Refinements to git setup

It is worth noting that for the above to work you need to set up git in the jail not to require a username and password as otherwise git will cause the runner to hang when requesting authentication. One way to do this is to run git config credential.helper store and then git pull origin in the repo in the jail. This will request and store the username and password in the git config and will no longer request authentication when the runner attempts a remote git command. There are other ways to solve this that may be more appropriate in your security environment.

Run the workflow

Commit these changes to the git repo, which should cause an Actions item to be added to the functions available in Forgejo, and the workflow that you created should be visible in the "All workflows" box on the left.

Click on the workflow name, and a blue bar will appear offering you a drop down to provide the needed input or just run the workflow.

Provide the input needed, and click the Run workflow button.

Tidying up (optional)

I like to make sure all service log files are rotated appropriately, so I have the following in <template>/usr/local/etc/newsyslog.conf.d/act_runner.conf:

# logfilename                   [owner:group]   mode    count   size    when    flags   [/pid_file] [sig_num]
/var/log/act_runner/act_runner.log                      644     4       *       @T00    J       /var/run/act_runner/act_runner.pid

Document history