# 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 ```bash # 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:///user/settings/applications" fi ``` ## Creating a PR ```bash 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:///api/v1` | | Auth header | `Authorization: token ` | Same format | | SSH remote | `git@github.com:o/r.git` | `git@: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): ```bash 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 ```bash # 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**: ```bash # 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: ```json { "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: ```bash curl -s "https:///$OWNER/$REPO/raw/branch/$BRANCH/$FILE_PATH" ``` For **private repos**, use the API contents endpoint: ```bash 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 ```bash PR_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 accumulate** — `GET /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.