287 lines
10 KiB
Markdown
287 lines
10 KiB
Markdown
|
|
# go-forge Design Document
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
**go-forge** is a full-coverage Go client for the Forgejo API (450 endpoints, 284 paths, 229 types). It uses a generic `Resource[T, C, U]` pattern for CRUD operations (91% of endpoints) and hand-written methods for 39 unique action endpoints. Types are generated from Forgejo's `swagger.v1.json` spec.
|
||
|
|
|
||
|
|
**Module path:** `forge.lthn.ai/core/go-forge`
|
||
|
|
|
||
|
|
**Origin:** Extracted from `go-scm/forge/` (45 methods covering 10% of API), expanded to full coverage.
|
||
|
|
|
||
|
|
## Architecture
|
||
|
|
|
||
|
|
```
|
||
|
|
forge.lthn.ai/core/go-forge
|
||
|
|
├── client.go # HTTP client: auth, headers, rate limiting, context.Context
|
||
|
|
├── pagination.go # Generic paginated request helper
|
||
|
|
├── resource.go # Resource[T, C, U] generic CRUD (List/Get/Create/Update/Delete)
|
||
|
|
├── errors.go # Typed error handling (APIError, NotFound, Forbidden, etc.)
|
||
|
|
├── forge.go # Top-level Forge client aggregating all services
|
||
|
|
│
|
||
|
|
├── types/ # Generated from swagger.v1.json
|
||
|
|
│ ├── generate.go # //go:generate directive
|
||
|
|
│ ├── repo.go # Repository, CreateRepoOption, EditRepoOption
|
||
|
|
│ ├── issue.go # Issue, CreateIssueOption, EditIssueOption
|
||
|
|
│ ├── pr.go # PullRequest, CreatePullRequestOption
|
||
|
|
│ ├── user.go # User, CreateUserOption
|
||
|
|
│ ├── org.go # Organisation, CreateOrgOption
|
||
|
|
│ ├── team.go # Team, CreateTeamOption
|
||
|
|
│ ├── label.go # Label, CreateLabelOption
|
||
|
|
│ ├── release.go # Release, CreateReleaseOption
|
||
|
|
│ ├── branch.go # Branch, BranchProtection
|
||
|
|
│ ├── milestone.go # Milestone, CreateMilestoneOption
|
||
|
|
│ ├── hook.go # Hook, CreateHookOption
|
||
|
|
│ ├── key.go # DeployKey, PublicKey, GPGKey
|
||
|
|
│ ├── notification.go # NotificationThread, NotificationSubject
|
||
|
|
│ ├── package.go # Package, PackageFile
|
||
|
|
│ ├── action.go # ActionRunner, ActionSecret, ActionVariable
|
||
|
|
│ ├── commit.go # Commit, CommitStatus, CombinedStatus
|
||
|
|
│ ├── content.go # ContentsResponse, FileOptions
|
||
|
|
│ ├── wiki.go # WikiPage, WikiPageMetaData
|
||
|
|
│ ├── review.go # PullReview, PullReviewComment
|
||
|
|
│ ├── reaction.go # Reaction
|
||
|
|
│ ├── topic.go # TopicResponse
|
||
|
|
│ ├── misc.go # Markdown, License, GitignoreTemplate, NodeInfo
|
||
|
|
│ ├── admin.go # Cron, QuotaGroup, QuotaRule
|
||
|
|
│ ├── activity.go # Activity, Feed
|
||
|
|
│ └── common.go # Shared types: Permission, ExternalTracker, etc.
|
||
|
|
│
|
||
|
|
├── repos.go # RepoService: CRUD + fork, mirror, transfer, template
|
||
|
|
├── issues.go # IssueService: CRUD + pin, deadline, reactions, stopwatch
|
||
|
|
├── pulls.go # PullService: CRUD + merge, update, reviews, dismiss
|
||
|
|
├── orgs.go # OrgService: CRUD + members, avatar, block, hooks
|
||
|
|
├── users.go # UserService: CRUD + keys, followers, starred, settings
|
||
|
|
├── teams.go # TeamService: CRUD + members, repos
|
||
|
|
├── admin.go # AdminService: users, orgs, cron, runners, quota, unadopted
|
||
|
|
├── branches.go # BranchService: CRUD + protection rules
|
||
|
|
├── releases.go # ReleaseService: CRUD + assets
|
||
|
|
├── labels.go # LabelService: repo + org + issue labels
|
||
|
|
├── webhooks.go # WebhookService: CRUD + test hook
|
||
|
|
├── notifications.go # NotificationService: list, mark read
|
||
|
|
├── packages.go # PackageService: list, get, delete
|
||
|
|
├── actions.go # ActionsService: runners, secrets, variables, workflow dispatch
|
||
|
|
├── contents.go # ContentService: file read/write/delete via API
|
||
|
|
├── wiki.go # WikiService: pages
|
||
|
|
├── commits.go # CommitService: status, notes, diff
|
||
|
|
├── misc.go # MiscService: markdown, licenses, gitignore, nodeinfo
|
||
|
|
│
|
||
|
|
├── config.go # URL/token resolution: env → config file → flags
|
||
|
|
│
|
||
|
|
├── cmd/forgegen/ # Code generator: swagger.v1.json → types/*.go
|
||
|
|
│ ├── main.go
|
||
|
|
│ ├── parser.go # Parse OpenAPI 2.0 definitions
|
||
|
|
│ ├── generator.go # Render Go source files
|
||
|
|
│ └── templates/ # Go text/template files for codegen
|
||
|
|
│
|
||
|
|
└── testdata/
|
||
|
|
└── swagger.v1.json # Pinned spec for testing + generation
|
||
|
|
```
|
||
|
|
|
||
|
|
## Key Design Decisions
|
||
|
|
|
||
|
|
### 1. Generic Resource[T, C, U]
|
||
|
|
|
||
|
|
Three type parameters: T (resource type), C (create options), U (update options).
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Resource[T any, C any, U any] struct {
|
||
|
|
client *Client
|
||
|
|
path string // e.g. "/api/v1/repos/{owner}/{repo}/issues"
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *Resource[T, C, U]) List(ctx context.Context, params Params, opts ListOptions) ([]T, error)
|
||
|
|
func (r *Resource[T, C, U]) Get(ctx context.Context, params Params, id string) (*T, error)
|
||
|
|
func (r *Resource[T, C, U]) Create(ctx context.Context, params Params, body *C) (*T, error)
|
||
|
|
func (r *Resource[T, C, U]) Update(ctx context.Context, params Params, id string, body *U) (*T, error)
|
||
|
|
func (r *Resource[T, C, U]) Delete(ctx context.Context, params Params, id string) error
|
||
|
|
```
|
||
|
|
|
||
|
|
`Params` is `map[string]string` resolving path variables: `{"owner": "core", "repo": "go-forge"}`.
|
||
|
|
|
||
|
|
This covers 411 of 450 endpoints (91%).
|
||
|
|
|
||
|
|
### 2. Service Structs Embed Resource
|
||
|
|
|
||
|
|
```go
|
||
|
|
type IssueService struct {
|
||
|
|
Resource[types.Issue, types.CreateIssueOption, types.EditIssueOption]
|
||
|
|
}
|
||
|
|
|
||
|
|
// CRUD comes free. Actions are hand-written:
|
||
|
|
func (s *IssueService) Pin(ctx context.Context, owner, repo string, index int64) error
|
||
|
|
func (s *IssueService) SetDeadline(ctx context.Context, owner, repo string, index int64, deadline *time.Time) error
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Top-Level Forge Client
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Forge struct {
|
||
|
|
client *Client
|
||
|
|
Repos *RepoService
|
||
|
|
Issues *IssueService
|
||
|
|
Pulls *PullService
|
||
|
|
Orgs *OrgService
|
||
|
|
Users *UserService
|
||
|
|
Teams *TeamService
|
||
|
|
Admin *AdminService
|
||
|
|
Branches *BranchService
|
||
|
|
Releases *ReleaseService
|
||
|
|
Labels *LabelService
|
||
|
|
Webhooks *WebhookService
|
||
|
|
Notifications *NotificationService
|
||
|
|
Packages *PackageService
|
||
|
|
Actions *ActionsService
|
||
|
|
Contents *ContentService
|
||
|
|
Wiki *WikiService
|
||
|
|
Commits *CommitService
|
||
|
|
Misc *MiscService
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewForge(url, token string, opts ...Option) *Forge
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Codegen from swagger.v1.json
|
||
|
|
|
||
|
|
The `cmd/forgegen/` tool reads the OpenAPI 2.0 spec and generates:
|
||
|
|
- Go struct definitions with JSON tags and doc comments
|
||
|
|
- Enum constants
|
||
|
|
- Type mapping (OpenAPI → Go)
|
||
|
|
|
||
|
|
229 type definitions → ~25 grouped Go files in `types/`.
|
||
|
|
|
||
|
|
Type mapping rules:
|
||
|
|
| OpenAPI | Go |
|
||
|
|
|---------|-----|
|
||
|
|
| `string` | `string` |
|
||
|
|
| `string` + `date-time` | `time.Time` |
|
||
|
|
| `integer` + `int64` | `int64` |
|
||
|
|
| `integer` | `int` |
|
||
|
|
| `boolean` | `bool` |
|
||
|
|
| `array` of T | `[]T` |
|
||
|
|
| `$ref` | `*T` (pointer) |
|
||
|
|
| nullable | pointer type |
|
||
|
|
| `binary` | `[]byte` |
|
||
|
|
|
||
|
|
### 5. HTTP Client
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Client struct {
|
||
|
|
baseURL string
|
||
|
|
token string
|
||
|
|
httpClient *http.Client
|
||
|
|
userAgent string
|
||
|
|
}
|
||
|
|
|
||
|
|
func New(url, token string, opts ...Option) *Client
|
||
|
|
|
||
|
|
func (c *Client) Get(ctx context.Context, path string, out any) error
|
||
|
|
func (c *Client) Post(ctx context.Context, path string, body, out any) error
|
||
|
|
func (c *Client) Patch(ctx context.Context, path string, body, out any) error
|
||
|
|
func (c *Client) Put(ctx context.Context, path string, body, out any) error
|
||
|
|
func (c *Client) Delete(ctx context.Context, path string) error
|
||
|
|
```
|
||
|
|
|
||
|
|
Options: `WithHTTPClient`, `WithUserAgent`, `WithRateLimit`, `WithLogger`.
|
||
|
|
|
||
|
|
### 6. Pagination
|
||
|
|
|
||
|
|
Forgejo uses `page` + `limit` query params and `X-Total-Count` response header.
|
||
|
|
|
||
|
|
```go
|
||
|
|
type ListOptions struct {
|
||
|
|
Page int
|
||
|
|
Limit int // default 50, max configurable
|
||
|
|
}
|
||
|
|
|
||
|
|
type PagedResult[T any] struct {
|
||
|
|
Items []T
|
||
|
|
TotalCount int
|
||
|
|
Page int
|
||
|
|
HasMore bool
|
||
|
|
}
|
||
|
|
|
||
|
|
// ListAll fetches all pages automatically.
|
||
|
|
func (r *Resource[T, C, U]) ListAll(ctx context.Context, params Params) ([]T, error)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 7. Error Handling
|
||
|
|
|
||
|
|
```go
|
||
|
|
type APIError struct {
|
||
|
|
StatusCode int
|
||
|
|
Message string
|
||
|
|
URL string
|
||
|
|
}
|
||
|
|
|
||
|
|
func IsNotFound(err error) bool
|
||
|
|
func IsForbidden(err error) bool
|
||
|
|
func IsConflict(err error) bool
|
||
|
|
```
|
||
|
|
|
||
|
|
### 8. Config Resolution (from go-scm/forge)
|
||
|
|
|
||
|
|
Priority: flags → environment → config file.
|
||
|
|
|
||
|
|
```go
|
||
|
|
func NewFromConfig(flagURL, flagToken string) (*Forge, error)
|
||
|
|
func ResolveConfig(flagURL, flagToken string) (url, token string, err error)
|
||
|
|
func SaveConfig(url, token string) error
|
||
|
|
```
|
||
|
|
|
||
|
|
Env vars: `FORGE_URL`, `FORGE_TOKEN`. Config file: `~/.config/forge/config.json`.
|
||
|
|
|
||
|
|
## API Coverage
|
||
|
|
|
||
|
|
| Category | Endpoints | CRUD | Actions |
|
||
|
|
|----------|-----------|------|---------|
|
||
|
|
| Repository | 175 | 165 | 10 (fork, mirror, transfer, template, avatar, diffpatch) |
|
||
|
|
| User | 74 | 70 | 4 (avatar, GPG verify) |
|
||
|
|
| Issue | 67 | 57 | 10 (pin, deadline, reactions, stopwatch, labels) |
|
||
|
|
| Organisation | 63 | 59 | 4 (avatar, block/unblock) |
|
||
|
|
| Admin | 39 | 35 | 4 (cron run, rename, adopt, quota set) |
|
||
|
|
| Miscellaneous | 12 | 7 | 5 (markdown render, markup, nodeinfo) |
|
||
|
|
| Notification | 7 | 7 | 0 |
|
||
|
|
| ActivityPub | 6 | 3 | 3 (inbox POST) |
|
||
|
|
| Package | 4 | 4 | 0 |
|
||
|
|
| Settings | 4 | 4 | 0 |
|
||
|
|
| **Total** | **450** | **411** | **39** |
|
||
|
|
|
||
|
|
## Integration Points
|
||
|
|
|
||
|
|
### go-api
|
||
|
|
|
||
|
|
Services implement `DescribableGroup` from go-api Phase 3, enabling:
|
||
|
|
- REST endpoint generation via ToolBridge
|
||
|
|
- Auto-generated OpenAPI spec
|
||
|
|
- Multi-language SDK codegen
|
||
|
|
|
||
|
|
### go-scm
|
||
|
|
|
||
|
|
go-scm/forge/ becomes a thin adapter importing go-forge types. Existing go-scm users are unaffected — the multi-provider abstraction layer stays.
|
||
|
|
|
||
|
|
### go-ai/mcp
|
||
|
|
|
||
|
|
The MCP subsystem can register go-forge operations as MCP tools, giving AI agents full Forgejo API access.
|
||
|
|
|
||
|
|
## 39 Unique Action Methods
|
||
|
|
|
||
|
|
These require hand-written implementation:
|
||
|
|
|
||
|
|
**Repository:** migrate, fork, generate (template), transfer, accept/reject transfer, mirror sync, push mirror sync, avatar, diffpatch, contents (multi-file modify)
|
||
|
|
|
||
|
|
**Pull Requests:** merge, update (rebase), submit review, dismiss/undismiss review
|
||
|
|
|
||
|
|
**Issues:** pin, set deadline, add reaction, start/stop stopwatch, add issue labels
|
||
|
|
|
||
|
|
**Comments:** add reaction
|
||
|
|
|
||
|
|
**Admin:** run cron task, adopt unadopted, rename user, set quota groups
|
||
|
|
|
||
|
|
**Misc:** render markdown, render raw markdown, render markup, GPG key verify
|
||
|
|
|
||
|
|
**ActivityPub:** inbox POST (actor, repo, user)
|
||
|
|
|
||
|
|
**Actions:** dispatch workflow
|
||
|
|
|
||
|
|
**Git:** set note on commit, test webhook
|