layered-soul/skills/forgejo-operations/SKILL.md
Hermes & Sam 5c5df32101 Populate layered-soul: identity, memories, skills, plan (Hermes & Sam)
- SOUL.md: full agent identity, operating principles, voice
- IDENTITY.md: runtime identity, hosts, boundaries
- USER.md: operator context imported from hermes-soul
- AGENTS.md: actual operating rules, infrastructure, quick reference
- memories/curated/: 5 topics (tailscale, forgejo, agents, projects, vaultwarden)
- skills/: 9 cross-harness skills imported from hermes-soul after review
- docs/PLAN-CONFIGURE-PRIVATE-REPO.md: configuration plan
- Validate: passes clean
2026-06-14 00:21:26 +02:00

9.5 KiB

name description version author platforms metadata
forgejo-operations Manage self-hosted Forgejo/Gitea instances — repos, users, SSH keys, tokens, collaborators, API operations. 1.0.0 Sam & Hermes
linux
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.

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

# 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)

# 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

# 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:

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:

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, !") tells you. If wrong, delete from current account, re-add to correct one via admin API.

PR State Inspection

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:

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