Documentation
¶
Overview ¶
Package gitea provides a forge.Provider implementation for Gitea and Forgejo instances, including Codeberg (codeberg.org). It also implements the optional forge.KeyManager capability (SSH public-key upload). It does NOT implement forge.Authenticator: Gitea is personal-access-token only, so interactive login falls back to manual token entry. Provider construction uses package-owned Settings; GTB config integration lives in SettingsFromConfig.
Index ¶
- Constants
- Variables
- func NewProviderFromClient(_ context.Context, client *giteasdk.Client, settings Settings) (forge.Provider, error)
- type GiteaReleaseProvider
- func (p *GiteaReleaseProvider) AddReleaseAsset(ctx context.Context, owner, repo, tagName, name string, _ int64, ...) error
- func (p *GiteaReleaseProvider) AddReleaseAssetLocation(ctx context.Context, owner, repo, tagName string, ...) error
- func (p *GiteaReleaseProvider) Close(ctx context.Context, owner, repo string, number int) error
- func (p *GiteaReleaseProvider) Comment(ctx context.Context, owner, repo string, number int, draft forge.CommentDraft) (forge.Comment, error)
- func (p *GiteaReleaseProvider) Create(ctx context.Context, owner, repo string, draft forge.PullRequestDraft) (forge.PullRequest, error)
- func (p *GiteaReleaseProvider) CreateIssue(ctx context.Context, owner, repo string, draft forge.IssueDraft) (forge.Issue, error)
- func (p *GiteaReleaseProvider) CreateIssueComment(ctx context.Context, owner, repo string, number int, draft forge.CommentDraft) (forge.Comment, error)
- func (p *GiteaReleaseProvider) CreateRelease(ctx context.Context, owner, repo string, draft forge.ReleaseDraft) (forge.Release, error)
- func (p *GiteaReleaseProvider) CreateReleaseWithAssets(ctx context.Context, owner, repo string, draft forge.ReleaseDraft, ...) (forge.Release, error)
- func (p *GiteaReleaseProvider) CreateRepository(ctx context.Context, owner string, draft forge.RepositoryDraft) (forge.Repository, error)
- func (p *GiteaReleaseProvider) CreateWikiPage(ctx context.Context, owner, repo string, page forge.WikiPage, message string) error
- func (p *GiteaReleaseProvider) DownloadReleaseAsset(ctx context.Context, _, _ string, asset forge.ReleaseAsset) (io.ReadCloser, string, error)
- func (p *GiteaReleaseProvider) Find(ctx context.Context, owner, repo, sourceBranch string) (forge.PullRequest, error)
- func (p *GiteaReleaseProvider) FindLastMerged(ctx context.Context, owner, repo, sourceBranch string) (forge.PullRequest, error)
- func (p *GiteaReleaseProvider) GetFile(ctx context.Context, owner, repo, path, ref string, maxBytes int64) ([]byte, error)
- func (p *GiteaReleaseProvider) GetIssue(ctx context.Context, owner, repo string, number int) (forge.Issue, error)
- func (p *GiteaReleaseProvider) GetLatestRelease(ctx context.Context, owner, repo string) (forge.Release, error)
- func (p *GiteaReleaseProvider) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (forge.Release, error)
- func (p *GiteaReleaseProvider) GetSite(ctx context.Context, owner, repo string) (forge.Site, error)
- func (p *GiteaReleaseProvider) GetWikiPage(ctx context.Context, owner, repo, path string, maxBytes int64) (forge.WikiPage, error)
- func (p *GiteaReleaseProvider) ListComments(ctx context.Context, owner, repo string, number int, q forge.CommentQuery, ...) error
- func (p *GiteaReleaseProvider) ListReleases(ctx context.Context, owner, repo string, limit int) ([]forge.Release, error)
- func (p *GiteaReleaseProvider) ListRepositories(ctx context.Context, namespace string, opts forge.RepositoryListOptions, ...) error
- func (p *GiteaReleaseProvider) ListRequestComments(ctx context.Context, owner, repo string, number int) ([]forge.Comment, error)
- func (p *GiteaReleaseProvider) ListWikiPages(ctx context.Context, owner, repo string) ([]forge.WikiPage, error)
- func (p *GiteaReleaseProvider) Merge(ctx context.Context, owner, repo string, number int, opts forge.MergeOptions) (forge.MergeOutcome, error)
- func (p *GiteaReleaseProvider) PublishRelease(ctx context.Context, owner, repo, tagName string) (forge.Release, error)
- func (p *GiteaReleaseProvider) ResolveMergedCommit(ctx context.Context, owner, repo string, number int) (string, error)
- func (p *GiteaReleaseProvider) SearchIssues(ctx context.Context, owner, repo string, q forge.IssueQuery, ...) error
- func (p *GiteaReleaseProvider) Update(ctx context.Context, owner, repo string, number int, title, body string) error
- func (p *GiteaReleaseProvider) UpdateRelease(ctx context.Context, owner, repo, tagName, name, body string) error
- func (p *GiteaReleaseProvider) UpdateWikiPage(ctx context.Context, owner, repo string, page forge.WikiPage, message string) error
- func (p *GiteaReleaseProvider) UploadKey(ctx context.Context, name string, publicKey []byte) error
- type Settings
Constants ¶
const (
// CodebergHost is the base URL for Codeberg, the public Forgejo instance.
CodebergHost = "https://codeberg.org"
)
Variables ¶
"credential is configured in a config section this source type no longer reads")
ErrCredentialInSharedSection reports a credential still sitting in the config section a source type used to resolve from, after it gained one of its own.
Codeberg read this module's `gitea` section until it was given `codeberg`. A configuration migrated in code but not on disk would otherwise fail silently: the section looks correct, the provider finds no credential, and the 401 surfaces a long way from its cause.
That is the same failure shape forge.ErrStaleAuthKeys exists for, and it is reported the same way — deliberately not fatal on its own, see [misplacedCredential].
var ErrCredentialWithClient = errors.NewSentinel("forge_gitea.credential_with_client",
"a credential was supplied alongside an injected client; the client carries its own")
ErrCredentialWithClient reports a credential supplied alongside an injected client.
It is an error rather than a silently ignored field because the two answers a caller might expect are both wrong. Ignoring it would let someone believe their credential is in play when the client's is; layering it would mean two credentials on one connection, with no way to say which the forge saw.
var ErrHostRequiredWithClient = errors.NewSentinel("forge_gitea.host_required_with_client",
"an injected client needs Endpoint.Host set; the Gitea SDK does not expose its own URL")
ErrHostRequiredWithClient reports an injected client with no [Endpoint.Host].
The Gitea SDK keeps its instance URL unexported and offers no accessor, so — unlike go-github and go-gitlab — this module cannot ask the client which instance it addresses. The host has to be supplied, and guessing it would mean guessing where a published site lives.
Functions ¶
func NewProviderFromClient ¶ added in v0.13.0
func NewProviderFromClient( _ context.Context, client *giteasdk.Client, settings Settings, ) (forge.Provider, error)
NewProviderFromClient builds a provider on a Gitea SDK client the caller already has — rung 1 of the ladder in spec 0008 D10.
This rung transfers the credential obligation ¶
The client carries its own authentication and this provider adds none. Settings.Credential must be nil, and supplying one is ErrCredentialWithClient.
Endpoint.Host is REQUIRED here ¶
Every other adapter reads the instance back off the injected client. This one cannot: gitea.dev/sdk keeps its url field unexported and exposes no accessor (checked at v1.2.0 — every exported method on its Client is a typed API call or a setter). The host is what the Sites capability derives a published site's domain from, so an absent one is ErrHostRequiredWithClient rather than a default that would point at the wrong instance.
Asset downloads are ANONYMOUS at this rung ¶
A Gitea release asset is fetched from browser_download_url, which is not an API call — the SDK never makes it, and offers no method that would. This provider therefore fetches it directly and would normally attach its own Authorization header, gated by forge.HostTrusted. It has no credential to attach here, and the SDK exposes no way to read one back.
A PUBLIC asset downloads normally. A private one will fail. A caller who needs authenticated downloads should use a lower rung — forge.WithHTTPTransport, or Settings with a Credential — where this module still owns the credential.
Types ¶
type GiteaReleaseProvider ¶
type GiteaReleaseProvider struct {
// contains filtered or unexported fields
}
GiteaReleaseProvider implements forge.Provider for Gitea/Forgejo/Codeberg.
func NewReleaseProvider ¶
func NewReleaseProvider(ctx context.Context, settings Settings) (*GiteaReleaseProvider, error)
NewReleaseProvider constructs a GiteaReleaseProvider from explicit typed settings.
The host is resolved in order:
- settings.APIURL (allows runtime override)
- settings.Endpoint.Host
The credential comes from Settings.Credential, or — when that is nil — from Settings.TokenFallbackEnv. The context bounds its resolution: a source the caller supplied may reach a keychain or a remote secret store.
func (*GiteaReleaseProvider) AddReleaseAsset ¶ added in v0.16.0
func (p *GiteaReleaseProvider) AddReleaseAsset( ctx context.Context, owner, repo, tagName, name string, _ int64, content io.Reader, ) error
AddReleaseAsset attaches a file to the release on tagName.
Gitea takes the bytes directly and its SDK already accepts an io.Reader, so this is the one adapter where the contract's shape and the platform's agree without adaptation. The size is unused for the same reason: the SDK builds the multipart body itself.
func (*GiteaReleaseProvider) AddReleaseAssetLocation ¶ added in v0.18.0
func (p *GiteaReleaseProvider) AddReleaseAssetLocation( ctx context.Context, owner, repo, tagName string, asset forge.ReleaseAssetSource, ) error
AddReleaseAssetLocation reports that Gitea cannot hold a location as an attachment. See forge.ReleaseAssetPublisher.
There is no footer fallback here, deliberately ¶
Rendering into the notes would mean reading the release body, appending, and writing it back — and that read-modify-write carries the lost-update race UpdateRelease already documents as unsolved, on every call. A caller that needs the location recorded uses CreateReleaseWithAssets, where the body is composed once and nothing existing can be lost.
func (*GiteaReleaseProvider) Close ¶ added in v0.15.0
Close closes a pull request without merging it.
func (*GiteaReleaseProvider) Comment ¶ added in v0.21.0
func (p *GiteaReleaseProvider) Comment( ctx context.Context, owner, repo string, number int, draft forge.CommentDraft, ) (forge.Comment, error)
Comment posts a comment on a pull request's conversation.
func (*GiteaReleaseProvider) Create ¶ added in v0.15.0
func (p *GiteaReleaseProvider) Create( ctx context.Context, owner, repo string, draft forge.PullRequestDraft, ) (forge.PullRequest, error)
Create opens a pull request.
func (*GiteaReleaseProvider) CreateIssue ¶ added in v0.4.0
func (p *GiteaReleaseProvider) CreateIssue( ctx context.Context, owner, repo string, draft forge.IssueDraft, ) (forge.Issue, error)
CreateIssue files an issue.
Labels are IDs here, not names ¶
Gitea's create endpoint takes label IDs, while the contract carries names. So the names are resolved against the repository's labels first, and an unresolvable name is an ERROR rather than a silent omission.
That choice is deliberate and is the harsher of the two. A label that quietly fails to apply leaves an issue that exists but cannot be found by the label the caller uses to find it — and a caller filing for provenance discovers that only when it goes looking. Failing loudly costs one create; failing quietly costs the ability to audit the whole stream.
func (*GiteaReleaseProvider) CreateIssueComment ¶ added in v0.21.0
func (p *GiteaReleaseProvider) CreateIssueComment( ctx context.Context, owner, repo string, number int, draft forge.CommentDraft, ) (forge.Comment, error)
CreateIssueComment posts a comment on an issue.
func (*GiteaReleaseProvider) CreateRelease ¶ added in v0.16.0
func (p *GiteaReleaseProvider) CreateRelease( ctx context.Context, owner, repo string, draft forge.ReleaseDraft, ) (forge.Release, error)
CreateRelease publishes a release for draft.TagName.
func (*GiteaReleaseProvider) CreateReleaseWithAssets ¶ added in v0.18.0
func (p *GiteaReleaseProvider) CreateReleaseWithAssets( ctx context.Context, owner, repo string, draft forge.ReleaseDraft, assets []forge.ReleaseAssetSource, ) (forge.Release, error)
CreateReleaseWithAssets publishes a release that already carries its assets. See forge.ReleaseAssetPublisher.
The draft is the MECHANISM, the same shape forge-github uses ¶
Gitea's create endpoint takes no attachments: they are posted to the release afterwards, so a release created published is empty for as long as the uploads take.
A DRAFT is invisible to anyone without write access, and that is enforced in Gitea's own handler rather than merely documented: GetRelease answers not-found when release.IsDraft and the caller cannot write, and ListReleases passes IncludeDrafts only for a signed-in writer. So this creates a draft, attaches into it, and publishes last.
A caller that asked for a draft ITSELF keeps one: the same route runs and the final publish step is not taken.
A LOCATION cannot be an attachment here, so it becomes a footer ¶
Gitea has no concept of a release asset that is a link. Checked against the pinned SDK — CreateReleaseAttachment takes an io.Reader and a filename, and Attachment carries a server-issued UUID and a browser_download_url Gitea generates — and against Gitea's CURRENT source, because a pinned SDK is exactly where an absence claim goes wrong: gitea.dev/sdk v1.2.0 is the latest published version, and routers/api/v1/repo/release_attachment.go on main accepts only multipart form data or a raw binary body, with no external_url parameter.
So a location is rendered into the release notes by forge.RenderAssetFooter AND reported with forge.ErrNotHonoured. Nothing is fetched.
func (*GiteaReleaseProvider) CreateRepository ¶ added in v0.21.0
func (p *GiteaReleaseProvider) CreateRepository( ctx context.Context, owner string, draft forge.RepositoryDraft, ) (forge.Repository, error)
CreateRepository creates a repository in the namespace named by owner.
func (*GiteaReleaseProvider) CreateWikiPage ¶ added in v0.20.0
func (p *GiteaReleaseProvider) CreateWikiPage( ctx context.Context, owner, repo string, page forge.WikiPage, message string, ) error
CreateWikiPage adds a page.
Unlike GitLab, Gitea carries a commit message, so nothing is dropped and ErrNotHonoured never fires here.
func (*GiteaReleaseProvider) DownloadReleaseAsset ¶
func (p *GiteaReleaseProvider) DownloadReleaseAsset(ctx context.Context, _, _ string, asset forge.ReleaseAsset) (io.ReadCloser, string, error)
func (*GiteaReleaseProvider) Find ¶ added in v0.15.0
func (p *GiteaReleaseProvider) Find( ctx context.Context, owner, repo, sourceBranch string, ) (forge.PullRequest, error)
Find returns the OPEN pull request opened from sourceBranch.
Why not GetPullRequestByBaseHead ¶
The SDK has one, and it is the obvious choice. It is not used, for two reasons. It requires a BASE as well as a head, and this contract's Find takes only the source branch — the target is not the caller's to know here. And it carries a Gitea 1.22.0 server floor that listing does not, so using it would narrow which instances this method works against for no gain.
func (*GiteaReleaseProvider) FindLastMerged ¶ added in v0.15.0
func (p *GiteaReleaseProvider) FindLastMerged( ctx context.Context, owner, repo, sourceBranch string, ) (forge.PullRequest, error)
FindLastMerged returns the most recently merged pull request from sourceBranch, ordered by merge time.
Gitea cannot order by merge time, so this does it here ¶
ListPullRequestsOptions.Sort accepts oldest, recentupdate, leastupdate, mostcomment, leastcomment and priority. None of those is merge time, and recentupdate is not a substitute: a comment posted after a merge moves it.
So the ordering is done client-side over the closed set, which is bounded by resolveScanLimit pull requests. A branch with more merged pull requests than that is not one this method can answer honestly, and it says so rather than returning the newest of a truncated page.
Gitea has no "merged" state ¶
A merged pull request is CLOSED with HasMerged set. Filtering on state alone would return declined ones too, which is the wrong answer to "what landed".
func (*GiteaReleaseProvider) GetFile ¶ added in v0.3.0
func (p *GiteaReleaseProvider) GetFile( ctx context.Context, owner, repo, path, ref string, maxBytes int64, ) ([]byte, error)
GetFile reads one file at a ref without cloning.
As on GitLab, the bound is applied by asking for the size FIRST: the SDK's GetFile buffers the whole body before returning it, so a length check afterwards would measure rather than limit. GetContents reports Size without the bytes.
func (*GiteaReleaseProvider) GetIssue ¶ added in v0.4.0
func (p *GiteaReleaseProvider) GetIssue( ctx context.Context, owner, repo string, number int, ) (forge.Issue, error)
GetIssue returns one issue by its per-repository index.
func (*GiteaReleaseProvider) GetLatestRelease ¶
func (*GiteaReleaseProvider) GetReleaseByTag ¶
func (*GiteaReleaseProvider) GetSite ¶ added in v0.3.0
func (p *GiteaReleaseProvider) GetSite( ctx context.Context, owner, repo string, ) (forge.Site, error)
GetSite reports a repository's published site.
Gitea itself has no Pages feature, so this returns ErrNotSupported for a plain instance — the documented "configured off" refusal, which a caller treats exactly like the interface being absent.
Codeberg does publish sites, but NOT as a forge object: Codeberg Pages is a separate service, triggered by a webhook the owner configures against a branch named "pages". Two things follow, and they pull in opposite directions:
- Publication cannot be confirmed. Webhooks are visible only to repository admins, so a "pages" branch is NECESSARY but not SUFFICIENT. State is therefore reported as Unknown rather than guessed at.
- The URL is free. It is deterministic from data the caller already has, which is the exact inverse of GitLab and GitHub, where the URL is the part a privileged endpoint is needed to learn.
The branch check is what stops this being a guess. The URL template always composes, so returning it unconditionally would mean GetSite never reports ErrNotFound and every repository appears to have a site.
func (*GiteaReleaseProvider) GetWikiPage ¶ added in v0.20.0
func (p *GiteaReleaseProvider) GetWikiPage( ctx context.Context, owner, repo, path string, maxBytes int64, ) (forge.WikiPage, error)
GetWikiPage returns one page and its content.
func (*GiteaReleaseProvider) ListComments ¶ added in v0.4.0
func (p *GiteaReleaseProvider) ListComments( ctx context.Context, owner, repo string, number int, q forge.CommentQuery, yield func(forge.Comment) bool, ) error
ListComments yields an issue's comments.
Gitea takes Since server-side, so unlike the GitLab adapter this needs no newest-first emulation — and consequently yields OLDEST-first. The contract promises no ordering precisely because those two are opposite.
func (*GiteaReleaseProvider) ListReleases ¶
func (p *GiteaReleaseProvider) ListReleases(ctx context.Context, owner, repo string, limit int) ([]forge.Release, error)
ListReleases returns up to limit releases, paginating across Gitea's pages (signalled by the Link header's rel="next") until the limit is met or history is exhausted. A limit <= 0 means "no explicit bound" — the natural first page. See forge.Provider.
func (*GiteaReleaseProvider) ListRepositories ¶ added in v0.3.0
func (p *GiteaReleaseProvider) ListRepositories( ctx context.Context, namespace string, opts forge.RepositoryListOptions, yield func(forge.Repository) bool, ) error
ListRepositories enumerates the repositories owned by a namespace.
Gitea splits organisations from users across two endpoints, and the namespace string does not say which it is. The kind is RESOLVED rather than guessed: a dedicated lookup decides, and only then does enumeration begin.
The tempting shortcut — call the org endpoint and fall back to the user endpoint on 404 — is forbidden by the contract, because on forges where the user endpoint also answers for organisations it returns their PUBLIC repositories only. That yields a short list indistinguishable from a complete one, which is precisely the silent truncation this API is shaped to prevent.
func (*GiteaReleaseProvider) ListRequestComments ¶ added in v0.21.0
func (p *GiteaReleaseProvider) ListRequestComments( ctx context.Context, owner, repo string, number int, ) ([]forge.Comment, error)
ListRequestComments returns every comment on a pull request.
func (*GiteaReleaseProvider) ListWikiPages ¶ added in v0.20.0
func (p *GiteaReleaseProvider) ListWikiPages( ctx context.Context, owner, repo string, ) ([]forge.WikiPage, error)
ListWikiPages returns every page in the project's wiki, without content.
func (*GiteaReleaseProvider) Merge ¶ added in v0.24.0
func (p *GiteaReleaseProvider) Merge( ctx context.Context, owner, repo string, number int, opts forge.MergeOptions, ) (forge.MergeOutcome, error)
Merge merges the pull request, or with WhenReady schedules it.
One endpoint does both on Gitea. With merge_when_checks_succeed the handler ALWAYS schedules (201, empty body) and the auto-merge queue runs a check straight away, so a ready pull request merges moments later and the outcome is Scheduled every time (routers/api/v1/repo/pull.go, read 2026-09-15). A pull request already scheduled answers 409 and is reported Scheduled again, since that is the state that was asked for.
The SDK requires a merge style on every call, and the contract carries none, so the repository's default_merge_style is read and sent (spec 0024 D7). The SDK also pins the head commit it read, so a push in between is a 409 "head out of date", reported as forge.ErrConflict.
A pull request that has already merged is answered from the read: Gitea would answer 404 (ErrIsClosed) to the merge, which is the wrong sentinel for a request that is right there. Spec 0024 D4.
func (*GiteaReleaseProvider) PublishRelease ¶ added in v0.22.0
func (p *GiteaReleaseProvider) PublishRelease( ctx context.Context, owner, repo, tagName string, ) (forge.Release, error)
PublishRelease publishes the release on tagName.
Gitea can find it by tag, and GitHub cannot ¶
Its GetRelease handler answers not-found for a draft only when the caller cannot write, so a writer sees one — measured on 1.27.3, where the tag lookup returned the draft with a 200 and anonymous access got a 404. GitHub answers 404 either way and has to scan the release listing instead.
The ambiguous case spec 0022 D4 describes cannot arise here ¶
Gitea permits ONE release per tag: a second create answers 409, measured. So there is no "which of these do you mean" to refuse, and no code for it. The refusal is GitHub's problem because GitHub is the forge that allows the state.
func (*GiteaReleaseProvider) ResolveMergedCommit ¶ added in v0.15.0
func (p *GiteaReleaseProvider) ResolveMergedCommit( ctx context.Context, owner, repo string, number int, ) (string, error)
ResolveMergedCommit returns the commit ON THE TARGET BRANCH that this pull request produced.
Candidates cheapest first: the recorded merge commit, then a reverse-lookup walk of the target branch. EVERY candidate goes through confirmOnBranch before it is returned, and one that does not confirm is discarded rather than reported.
Returns an error wrapping forge.ErrNotFound when nothing confirms. Never a best guess: a plausible wrong SHA becomes a permanent tag.
func (*GiteaReleaseProvider) SearchIssues ¶ added in v0.4.0
func (p *GiteaReleaseProvider) SearchIssues( ctx context.Context, owner, repo string, q forge.IssueQuery, yield func(forge.Issue) bool, ) error
SearchIssues yields issues matching q.
func (*GiteaReleaseProvider) Update ¶ added in v0.15.0
func (p *GiteaReleaseProvider) Update( ctx context.Context, owner, repo string, number int, title, body string, ) error
Update replaces the title and body.
Both are sent unconditionally, as the contract requires: a forge distinguishes "field omitted, leave it" from "field sent empty, clear it", and sending both every time is what makes the caller's intent unambiguous.
func (*GiteaReleaseProvider) UpdateRelease ¶ added in v0.16.0
func (p *GiteaReleaseProvider) UpdateRelease( ctx context.Context, owner, repo, tagName, name, body string, ) error
UpdateRelease amends an existing release's name and notes.
func (*GiteaReleaseProvider) UpdateWikiPage ¶ added in v0.20.0
func (p *GiteaReleaseProvider) UpdateWikiPage( ctx context.Context, owner, repo string, page forge.WikiPage, message string, ) error
UpdateWikiPage replaces a page's content.
The sub_url is resolved rather than computed — see the file comment. The title is sent alongside it so the page keeps the identity the caller named.
func (*GiteaReleaseProvider) UploadKey ¶ added in v0.2.0
UploadKey implements the optional forge.KeyManager capability: it registers an OpenSSH-format public key on the authenticated account via Gitea's user-keys API. The provider's resolved token (see Settings.Credential) authorises the call; name is the label shown in the account's key list.
Gitea's key API requires authentication, so an unauthenticated provider returns forge.ErrNotSupported; the caller then falls back to manual key entry.
type Settings ¶
type Settings struct {
// Endpoint addresses this instance. Type is [forge.SourceTypeGitea] or
// [forge.SourceTypeCodeberg] — this module registers both — and also selects
// the configuration subtree read by [SettingsFromConfig]. Host is the
// instance root; Name selects which configured source this is.
Endpoint forge.Endpoint
APIURL string `json:"api_url" yaml:"api_url"`
// Credential yields the token this provider authenticates with.
//
// Nil is not an error: [NewReleaseProvider] falls back to
// TokenFallbackEnv, so construction from configuration alone keeps working
// and a public repository needs nothing at all. Set it to take over
// entirely — including to hand in a token directly:
//
// Credential: forge.StaticCredential(token)
Credential forge.CredentialSource
// TokenFallbackEnv names the environment variable consulted last when
// Credential is nil. This module registers two source types, and the only
// credential difference between them is this name.
TokenFallbackEnv string
// Logger receives this provider's diagnostics. Nil discards them, and is
// never [slog.Default]. See [forge.WithLogger] for the registry route.
Logger *slog.Logger
// ConfigWarning describes a configuration problem detected while building
// these settings. It is NOT an error: the settings are usable and the
// provider will build. The constructor logs it at WARN.
//
// [SettingsFromConfig] sets it when the configuration it was handed looks
// like a pre-scoped subtree rather than the root — a mistake that resolves
// no credential and reports nothing, so a diagnostic is the only way a
// caller learns of it. See [forge.PreScopedConfig] and spec 0011.
ConfigWarning error
// HTTPTransport is the transport this provider builds its clients on, so
// several providers share one connection pool and TLS session cache. Nil
// means it builds its own, which is the default and always valid.
//
// This provider still builds the client, and so keeps its own redirect
// policy. It is the rung to prefer. Set through the registry with
// [forge.WithHTTPTransport].
HTTPTransport http.RoundTripper
// HTTPClient replaces the client this provider would have built for its own
// API requests — redirect policy included, and the obligation with it.
//
// It is NOT used for asset downloads; see connection.go for why. Set
// through the registry with [forge.WithHTTPClient].
HTTPClient *http.Client
// PagesDomain is the domain a pages service serves this instance's
// repository sites under, enabling the [forge.Sites] capability.
//
// Gitea and Forgejo have no built-in Pages feature, so sites are published
// by a SEPARATE service and the forge API knows nothing about them — which
// means the URL can only be composed, and only if the domain is known.
// Codeberg is defaulted (codeberg.page); a self-hosted Forgejo running its
// own pages server sets this to enable the capability, either here or
// through the section's `pages.domain` key.
//
// Empty means the instance publishes no sites, and GetSite returns
// [forge.ErrNotSupported].
PagesDomain string `json:"pages_domain" yaml:"pages_domain"`
}
Settings contains the typed configuration needed to construct a Gitea release provider without binding the provider to GTB config.
Fields are populated by the config adapter via the narrow forge.Config seam, not decoded with mapstructure. The json/yaml tags are for documentation and serialisation only.
func SettingsFromConfig ¶
SettingsFromConfig adapts one source type's config subtree into typed provider settings. `url.api`, `pages.domain` and the credential key all resolve from the endpoint's own section — `gitea.url.api` and `gitea.auth.value` for the gitea source type, `codeberg.url.api` and `codeberg.auth.value` for codeberg — matching how the GitHub and GitLab adapters read their own sections.
Note the config keys are not the struct tags: Settings serialises PagesDomain as `pages_domain`, exactly as it serialises APIURL as `api_url` while reading `url.api`. The tags describe the settings value, the keys describe the config tree, and they are deliberately separate spellings.
The two source types resolve from SEPARATE sections. Sharing one meant a token stored for either forge was stored for both, and that `gitea.url.api` redirected Codeberg lookups at a self-hosted instance. The registry factory composes an extra report over the credential for a source type whose section moved — see [misplacedCredential]; it is not part of this adapter, which sees only the one section it was handed.
The credential composition IS the precedence, and it is written here rather than hidden in a resolution chain: the configured key, then the well-known variable for whichever source type registered this provider. A caller wanting a different order — or a different key entirely — builds their own forge.CredentialSource and assigns Settings.Credential.
A nil cfg is not special-cased: forge.ConfigCredential treats a nil config as contributing nothing, so a config-free public lookup still reaches the environment fallback.
cfg is the ROOT configuration, not a pre-scoped subtree: the endpoint resolves its own section, because which subtree a source reads is part of what the endpoint means. Gitea and Codeberg therefore keep resolving from their own sections — which is what spec 0001 settled — without this module tracking the section name separately from the source type.