Adds a comprehensive opencode skill under .opencode/skills/forgejo-api/
covering all 473 Forgejo REST API endpoints across 25 reference categories.
- 78 files, 23,000+ lines, 149 distinct path parameter types
- Every curl command parameterised ({owner}/{repo}/{index}/etc) and
tested against the live git.cleverthis.com server
- SKILL.md: 917-line entry point with quick-answer curl commands (35),
jq cheat sheet for chaining API calls, 14 decision trees, 12 critical
concepts (exclusive labels, lazy mergeability, SHA locking, auto-close
keywords, search envelope differences, 412 stale-edit protection), full
HTTP status code table, and environment variable reference
- references/pull-requests/: CRUD, 6 merge styles, automerge, server-side
rebase without local clone, inline review comments, diff/patch
- references/issues/: comments, reactions, attachments, dependencies,
time tracking, stopwatches, pinning
- references/labels/: repo + org labels, exclusive label groups,
GET/POST/PUT/DELETE on issues and PRs
- references/ci-actions/ + references/commit-statuses/: workflow runs,
dispatch, secrets, variables, quality gate verification
- references/web-interface/ci-logs.md: step-by-step CI log access via
CSRF web session (not available through REST API)
- references/complex-workflows/: 10 multi-step recipes including
PR review cycle, issue lifecycle, CI status check, server-side rebase,
automerge, release workflow, org setup, fork contribution
40 KiB
name, description, references
| name | description | references | |||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| forgejo-api | Exhaustive Forgejo REST API skill covering all 473 endpoints across 25 categories. Use this skill whenever you need to interact with the Forgejo API via curl or need to understand Forgejo API behavior, parameters, response shapes, or error handling. Covers: authentication (tokens, basic auth, OAuth2, web login/CSRF), repositories (CRUD, settings, forks, deploy keys, topics, avatars, flags, issue config, transfers, mirrors, collaborators, stars/watchers), issues (CRUD, comments, reactions, attachments, dependencies, time tracking, stopwatches, pinning, subscriptions), pull requests (CRUD, reviews with inline comments, merging all 6 styles, automerge scheduling, server-side rebase without local clone, changed files, diff/patch), branches and tags (CRUD, rename, branch protections, tag protections), labels (repo-level, org-level, exclusive labels, label templates), milestones, organizations (CRUD, teams, members, quota, blocks), users (profile, SSH/GPG keys, API tokens with scopes, emails, quota, followers, settings), files and content (CRUD with SHA locking, raw, media, archives, diffpatch, multi-file commits), CI/CD actions (workflow runs, dispatch, secrets, variables, runner registration), commit statuses (quality gates, combined status, individual checks), webhooks (repo, org, user, system, git hooks, all event types), notifications (threads, filtering, pinning), releases (CRUD, asset upload), wiki, packages (20 package types), git objects (blobs, commits, trees, refs, notes), ActivityPub federation, miscellaneous (gitignore/license templates, markdown rendering, nodeinfo), activity feeds, admin operations (users, orgs, cron, quota, unadopted repos), web interface (CI log access via CSRF session), and 10 complex multi-step workflow recipes (PR review cycle, issue lifecycle, CI status check, release, branch protection setup, org setup, fork contribution, server-side rebase, automerge, tips & patterns). |
|
Forgejo REST API Skill
Complete reference for the Forgejo REST API. 473 endpoints across 25 categories, 77 reference files, 149 distinct path parameter types, all curl commands fully parameterized and tested against a live Forgejo 14.0.4 server.
🌐 Environment Variables
This skill assumes the following shell variables are set:
| Variable | Purpose | Example |
|---|---|---|
${FORGEJO_URL} |
Base server URL (no trailing slash) | https://git.cleverthis.com |
${FORGEJO_PAT} |
Personal Access Token (primary bot) | abc123... |
${FORGEJO_USERNAME} |
Username of the token owner | HAL9000 |
${FORGEJO_PASSWORD} |
Password (basic auth, token mgmt only) | secret |
${FORGEJO_REVIEWER_PAT} |
PAT for reviewer bot (second identity) | def456... |
${FORGEJO_REVIEWER_USERNAME} |
Reviewer bot username | reviewer-bot |
Validate your token is working:
curl -s "${FORGEJO_URL}/api/v1/user" -H "Authorization: token ${FORGEJO_PAT}" | jq '.login'
⚡ Quick Answers (No Reference Load Needed)
Issues & PRs — Most Common Operations
Get a single issue or PR (with labels, milestone, assignees):
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
-H "Authorization: token ${FORGEJO_PAT}"
List open issues:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues?state=open&type=issues&limit=50" \
-H "Authorization: token ${FORGEJO_PAT}"
List open PRs:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls?state=open&limit=50" \
-H "Authorization: token ${FORGEJO_PAT}"
Get a PR's full state (mergeable, CI, labels, milestone, base/head SHAs):
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
-H "Authorization: token ${FORGEJO_PAT}"
Add a comment to an issue or PR:
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/comments" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"body": "{comment_text}"}'
List comments on an issue or PR:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/comments" \
-H "Authorization: token ${FORGEJO_PAT}"
Close an issue or PR:
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"state": "closed"}'
Edit issue or PR title/body/assignees/milestone:
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"title": "{new_title}", "milestone": {milestone_id}}'
Edit a PR (update title, body, base branch, assignees, allow maintainer edit):
curl -s -X PATCH "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"title": "{new_title}", "body": "{new_body}"}'
Create an issue:
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"title": "{title}", "body": "{body}", "labels": [{label_id}], "milestone": {milestone_id}}'
Create a pull request:
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"title": "{title}", "body": "{body}", "head": "{head_branch}", "base": "{base_branch}"}'
List PR reviews:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews" \
-H "Authorization: token ${FORGEJO_PAT}"
Approve a PR:
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/reviews" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"event": "APPROVED", "body": "{review_comment}"}'
Merge a PR:
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/merge" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"Do": "squash", "delete_branch_after_merge": true}'
List files changed in a PR:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/files" \
-H "Authorization: token ${FORGEJO_PAT}"
Get PR diff:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}.diff" \
-H "Authorization: token ${FORGEJO_PAT}"
Add issue dependency (this issue depends on another):
curl -s -X POST "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"index": {dependency_index}}'
List issue dependencies:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/dependencies" \
-H "Authorization: token ${FORGEJO_PAT}"
Labels
Get all repo labels (with IDs):
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/labels?limit=50" \
-H "Authorization: token ${FORGEJO_PAT}"
Get all org labels (with IDs):
curl -s "${FORGEJO_URL}/api/v1/orgs/{org}/labels?limit=50" \
-H "Authorization: token ${FORGEJO_PAT}"
Get labels currently on an issue or PR:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels" \
-H "Authorization: token ${FORGEJO_PAT}"
Apply labels to an issue or PR (replace all):
curl -s -X PUT "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels" \
-H "Authorization: token ${FORGEJO_PAT}" -H "Content-Type: application/json" \
-d '{"labels": [{label_id_1}, {label_id_2}]}'
Remove a single label by ID:
curl -s -X DELETE "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}/labels/{label_id}" \
-H "Authorization: token ${FORGEJO_PAT}"
Milestones
List all milestones:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=all&limit=50" \
-H "Authorization: token ${FORGEJO_PAT}"
Get milestone completion percentage:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}" \
-H "Authorization: token ${FORGEJO_PAT}" \
| jq '{title, state, open: .open_issues, closed: .closed_issues,
pct: (if (.open_issues + .closed_issues) > 0
then (.closed_issues * 100 / (.open_issues + .closed_issues) | floor)
else 0 end)}'
CI & Commits
Get combined CI status for a branch or SHA:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/commits/{branch_or_sha}/status" \
-H "Authorization: token ${FORGEJO_PAT}"
List workflow runs:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/actions/runs?limit=20" \
-H "Authorization: token ${FORGEJO_PAT}"
Get a specific workflow run:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/actions/runs/{run_id}" \
-H "Authorization: token ${FORGEJO_PAT}"
List recent commits on a branch:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/commits?sha={branch}&limit=20" \
-H "Authorization: token ${FORGEJO_PAT}"
Files & Repos
Get file content (returns base64, also gives current SHA for updates):
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/contents/{filepath}?ref={branch}" \
-H "Authorization: token ${FORGEJO_PAT}"
List branches:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/branches?limit=50" \
-H "Authorization: token ${FORGEJO_PAT}"
List repo notifications:
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/notifications?status-types=unread" \
-H "Authorization: token ${FORGEJO_PAT}"
🔑 Authentication Quick Reference
| Method | Header / Flag | When to use |
|---|---|---|
| Token (PAT) | -H "Authorization: token ${FORGEJO_PAT}" |
All REST API calls — primary method |
| Basic Auth | -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}" |
Only for token CRUD (/users/{username}/tokens) |
| OAuth2 Bearer | -H "Authorization: Bearer {access_token}" |
After OAuth2 authorization code flow |
| Reviewer PAT | -H "Authorization: token ${FORGEJO_REVIEWER_PAT}" |
Approving PRs as a second identity |
| Web Session | Cookie-based CSRF flow | CI action logs only — not in REST API |
Multi-identity auth:
${FORGEJO_REVIEWER_PAT}/${FORGEJO_REVIEWER_USERNAME}are the second bot account. Use them when branch protection requires approval from someone other than the PR author. Seereferences/authentication/README.md.
🚨 Critical Concepts (Always Keep in Mind)
PRs Are Issues
Every pull request is also an issue. PR index = issue index — the same number. All issue endpoints work on PRs: comments, labels, reactions, subscriptions, pins, timeline, dependencies, time tracking.
# These are equivalent for a PR numbered {index}:
GET /repos/{owner}/{repo}/issues/{index}/comments # ✓ works for PRs
GET /repos/{owner}/{repo}/issues/{index}/labels # ✓ works for PRs
Exclusive Labels
Labels with exclusive: true and a name containing / (e.g. State/Open,
State/Closed) are mutually exclusive within their prefix group. Applying one
automatically removes the others in the same group.
Always use PUT (replace-all) not POST (add) when managing exclusive label
groups, to avoid leaving conflicting labels behind.
Mergeability Is Lazy-Computed
PR.mergeable is null immediately after a push — Forgejo hasn't computed it yet.
Poll until it is true or false. true = no conflicts, false = conflicts exist.
Label Lookup: IDs and Names Both Work
PUT/POST /issues/{index}/labels accepts either integer IDs or string names:
{"labels": [42, 7]} // by ID (faster)
{"labels": ["bug", "State/Open"]} // by name
Labels also come embedded in every issue/PR response — no separate fetch needed when you've already loaded the issue.
SHA Locking for File Updates
PUT /repos/{owner}/{repo}/contents/{filepath} requires "sha": "{current_sha}" in
the body — the SHA of the current version of the file. Fetch it first:
SHA=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/contents/{filepath}" \
-H "Authorization: token ${FORGEJO_PAT}" | jq -r '.sha')
Then update with "sha": "${SHA}". Returns 409 if the SHA is stale.
Auto-Close Keywords in PR Bodies
If a PR body contains Closes #N, Fixes #N, or Resolves #N (one per line),
Forgejo automatically closes those issues when the PR is merged. Multiple issues
can be referenced. Example:
Closes #42
Closes #43
Token Scopes
Tokens need explicit scopes at creation. The "all" scope grants full access.
Common scopes: read:repository, write:repository, read:issue, write:issue,
read:user, write:user, read:organization, write:organization.
The full token value (sha1) is only returned once at creation — save it.
CI Logs Require Web Login
Workflow step logs are not available via REST API. Commit statuses (via
GET /commits/{sha}/status) contain target_url fields with links to run/job pages.
Access logs through those URLs using a web session.
→ references/web-interface/ci-logs.md
Search Response Envelopes Differ
Different search endpoints return different shapes — always check:
GET /repos/search → {"ok": true, "data": [...]} use .data[]
GET /repos/issues/search → [...] direct array
GET /users/search → {"ok": true, "data": [...]} use .data[]
GET /topics/search → {"topics": [...]} use .topics[]
GET /orgs/teams/search → [...] direct array
All other list endpoints → [...] direct array
Error Response Format
All API errors return:
{"message": "error description", "url": "https://.../api/swagger", "errors": [...]}
Use jq -r '.message' to extract the error message. The errors array has details.
Notification Parameters Are Query Strings
PUT /notifications uses query parameters (?to-status=read&status-types=unread),
not a JSON body. Both forms work in practice, but query params are canonical per spec.
412 Precondition Failed — Stale Edit Protection
When editing issues or PRs via PATCH, you can include "updated_at": "{timestamp}"
in the body. If the server's record is newer, it returns 412 to prevent overwriting
a concurrent edit. Omit updated_at to skip this check.
🔧 jq Cheat Sheet for Chaining API Calls
These patterns extract the values most commonly needed to chain one API call into the next:
# From a PR response:
PR=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}" \
-H "Authorization: token ${FORGEJO_PAT}")
HEAD_SHA=$(echo "$PR" | jq -r '.head.sha') # head commit SHA
HEAD_BRANCH=$(echo "$PR" | jq -r '.head.ref') # head branch name
BASE_BRANCH=$(echo "$PR" | jq -r '.base.ref') # base branch name
MERGE_BASE=$(echo "$PR" | jq -r '.merge_base') # common ancestor SHA
MERGEABLE=$(echo "$PR" | jq -r '.mergeable') # true/false/null
MERGED=$(echo "$PR" | jq -r '.merged') # true/false
STATE=$(echo "$PR" | jq -r '.state') # open/closed
PR_NUM=$(echo "$PR" | jq -r '.number') # PR index number
LABELS=$(echo "$PR" | jq '[.labels[].id]') # label IDs as array
# From a combined commit status response:
STATUS=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/commits/${HEAD_SHA}/status" \
-H "Authorization: token ${FORGEJO_PAT}")
COMBINED_STATE=$(echo "$STATUS" | jq -r '.state') # success/failure/pending/error
TOTAL=$(echo "$STATUS" | jq -r '.total_count') # number of checks
FAILING=$(echo "$STATUS" | jq -r '.statuses[] | select(.state=="failure") | .context')
TARGET_URLS=$(echo "$STATUS" | jq -r '.statuses[].target_url') # links to CI jobs
# From an issue response:
ISSUE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
-H "Authorization: token ${FORGEJO_PAT}")
MILESTONE_ID=$(echo "$ISSUE" | jq -r '.milestone.id // empty')
LABEL_IDS=$(echo "$ISSUE" | jq '[.labels[].id]')
LABEL_NAMES=$(echo "$ISSUE" | jq -r '[.labels[].name] | join(",")')
ASSIGNEES=$(echo "$ISSUE" | jq -r '[.assignees[].login] | join(",")')
# Find label ID by name from a label list:
LABELS=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/labels?limit=50" \
-H "Authorization: token ${FORGEJO_PAT}")
LABEL_ID=$(echo "$LABELS" | jq -r '.[] | select(.name == "{label_name}") | .id')
# Find milestone ID by title:
MILESTONES=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=all&limit=50" \
-H "Authorization: token ${FORGEJO_PAT}")
MS_ID=$(echo "$MILESTONES" | jq -r '.[] | select(.title == "{milestone_title}") | .id')
# From a file content response:
FILE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/contents/{filepath}" \
-H "Authorization: token ${FORGEJO_PAT}")
FILE_SHA=$(echo "$FILE" | jq -r '.sha') # required for updates
CONTENT=$(echo "$FILE" | jq -r '.content' | base64 -d) # decoded content
# From a workflow run list (repos/search has .data wrapper):
RUNS=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/actions/runs?limit=10" \
-H "Authorization: token ${FORGEJO_PAT}")
RUN_ID=$(echo "$RUNS" | jq -r '.workflow_runs[0].id')
RUN_STATUS=$(echo "$RUNS" | jq -r '.workflow_runs[0].status') # waiting/running/success/failure
# From repos/search (note: wrapped in .data):
REPOS=$(curl -s "${FORGEJO_URL}/api/v1/repos/search?q={query}&limit=20" \
-H "Authorization: token ${FORGEJO_PAT}")
REPO_NAMES=$(echo "$REPOS" | jq -r '.data[].full_name')
# Check HTTP status code:
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
"${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}/merge" \
-H "Authorization: token ${FORGEJO_PAT}")
# 204 = PR is merged, 404 = PR not merged
# Handle errors gracefully:
RESPONSE=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}" \
-H "Authorization: token ${FORGEJO_PAT}")
if echo "$RESPONSE" | jq -e '.message' > /dev/null 2>&1; then
echo "Error: $(echo "$RESPONSE" | jq -r '.message')"
else
# process successful response
echo "$RESPONSE" | jq -r '.title'
fi
🗂️ Navigation Decision Trees
"I need to work with pull requests"
Pull request operations?
├─ List/get PRs → GET /repos/{owner}/{repo}/pulls
│ ├─ Filter by author → ?poster={username}
│ ├─ Filter by label → ?labels={label_name}
│ ├─ Check if mergeable → PR.mergeable (null=computing, true=ok, false=conflicts)
│ ├─ Check if PR is stale → compare PR.merge_base vs current base branch HEAD SHA
│ └─ Full reference → references/pull-requests/README.md
├─ Review a PR
│ ├─ List all reviews → GET /pulls/{index}/reviews
│ ├─ Approve → POST /pulls/{index}/reviews {"event": "APPROVED"}
│ ├─ Request changes → POST /pulls/{index}/reviews {"event": "REQUEST_CHANGES"}
│ ├─ Add inline comments → POST /pulls/{index}/reviews with comments[{path,body,new_position}]
│ ├─ Request specific reviewers → POST /pulls/{index}/requested_reviewers
│ └─ Full guide → references/pull-requests/reviews.md
├─ Merge a PR
│ ├─ Styles: merge | rebase | rebase-merge | squash | fast-forward-only
│ ├─ Auto-merge when CI passes → body: {"merge_when_checks_succeed": true, "Do": "squash"}
│ ├─ Cancel auto-merge → DELETE /pulls/{index}/merge
│ ├─ Check if auto-merge is set → DELETE and check: 204=was set, 404=not set
│ └─ Full guide → references/pull-requests/merging.md
├─ Update stale PR branch (no local clone)
│ ├─ POST /pulls/{index}/update?style=rebase (rebase head onto base)
│ ├─ POST /pulls/{index}/update?style=merge (merge base into head)
│ └─ Full guide → references/complex-workflows/server-side-rebase.md
└─ Get changed files / diff / patch → references/pull-requests/files.md
"I need to work with issues"
Issue operations?
├─ List/get/create/update/close → references/issues/README.md
│ ├─ Filter by label → ?labels={label_name}
│ ├─ Filter by milestone → ?milestones={milestone_name}
│ ├─ Filter by assignee → ?assigned_by={username}
│ ├─ Filter by author → ?created_by={username}
│ └─ Search text → ?q={search_term}
├─ Comments → references/issues/comments.md
│ └─ NOTE: Same endpoint for PRs — POST /issues/{index}/comments
├─ Labels → references/labels/issue-pr-labels.md
│ ├─ Read labels → GET /issues/{index}/labels (also embedded in issue response)
│ ├─ Add labels → POST /issues/{index}/labels {"labels": [{id},...]}
│ ├─ Replace all → PUT /issues/{index}/labels {"labels": [{id},...]}
│ └─ Remove one → DELETE /issues/{index}/labels/{identifier}
├─ Dependencies (blocks/is-blocked-by) → references/issues/dependencies.md
├─ Reactions (emoji) → references/issues/reactions.md
├─ Attachments → references/issues/attachments.md
├─ Time tracking & stopwatches → references/issues/time-tracking.md
├─ Pinning → POST /issues/{index}/pin
└─ Full lifecycle cookbook → references/complex-workflows/issue-lifecycle.md
"I need CI/CD information"
CI/CD operations?
├─ Check if a PR passes all quality gates
│ ├─ HEAD_SHA=$(PR | jq -r '.head.sha')
│ ├─ Combined state → GET /commits/{HEAD_SHA}/status → .state
│ │ (success=all pass, failure/error=blocked, pending=running, warning=caution)
│ ├─ Per-check details → GET /commits/{HEAD_SHA}/statuses → .state per .context
│ ├─ Find run → .target_url format: /{owner}/{repo}/actions/runs/{run_id}/jobs/{n}
│ └─ Full guide → references/complex-workflows/ci-status-check.md
├─ Get CI action run logs (requires web login!)
│ └─ references/web-interface/ci-logs.md
├─ List workflow runs → GET /repos/{owner}/{repo}/actions/runs
├─ Get specific run → GET /repos/{owner}/{repo}/actions/runs/{run_id}
├─ Trigger workflow manually → POST /actions/workflows/{workflow_file}/dispatches
├─ Manage secrets (repo/org/user) → references/ci-actions/secrets.md
├─ Manage variables (repo/org/user) → references/ci-actions/variables.md
└─ Register a runner → references/ci-actions/workflows.md
"I need to manage labels"
Label operations?
├─ Repo labels (scoped to one repo)
│ ├─ List with IDs → GET /repos/{owner}/{repo}/labels?limit=50
│ └─ CRUD → references/labels/repo-labels.md
├─ Org labels (inherited by all repos in the org)
│ ├─ List with IDs → GET /orgs/{org}/labels?limit=50
│ └─ CRUD → references/labels/org-labels.md
├─ Apply/remove on issues or PRs → references/labels/issue-pr-labels.md
│ ├─ Labels embedded in issue/PR response — no extra call needed to READ
│ ├─ Dedicated GET → GET /issues/{index}/labels
│ └─ Apply by name or ID — both work
├─ Exclusive labels (mutually exclusive within prefix group)
│ ├─ E.g. "State/Open", "State/Closed" — only one active at a time
│ ├─ Use PUT (replace-all) to safely switch
│ └─ references/labels/README.md
└─ Built-in template sets → references/labels/label-templates.md
"I need to manage milestones"
Milestone operations?
├─ List milestones (open/closed/all) → GET /repos/{owner}/{repo}/milestones?state=all
├─ Get single milestone → GET /repos/{owner}/{repo}/milestones/{milestone_id}
│ └─ Completion: closed_issues / (open_issues + closed_issues) * 100
├─ Create → POST /repos/{owner}/{repo}/milestones {"title", "due_on", "description"}
├─ Close (mark done) → PATCH /repos/{owner}/{repo}/milestones/{id} {"state": "closed"}
├─ Delete → DELETE /repos/{owner}/{repo}/milestones/{id}
├─ Search by name → GET /milestones?q={title_fragment} (query param: name)
└─ Full reference → references/milestones/README.md
"I need to manage branches"
Branch operations?
├─ List/create/get/delete → references/branches-tags/branches.md
│ └─ Create from SHA → body: {"new_branch_name": "{name}", "old_ref_name": "{sha}"}
├─ Rename a branch → PATCH /repos/{owner}/{repo}/branches/{branch} {"name": "{new}"}
├─ Branch protection rules
│ ├─ Require N approvals, specific CI checks, signed commits, push restrictions
│ ├─ CRUD → GET/POST/PATCH/DELETE /repos/{owner}/{repo}/branch_protections
│ └─ references/branches-tags/protections.md
├─ Tag CRUD → references/branches-tags/tags.md
├─ Tag protection rules → references/branches-tags/protections.md
└─ Server-side PR rebase → references/complex-workflows/server-side-rebase.md
"I need to manage releases"
Release operations?
├─ List releases → GET /repos/{owner}/{repo}/releases?limit=20
├─ Get latest → GET /repos/{owner}/{repo}/releases/latest
├─ Get by tag → GET /repos/{owner}/{repo}/releases/tags/{tag}
├─ Create release
│ └─ POST /repos/{owner}/{repo}/releases
│ {"tag_name": "{tag}", "target_commitish": "{branch_or_sha}",
│ "name": "{title}", "body": "{changelog}", "draft": false, "prerelease": false}
├─ Upload asset to release
│ └─ POST /repos/{owner}/{repo}/releases/{release_id}/assets?name={filename}
│ -F "attachment=@{/path/to/file}"
├─ Edit/delete release → PATCH/DELETE /repos/{owner}/{repo}/releases/{id}
└─ Full reference + release workflow → references/releases/README.md
references/complex-workflows/release-workflow.md
"I need to search or discover things"
Search operations?
├─ Search repos → GET /repos/search?q={term}&sort=updated&order=desc
│ Response: {"ok": true, "data": [...]} ← note: wrapped in .data
├─ Search issues/PRs across all repos → GET /repos/issues/search?q={term}&state=open
│ Response: [...] (direct array)
│ Extra filters: ?labels=, ?milestones=, ?type=pulls, ?review_requested=true
├─ Search users → GET /users/search?q={term}
│ Response: {"ok": true, "data": [...]} ← note: wrapped in .data
├─ Search topics → GET /topics/search?q={term}
│ Response: {"topics": [...]} ← note: wrapped in .topics
├─ Search team → GET /orgs/{org}/teams/search?q={term}
│ Response: [...] (direct array)
└─ Full reference → references/search/README.md
"I need to work with organizations"
Organization operations?
├─ Org CRUD, rename, avatar, block users → references/organizations/README.md
├─ Team management (units, permissions, repos, members) → references/organizations/teams.md
├─ Member management → references/organizations/members.md
├─ Org secrets/variables → references/ci-actions/secrets.md / variables.md
├─ Org webhooks → references/webhooks/README.md
├─ Org labels → references/labels/org-labels.md
├─ Org quota → references/users/quota.md
└─ Full org setup cookbook → references/complex-workflows/org-setup.md
"I need to manage users"
User operations?
├─ Current user / search / heatmap → references/users/profile.md
├─ SSH keys / GPG keys → references/users/keys.md
├─ API tokens → references/users/tokens.md
│ └─ REQUIRES BASIC AUTH: -u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"
├─ Email management → references/users/emails.md
├─ Quota → references/users/quota.md
├─ Followers / following / teams → references/users/social.md
├─ User settings → references/users/settings.md
├─ OAuth2 apps → references/authentication/oauth2.md
└─ Admin: create/edit/delete users → references/settings-admin/README.md
"I need to work with repositories"
Repository operations?
├─ CRUD, migration, create from template → references/repositories/README.md
│ └─ Migrate from GitHub/GitLab/Gitea/plain git (preserving issues, labels, etc.)
├─ Settings (merge styles, features, default branch) → references/repositories/settings.md
├─ Collaborators + team access → references/repositories/collaborators.md
├─ Deploy keys (SSH for CI/CD read/write) → references/repositories/deploy-keys.md
├─ Fork / sync with upstream → references/repositories/forks.md
├─ Push mirrors / transfer / convert fork → references/repositories/transfers.md
├─ Topics → references/repositories/topics.md
├─ Stars and watchers → references/repositories/stars-watchers.md
├─ Avatar → references/repositories/avatars.md
├─ Issue config and templates → references/repositories/issue-config.md
└─ Repository flags (admin) → references/repositories/flags.md
"I need to read or write files"
File content operations?
├─ List directory → GET /repos/{owner}/{repo}/contents/{dirpath}?ref={branch}
├─ Get file (base64 encoded + SHA) → GET /repos/{owner}/{repo}/contents/{filepath}?ref={branch}
├─ Create file → POST /repos/{owner}/{repo}/contents/{filepath}
│ body: {"message": "{msg}", "content": "$(echo -n '{text}' | base64)", "branch": "{branch}"}
├─ Update file (SHA required!) → PUT /repos/{owner}/{repo}/contents/{filepath}
│ body: {"message": "{msg}", "content": "$(echo -n '{text}' | base64)",
│ "sha": "{current_sha}", "branch": "{branch}"}
├─ Delete file (SHA required) → DELETE /repos/{owner}/{repo}/contents/{filepath}
│ body: {"message": "{msg}", "sha": "{current_sha}"}
├─ Create multiple files in one commit → POST /repos/{owner}/{repo}/contents
│ body: {"files": [{"path": "{filepath}", "content": "{base64}", "operation": "create"},...]}
├─ Get raw file → GET /repos/{owner}/{repo}/raw/{filepath}?ref={branch}
├─ Download archive → GET /repos/{owner}/{repo}/archive/{branch}.tar.gz
└─ Full reference → references/files-content/
"I need admin access"
Admin operations?
├─ List/create/edit/delete users → GET/POST/PATCH/DELETE /admin/users
├─ List all organizations → GET /admin/orgs
├─ Rename user → POST /admin/users/{username}/rename {"new_username": "{name}"}
├─ Create repo for user → POST /admin/users/{username}/repos
├─ Run cron task → POST /admin/cron/{task_name}
│ Common tasks: update_mirrors, repo_health_check
├─ Quota management (groups, rules, user assignments) → references/users/quota.md
├─ Unadopted repos → GET /admin/unadopted → adopt with POST, purge with DELETE
├─ System webhooks → GET/POST/PATCH/DELETE /admin/hooks
└─ Full reference → references/settings-admin/README.md
"I need to manage notifications or webhooks"
Notifications?
├─ List unread → GET /notifications?status-types=unread
├─ List for a repo → GET /repos/{owner}/{repo}/notifications
├─ Mark all read → PUT /notifications?to-status=read ← query param, not body!
├─ Mark thread read → PATCH /notifications/threads/{id}?to-status=read
├─ Pin thread → PATCH /notifications/threads/{id}?to-status=pinned
└─ Full reference → references/notifications/README.md
Webhooks?
├─ Repo hooks → GET/POST/PATCH/DELETE /repos/{owner}/{repo}/hooks
├─ Org hooks → GET/POST/PATCH/DELETE /orgs/{org}/hooks
├─ Test a hook → POST /repos/{owner}/{repo}/hooks/{hook_id}/tests
├─ Events: push, pull_request, issues, issue_comment, release, and 15+ more
└─ Full reference → references/webhooks/README.md
📏 API Conventions
Pagination
All list endpoints support ?page=1&limit=50 (max limit is 50):
PAGE=1
while true; do
BATCH=$(curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues?page=${PAGE}&limit=50&state=open" \
-H "Authorization: token ${FORGEJO_PAT}")
echo "$BATCH" | jq -r '.[].number' # process results
[ "$(echo "$BATCH" | jq length)" -lt 50 ] && break
PAGE=$((PAGE + 1))
done
Note: Forgejo does not return an
X-Total-Countheader. The only way to know if more pages exist is whether the current page returnedlimititems.
HTTP Status Codes
| Code | Meaning | Common trigger |
|---|---|---|
| 200 | OK | Successful GET, PATCH |
| 201 | Created | Successful POST |
| 204 | No Content | DELETE succeeded; or "resource exists" checks |
| 304 | Not Modified | Already in target state (e.g., subscription) |
| 400 | Bad Request | Invalid params / body |
| 401 | Unauthorized | Missing or invalid auth |
| 403 | Forbidden | Authenticated but insufficient permission |
| 404 | Not Found | Resource missing; also: "PR not merged" on merge check |
| 405 | Method Not Allowed | Blocked by protection (protected tag/branch, archived) |
| 409 | Conflict | Merge conflict; duplicate resource; optimistic lock |
| 412 | Precondition Failed | updated_at in body is stale |
| 413 | Entity Too Large | Quota exceeded or file too large |
| 422 | Unprocessable Entity | Validation error |
| 423 | Locked | Repo archived, mid-transfer, or otherwise locked |
| 429 | Too Many Requests | Rate limited — back off and retry |
Common Curl Patterns
# Standard GET
curl -s "${FORGEJO_URL}/api/v1/..." \
-H "Authorization: token ${FORGEJO_PAT}"
# POST/PUT/PATCH with JSON
curl -s -X POST "${FORGEJO_URL}/api/v1/..." \
-H "Authorization: token ${FORGEJO_PAT}" \
-H "Content-Type: application/json" \
-d '{"key": "{value}"}'
# File upload (multipart)
curl -s -X POST "${FORGEJO_URL}/api/v1/.../assets?name={filename}" \
-H "Authorization: token ${FORGEJO_PAT}" \
-F "attachment=@{/path/to/file}"
# Check HTTP status code only
curl -s -o /dev/null -w "%{http_code}" \
"${FORGEJO_URL}/api/v1/..." \
-H "Authorization: token ${FORGEJO_PAT}"
# Basic auth (token management only)
curl -s "${FORGEJO_URL}/api/v1/users/${FORGEJO_USERNAME}/tokens" \
-u "${FORGEJO_USERNAME}:${FORGEJO_PASSWORD}"
# Get raw diff
curl -s "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/pulls/{index}.diff" \
-H "Authorization: token ${FORGEJO_PAT}"
📂 Complete Reference Index
Core Operations
| Reference | Files | Covers |
|---|---|---|
references/pull-requests/ |
README, reviews, merging, files | PR CRUD, 6 merge methods, automerge, server-side rebase, inline review comments, reviewer requests, diff/patch/compare |
references/issues/ |
README, comments, reactions, attachments, dependencies, time-tracking | Issue CRUD, comments, emoji reactions, file attachments, dependency/blocking chains, time logging, stopwatches, pinning |
references/branches-tags/ |
branches, protections, tags | Branch CRUD/rename, tag CRUD, branch protection (approvals, CI checks, push restrictions), tag protections |
references/files-content/ |
README, crud, raw-media | File CRUD with SHA locking, multi-file commits, diffpatch, raw, media, archives, compare |
references/labels/ |
README, repo-labels, org-labels, issue-pr-labels, label-templates | Repo/org label CRUD, exclusive labels, apply/replace/remove on issues+PRs, template sets |
references/milestones/ |
README | Milestone CRUD, progress %, due dates |
references/commit-statuses/ |
README | Combined status, per-check states, create custom checks, quality gate verification |
People & Organizations
| Reference | Files | Covers |
|---|---|---|
references/organizations/ |
README, teams, members | Org CRUD/rename/avatar/blocks, team CRUD with units/permissions, member management |
references/users/ |
profile, keys, tokens, emails, quota, settings, social | Profile/search/heatmap, SSH/GPG keys, API token CRUD (scopes), emails, quota, settings, followers/teams |
references/authentication/ |
README, tokens, oauth2, web-login | Token, basic auth, OAuth2 code flow, CSRF web session |
DevOps & Automation
| Reference | Files | Covers |
|---|---|---|
references/ci-actions/ |
README, runs, secrets, variables, workflows | Workflow runs, manual dispatch, repo/org/user secrets+variables, runner registration |
references/webhooks/ |
README | Repo/org/user/system/git hooks, 20+ event types, testing, git hooks |
references/repositories/ |
README + 10 more | Repo lifecycle: CRUD, migration, templates, settings, collaborators, deploy keys, forks, mirrors, transfer, topics, stars, avatar, issue config, flags |
Content & Discovery
| Reference | Files | Covers |
|---|---|---|
references/git-objects/ |
README | Blobs (batch), commits, trees, refs, annotated tags, notes, language stats |
references/releases/ |
README | Release CRUD, asset upload, latest/by-tag lookup |
references/wiki/ |
README | Wiki page CRUD, revision history |
references/packages/ |
README | 20 package types (npm, pypi, container, helm, maven…), repo linking |
references/search/ |
README | Repo/issue/user/topic search — response envelope differences documented |
references/notifications/ |
README | List/filter, mark-read, thread management, pinning |
references/activity-feeds/ |
README | Repo/org/team/user activity, all 28 op_type values |
System & Meta
| Reference | Files | Covers |
|---|---|---|
references/settings-admin/ |
README | Public settings; admin: user/org CRUD, cron tasks, quota groups/rules, unadopted repos, email search |
references/miscellaneous/ |
README | Gitignore/license templates, markdown/markup render, NodeInfo, version, signing keys |
references/activitypub/ |
README | Instance/repo/user actors, inbox/outbox for ForgeFed |
references/web-interface/ |
README, ci-logs | What requires web session vs REST; CI log access step-by-step |
Cookbook
| Recipe | Solves |
|---|---|
complex-workflows/pr-review-cycle.md |
Create branch → commit → PR → request reviewers → review → address → merge |
complex-workflows/issue-lifecycle.md |
Create issue → assign → label → comment → link PR → close |
complex-workflows/ci-status-check.md |
Get PR head SHA → combined status → failing jobs → run IDs → log access |
complex-workflows/server-side-rebase.md |
Detect staleness → POST /pulls/{index}/update?style=rebase |
complex-workflows/automerge-workflow.md |
Set automerge → poll CI → confirm merge or cancel |
complex-workflows/release-workflow.md |
Verify PRs → tag → release → assets → close milestone |
complex-workflows/branch-protection-setup.md |
Approvals + CI checks + signed commits + push restrictions |
complex-workflows/org-setup.md |
Org → teams → members → repos → labels → webhooks → secrets |
complex-workflows/fork-contribute.md |
Fork → branch → commit → cross-repo PR → upstream sync |
complex-workflows/tips-and-patterns.md |
Multi-identity auth, subtask checkboxes, auto-close, pagination, optimistic locking, base64, large diffs, 16-code HTTP reference |
🔎 Server Info
- Base URL:
${FORGEJO_URL}/api/v1 - Swagger UI:
${FORGEJO_URL}/api/swagger - Swagger JSON:
${FORGEJO_URL}/swagger.v1.json - Tested against: Forgejo 14.0.4 (Gitea 1.22.0 compatible)
- Max page size: 50 items per request
- Default page size: 30 items
- No X-Total-Count header: Use
len(results) == limitto detect more pages