--- 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//" # 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//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//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/" -H "Authorization: token $TOKEN" ``` ## Collaborator Management ```bash # Add/update collaborator with write permission curl -X PUT "$API/repos///collaborators/" \ -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 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/` returns a user, `/api/v1/orgs/` 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, !") 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//" # 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