Skip to main content
graphwiz.aigraphwiz.ai
← Back to Posts

Codeberg and Forgejo Actions: A Complete Guide for Self-Hosted CI/CD

DevOpsInfrastructure
DevOpsCodebergForgejoCI/CDself-hosteddigital-sovereignty

Table of Contents


Introduction

Codeberg is a free-software Git hosting platform running on Forgejo. Hosted in Europe, subject to GDPR, and governed by a non-profit, it answers a simple question: what if your code hosting wasn't at the mercy of a single corporation's pricing whims?

We're not here to sell you on Codeberg. The runner images are minimal. Some GitHub Actions don't have Forgejo equivalents. But for teams wanting self-hosted CI/CD without vendor lock-in, the trade-offs are worth understanding.

Forgejo Actions look almost like GitHub Actions. "Almost" hides some sharp edges. This guide covers the entire workflow, from CLI setup to production pipelines.


Getting Started with tea CLI

tea is the official CLI for Gitea and Forgejo. It handles repo management, issue tracking, secrets, and more from the terminal.

Installation

Method Command
Homebrew brew install tea
Chocolatey choco install gitea.tea
Scoop scoop install tea
Go go install code.gitea.io/tea@latest
AUR pacman -S tea
Binary dl.gitea.com/tea

Authentication

# Add your Codeberg login (use a personal access token from /user/settings/applications)
tea login add --name codeberg --url https://codeberg.org --token <TOKEN>

tea login default codeberg     # set as default
tea whoami                     # verify it works

Common Operations

tea repos list                               # list your repos
tea repo create --name my-project --private  # create a new repo
tea issues list                              # check issues
tea pr create --title "Fix auth"             # open a pull request
tea open                                     # open repo in browser

Use --repo owner/repo and --login codeberg when you need to target a specific repository or instance.


Managing Secrets

Secrets are the backbone of any CI/CD pipeline. You need them for deploy keys, API tokens, and SSH credentials.

Via CLI

tea actions secrets list                            # view existing secrets
tea actions secrets create DEPLOY_KEY               # create (prompts for value)
tea actions secrets delete DEPLOY_KEY               # remove a secret

Via Web UI

Navigate to /{owner}/{repo}/settings/actions/secrets in your browser.

Via API

For anything the CLI doesn't cover:

tea api /repos/{owner}/{repo}/actions/secrets \
  --method PUT \
  --body '{"name":"SSH_PRIVATE_KEY","data":"<base64-encoded-value>"}'

Setting Up an SSH Deploy Key

Here's a practical example. Generate a key pair, add the public key to your server's authorized_keys, then store the private key as a secret:

# Generate a deploy key
ssh-keygen -t ed25519 -f deploy_key -N ""

# Store the private key as a Codeberg secret
tea actions secrets create SSH_PRIVATE_KEY
# Paste the contents of deploy_key when prompted

# Add the public key to your target server
ssh-copy-id -i deploy_key.pub user@your-server.com

In workflows, reference secrets with ${{ secrets.SSH_PRIVATE_KEY }}. Forgejo also auto-injects useful variables: FORGEJO_TOKEN, FORGEJO_SERVER_URL, FORGEJO_REPOSITORY, FORGEJO_REF, and FORGEJO_SHA.


Forgejo Actions: Not Quite GitHub Actions

Forgejo Actions follow a "familiarity over compatibility" philosophy. The YAML looks like GitHub Actions. The behavior doesn't always match. This is the section to bookmark.

Key Differences

Aspect GitHub Actions Forgejo Actions
Job-level permissions Supported Ignored (use step-level)
Job-level timeout-minutes Supported Ignored (use step-level)
Job-level continue-on-error Supported Ignored (use step-level)
Default runner image ubuntu-latest (full suite) Debian bookworm + Node.js (minimal)
Context variable github.* only forgejo.*, forge.*, github.* (all aliased)
Action registry GitHub Marketplace https://data.forgejo.org/
LXC containers Not available Supported

The job-level vs. step-level distinction is the biggest migration headache. Set timeout-minutes: 10 at the job level and it silently does nothing. Move it to each step. Same for permissions and continue-on-error.

The runner image is the second surprise. GitHub's ubuntu-latest ships with Docker, SSH, and build tools. Forgejo's default runner is a lean Debian image with Node.js. Need SSH? Install it. Need Docker? Install it. Smaller images, but more verbose workflows.


Action URL Resolution

On GitHub, actions/checkout@v4 just works. On Forgejo, short-form resolution depends on instance configuration. Always use fully-qualified URLs to avoid surprises.

Common Action Mappings

GitHub Shorthand Forgejo URL
actions/checkout@v4 https://data.forgejo.org/actions/checkout@v4
actions/setup-node@v4 https://data.forgejo.org/actions/setup-node@v4
actions/setup-go@v5 https://data.forgejo.org/actions/setup-go@v5
actions/upload-artifact@v4 https://data.forgejo.org/actions/upload-artifact@v4
actions/download-artifact@v4 https://data.forgejo.org/actions/download-artifact@v4
actions/cache@v4 https://data.forgejo.org/actions/cache@v4

For actions that only exist on GitHub, reference them directly:

- uses: https://github.com/appleboy/ssh-action@v1

Note: the runner must reach github.com for this to work. In air-gapped setups, mirror or vendor those actions.


Practical CI/CD Example

Here's a complete deployment workflow that builds a Node.js app and deploys it via SSH:

# .forgejo/workflows/deploy.yml
name: Build and Deploy

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: docker  # Forgejo uses 'docker' label, not 'ubuntu-latest'
    steps:
      # Step-level timeout matters — job-level is ignored
      - name: Checkout
        timeout-minutes: 5
        uses: https://data.forgejo.org/actions/checkout@v4

      - name: Setup Node.js
        timeout-minutes: 5
        uses: https://data.forgejo.org/actions/setup-node@v4
        with:
          node-version: '22'

      - name: Install dependencies
        timeout-minutes: 10
        run: npm ci

      - name: Build
        timeout-minutes: 5
        run: npm run build

      # The runner is minimal — install SSH if the action needs it
      - name: Install SSH client
        timeout-minutes: 2
        run: apt-get update && apt-get install -y openssh-client

      # appleboy/ssh-action has no Forgejo mirror, use GitHub directly
      - name: Deploy via SSH
        timeout-minutes: 5
        uses: https://github.com/appleboy/ssh-action@v1
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: ${{ secrets.DEPLOY_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/app
            git pull origin main
            npm ci --production
            pm2 restart app

Notice the Forgejo-specific choices: the docker runner label, fully-qualified action URLs, and the explicit SSH client installation. Skip any of these and your pipeline breaks.


Common Pitfalls and Solutions

Problem Cause Fix
timeout-minutes ignored Set at job level Move to step level
Action not found Short-form URL without registry config Use fully-qualified URL
Runner missing SSH, Docker, etc. Default image is minimal apt-get install -y <package> in a step
Actions tab missing entirely Not enabled for the repo Settings → Units → enable Actions
Secret not accessible in workflow Wrong scope (env vs. repo) Ensure it's a repo or org secret
GitHub-only action fails in air-gapped env Runner can't reach github.com Mirror the action to your Forgejo instance
github.* context not working Old Forgejo version Update, or use forgejo.* / forge.* aliases

Conclusion

Codeberg makes sense when you need digital sovereignty, GDPR compliance, or freedom from platform lock-in. The trade-off is a smaller ecosystem and more verbose workflow files.

The key takeaways: always use fully-qualified action URLs, put timeouts and permissions at the step level, and expect a minimal runner image that needs explicit tool installation. Once you internalize those three rules, migrating from GitHub Actions to Forgejo Actions is straightforward.

If your team already knows GitHub Actions, the learning curve is measured in hours, not days. The tea CLI fills the gap that gh leaves behind, and the workflow syntax is close enough that muscle memory mostly transfers. Just watch out for those job-level gotchas.