Lee Cheng Hui

forrus - A bridge for Forgejo & Cirrus CI

Background

I have a personal git service powered by Forgejo and I’m actively looking for a CI solution to integrate with it. Unlike a managed service like GitHub where everything is provided, running my own instance means I have to sort things out on my own.

Forgejo offers a (mostly) drop in replacement for GitHub Actions called Forgejo Actions, which is quite nice after some light testing from my end. However, there are a few things that I feel like having a managed CI service is better compared to self-hosting it on a VPS:

Looking for solutions

I then found out about Cirrus CI, a managed CI provider that offers per-second billing. Their features tick my checkboxes and the pricing looks reasonable too. However, they only support GitHub, and many features depend heavily on GitHub app installed on the account.

I then found out about their GraphQL API and thought maybe I could trigger a build automatically by listening to the webhook event from my Forgejo instance. Vitaly wrote an article on his approach (cirrus-run) and it gave me a confirmation that this would work.

His approach is genius. He had a dummy GitHub repository to act as the owner of the build, and used custom clone script to clone the real repository from GitLab.

That’s done for Cirrus CI side, but how about Forgejo side? In Vitaly’s approach, he used GitLab CI runner to trigger the build and needed the runner to be active throughout the entire lifespan of the remote build. I don’t want to have a Forgejo runner doing the same thing. After some research, I discovered yojo, a CI bridge for Forgejo & SourceHut builds. It works by updating the commit status with the remote build status (Running…, Done, Pending…, etc) and a link to the remote build. It periodically polls the result from remote and update the status respectively.

Let’s make yojo and cirrus-run a baby

So, with both sides of the equation completed, I began to architect my solution. I could run a small binary on my VPS that actively listens to webhook events from my Forgejo instance. When a webhook arrived, it fires a remote build request to Cirrus CI and my program would periodically poll the status. It would update the commit status in my code, so I could get the little green tick:

commit-green-tick.png

Thanks to vibe-coding and the beauty of open source (cirrus-run and yojo), I was able to complete the skeleton code of the program rather quickly. I named it forrus (Forgejo + Cirrus CI) and the source code is hosted on Codeberg.

Furnishing with some details

Since I was submitting the job via API and used a dummy repo, I was missing tons of useful environment variables such as CIRRUS_BRANCH, CIRRUS_PR and CIRRUS_BASE_SHA, that would help in writing condition in the .cirrus.yml file.

For example, the following snippet wouldn’t work without the above variables:

build_task:
  only_if: ${CIRRUS_BRANCH} == "main"

To solve the above issue, I injected a top-level env block into the build yml file with some of the useful variables using the information from the webhook :

With that, I can use those variables to write my condition:

build_task:
-  only_if: ${CIRRUS_BRANCH} == "main"
+  only_if: ${FORRUS_BRANCH} == "main"

However, there are still some useful features that I couldn’t replicate at the moment, such as:

build_task:
  skip: "changesIncludeOnly('doc/**')"

which will conditionally trigger/skip the build based on the file changed in that commit. I think the GitHub app would query the commit changes and decide whether to submit that particular task. For now, I prefer not to reinvent the logic in forrus and let Cirrus CI handle any build logic.

But hey, I’m happy with the features that it supports now.