Skip to content

forge

Typed HTTP client for Forgejo (and Gitea) — repos, branches, issues, pull requests, hooks, runners. Generic Resource[T, C, U] and paginated helpers cover the long tail of CRUD shapes without hand-writing each endpoint. Built on core/go — every constructor that touches config returns a Result.

Terminal window
go get dappco.re/go/forge@latest
import "dappco.re/go/forge"

Two constructors — explicit URL+token or auto-pulled from config flags:

// Explicit
fg := forge.NewForge("https://forge.lthn.sh", os.Getenv("FORGEJO_TOKEN"))
// Config-driven (flags first, env fallback)
r := forge.NewForgeFromConfig("--forge-url", "--forge-token")
if !r.OK { return r }
fg := r.Value.(*forge.Forge)

Forge wraps a *Client plus typed Resource handles for every standard endpoint group. Direct HTTP access stays available via fg.Client() for calls the typed surface doesn’t cover yet.

Resource[T, C, U] parameterises CRUD over three types — the read shape, the create shape, and the update shape. The package exports pre-bound resources for the common Forgejo objects (repos, issues, PRs, hooks, runners, …); custom integrations can spin their own:

type MyKind struct { ID int; Name string }
type CreateMyKind struct { Name string }
type UpdateMyKind struct { Name *string }
r := forge.NewResource[MyKind, CreateMyKind, UpdateMyKind](
fg.Client(),
"/api/v1/orgs/{org}/my-kinds",
)
items, _ := r.List(ctx, map[string]string{"org": "dappcore"})
created, _ := r.Create(ctx, CreateMyKind{Name: "shiny"})

Pagination — three shapes, one signature

Section titled “Pagination — three shapes, one signature”

Forgejo paginates everything. The helpers give callers a choice of memory profile:

// One page at a time — caller drives the loop
page, _ := forge.ListPage[Repo](ctx, fg.Client(), "/repos/search", q, forge.ListOptions{
Page: 1,
PerPage: 50,
})
// All pages collected — slurp into memory
all, _ := forge.ListAll[Repo](ctx, fg.Client(), "/repos/search", q)
// Streaming iterator — Go 1.23+ range-over-func
for repo, err := range forge.ListIter[Repo](ctx, fg.Client(), "/repos/search", q) {
if err != nil { return err }
fmt.Println(repo.FullName)
}

ListIter is the right default for long results — it fetches pages on demand without holding the whole set in memory.

The canonical core/go Service pattern — register once on a Core and every consumer reaches Forgejo through c.Action("forge/..."):

c := core.New(core.Options{})
if r := forge.Register(c); !r.OK { return r }
// With options
svc := forge.NewService(forge.ServiceOptions{
URL: "https://forge.lthn.sh",
Token: os.Getenv("FORGEJO_TOKEN"),
})
if r := svc(c); !r.OK { return r }

The Service constructor reads config + env in the same order as NewForgeFromConfig so a single Core instance has one consistent Forge binding.

  • go/scm — Git operations on the repos forge serves
  • go/process — supervise long-running runners forge dispatches
  • go/webview — the same Result-returning Service shape for browser automation

github.com/dappcore/go-forge — full source, issues, and releases.