The forgejo actions feel very familliar to the CI of a well known git service. They are complete with env vars, secret handling, packages artifacts and releases. You can even self-host the runners and connect them to your forgejo instance (or even your account on codeberg.org). This HowTo will explain the setup of the runner in a k3s environment with an existing forgejo deployment and give some basic ideas on how to create a forgejo actions workflow touching some basics on the way.
Integrating the runner with the forgejo deployment
Consider there is already a working k3s forgejo like the one created by c4k-forgejo or forgejos k8s cluster . We will want to integrate the runner setup without much disturbance of the forgejo deployment. We’ll setup the runner with a docker daemon to run Docker in Docker (DIND) - this will allow the runner to start different containers at our leisure.
To get the runner to communicate with the forgejo instance and vice versa the runner needs to be registered with the instance. This requires the shared secret, runner name and the forgejo instance address. Registering on the runner side requires us to wait for the docker daemon and to create the runner file which creates the aforementioned variables. Registration on the instance side will only require runner name and the shared secret.
Not modifying the existing forgejo deployment means we’ll create a kubernetes job that waits for the forgejo deployment to be in healthy state and then execute a small registration script. We’ll create the following resources to integrate with our existing forgejo deployment:
- Runner Configmap
- Runner Secret
- Runner Deployment
- Runner Service
- Runner SetUp job
Notes for the Runner Configmap and Secret
You can find the configmap here
. It contains the config.yaml file needed by the runner and the runner-id field. RUNNER_ID should be replaced with a sensible value that fits your semantics.
You can find the secret here
. The secret holds the shared secret as stringData - replace RUNNER_SECRET with an appropriate value. Make sure to give your runner proper labels, otherwise it will not be able to pickup jobs. We used labels: ["ubuntu-latest:docker://ubuntu:latest"] for ours.
Note: The shared secret needs to be exactly 40 characters long and only consist of hexadecimal digits
. Using openssl you could do: openssl rand -hex 40.
Notes for Runner Deployment
See the deployment yaml here
. The FORGEJO_SERVICE_URL string needs to be replaced with the name of the forgejo service and the port of the pod it forwards to. Notice the runner containers command args. This is a script that will wait for the docker daemon to become ready, create the runner file which contains runner name, token and forgejo instance address. Then the runner is started in daemon mode.
The daemon container will be started with the docker-dind image. This allows the runner to pull and start docker containers for different kinds of jobs. The runner config is a volumeMount to a file under /conf. The runner deployment has the following internal architecture. Consider “localhost” to be the pod that host the runner and the DIND container.
block-beta
columns 1
Server:1
space:1
Node:1
space:1
Pod["Runner Deployment Pod"]:1
id1{{"Container can access internally via localhost"}}:1
space:1
block:cc
c1["Runner Container"]
c2["DIND Container"]
end
Server -- "hosts" --> Node
Node -- "hosts" --> Pod
Pod -- "hosts" --> c1
Pod -- "hosts" --> c2
Notes for the Runner Service
You can find the service definition here . It is plainly a service definition to allow the forgejo instance to find the runner and it is not exposing the runner to the outside world.
Notes for batch job
Find the setup batch job here
. This is the job that will do registration of the runner on the instance side. The FORGEJO_SERVICE_URL string needs to be replaced with the name of the forgejo service and the port of the pod it forwards to.
Note the command args of the forgejo container. This is the script that will be run when the jobs starts. First the script will wait until the forgejo instance is reachable under the given FORGEJO_INSTANCE_URL. Then registration will be attempted using the runner name provided by the configmap-runner.yaml and the secret stored in secret-runner.yaml. The job will thus run exactly once during the startup phase of the runner deployment.
See also
- Runner Kubernetes Resource Examples: https://code.forgejo.org/forgejo/runner/src/branch/main/examples/kubernetes/dind-docker.yaml
- Runner Config: https://forgejo.org/docs/next/admin/runner-installation/#configuration
- Labels: https://forgejo.org/docs/latest/admin/actions/#choosing-labels
Basic CI Setup
Caveats
-
When the workflow file is invalid, there will be no obvious error shown on the forgejo actions web-ui. Instead the workflow will just not appear on push. Errors can be followed in the forgejo logs though. Also when viewing the file directly in the web-ui, the user will be notified that the file is invalid.
-
The workflow file needs to exist on main to be picked up, even for branches and it needs to have triggers:
name: build-website
on:
pull_request:
push:
workflow_dispatch:
- If the access token does not have the correct permissions, uploading a package may fail silently (due to curl not failing on auth errors by default).
Getting And Using Actions
The forgejo actions code is hosted here .
#Example:
name: build-website
on:
pull_request:
push:
workflow_dispatch:
jobs:
build-site:
runs-on: ubuntu-latest
container:
image: 'node:24.4'
steps:
- name: checkout-code
uses: https://data.forgejo.org/actions/checkout@v4
Its worth checking out each actions repository as there are a lot of options on how to use the actions.
Secrets & Variables
Secrets and variables can be set on different levels:
- Repository
- Owner
- Organisation
They can always be found in the settings under the “Runner” category. See secrets documentation here: https://forgejo.org/docs/latest/user/actions/basic-concepts/#secrets
# Accessing secrets and vars example
name: build-website
on:
pull_request:
push:
workflow_dispatch:
jobs:
build-site:
runs-on: ubuntu-latest
container:
image: 'node:24.4'
steps:
- name: build
env:
USER: ${{ vars.WEBSITE_USER }}
TOKEN: ${{ secrets.WEBSITE_USER_TOKEN }}
run: |
echo "Hi, ${USER} placing your token ..."
echo "${TOKEN}" > ~/.token
Uploading Packages & Artifacts
Packages
Uploading packages will be done via respective package manager of the programming language. There is also a generic package registry which can be accessed with curl and proper authentication.
Packages are located in the package registry e.g. https://your-forgejo.org/api/packages/repo-owner/generic/package-name/package-version/package.zip (or any other filetype you chose to upload)
# Example upload to forgejo generic package registry
name: build
on:
push:
branches:
- main
jobs:
build-site:
env:
USER: ${{ vars.USER }}
TOKEN: ${{ secrets.TOKEN }}
BASE_URL: "my.forgejo.instance"
runs-on: ubuntu-latest
container:
image: 'node:24.4'
steps:
- name: Checkout Code
uses: https://data.forgejo.org/actions/checkout@v4
- name: Build
run: |
echo "${{ env.USER }} at ${{ env.BASE_URL }}"
echo "Build"
make build
- name: Upload as Package
env:
FILE: "build.zip"
MY_ORG: "my-org"
MY_REPO: "my-repo"
run: |
#!/bin/sh
set -e
zip -q -r ${FILE} my-build-dir
version=$( git rev-parse --short HEAD )
path=api/packages/${MY_ORG}$/generic/${MY_REPO}$/${version}
echo "Uploading Package with version: ${version} to https://${{ env.BASE_URL }}/${path}/${FILE}"
status=$(curl --user ${{ env.USER }}:${{ env.TOKEN }} --upload-file ${FILE} "https://${{ env.BASE_URL }}/${path}/${FILE}" --write-out "\nStatus: %{http_code}\n" | grep -o "201")
if [ ${status} != "201" ]; then echo "Upload failed, please check path and credentials"; exit 1; fi
Artifacts
Artifacts can be uploaded with the upload artifacts action . Note, that for self hosted runners v3 needs to be used. See Forgejo relevant info on uploading artifacts here: https://forgejo.org/docs/latest/user/actions/advanced-features/#artifacts . Artifacts are located under the respective pipeline run, e.g. https://your-forgejo.org/meissa/some-repo/actions/runs/24/artifacts/your-artifact .
# Example upload to forgejo artifact storage
name: build
on:
push:
branches:
- main
jobs:
build-site:
env:
USER: ${{ vars.USER }}
BASE_URL: "my.forgejo.instance"
runs-on: ubuntu-latest
container:
image: 'node:24.4'
steps:
- name: Checkout Code
uses: https://data.forgejo.org/actions/checkout@v4
- name: Build
run: |
echo "${{ env.USER }} at ${{ env.BASE_URL }}"
echo "Build"
make build
- name: Upload as Artifact
uses: https://data.forgejo.org/actions/upload-artifact@v3
with:
name: my-build
path: my-build-dir/
if-no-files-found: error
retention-days: 120
overwrite: true
Conclusion
The runner setup is fairly straight forward even considering registration and waiting for the forgejo deployment to be ready. Once registered on both sides, the runner shows up in all the settings, remember, we set it as global. The way it is designed here means it can be set up independently from the forgejo deployment which allows you get it running even on an older (but compatible) instance.
Creating workflows comes with some caveats that you need to know but it is all manageable. They are kept very close to the workflows of a well known git platform. So if you’re familliar with their actions, you’ll feel right at home with forgejo actions.