2026-07-18 23:38:41 +10:00

6.9 KiB

Gitea / Forgejo Workflow

Gitea (and its fork Forgejo) expose a GitHub-compatible REST API — most curl patterns from the GitHub skills work with minimal changes. This skill covers the differences and provides a complete workflow for self-hosted instances.

When to Use This Skill

  • The git remote points to a non-GitHub host (e.g. gitea@host:owner/repo.git)
  • gh CLI is not available or doesn't support the platform
  • You need to create PRs, check CI, or manage repos on a self-hosted Gitea instance

Auth Detection

# Extract owner/repo from the SSH remote
REMOTE_URL=$(git remote get-url origin)
OWNER_REPO=$(echo "$REMOTE_URL" | sed 's|.*:||; s|\.git$||')
OWNER=$(echo "$OWNER_REPO" | cut -d/ -f1)
REPO=$(echo "$OWNER_REPO" | cut -d/ -f2)

# Determine the Gitea API base from the remote
GITEA_HOST=$(echo "$REMOTE_URL" | sed 's|.*@||; s|:.*||')
GITEA_API="http://${GITEA_HOST}:3000/api/v1"  # default port, adjust if different

# Try to find a token
if [ -n "$GITEA_TOKEN" ]; then
  TOKEN="$GITEA_TOKEN"
elif [ -f "$HOME/.gitea_token" ]; then
  TOKEN=$(cat "$HOME/.gitea_token")
else
  echo "No GITEA_TOKEN found — API calls will fail for write operations"
  echo "Create a token at: https://<gitea-domain>/user/settings/applications"
fi

Creating a PR

BRANCH=$(git branch --show-current)

curl -s -X POST \
  -H "Authorization: token $TOKEN" \
  -H "Content-Type: application/json" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/pulls" \
  -d "{
    \"title\": \"feat: add user authentication\",
    \"body\": \"## Summary\\nAdds login and register API endpoints.\",
    \"head\": \"$BRANCH\",
    \"base\": \"master\"
  }"

Note: Gitea defaults to master not main for the base branch.

Key Differences from GitHub

Aspect GitHub Gitea
API base URL https://api.github.com https://<host>/api/v1
Auth header Authorization: token <PAT> Same format
SSH remote git@github.com:o/r.git git@<host>:o/r.git
gh CLI Works Not supported
Default branch main master
Auto-merge Supported via GraphQL Not supported

Posting Comments on a PR

Comments on a PR use the issues/comments endpoint (Gitea treats PRs as issues for comments):

curl -s -X POST \
  -H "Authorization: token $TOKEN" \
  -H "Content-Type: application/json" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/issues/${PR_NUMBER}/comments" \
  -d '{"body": "Your comment text here"}'

Checking PR Status

# Get PR details (state, mergeable, comment/review counts)
curl -s \
  -H "Authorization: token $TOKEN" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}" \
  | jq '{state, mergeable, comments, review_comments}'

# List comments on a PR
curl -s \
  -H "Authorization: token $TOKEN" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/issues/${PR_NUMBER}/comments" \
  | jq -r '.[] | "\(.id): \(.user.login) — \(.body[0:120])..."'

Monitoring CI Build Status

Gitea Actions exposes build status via the commit status API:

# Combined status (state: pending/success/failure/error)
curl -s -H "Authorization: token $TOKEN" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/commits/${SHA}/status" \
  | jq '{state, sha, total_count, statuses: [.statuses[] | {context, status, description, target_url}]}'

The response shape:

{
  "state": "pending",
  "sha": "a5342300...",
  "total_count": 1,
  "statuses": [{
    "context": "Build and Push Image / Build and push image (push)",
    "status": "pending",
    "description": "Waiting to run",
    "target_url": "/armistace/resbuilder_ai/actions/runs/25/jobs/0"
  }]
}

CRITICAL: Commit status API can return empty

The commit status API can return {"state":"","sha":"","total_count":0,"statuses":null} even when a build is actively running. This happens when:

  • The commit was pushed but the runner hasn't picked it up yet (push is still in progress — can take 30-60+ min for large images)
  • The runner is slow to report status back to Gitea
  • The build is running but hasn't updated the commit status yet

Do not treat an empty status response as "build complete" or "no build needed". Always cross-reference with runner logs to confirm.

Reading Raw File Content from a Branch

For public repos, the raw endpoint works directly:

curl -s "https://<gitea-domain>/$OWNER/$REPO/raw/branch/$BRANCH/$FILE_PATH"

For private repos, use the API contents endpoint:

curl -s \
  -H "Authorization: token $TOKEN" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/contents/${FILE_PATH}?ref=${BRANCH}" \
  | python3 -c "import sys,json,base64; raw=json.load(sys.stdin); print(base64.b64decode(raw['content']).decode())"

Important: The /contents endpoint resolves to the branch you specify in ?ref=. For PR head branches, use the branch name directly (e.g. ?ref=frontend-and-fixes) — using ref=pulls/9/head may return empty content for modified files because it resolves to the base branch's version of those files.

Merging a PR

PR_NUMBER=<number>

# Merge the PR via API (squash)
curl -s -X POST \
  -H "Authorization: token $TOKEN" \
  "${GITEA_API}/repos/${OWNER}/${REPO}/pulls/${PR_NUMBER}/merge" \
  -d '{"Do": "squash"}'

# Delete the remote branch after merge
BRANCH=$(git branch --show-current)
git push origin --delete $BRANCH
git checkout master && git pull origin master
git branch -d $BRANCH

What Doesn't Work

  • gh CLI — no Gitea backend. All operations use git + curl.
  • Gitea Actions REST API (/actions/runs) — returns 404 for non-admin users. Use the commit status API instead.
  • Gitea Actions web UI (/actions) — also returns 404 from bot tokens. Only the repo owner can see it via the browser.
  • Auto-merge — no GraphQL endpoint available.

Pitfalls

  • Gitea returns 404 for API calls without auth — even for public repos. Always include the token.
  • Default branch is master not main — adjust all base parameters.
  • HTTP vs HTTPS — many self-hosted instances run on plain HTTP. Match the protocol.
  • Token creation — at User Settings → Applications → Generate New Token. The repo scope covers everything.
  • PR comments use the issues endpoint — Gitea doesn't have a separate PR comment endpoint. Use /issues/{id}/comments.
  • Pushing to an existing PR branch — after pushing new commits, the PR updates automatically. No need to recreate it.
  • The raw endpoint — use /raw/branch/{branch}/{path} not /contents/{path} for direct file content.
  • Contents API with PR ref — using ?ref=pulls/N/head on the /contents endpoint returns the base branch version of modified files, not the PR head version. Always use the branch name directly.
  • Old statuses accumulateGET /commits/{sha}/statuses returns ALL statuses ever set for that commit. Filter by created_at to find the latest. Use GET /commits/{sha}/status (singular) for the combined/current state.