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.
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:
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
.
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
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.
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 FORGEJOINSTANCEURL. 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.
name: build-website
on:
pull_request:
push:
workflow_dispatch:
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 and variables can be set on different levels:
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 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 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
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.