- Status: Accepted
- Date: 2026-07-16
- Deciders: thulasi.ram (architect)
- Amends: 0010 (forge integration)
Context
Section titled “Context”The forge adapter is unbuilt: 8 outbound GithubForge methods are
unimplemented!() and production wires forge: None (2026-07-16 audit). Before
building it, a framing error surfaced: treating “use a GitHub App” as a
Scarab-level auth decision leaks a vendor concept into what CONTEXT.md and
ADR-0010 require to be forge-agnostic (“Forge is the domain concept; vendors are
adapters, never their own domain”). Forgejo/Codeberg have no App/installation
concept — they have OAuth2 apps and repo-scoped access tokens.
Decision
Section titled “Decision”Auth is adapter-internal; the ForgePort expresses domain capabilities
Section titled “Auth is adapter-internal; the ForgePort expresses domain capabilities”The port never mentions “App”, “installation”, or “JWT”. It expresses capabilities — read a file/dir at a ref, resolve a ref to a commit, post a status, ingest a normalized event, mint a scoped checkout credential. Each adapter satisfies them however its vendor allows. Auth config is adapter-specific (GitHub: App ID + PEM private key; Forgejo: OAuth2 client/secret or a bot access token).
Two adapters in v1 — GitHub and Forgejo
Section titled “Two adapters in v1 — GitHub and Forgejo”Both ship in v1, so the port is validated by construction: Forgejo cannot be
satisfied by anything GitHub-shaped. This yields a shared ForgePort
contract-test suite both adapters must pass — the mechanism that keeps future
adapters (GitLab, Bitbucket) honest. It also plants a flag GitHub Actions
structurally can’t: forge-native CI that is not GitHub-locked.
Per-capability mapping:
| Capability | GitHub | Forgejo / Codeberg |
|---|---|---|
| API auth | App JWT → installation token (~1h, cached) | OAuth2 token or bot access token |
| Scoped checkout credential (ADR-0045 S4) | installation token scoped contents:read, one repo |
repo-scoped access token / short OAuth token |
| Event ingest | one App webhook, all installations | per-repo/org webhook — register_webhook is real |
| Status feedback | commit status (+ optional Checks enrichment) | commit status |
| API base URL | api.github.com or GHES host |
self-hosted base URL |
Status feedback sits at commit-status level (LCD) + required run deep-link
Section titled “Status feedback sits at commit-status level (LCD) + required run deep-link”The port contract is set_status(repo, sha, state, context, description, target_url) — the level both forges guarantee. target_url (deep-link back
to the Scarab run) is required, not the current hardcoded None
(crates/scarab-server/src/lib.rs:1659-1661).
- The GitHub adapter may enrich internally to the Checks API — inline PR annotations (fed by ADR-0008 structured-emit diagnostics), rich output, richer conclusions — without the port knowing. Forgejo ignores what it can’t render.
- Follow-up (GitHub-only, not v1 core): wire Checks action buttons and
rerequested/requested_actionwebhooks to Scarab’s restart-a-step and gate-approve primitives — surfacing the durable-restart wedge inside the PR.
register_webhook is a real capability, not a GitHub no-op
Section titled “register_webhook is a real capability, not a GitHub no-op”Because Forgejo needs per-repo/org webhook registration (no single-app webhook),
register_webhook stays in the port. The GitHub adapter implements it as a
no-op (the App receives all installation events).
The registry — ForgeConnection + Project; per-forge webhook endpoints
Section titled “The registry — ForgeConnection + Project; per-forge webhook endpoints”There is no repo registry today (repos are faked in the UI; the ingest route is
hard-bound to GitHub, crates/scarab-server/src/lib.rs:1735,1746). Two forges
make a real one unavoidable — resolving which forge, which base URL, which
credentials for a given repo.
ForgeConnection(pure type inscarab-forge) links Scarab to a forge account:{forge_kind, base_url, credential_ref}owning a set ofRepoRefs. A GitHub App installation and a Forgejo connection are both instances. Its persistence is a store port + Postgres adapter (I/O stays out of the pure crate); its credentials live inSecretProvider(App PEM / Forgejo token), referenced by handle; the installation-token cache is adapter-internal state.RepoRefvsProject(redefines the tenancy model):RepoRefis the forge coordinate ({owner, name}+ forge) — external, mutable, carried byEvent/Status, the only concept named “Repo”. AProjectis the governed CI unit — the aggregate root under anOrgthat binds aRepoRefto its governance (Environments/ProtectionRules/privilege/secret-scope) and owns its pipelines/runs.ForgeConnectionresolvesRepoRef→Project. 1 Project : 1 RepoRef in v1 (monorepo per-subdir governance deferred to an optional path scope). The old governedRepostruct is absorbed intoProject;Org → Project → Environment. Thescarab-projectscrate is renamedscarab-project; the vestigialProjectstruct becomes this aggregate.- Webhook routing: separate endpoints per forge (
/webhooks/github,/webhooks/forgejo), each bound to its adapter and verification secret — no payload-sniffing on a shared endpoint. - Registration flows (a good agnosticism test): GitHub auto-registers via
installation/installation_repositorieswebhooks (installing the App is registration); Forgejo connections are admin-registered (base URL + credential), repos added and per-repo webhooks created viaregister_webhook. - Delivery-id dedup: a webhook delivery-id store guards replay (absent
today; the
x-github-deliveryid is read but never checked,crates/scarab-server/src/lib.rs:1741).
Adapter-internal (recorded, not port concerns)
Section titled “Adapter-internal (recorded, not port concerns)”Installation-token caching across replicas, pagination, and secondary-rate-limit backoff live inside each adapter.
Consequences
Section titled “Consequences”- A new agnostic port method for scoped checkout credentials (ADR-0045 S4 is this capability, not “installation token”).
- Base URL becomes adapter config even for GitHub (GHES).
- Two adapters + a contract-test suite is more v1 work, bought deliberately to guarantee forge-agnosticism.
Alternatives considered
Section titled “Alternatives considered”- GitHub-only adapter, Forgejo mapping on paper: cheaper, but an unbuilt second adapter hides GitHub assumptions the paper exercise misses. Rejected in favor of two real adapters.
- Elevate Checks into the port: richer on GitHub, but unfulfillable on Forgejo — a leaky contract. Rejected; Checks is adapter-internal enrichment.