Skip to content

Gitea, Forgejo & Codeberg

This guide covers lightweight, GitHub-compatible Git forges:

  • Gitea - Self-hosted Git service
  • Forgejo - Community fork of Gitea
  • Codeberg - Hosted Forgejo instance for open source
  • Gogs - Predecessor to Gitea (limited API)

These platforms share similar APIs based on GitHub’s design, making integration straightforward.

origin = git.origin(
url = "https://gitea.company.com/owner/repo.git",
ref = "main",
)
destination = git.destination(
url = "https://codeberg.org/owner/repo.git",
push = "copybara/sync", # Feature branch for PR
)
core.workflow(
name = "sync",
origin = git.origin(
url = "https://github.com/org/source-repo.git",
ref = "main",
),
destination = git.destination(
url = "https://codeberg.org/owner/dest-repo.git",
push = "copybara/sync",
),
authoring = authoring.overwrite("Sync Bot <sync@example.com>"),
transformations = [
metadata.squash_notes(),
],
mode = "SQUASH",
)

Create an access token in the platform’s settings:

  1. Go to SettingsApplicationsAccess Tokens
  2. Create token with permissions: write:repository, write:issue
  3. Use token in URL
Terminal window
# Configure credentials
git config --global credential.helper store
echo "https://username:$TOKEN@codeberg.org" >> ~/.git-credentials

The tea CLI is the official Gitea command-line tool:

Terminal window
# Install tea
go install code.gitea.io/tea@latest
# Or download binary
curl -fsSL https://dl.gitea.io/tea/latest/tea-latest-linux-amd64 -o tea
chmod +x tea
# Login (interactive)
tea login add
# Or login non-interactively
tea login add \
--name codeberg \
--url https://codeberg.org \
--token $GITEA_TOKEN
#!/bin/bash
set -e
# Run Copybara first
java -jar copybara.jar migrate copy.bara.sky sync
# Create PR using tea
tea pr create \
--head copybara/sync \
--base main \
--title "Sync from source repository" \
--description "Automated sync via Copybara."
Terminal window
tea pr create \
--head copybara/sync \
--base main \
--title "Sync from source repository" \
--assignees "reviewer1,reviewer2" \
--labels "automated,sync" \
--milestone "v1.0"
#!/bin/bash
set -e
GITEA_URL="https://codeberg.org" # Or your Gitea instance
OWNER="your-owner"
REPO="your-repo"
SOURCE_BRANCH="copybara/sync"
TARGET_BRANCH="main"
# Run Copybara
java -jar copybara.jar migrate copy.bara.sky sync
# Check if branch exists
if git ls-remote --exit-code origin "$SOURCE_BRANCH" >/dev/null 2>&1; then
# Create PR
curl -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"title\": \"Sync from source repository\",
\"head\": \"$SOURCE_BRANCH\",
\"base\": \"$TARGET_BRANCH\",
\"body\": \"Automated sync via Copybara.\"
}" \
"$GITEA_URL/api/v1/repos/$OWNER/$REPO/pulls"
fi

Use Copybara’s http.endpoint() for fully integrated PR creation:

def _create_gitea_pr(ctx):
"""Create a Gitea/Forgejo/Codeberg pull request."""
gitea_host = "codeberg.org" # Or your Gitea instance
owner = "your-owner"
repo = "your-repo"
response = ctx.destination.post(
url = "https://{}/api/v1/repos/{}/{}/pulls".format(
gitea_host, owner, repo
),
headers = {"Content-Type": "application/json"},
body = http.json({
"title": "Sync from source: " + ctx.refs[0].ref,
"head": "copybara/sync",
"base": "main",
"body": "Automated sync via Copybara.\n\nSource: " + ctx.refs[0].url,
}),
)
if response.status_code == 201:
return ctx.success()
elif response.status_code == 409:
# PR already exists
ctx.console.info("PR already exists, skipping creation")
return ctx.success()
else:
return ctx.error("Failed to create PR: " + str(response.status_code))
core.feedback(
name = "create_gitea_pr",
origin = git.github_trigger(
url = "https://github.com/org/source-repo",
events = ["push"],
),
destination = http.endpoint(
hosts = [
http.host(
host = "codeberg.org", # Or your Gitea instance
auth = http.bearer_auth(
creds = credentials.static_secret("gitea_token", "GITEA_TOKEN"),
),
),
],
),
actions = [core.action(impl = _create_gitea_pr)],
)

name: Sync to Codeberg
on:
push:
branches: [main]
workflow_dispatch:
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Download Copybara
run: |
curl -fsSL -o copybara.jar \
https://github.com/google/copybara/releases/latest/download/copybara_deploy.jar
- name: Configure Git
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
- name: Run Copybara
env:
GITEA_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
run: |
# Configure Codeberg credentials
echo "https://token:$GITEA_TOKEN@codeberg.org" >> ~/.git-credentials
git config --global credential.helper store
# Run Copybara
java -jar copybara.jar migrate copy.bara.sky sync --ignore-noop
- name: Create Pull Request
if: success()
env:
GITEA_TOKEN: ${{ secrets.CODEBERG_TOKEN }}
run: |
# Check if PR already exists
EXISTING_PR=$(curl -s \
-H "Authorization: token $GITEA_TOKEN" \
"https://codeberg.org/api/v1/repos/${{ vars.CODEBERG_OWNER }}/${{ vars.CODEBERG_REPO }}/pulls?state=open" \
| jq '[.[] | select(.head.ref == "copybara/sync")] | length')
if [ "$EXISTING_PR" = "0" ]; then
curl -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"title": "Sync from GitHub",
"head": "copybara/sync",
"base": "main"
}' \
"https://codeberg.org/api/v1/repos/${{ vars.CODEBERG_OWNER }}/${{ vars.CODEBERG_REPO }}/pulls"
else
echo "PR already exists, skipping creation"
fi

If your source is in Gitea and you’re syncing elsewhere:

.gitea/workflows/sync.yml
name: Sync to Destination
on:
push:
branches: [main]
jobs:
sync:
runs-on: ubuntu-latest
container:
image: eclipse-temurin:21-jdk
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download Copybara
run: |
curl -fsSL -o copybara.jar \
https://github.com/google/copybara/releases/latest/download/copybara_deploy.jar
- name: Configure Git
run: |
git config --global user.name "Gitea Actions"
git config --global user.email "actions@gitea.local"
echo "https://token:${{ secrets.DEST_TOKEN }}@github.com" >> ~/.git-credentials
git config --global credential.helper store
- name: Run Copybara
run: java -jar copybara.jar migrate copy.bara.sky sync --ignore-noop

For Gitea/Forgejo with Woodpecker CI:

.woodpecker.yml
pipeline:
sync:
image: eclipse-temurin:21-jdk
commands:
- curl -fsSL -o copybara.jar https://github.com/google/copybara/releases/latest/download/copybara_deploy.jar
- git config --global user.name "Woodpecker CI"
- git config --global user.email "woodpecker@ci.local"
- echo "https://token:$DEST_TOKEN@github.com" >> ~/.git-credentials
- git config --global credential.helper store
- java -jar copybara.jar migrate copy.bara.sky sync --ignore-noop
secrets: [DEST_TOKEN]
when:
branch: main

Base URL: https://your-instance.com/api/v1 (or https://codeberg.org/api/v1)

EndpointMethodDescription
/repos/{owner}/{repo}/pullsPOSTCreate PR
/repos/{owner}/{repo}/pullsGETList PRs
/repos/{owner}/{repo}/pulls/{id}GETGet PR
/repos/{owner}/{repo}/pulls/{id}PATCHUpdate PR
/repos/{owner}/{repo}/pulls/{id}/mergePOSTMerge PR

Authentication: Authorization: token YOUR_TOKEN

Documentation:

Terminal window
curl -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/$OWNER/$REPO/pulls?state=open"
Terminal window
# Filter in response - API doesn't have direct filter
curl -s -H "Authorization: token $GITEA_TOKEN" \
"$GITEA_URL/api/v1/repos/$OWNER/$REPO/pulls?state=open" \
| jq '.[] | select(.head.ref == "copybara/sync")'
Terminal window
curl -X PATCH \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"labels": [1, 2, 3]}' \
"$GITEA_URL/api/v1/repos/$OWNER/$REPO/pulls/$PR_ID"
Terminal window
curl -X POST \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"reviewers": ["username1", "username2"]}' \
"$GITEA_URL/api/v1/repos/$OWNER/$REPO/pulls/$PR_ID/requested_reviewers"

FeatureGiteaForgejoCodebergGogs
API CompatibilityGitHub-likeGitHub-likeGitHub-likeLimited
Actions CIYesYesYesNo
Tea CLIYesYesYesNo
PR ReviewersYesYesYesNo
Draft PRsYes (1.17+)YesYesNo

fatal: Authentication failed for 'https://codeberg.org/...'

Solutions:

  • Verify token has write:repository scope
  • Check token hasn’t expired
  • Ensure correct URL format (https://token:$TOKEN@host)
{ "message": "head and base must be different" }

Solutions:

  • Verify source and target branches are different
  • Check branch names are correct
  • Ensure Copybara actually pushed changes
{ "message": "pull request already exists" }

Solutions:

  • Query for existing PRs before creating
  • Update the existing PR instead
  • Close old PR and create new one
{ "message": "branch does not exist" }

Solutions:

  • Verify Copybara pushed the branch successfully
  • Check branch name matches exactly (case-sensitive)
  • Wait for push to complete before API call