228 lines
9.5 KiB
Markdown
228 lines
9.5 KiB
Markdown
|
|
---
|
||
|
|
name: forgejo-operations
|
||
|
|
description: "Manage self-hosted Forgejo/Gitea instances — repos, users, SSH keys, tokens, collaborators, API operations."
|
||
|
|
version: 1.0.0
|
||
|
|
author: Sam & Hermes
|
||
|
|
platforms: [linux]
|
||
|
|
metadata:
|
||
|
|
hermes:
|
||
|
|
tags: [forgejo, gitea, git, self-hosted, devops]
|
||
|
|
---
|
||
|
|
|
||
|
|
# Forgejo Operations
|
||
|
|
|
||
|
|
Manage a self-hosted Forgejo instance via API and SSH. Covers repo creation,
|
||
|
|
user management, SSH key registration, collaborator permissions, and token
|
||
|
|
lifecycle.
|
||
|
|
|
||
|
|
## Instance Details
|
||
|
|
|
||
|
|
- URL: `https://code.smilepowered.org`
|
||
|
|
- SSH: port 2222, user `git`
|
||
|
|
- SSH config entry: `Host code.smilepowered.org` with `Port 2222`
|
||
|
|
- API base: `https://code.smilepowered.org/api/v1`
|
||
|
|
|
||
|
|
## PR Creation + Merge (API token)
|
||
|
|
|
||
|
|
When branch protection blocks direct push, use the API token to create
|
||
|
|
and merge PRs programmatically. Token needs `write:repository` scope
|
||
|
|
(minimum). `write:organization` only needed for adjusting branch
|
||
|
|
protection rules.
|
||
|
|
|
||
|
|
**Rule:** No auto-merge until cross-platform validation is posted.
|
||
|
|
For Colibri: Linux `cargo test --workspace` AND FreeBSD/OSA
|
||
|
|
`cargo test --workspace` must both pass before merging.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
source ~/.hermes/.env 2>/dev/null
|
||
|
|
API="https://code.smilepowered.org/api/v1/repos/<owner>/<repo>"
|
||
|
|
|
||
|
|
# Create PR
|
||
|
|
PR=$(curl -s -X POST -H "Authorization: token $FORGEJO_API_TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"title":"feat: ...","head":"branch-name","base":"main","body":"..."}' \
|
||
|
|
"$API/pulls")
|
||
|
|
NUM=$(echo "$PR" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
|
||
|
|
|
||
|
|
# Merge (returns HTTP 200 with empty body on success)
|
||
|
|
curl -s -X POST -H "Authorization: token $FORGEJO_API_TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"Do":"merge","delete_branch_after_merge":true}' "$API/pulls/$NUM/merge"
|
||
|
|
|
||
|
|
# Verify (merge response is empty on success — use GET to confirm)
|
||
|
|
curl -s -H "Authorization: token $FORGEJO_API_TOKEN" "$API/pulls/$NUM" | \
|
||
|
|
python3 -c "import sys,json; d=json.load(sys.stdin); print(f'merged: {d[\"merged\"]}')"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Merge parameter casing:** Forgejo requires `"Do"` (capital D), not `"do"`.
|
||
|
|
Lowercase returns HTTP 422 `[Do]: Required`.
|
||
|
|
|
||
|
|
## Repo Creation
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Under user
|
||
|
|
curl -X POST "$API/user/repos" \
|
||
|
|
-H "Authorization: token $TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"name":"repo-name","description":"...","private":false}'
|
||
|
|
|
||
|
|
# Under org (needs write:organization scope)
|
||
|
|
curl -X POST "$API/orgs/<org>/repos" \
|
||
|
|
-H "Authorization: token $TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"name":"repo-name","description":"...","private":false}'
|
||
|
|
```
|
||
|
|
|
||
|
|
## User Management (admin scope required)
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Create user
|
||
|
|
curl -X POST "$API/admin/users" \
|
||
|
|
-H "Authorization: token $TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"username":"agent-name","email":"agent@example.com","password":"random","must_change_password":false,"send_notify":false}'
|
||
|
|
|
||
|
|
# Add SSH key to another user's account
|
||
|
|
curl -X POST "$API/admin/users/<username>/keys" \
|
||
|
|
-H "Authorization: token $TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"key":"ssh-ed25519 AAAA...","title":"key-label","read_only":false}'
|
||
|
|
|
||
|
|
# List SSH keys on your own account
|
||
|
|
curl "$API/user/keys" -H "Authorization: token $TOKEN"
|
||
|
|
|
||
|
|
# Delete SSH key from your own account
|
||
|
|
curl -X DELETE "$API/user/keys/<id>" -H "Authorization: token $TOKEN"
|
||
|
|
```
|
||
|
|
|
||
|
|
## Collaborator Management
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Add/update collaborator with write permission
|
||
|
|
curl -X PUT "$API/repos/<owner>/<repo>/collaborators/<username>" \
|
||
|
|
-H "Authorization: token $TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
-d '{"permission":"write"}'
|
||
|
|
|
||
|
|
# Returns 204 on success (no body)
|
||
|
|
# Needs token with write:repository on that repo
|
||
|
|
```
|
||
|
|
|
||
|
|
## Branch Protection & Force Push
|
||
|
|
|
||
|
|
Clawdie repos have branch protection on `main`: direct push rejected with
|
||
|
|
`pre-receive hook declined`. All changes go through pull requests.
|
||
|
|
|
||
|
|
### Stacked branch pitfall
|
||
|
|
|
||
|
|
Always branch from `main`. If a branch is created from another feature branch,
|
||
|
|
its PR includes all parent commits. Fix with cherry-pick onto a clean branch:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
git checkout -b clean-branch forgejo/main
|
||
|
|
git cherry-pick <commit1> <commit2>
|
||
|
|
git diff --check forgejo/main...HEAD # verify clean
|
||
|
|
```
|
||
|
|
|
||
|
|
## Pitfalls
|
||
|
|
|
||
|
|
### Push-to-create is usually off
|
||
|
|
Forgejo rejects `shallow update not allowed`. Fixes:
|
||
|
|
- `git fetch --unshallow origin` (needs full history from origin)
|
||
|
|
- If origin unreachable: `git filter-branch` to make first commit rootless,
|
||
|
|
then push. This rewrites history — only for migration cutover.
|
||
|
|
- Best: fresh full clone from Forgejo itself.
|
||
|
|
|
||
|
|
### Commit hooks timing out
|
||
|
|
Some repos have pre-commit/prepare-commit-msg hooks that need Node or other
|
||
|
|
tools not on PATH. Use `-c core.hooksPath=/dev/null` to bypass all hooks:
|
||
|
|
```bash
|
||
|
|
git -c core.hooksPath=/dev/null commit -m "message"
|
||
|
|
```
|
||
|
|
|
||
|
|
### Not an org — check first
|
||
|
|
`/api/v1/users/<name>` returns a user, `/api/v1/orgs/<name>` returns an org.
|
||
|
|
If you get "user redirect does not exist" on org creation, the name is already
|
||
|
|
a user. User and org can't share names in Forgejo.
|
||
|
|
|
||
|
|
### SSH key on wrong account
|
||
|
|
To verify which user a key belongs to: `ssh -T git@host`. The greeting
|
||
|
|
("Hi there, <username>!") tells you. If wrong, delete from current account,
|
||
|
|
re-add to correct one via admin API.
|
||
|
|
|
||
|
|
## PR State Inspection
|
||
|
|
|
||
|
|
```bash
|
||
|
|
source ~/.hermes/.env 2>/dev/null
|
||
|
|
API="https://code.smilepowered.org/api/v1/repos/<owner>/<repo>"
|
||
|
|
|
||
|
|
# List open PRs
|
||
|
|
curl -s -H "Authorization: token $FORGEJO_API_TOKEN" "$API/pulls?state=open"
|
||
|
|
|
||
|
|
# Check specific PR state
|
||
|
|
curl -s -H "Authorization: token $FORGEJO_API_TOKEN" "$API/pulls/$PR_NUMBER" | \
|
||
|
|
python3 -c "import sys,json; d=json.load(sys.stdin); print(f'#{d[\"number\"]}: state={d[\"state\"]}, merged={d[\"merged\"]}, mergeable={d.get(\"mergeable\",\"?\")}')"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pitfall: `read` pipe loses variable in subshell.** Piping into `read` (e.g.
|
||
|
|
`echo "$PR" | python3 -c "..." | read NUM`) silently fails — the variable
|
||
|
|
is set in a subshell and lost when the pipe exits. Use command substitution
|
||
|
|
instead: `NUM=$(echo "$PR" | python3 -c "...")`.
|
||
|
|
|
||
|
|
### Merge rate-limit ("Please try again later")
|
||
|
|
Forgejo may return HTTP 200 with `{\"message\":\"Please try again later\"}`
|
||
|
|
on the merge endpoint. Wait 3-5 seconds and retry. If it persists after
|
||
|
|
3 retries, fall back to web UI merge. This appears to be backend-side
|
||
|
|
processing delay, not a token scope issue.
|
||
|
|
|
||
|
|
**Pitfall: HTTP 405 on merge = PR not mergeable OR already merged.**
|
||
|
|
HTTP 405 from the merge endpoint means either (a) the PR has merge
|
||
|
|
conflicts Forgejo can't auto-resolve, or (b) the PR was already merged
|
||
|
|
(merge on an already-merged PR returns 405, not 409). Always check PR
|
||
|
|
state before retrying — the merge may have succeeded silently on a prior
|
||
|
|
call (Forgejo sometimes returns empty 200/204 on success, which trips up
|
||
|
|
JSON parsers). Verify with:
|
||
|
|
```bash
|
||
|
|
curl -s -H "Authorization: token $TOKEN" "$API/pulls/$NUM" | \
|
||
|
|
python3 -c "import sys,json; d=json.load(sys.stdin); print(f'mergeable: {d[\"mergeable\"]}')"
|
||
|
|
```
|
||
|
|
If `mergeable: false`, conflicts exist. Resolution options:
|
||
|
|
- **Web UI**: visit the PR page and use Forgejo's conflict resolver
|
||
|
|
- **Local resolve**: `git merge`, fix conflicts, push resolved branch to a new ref, open fresh PR
|
||
|
|
- **Superseding branch**: if another branch already contains the same fixes plus more, close this PR and point to the newer branch
|
||
|
|
|
||
|
|
Do NOT retry the merge API — 405 on an unmergeable PR will never self-resolve.
|
||
|
|
This is different from the transient \"Please try again later\" rate-limit response
|
||
|
|
which returns HTTP 200 with an error message body.
|
||
|
|
|
||
|
|
**Pitfall: token display when used in-shell.** When using the token in `curl`
|
||
|
|
commands via a shell script, load it from env rather than pasting inline.
|
||
|
|
`source ~/.hermes/.env` works if `.env` is mode 0600. If the token value appears
|
||
|
|
in the command string (even with `export TOKEN=...`), the agent may redact it
|
||
|
|
mid-command, resulting in truncated tokens. Use `source ~/.hermes/.env` before
|
||
|
|
the curl call, or store it in `$FORGEJO_API_TOKEN` and never print it.
|
||
|
|
|
||
|
|
## Subtopics
|
||
|
|
|
||
|
|
This skill covers Forgejo operations end-to-end. For deep dives into specific workflows, see:
|
||
|
|
|
||
|
|
| Topic | Reference | Description |
|
||
|
|
|-------|-----------|-------------|
|
||
|
|
| Agent Onboarding | `references/agent-onboarding.md` | Create users, register SSH keys, add collaborators, verify access |
|
||
|
|
| Multi-Agent Infrastructure | `references/multi-agent-infrastructure.md` | Architecture, setup order, SSH config, Vaultwarden integration |
|
||
|
|
| Branch Protection | `references/branch-protection.md` | Web UI steps for requiring PR on main, verification |
|
||
|
|
| Branch Protection API | `references/branch-protection-api.md` | Programmatic remove/re-create protection (for force push) |
|
||
|
|
| SSH Key Transfer | `references/key-transfer.md` | Move an SSH key between Forgejo user accounts |
|
||
|
|
| Self-Hosted Setup | `references/self-hosted-setup.md` | Codeberg migration, shallow clone fixes, password rotation |
|
||
|
|
| API Token Scopes | `references/forgejo-token-scopes.md` | Complete scope reference table |
|
||
|
|
| Git Shallow Fixes | `references/git-shallow-fixes.md` | Unshallowing clones, filter-branch root commits |
|
||
|
|
| Vaultwarden bw CLI | `references/vaultwarden-bw-cli.md` | bw CLI login/unlock/CRUD for agent secrets |
|
||
|
|
| API Merge Pattern | `references/api-merge-pattern.md` | Programmatic PR create + merge via curl |
|
||
|
|
| API Token Setup | `references/api-token-setup.md` | Token creation workflow and storage |
|
||
|
|
| Colibri Validation | `references/colibri-validation-commands.md` | Cross-platform validation commands |
|
||
|
|
| Git History Flatten | `references/git-history-flatten.md` | Flatten merge-heavy history via first-parent cherry-pick |
|
||
|
|
|
||
|
|
### Template
|
||
|
|
|
||
|
|
- `templates/FORGEJO-SETUP.md` — Agent self-bootstrap documentation template
|