CLI for Forgejo API — issues, PRs, repos, labels, webhooks, Actions runners. Dual auth (AGENT/USER).
Scanned 9/10/2026
Install to Claude Code
npx -y skills add theheavenlyd3mon/hermes-profiles --skill forgejo-cli --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Forgejo Cli?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/theheavenlyd3mon-forgejo-cli)More formats (shields.io, HTML) on the badges page.
---
name: forgejo-cli
description: "CLI for Forgejo API — issues, PRs, repos, labels, webhooks, Actions runners. Dual auth (AGENT/USER)."
version: 1.1.1
tags: [forgejo, git, api, code-review]
---
# forgejo-cli
Python CLI at `~/.hermes/scripts/forgejo-cli` wrapping the Forgejo API v1.
## Auth
Two tokens stored in `~/.hermes/.env`:
- `FORGEJO_AGENT_TOKEN` — jasper bot (default)
- `FORGEJO_USER_TOKEN` — magnus user (use `--user` flag)
## Usage
```
forgejo-cli <command> [<subcommand>] [OPTIONS]
Commands:
issue Manage issues (list, show, create, comment, label, assign)
pr Manage pull requests (list, show, create, diff, review, comment, merge)
repo Manage repositories (list, show, create, search)
label Manage labels (list, create)
hook Manage webhooks (list, create, delete)
user User info and settings
comment Manage comments (list, create, delete)
Global flags:
--json Machine-readable JSON output
--dry-run Preview without making changes
--agent Use AGENT token (default)
--user Use USER token
--force Skip confirmations
--quiet Suppress non-essential output
```
## Common Operations
```bash
# List issues
forgejo-cli issue list --owner magnus --repo test
# Show issue
forgejo-cli issue show --owner magnus --repo test --index 3
# Add comment
forgejo-cli issue comment --owner magnus --repo test --index 3 --body "Fixed"
# Get PR diff for review
forgejo-cli pr diff --owner magnus --repo myrepo --index 1
# Submit PR review (as jasper)
forgejo-cli pr review --owner magnus --repo myrepo --index 1 --body "LGTM" --event approve
# Merge a PR
forgejo-cli pr merge --owner magnus --repo myrepo --index 3 --dry-run # Preview first
forgejo-cli pr merge --owner magnus --repo myrepo --index 3 --force # Execute merge
# Merge via API (when CLI returns 405 or PR has conflicts to resolve first)
# See references/pr-merge-via-api.md for full workflow
# Create a PR
forgejo-cli pr create --owner magnus --repo myrepo --title "feat: add auth" --head feat/add-auth --base main --body "Closes #42"
forgejo-cli pr create --owner magnus --repo myrepo --title "draft: WIP" --head feat/wip --base main --draft
# Create an issue with labels (label IDs are NUMERIC)
forgejo-cli issue create --owner magnus --repo myrepo --title "Bug: login fails" --body "Details here" --labels 8,9
# List repos
forgejo-cli repo list --json
# Create a repo (NOT YET IMPLEMENTED in CLI — use API directly, see references/repo-creation-via-api.md)
# Documentation says `repo create` but the method isn't coded yet
# List labels
forgejo-cli label list --owner magnus --repo test
# Get current user info
forgejo-cli user show
forgejo-cli --user user show
```
## Server Setup
The Forgejo instance runs via Docker on `phatalbert`. See `references/server-setup.md` for the docker-compose.yml, SSH port mapping details (rootless gotcha), volume strategy, and admin accounts.
## Test Suite
Test script at `~/.hermes/scripts/forgejo-cli-test.sh`. Run with:
```bash
bash ~/.hermes/scripts/forgejo-cli-test.sh
```
## Forgejo Docker Deployment
See `references/fj-deployment.md` for Forgejo-specific Docker patterns: rootless image quirks, SSH port config, entrypoint config generation, `INSTALL_LOCK` requirements, database setup, and the `***` secrets masking pitfall.
## Forgejo Actions (CI/CD)
Forgejo Actions is a CI/CD system similar to GitHub Actions. Requires both server-side config and a runner. The `forgejo-actions` skill covers runner lifecycle, step container behavior, workflow patterns, and debugging in detail.
### Enabling Actions on Forgejo
Add to `/data/gitea/conf/app.ini` inside the forgejo container:
```bash
docker exec forgejo sh -c 'printf "\n[actions]\nENABLED=true\n" >> /data/gitea/conf/app.ini'
docker restart forgejo
```
### Registering a runner
```bash
# Get registration token
curl -s "https://git.brandyapple.com/api/v1/admin/runners/registration-token" \
-H "Authorization: token $FORGEJO_USER_TOKEN"
# Register and start on the target host
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
-v runner-data:/data \
data.forgejo.org/forgejo/runner:4.0.0 \
forgejo-runner register \
--instance https://git.brandyapple.com \
--token <token> --name <host>-runner \
--labels docker:docker://node:20-bookworm --no-interactive
# Run daemon
docker run -d --name forgejo-runner --user root \
-v /var/run/docker.sock:/var/run/docker.sock \
-v runner-data:/data --restart unless-stopped \
data.forgejo.org/forgejo/runner:4.0.0 \
forgejo-runner daemon
```
### Critical runner config
After registration, edit `/data/config.yaml` in the runner volume. See the `forgejo-actions` skill for full config reference — key settings:
- `container.docker_host: automount` — required to mount host Docker socket (runner > 5.0.3). Default is `"-"` which skips mounting.
- `container.valid_volumes: ['**']` — allows volume mounts from host
### Debugging
Step output is only visible in the Forgejo web UI, not in `docker logs forgejo-runner`. See `forgejo-actions` skill for Docker events debugging patterns.
Full runner/deploy workflow details in the `forgejo-gitea` skill's "Forgejo Actions (CI/CD)" section.
## Release Workflow
The forgejo-cli does not implement `release create`. Use the Forgejo API directly for the full release lifecycle:
```bash
# 1. Tag and push
git tag -a vX.Y.Z -m "vX.Y.Z — Title"
git push origin vX.Y.Z
# 2. Write release notes and POST data to a JSON file
# (Use the JSON-file approach to avoid shell escaping issues)
cat > /tmp/release-data.json << 'ENDJSON'
{
"tag_name": "vX.Y.Z",
"name": "vX.Y.Z — Release Title",
"body": "## What's New\n\nRelease notes here.\n",
"draft": false,
"prerelease": false
}
ENDJSON
# 3. Create the release
curl -s -X POST "https://git.brandyapple.com/api/v1/repos/{owner}/{repo}/releases" \
-H "Authorization: token $FORGEJO_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d @/tmp/release-data.json
# 4. Get the release ID for any subsequent edits
curl -s "https://git.brandyapple.com/api/v1/repos/{owner}/{repo}/releases" \
-H "Authorization: token $FORGEJO_AGENT_TOKEN" | \
python3 -c "import sys,json; [print(f'ID: {r[\"id\"]} Tag: {r[\"tag_name\"]}') for r in json.load(sys.stdin)]"
```
### Gotcha: `name` not `title`
Forgejo's release API uses **`name`** as the release display title, **not** `title`. If you send `"title": "vX.Y.Z — Release"`, the field is silently ignored and the tag name is used as a fallback. The correct field:
```json
{"tag_name": "vX.Y.Z", "name": "vX.Y.Z — Release Title", "body": "..."}
```
To fix a release that was created with the wrong name, PATCH by release ID:
```bash
curl -s -X PATCH "https://git.brandyapple.com/api/v1/repos/{owner}/{repo}/releases/{id}" \
-H "Authorization: token $FORGEJO_AGENT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "vX.Y.Z — Corrected Title"}'
```
PATCH by tag (`/releases/tag/{tag}`) returns 404 — you must use the numeric release ID.
📄 **`references/release-workflow.md`** — Full worked example with rollback instructions, the complete API sequence, and recovery steps for release mistakes.
## PR Review Workflow
The `references/pr-review-workflow.md` file covers the end-to-end automated code review workflow triggered by forgejo-prs webhooks: fetching diffs, composing review bodies with complex JSON, submitting reviews via API, and handling inline comments vs summary reviews.
## Pitfalls
### `--labels` requires numeric IDs (not strings)
The `--labels` flag accepts comma-separated label IDs. These must be **integers**. Non-numeric values are silently dropped.
```bash
# ✅ Correct: numeric IDs
forgejo-cli issue create --owner magnus --repo test --title "Bug" --labels 8,9
# ❌ Wrong: string values cause 422 API error
forgejo-cli issue create --owner magnus --repo test --title "Bug" --labels "8,9"
```
To look up label IDs by name:
```bash
forgejo-cli label list --owner magnus --repo test --json
```
This returns labels with their numeric `id` field. See issue #48 for the v1.1.1 fix history.
### Shell metacharacters in `--body` break `issue create`
The `--body` value is passed through the shell, so text containing `$`, backticks, parentheses, `&`, `|`, `;`, or unbalanced quotes causes parsing errors or silent truncation.
**Fix:** Use the Forgejo API directly with a JSON file for complex bodies:
```bash
# Write body to file
cat > /tmp/body.json << 'ENDOFBODY'
{"title": "Issue title", "body": "Complex body with (parens) and $dollar signs"}
ENDOFBODY
# POST via API
. ~/.hermes/.env 2>/dev/null
curl -s -X POST "https://git.brandyapple.com/api/v1/repos/{owner}/{repo}/issues" \
-H "Authorization: Bearer $FORGEJO_USER_TOKEN" \
-H "Content-Type: application/json" \
-d @/tmp/body.json
```
Or pipe from Python to avoid any shell escaping:
```bash
. ~/.hermes/.env 2>/dev/null
python3 -c "import json; body = open('/tmp/body.md').read(); print(json.dumps({'title': '...', 'body': body}))" \
| curl -s -X POST "https://git.brandyapple.com/api/v1/repos/{owner}/{repo}/issues" \
-H "Authorization: Bearer $FORGE...EN" \
-H "Content-Type: application/json" \
-d @-
```
**Best option for complex bodies: Use `execute_code` with `urllib.request`.**
This eliminates ALL shell interaction — no quoting, no temp files, no token expansion:
```python
import json, urllib.request, os
env_path = os.path.expanduser("~/.hermes/.env")
token = None
with open(env_path) as f:
for line in f:
line = line.strip()
if "FORGEJO_AGENT_TOKEN" in line and "=" in line:
token = line.split("=", 1)[1].strip().strip('"').strip("'")
body = open("/tmp/body.md").read()
payload = json.dumps({"title": "Issue title", "body": body})
req = urllib.request.Request(
"https://git.brandyapple.com/api/v1/repos/{owner}/{repo}/issues",
data=payload.encode(),
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
method="POST"
)
with urllib.request.urlopen(req) as resp:
r = json.loads(resp.read())
print(f"Created #{r['number']}: {r['title']}")
```
The same pattern works for PR creation — POST to `/pulls` instead of `/issues` with `head` and `base` fields. See `references/pr-creation-via-api.md`.
## Known Gaps ⚠️
| Claimed Feature | Actual Status | Workaround |
|---|---|---|
| `repo create` | Not implemented (only `list`, `show`, `search` exist) | Use raw API — see `references/repo-creation-via-api.md` |
| `repo show` | Accepts `--owner --repo` | `repo get` in code; `repo show` alias may not exist — try `--json` on `repo list` filtered by name |
| `pr merge` | Requires `--force` or `--dry-run` flag (not obvious from help output). Returns 405 when PR isn't mergeable (branch divergence, conflicts) | API-based merge — see `references/pr-merge-via-api.md` |
| `release create` | Not implemented (no release commands exist at all) | Use raw API — see `references/release-workflow.md` |
| Standalone PR comment (merged PR) | No subcommand for commenting on already-merged PRs | Use `POST /issues/{id}/comments` — see `references/pr-review-workflow.md` |
When a CLI subcommand is missing, the Forgejo REST API at `git.brandyapple.com/api/v1` is the backup. The `references/repo-creation-via-api.md` file has the exact curl incantation for repo creation, and `references/pr-creation-via-api.md` covers PR creation.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!