5. Forge abstraction layer
Date: 2026-04-02
Status
Accepted
Context
Fullsend must eventually support GitHub, GitLab, and Forgejo. Every operation that touches the git forge — creating repos, managing secrets, writing files, listing installations — must work across all three. Without a shared abstraction, forge-specific logic would spread throughout the codebase, making multi-forge support a rewrite rather than an extension.
Decision
All forge operations go through the forge.Client interface (internal/forge/forge.go). The interface uses forge-neutral vocabulary: ChangeProposal instead of "pull request" or "merge request," CreateChangeProposal instead of CreatePR. Forge-specific implementations live in sub-packages (internal/forge/github/). A thread-safe FakeClient exists for testing without forge access.
No code outside internal/forge/ imports forge-specific packages directly.
Consequences
- Adding a new forge (GitLab, Forgejo) requires implementing
forge.Client— no changes to layers, CLI, or app setup code. - Forge-neutral naming occasionally feels awkward (e.g.,
ChangeProposal), but prevents GitHub-centric thinking from leaking into the model. - The interface will grow as new operations are needed; keeping it cohesive requires discipline.
- The
FakeClientenables deterministic testing of every layer without network calls. - Sentinel errors (
ErrNotFound,ErrBranchProtected,ErrAlreadyExists) witherrors.Is()helpers provide forge-agnostic error classification.ErrNotFoundandErrAlreadyExistsare mapped inAPIError.Unwrap()for automatic propagation.ErrBranchProtectedis wrapped contextually at the call site (e.g.,commitFilesTo) where the operation context disambiguates branch-protection 422s from other validation failures. CommitFilesToBranchcomplementsCommitFiles(default branch) by targeting a specific branch, enabling the protected-branch fallback path where scaffold files are committed to a feature branch and delivered via PR.
