Codeberg and Forgejo Actions: A Complete Guide for Self-Hosted CI/CD
Table of Contents
- Introduction
- Getting Started with tea CLI
- Managing Secrets
- Forgejo Actions: Not Quite GitHub Actions
- Action URL Resolution
- Practical CI/CD Example
- Common Pitfalls and Solutions
- Conclusion
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
```text
### Common Operations
```bash
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
```text
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
```bash
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
```text
### Via Web UI
Navigate to `/{owner}/{repo}/settings/actions/secrets` in your browser.
### Via API
For anything the CLI doesn't cover:
```bash
tea api /repos/{owner}/{repo}/actions/secrets \
--method PUT \
--body '{"name":"SSH_PRIVATE_KEY","data":"<base64-encoded-value>"}'
```text
### 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:
```bash
# 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
```text
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:
```yaml
- uses: https://github.com/appleboy/ssh-action@v1
```text
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:
```yaml
# .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
```text
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.