initial
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
# 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://<gitea-domain>/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://<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):
|
||||
|
||||
```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://<gitea-domain>/$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=<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.
|
||||
Reference in New Issue
Block a user