Files
cleveragents-core/.opencode/skills/forgejo-api/SKILL.md
CleverAgents Build Agent 237e776951
CI / lint (push) Successful in 20s
CI / quality (push) Successful in 20s
CI / helm (push) Successful in 24s
CI / build (push) Successful in 24s
CI / push-validation (push) Successful in 39s
CI / security (push) Successful in 1m1s
CI / e2e_tests (push) Successful in 3m13s
CI / typecheck (push) Successful in 4m23s
CI / unit_tests (push) Successful in 6m44s
CI / integration_tests (push) Successful in 6m49s
CI / docker (push) Successful in 11s
CI / coverage (push) Successful in 6m56s
CI / status-check (push) Successful in 0s
feat(skills): add exhaustive Forgejo REST API agent skill
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
2026-04-15 00:45:24 -04:00

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).
authentication
repositories
issues
pull-requests
branches-tags
labels
milestones
organizations
users
files-content
ci-actions
commit-statuses
notifications
releases
search
webhooks
settings-admin
wiki
packages
git-objects
activitypub
miscellaneous
activity-feeds
web-interface
complex-workflows

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. See references/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-Count header. The only way to know if more pages exist is whether the current page returned limit items.

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) == limit to detect more pages