gitea

package module
v0.25.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 15, 2026 License: MIT Imports: 14 Imported by: 4

README

forge-gitea

Gitea and Codeberg release provider for forge

Go Reference Pipeline Coverage phpboyscout Go toolkit

Part of the phpboyscout Go toolkit. Full documentation lives on the core module's site: forge.go.phpboyscout.uk


Implements the forge.Provider release contract for Gitea-family forges, using the official gitea.dev/sdk client, plus the optional KeyManager (SSH-key upload) capability.

Capabilities

Capability Interface Notes
Releases forge.Provider list/get/download releases + checksum/signature
SSH-key upload forge.KeyManager registers a public key on the authenticated account; needs a token, else returns forge.ErrNotSupported
Interactive login forge.Authenticator not implemented — Gitea is personal-access-token only

Gitea has no interactive-login capability: it authenticates with a personal access token, so there is no OAuth/device flow to expose. A caller that type-asserts this provider for forge.Authenticator misses, and setup falls back to manual token entry. Likewise UploadKey returns forge.ErrNotSupported when no token is configured — the key API needs auth — so the caller instructs the user to add the key by hand.

Two source types, one implementation

Codeberg is a Gitea instance, so both register from the same provider:

Source type Host Config section Token fallback
gitea from Endpoint.Host gitea GITEA_TOKEN
codeberg pre-seeded https://codeberg.org codeberg CODEBERG_TOKEN

They differ only in those three values. Splitting them into separate modules would mean duplicating the provider to change three constants.

To a user, Gitea and Codeberg are two different places they hold accounts — a company instance and the public one — so each resolves from its own config section. Configuring either leaves the other untouched.

Use it

import (
    "gitlab.com/phpboyscout/go/forge"

    _ "gitlab.com/phpboyscout/go/forge-gitea"
)

factory, err := forge.Lookup("codeberg") // or "gitea"
provider, err := factory(ctx, source, cfg)
go get gitlab.com/phpboyscout/go/forge-gitea

Configuration

Each source type reads its own section — gitea.* for gitea, codeberg.* for codeberg:

Key Purpose
<section>.auth.value The token, read through forge.ConfigCredential
<section>.url.api Override the API endpoint
<section>.pages.domain Domain a pages service serves this instance's sites under, enabling the forge.Sites capability
GITEA_TOKEN / CODEBERG_TOKEN Well-known fallback, per source type

Gitea and Forgejo have no built-in Pages feature — sites are published by a separate service the forge API knows nothing about, so the URL can only be composed, and only if the domain is known. codeberg defaults to codeberg.page; a self-hosted Forgejo running its own pages server sets pages.domain to enable the capability. Left unset, GetSite returns forge.ErrNotSupported.

gitea:
  url:
    api: https://git.example.com
  pages:
    domain: pages.example.com
  auth:
    value: <token for the company instance>

codeberg:
  auth:
    value: <token for codeberg.org>

A section you do not define is simply absent — the source type falls back to its well-known variable, and a public repository needs neither.

[!IMPORTANT] Codeberg used to read the gitea section. If you configured Codeberg against an earlier release, move gitea.auth.value to codeberg.auth.value — see the CHANGELOG for the version. A Codeberg provider that finds no credential of its own while one sits under gitea fails with ErrCredentialInSharedSection naming both keys, rather than quietly resolving nothing. Nothing changes for gitea.

[!IMPORTANT] gitea.auth.env and gitea.auth.keychain are no longer read. Ordering now belongs to your config stack rather than to a ladder inside this module: an environment reference becomes an env layer (or forge.EnvCredential), and a keychain reference becomes a config-keychain layer. Configuration still carrying either key reports forge.ErrStaleAuthKeys, but only when nothing else supplied a credential. See the migration table in forge's README.

To read the token from somewhere else entirely — a different key, Vault, your own login flow — set Settings.Credential to a forge.CredentialSource and construct the provider directly.

Endpoint.Host selects the instance; the SDK owns the /api/v1 path.

A token is optional — public repositories resolve unauthenticated. A private repository needs a personal access token with the read:repository scope; without it the API answers 404 rather than 403, which reads as "no such release" and is the usual cause of a confusing first failure.

The token travels as Authorization: token <token>, Gitea's standard scheme. Unlike GitLab, Gitea reports real draft status through GetDraft().

[!NOTE] The api_version param is inert. The hand-rolled client this provider replaced built the API path itself, so an instance could in principle be pinned to a non-v1 API. The SDK targets v1 — the only version Gitea has ever shipped — so the param no longer does anything. It is ignored rather than rejected, and a test pins that behaviour so it cannot be silently reintroduced without being reconnected.

Security

The token travels as an Authorization header and is attached only to asset downloads on the configured instance, via forge.HostTrusted. Host, port and scheme must all match, which also refuses a downgrade to plaintext HTTP. Asset URLs come from release metadata a release author controls, so an unpinned credential is an exfiltration primitive.

Documentation

Guides, the provider contract, and how to author your own: forge.go.phpboyscout.uk.

API reference: pkg.go.dev.

License

See LICENSE.

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

View Source
const (
	// CodebergHost is the base URL for Codeberg, the public Forgejo instance.
	CodebergHost = "https://codeberg.org"
)

Variables

View Source
var ErrCredentialInSharedSection = errors.New(
	"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].

View Source
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.

View Source
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:

  1. settings.APIURL (allows runtime override)
  2. 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.

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

func (p *GiteaReleaseProvider) Close(ctx context.Context, owner, repo string, number int) error

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.

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 (p *GiteaReleaseProvider) GetLatestRelease(ctx context.Context, owner, repo string) (forge.Release, error)

func (*GiteaReleaseProvider) GetReleaseByTag

func (p *GiteaReleaseProvider) GetReleaseByTag(ctx context.Context, owner, repo, tag string) (forge.Release, error)

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

func (p *GiteaReleaseProvider) UploadKey(ctx context.Context, name string, publicKey []byte) error

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

func SettingsFromConfig(ep forge.Endpoint, cfg forge.Config, tokenFallbackEnv string) Settings

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL