Introduction

When I decided to set up continuous integration (CI) on Codeberg, I ran into a fairly common situation: plenty of concepts are mentioned, but very few explanations are truly concrete. You can generally understand what Forgejo Actions is, but rarely how it actually translates into a repository, or how the different pieces interact with each other.

This article came out of that observation. Its goal is simple: to clearly explain how to use Codeberg’s CI for a concrete use case—building and publishing a Hugo static site.

What Codeberg actually provides as CI

Codeberg is built on Forgejo and provides an integrated CI system called Forgejo Actions. There is no external service to connect, no machines to manage, and no infrastructure to maintain. CI is available by default as soon as a repository exists.

Workflows are executed on shared Linux runners operated by Codeberg. These runners are deliberately limited in terms of resources and execution time. This constraint clearly defines the scope of the platform: Forgejo Actions is designed for simple, reproducible, and fast tasks.

Within this context, generating a static site naturally fits the intended use cases.

How Codeberg detects a workflow

For a workflow to be executed, Forgejo Actions enforces a strict rule: the YAML file must be located in a specific directory of the repository.

On Codeberg, workflows must be placed in:

.forgejo/workflows/

For example:

.forgejo/workflows/hugo.yml

As soon as a valid file is present at this location and pushed to the repository, Codeberg automatically analyzes it. If triggering events are defined, the CI runs without any additional action in the web interface.

A perfectly written pipeline placed anywhere else in the repository will never be executed.

A simple and explicit Hugo pipeline

To understand Codeberg’s CI, it is important to start with a deliberately simple pipeline. The goal is not to cover every possible scenario, but to show a real, readable workflow that is sufficient for a static site.

The .forgejo/workflows/hugo.yml file can, for example, contain:

name: Build Hugo site

This line gives the workflow a name. It is displayed as-is in the Actions tab of the repository and makes it easy to quickly identify what the CI does.

The triggering events are then defined:

on:
  push:
    branches: [ main ]
  pull_request:

The workflow runs on every push to the main branch, as well as on every pull request. This makes it possible to verify that the site builds correctly before a merge and to ensure that the main branch always remains publishable.

The workflow then defines a single job:

jobs:
  build:

A workflow can contain multiple jobs, but in this case a single one is sufficient. The name build is arbitrary and serves only for readability.

The job runs on a Linux runner provided by Codeberg:

    runs-on: ubuntu-latest

This is a standard, shared environment, but more than adequate for building a static site.

The steps of the job are then described in order:

    steps:
      - uses: actions/checkout@v4

This step retrieves the contents of the repository into the runner environment. This line uses an action, meaning a reusable component of the Forgejo Actions engine used by Codeberg.

Forgejo Actions largely follows the GitHub Actions model. Some well-known actions, such as actions/checkout, work on Codeberg because they are mirrored and do not depend on GitHub-specific services.

Actions compatible with Forgejo are mainly hosted on the official Forgejo instance and can be explored here: https://code.forgejo.org/explore/repos

Hugo is then installed explicitly:

      - name: Install Hugo
        run: sudo apt-get update && sudo apt-get install -y hugo

There is currently no dedicated Forgejo action for installing Hugo. Installing it via the system package manager is therefore the simplest and most readable solution. It makes it clear exactly what is installed and how.

Finally, the site generation is triggered:

      - name: Build site
        run: hugo

If this command fails, the job fails immediately, and the CI clearly indicates that the site cannot be built in its current state.

Secrets: where they are defined

As long as you only build the site, no secrets are required. Secrets come into play only when publishing.

On Codeberg, secrets are defined directly in the repository’s web interface, not in the workflow file. They are stored server-side and are never included in the source code.

Concretely, from the repository page, they are added via:

Settings → Secrets → Actions

A secret is defined by a name and a value. The name is visible; the value is never shown again after being saved. Once created, the secret is associated with the repository and is accessible only during the execution of a Forgejo Actions workflow.

How a secret is used in a workflow

A secret is never injected automatically. The workflow must explicitly decide where and how it is used.

In a YAML file, a secret is referenced via the secrets context. For example, to expose a key to a deployment command:

      - name: Deploy site
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
        run: |
          echo "$DEPLOY_KEY" > key
          chmod 600 key
          # deployment commands

The value of the secret never appears in the logs unless it is explicitly printed. It exists only in the job’s environment, for the duration of its execution.

This approach clearly separates versioned code from sensitive information, while keeping the workflow readable.

Conclusion

The CI provided by Codeberg is deliberately minimalistic, but consistent. It does not aim to cover every possible use case and instead operates within a clearly defined scope.

By understanding where to place a workflow, how it is triggered, and how secrets are defined and used, it is possible to set up a reliable, readable, and easy-to-maintain automation for publishing a Hugo static site.