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) ghCLI 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
ghCLI — no Gitea backend. All operations usegit+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
masternotmain— adjust allbaseparameters. - HTTP vs HTTPS — many self-hosted instances run on plain HTTP. Match the protocol.
- Token creation — at
User Settings → Applications → Generate New Token. Thereposcope 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
rawendpoint — use/raw/branch/{branch}/{path}not/contents/{path}for direct file content. - Contents API with PR ref — using
?ref=pulls/N/headon the/contentsendpoint returns the base branch version of modified files, not the PR head version. Always use the branch name directly. - Old statuses accumulate —
GET /commits/{sha}/statusesreturns ALL statuses ever set for that commit. Filter bycreated_atto find the latest. UseGET /commits/{sha}/status(singular) for the combined/current state.