# Forge Client Config-driven forge abstraction (issue #121): Rune's automation creates issues, comments, and reads state through a `ForgeClient` protocol with adapters for Gitea, Forgejo, or no forge at all. Nothing is hardcoded to a specific setup — this is the open-project readiness foundation and the prerequisite for the failure auto-filer (#122). ## Components (src/rune/forge_client.py) - `ForgeClient` — `@runtime_checkable` Protocol: `create_issue(title, body)`, `add_comment(issue_number, body)`, `get_issue_state(issue_number)`, `list_open_issues()`, `search_issues(query)` (#122) - `NullAdapter` — every method is a no-op dict; zero errors; the graceful dormant path for unconfigured/standalone use - `GiteaForgejoAdapter` — real HTTP adapter via Gitea/Forgejo `/api/v1` paths (identical on both platforms); `Authorization: token ` - `create_forge_client(config)` — factory; unconfigured → `NullAdapter` `search_issues(query)` (#122) is the repo-scoped marker search over the paginated LIST endpoint (`GET /issues?state=open&type=issues&q=` + client-side marker contains-check on title/body). Gitea/Forgejo has NO repo-scoped `/issues/search` path — the failure auto-filer dedups through the list endpoint precisely because it is paginated and server-filterable. ## Configuration The factory checks a config dict first, then environment variables: | Key | Env var | Meaning | |-----|---------|---------| | `forge.url` | `FORGE_URL` | Forge base URL, e.g. `https://forge.example.com` | | `forge.token` | `FORGE_TOKEN` | API token | | `forge.owner` | `FORGE_OWNER` | Repo owner | | `forge.repo` | `FORGE_REPO` | Repo name | If URL **or** token is missing → `NullAdapter` (never raises). # Standalone (no forge): nothing to do — NullAdapter is dormant. # Gitea/Forgejo: export FORGE_URL="https://forge.example.com" export FORGE_TOKEN="..." export FORGE_OWNER="o" # optional export FORGE_REPO="r" # optional ## API paths used (Gitea == Forgejo) POST {url}/api/v1/repos/{owner}/{repo}/issues POST {url}/api/v1/repos/{owner}/{repo}/issues/{n}/comments GET {url}/api/v1/repos/{owner}/{repo}/issues/{n} GET {url}/api/v1/repos/{owner}/{repo}/issues?state=open&limit=50 GET {url}/api/v1/repos/{owner}/{repo}/issues?state=open&type=issues&q=&limit=50&page=N ## Run PYTHONPATH=src:. python3 -m pytest tests/test_forge_client.py -q