gitlab

package module
v0.20.0 Latest Latest
Warning

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

Go to latest
Published: Sep 7, 2026 License: MIT Imports: 17 Imported by: 0

README

forge-gitlab

GitLab 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 contract for GitLab releases, using gitlab-org/api/client-go.

It is its own module so a tool that only supports GitLab never compiles GitHub's client, Gitea's, or Bitbucket's. A depfootprint guard asserts that.

Use it

Blank-import to register, then resolve through the registry — your code never names a GitLab type:

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

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

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

Configuration

Key Purpose
gitlab.auth.value The token, read through forge.ConfigCredential
gitlab.auth.client_id OAuth app client ID for device-flow login (else GITLAB_CLIENT_ID)
gitlab.url.api Override the API endpoint
GITLAB_TOKEN Well-known fallback

[!IMPORTANT] gitlab.auth.env and gitlab.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 — so a stale key beside a working variable stays quiet. See the migration table in forge's README.

Endpoint.Host selects a self-hosted instance; empty means gitlab.com. A token is optional — public projects resolve unauthenticated.

Platform differences

GitLab's release model differs from GitHub's in ways the shared contract cannot hide:

Contract On GitLab
GetDraft() Always false — GitLab has no draft-release concept, so there is nothing to report. Code branching on it will treat every GitLab release as published.
DownloadReleaseAsset redirect Always empty. Only GitHub redirects asset requests to a CDN.
GetLatestRelease Fetches the first page of releases sorted newest-first, rather than a dedicated latest endpoint.

Security

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

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 gitlab implements the VCS release provider for GitLab repositories, supporting both public and token-authenticated access; the owner may be a slash-separated group path passed through to the GitLab API. Provider construction uses package-owned Settings; GTB config integration lives in SettingsFromConfig.

Index

Constants

View Source
const DefaultClientIDEnv = "GITLAB_CLIENT_ID"

DefaultClientIDEnv is the well-known environment variable consulted for the OAuth application client ID the interactive device-flow login ([Authenticator]) requires, when Settings.ClientID is empty.

View Source
const DefaultTokenEnv = "GITLAB_TOKEN"

DefaultTokenEnv is the well-known environment variable the default credential composition consults last. See SettingsFromConfig.

This is the one rung of the old resolution chain that layer composition cannot express — an unprefixed, forge-chosen variable name — and it is what CI injects, so it is preserved as a composed default rather than dropped.

Variables

View Source
var ErrCredentialWithClient = errors.NewSentinel("forge_gitlab.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 ErrEndpointWithClient = errors.NewSentinel("forge_gitlab.endpoint_with_client",
	"an API URL was supplied alongside an injected client; the client already has its own")

ErrEndpointWithClient reports an API URL supplied alongside an injected client.

Settings.APIURL configures the client this module would otherwise build. Alongside one that already exists it is read by nothing, so a caller setting it is addressing an instance the provider will never contact.

Functions

func NewProviderFromClient added in v0.12.0

func NewProviderFromClient(
	_ context.Context, client *gitlabsdk.Client, settings Settings,
) (forge.Provider, error)

NewProviderFromClient builds a provider on a go-gitlab 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, which is the point of the rung: a caller reaching for it has authentication go-gitlab can express and this module cannot. Settings.Credential must be nil, and supplying one is ErrCredentialWithClient.

Asset downloads are ANONYMOUS at this rung

This is the consequence worth knowing before choosing it, and it is specific to GitLab. A release asset here is a LINK — an arbitrary URL a release author supplied — so fetching it is not an API call and go-gitlab never makes it. This provider therefore fetches it directly, and authenticates that request by attaching PRIVATE-TOKEN itself.

It cannot do that with an injected client. go-gitlab exposes no way to read the credential back: every method on its Client was checked at v2.58.0, and none yields the token or the AuthSource. Its own arbitrary-URL method, NewRequestToURL, does attach the credential — and follows a redirect off the instance still carrying it, which is the leak this module's credential pinning exists to prevent, so it is not an option either.

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 and the redirect policy that keeps it on the pinned host.

The API base is read from the CLIENT rather than from Settings, because the client is what will make the requests.

func NewReleaseProvider

func NewReleaseProvider(ctx context.Context, settings Settings) (forge.Provider, error)

NewReleaseProvider builds a GitLab release provider from explicit typed settings. A public repository needs no credential; Host selects the instance, empty means gitlab.com, and APIURL can override the derived API endpoint.

The credential comes from Settings.Credential, or — when that is nil — from DefaultTokenEnv. The context bounds its resolution: a source the caller supplied may reach a keychain or a remote secret store.

Types

type GitLabReleaseProvider

type GitLabReleaseProvider struct {
	// contains filtered or unexported fields
}

GitLabReleaseProvider implements forge.Provider.

func (*GitLabReleaseProvider) AddReleaseAsset added in v0.15.0

func (p *GitLabReleaseProvider) AddReleaseAsset(
	ctx context.Context, owner, repo, tagName, name string, _ int64, content io.Reader,
) error

AddReleaseAsset attaches a file to the release on tagName.

There is no endpoint that accepts a binary into a release: the create API's asset parameter is `assets:links`, and a link must be a resolvable http, https or ftp URL. So the bytes go to the project's generic package registry and the release links the result.

That is what goreleaser does for the same reason, and what this estate already relies on. It is a visible side effect: a package named "releases" appears in the project's registry, versioned by tag.

The obvious helper for the link does NOT work. FormatPackageURL returns a relative path with no scheme and percent-encodes the dots in the version, and GitLab rejects it with "Links url is blocked: Only allowed schemes are http, https, ftp". The URL is composed here against the instance's own API base, so it is correct for self-hosted instances too.

The conventional address

The link also carries direct_asset_path, which is what makes the asset fetchable at <host>/<owner>/<repo>/-/releases/<tag>/downloads/<name>. Without it that address 404s while the asset resolves perfectly at its own URL, and nothing except a fetch notices.

A name GitLab refuses in a filepath is linked WITHOUT one and reported with forge.ErrNotHonoured, because the rejection kills the whole link rather than dropping the field. See [directAssetPath].

func (*GitLabReleaseProvider) AddReleaseAssetLocation added in v0.17.0

func (p *GitLabReleaseProvider) AddReleaseAssetLocation(
	ctx context.Context, owner, repo, tagName string, asset forge.ReleaseAssetSource,
) error

AddReleaseAssetLocation links an already-hosted asset into an existing release. See forge.ReleaseAssetPublisher.

Nothing is dropped and nothing is rendered into the notes: a link is what a GitLab release asset IS, so this is the native operation rather than a fallback.

The link carries direct_asset_path, so the location is reachable at GitLab's conventional address as well as at its own. A name GitLab refuses in a filepath is linked without one and reported with forge.ErrNotHonoured.

That sentinel means something else here on a platform that cannot hold a location at all, where it says NOTHING was written. The hint is what tells the two apart, and it names the asset.

func (*GitLabReleaseProvider) Close added in v0.13.0

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

Close closes a merge request without merging it.

func (*GitLabReleaseProvider) Comment added in v0.20.0

func (p *GitLabReleaseProvider) Comment(
	ctx context.Context, owner, repo string, number int, draft forge.CommentDraft,
) (forge.Comment, error)

Comment posts a comment on a merge request.

It is the CONVERSATION comment. GitLab keeps diff comments on the discussions API, so unlike GitHub there is no adjacent method here waiting to be called by mistake.

func (*GitLabReleaseProvider) Create added in v0.13.0

func (p *GitLabReleaseProvider) Create(
	ctx context.Context, owner, repo string, draft forge.PullRequestDraft,
) (forge.PullRequest, error)

Create opens a merge request.

func (*GitLabReleaseProvider) CreateIssue added in v0.4.0

func (p *GitLabReleaseProvider) CreateIssue(
	ctx context.Context, owner, repo string, draft forge.IssueDraft,
) (forge.Issue, error)

CreateIssue files an issue.

Sanitisation

Title and Body go through forge.Sanitise unless the draft opts out, and the idempotency trailer is appended AFTERWARDS. The order is contractual: an opaque key of 41 or more characters trips redact's long-token pattern, so sanitising last would rewrite the key and the pre-create search would then look for one the issue does not carry — breaking at-most-once silently.

At-most-once

GitLab offers no idempotency primitive. Its create endpoint accepts an `iid`, which would serve, but it is documented as requiring administrator or project owner rights — incompatible with the least-privilege token this capability is meant to be usable with. So the key is written into the body as a searchable marker and looked for first.

func (*GitLabReleaseProvider) CreateIssueComment added in v0.20.0

func (p *GitLabReleaseProvider) CreateIssueComment(
	ctx context.Context, owner, repo string, number int, draft forge.CommentDraft,
) (forge.Comment, error)

CreateIssueComment posts a comment on an issue.

func (*GitLabReleaseProvider) CreateRelease added in v0.15.0

func (p *GitLabReleaseProvider) CreateRelease(
	ctx context.Context, owner, repo string, draft forge.ReleaseDraft,
) (forge.Release, error)

CreateRelease publishes a release for draft.TagName.

The tag must already exist and must point at draft.Commit; see the checks below, each of which prevents a specific silent failure.

func (*GitLabReleaseProvider) CreateReleaseWithAssets added in v0.17.0

func (p *GitLabReleaseProvider) 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.

GitLab is the one platform that reaches ONE visible state

Its release assets ARE links, and its create endpoint takes them inline (CreateReleaseOptions.Assets.Links). The bytes go to the generic package registry, which needs no release, so by the time the release is created every link already resolves. There is no draft to publish and no window at all.

A LOCATION costs nothing here: it is already the native shape, so it is linked as given and never reported as unhonoured. That is the opposite of GitHub and Gitea, and it is why the contract states a property rather than a mechanism.

Both shapes, and the same way, because the address is a property of the RELEASE rather than of where the bytes are hosted. An asset whose name GitLab refuses in a filepath is linked without one and named in the forge.ErrNotHonoured returned beside the release, which is the same sentinel a dropped draft uses and reaches the caller in the same error.

What a failure leaves behind

The uploads precede the create, so a create that fails leaves package files in the registry for a release that does not exist. They are addressable and a retry overwrites them, so nothing is lost or duplicated — but they are visible in the project's registry UI, and this contract has no verb that would remove them.

func (*GitLabReleaseProvider) CreateSnippet added in v0.8.0

CreateSnippet creates a snippet in the requested scope.

func (*GitLabReleaseProvider) CreateWikiPage added in v0.19.0

func (p *GitLabReleaseProvider) CreateWikiPage(
	ctx context.Context, owner, repo string, page forge.WikiPage, message string,
) error

CreateWikiPage adds a page.

Title carries the PATH, not a display name — see UpdateWikiPage for why that is the whole trick.

func (*GitLabReleaseProvider) DeleteSnippet added in v0.8.0

func (p *GitLabReleaseProvider) DeleteSnippet(
	ctx context.Context, scope forge.SnippetScope, id string,
) error

DeleteSnippet removes a snippet by its opaque ID.

func (*GitLabReleaseProvider) DownloadReleaseAsset

func (p *GitLabReleaseProvider) DownloadReleaseAsset(ctx context.Context, owner, repo string, asset forge.ReleaseAsset) (io.ReadCloser, string, error)

DownloadReleaseAsset is more complex for GitLab.

func (*GitLabReleaseProvider) Find added in v0.13.0

func (p *GitLabReleaseProvider) Find(
	ctx context.Context, owner, repo, sourceBranch string,
) (forge.PullRequest, error)

Find returns the OPEN merge request opened from sourceBranch.

func (*GitLabReleaseProvider) FindLastMerged added in v0.13.0

func (p *GitLabReleaseProvider) FindLastMerged(
	ctx context.Context, owner, repo, sourceBranch string,
) (forge.PullRequest, error)

FindLastMerged returns the most recently merged merge request from sourceBranch.

Ordering is by MERGE time. GitLab accepts order_by=merged_at and validates the value — an unknown one is a 400 rather than a silent fallback — so this is the forge doing the ordering rather than this adapter hoping.

updated_at would be the obvious alternative and is wrong: activity after a merge moves it and leaves merged_at alone, so it answers "most recently touched" rather than "most recently merged".

func (*GitLabReleaseProvider) GetFile added in v0.3.0

func (p *GitLabReleaseProvider) GetFile(
	ctx context.Context, owner, repo, path, ref string, maxBytes int64,
) ([]byte, error)

GetFile reads one file at a ref without cloning.

The bound is enforced by asking for the size FIRST. client-go's GetRawFile copies the whole response body into a buffer before returning it, so checking len() afterwards would be a measurement rather than a limit. GetRawFileMetaData issues a HEAD and reports Size, which lets an oversized file be refused before a byte of it is fetched.

The length is re-checked after the read, catching a file that grew between the two calls. What remains uncovered is a server that LIES about the size — which is acceptable here in a way it would not be for a release asset, because this URL is the pinned API host the provider authenticated against rather than a URL a release author chose.

func (*GitLabReleaseProvider) GetIssue added in v0.4.0

func (p *GitLabReleaseProvider) GetIssue(
	ctx context.Context, owner, repo string, number int,
) (forge.Issue, error)

GetIssue returns one issue by its per-project number (GitLab's iid).

func (*GitLabReleaseProvider) GetLatestRelease

func (p *GitLabReleaseProvider) GetLatestRelease(ctx context.Context, owner, repo string) (forge.Release, error)

func (*GitLabReleaseProvider) GetReleaseByTag

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

func (*GitLabReleaseProvider) GetSite added in v0.3.0

func (p *GitLabReleaseProvider) GetSite(
	ctx context.Context, owner, repo string,
) (forge.Site, error)

GetSite reports a project's GitLab Pages site.

Reading Pages settings requires the Maintainer or Owner role, so a 403 is routine rather than exceptional here — and it is deliberately NOT folded into ErrNotFound. Telling a caller "this project has no site" when the truth is "your token could not ask" is how a repository silently drops out of a corpus.

func (*GitLabReleaseProvider) GetSnippet added in v0.8.0

func (p *GitLabReleaseProvider) GetSnippet(
	ctx context.Context, scope forge.SnippetScope, id string,
) (forge.Snippet, error)

GetSnippet returns one snippet by its opaque ID, with file contents.

func (*GitLabReleaseProvider) GetWikiPage added in v0.19.0

func (p *GitLabReleaseProvider) GetWikiPage(
	ctx context.Context, owner, repo, path string, maxBytes int64,
) (forge.WikiPage, error)

GetWikiPage returns one page and its content.

The slug IS the path. The client percent-escapes it into the request path, so a nested page needs nothing special here.

maxBytes is enforced AFTER the fetch, which is the weaker half of the rule forge.Contents.GetFile states: GitLab's wiki API reports no size up front and buffers the body internally, so there is no length to check before reading. The bound still holds at the boundary — a caller never receives more than it asked for — it just cannot prevent the transfer.

func (*GitLabReleaseProvider) ListComments added in v0.4.0

func (p *GitLabReleaseProvider) ListComments(
	ctx context.Context,
	owner, repo string,
	number int,
	q forge.CommentQuery,
	yield func(forge.Comment) bool,
) error

ListComments yields an issue's comments.

Emulating Since

GitLab's notes endpoint has no created_after parameter — ListIssueNotesOptions carries only order_by and sort. So Since is honoured by asking for newest-first and stopping as soon as a note older than Since appears: the caller still pays only for what it asked for, rather than for the whole history filtered afterwards.

The consequence is that this provider yields NEWEST-FIRST while a forge with a server-side filter yields oldest-first. The contract promises no order for exactly this reason, because making them agree would mean buffering everything — which is what Since exists to avoid.

func (*GitLabReleaseProvider) ListReleases

func (p *GitLabReleaseProvider) ListReleases(ctx context.Context, owner, repo string, limit int) ([]forge.Release, error)

ListReleases returns up to limit releases, paginating across GitLab's pages (signalled by the X-Next-Page header) until the limit is met or history is exhausted. A limit <= 0 means "no explicit bound" — the natural first page. See forge.Provider.

func (*GitLabReleaseProvider) ListRepositories added in v0.3.0

func (p *GitLabReleaseProvider) ListRepositories(
	ctx context.Context,
	namespace string,
	opts forge.RepositoryListOptions,
	yield func(forge.Repository) bool,
) error

ListRepositories enumerates the projects in a GitLab group.

WithShared is pinned to false, and that is the load-bearing line in this file. GitLab documents with_shared as defaulting to TRUE, so the natural call returns projects merely SHARED into the group alongside those owned by it — carrying their own, foreign namespace paths. A caller whose question is "is this in my group?" would get a different question answered, in the permissive direction, and the forge conformance harness checks the containment property precisely because this default is invisible in any fixture without a shared project.

func (*GitLabReleaseProvider) ListRequestComments added in v0.20.0

func (p *GitLabReleaseProvider) ListRequestComments(
	ctx context.Context, owner, repo string, number int,
) ([]forge.Comment, error)

ListRequestComments returns every comment on a merge request.

func (*GitLabReleaseProvider) ListSnippets added in v0.8.0

func (p *GitLabReleaseProvider) ListSnippets(
	ctx context.Context, scope forge.SnippetScope,
) ([]forge.Snippet, error)

ListSnippets returns the snippets in scope, without file content.

func (*GitLabReleaseProvider) ListWikiPages added in v0.19.0

func (p *GitLabReleaseProvider) ListWikiPages(
	ctx context.Context, owner, repo string,
) ([]forge.WikiPage, error)

ListWikiPages returns every page in the project's wiki, without content.

with_content is deliberately not requested. The contract says a listed page carries no body, and asking GitLab for every body turns a listing into a download of the whole wiki.

func (*GitLabReleaseProvider) Login added in v0.2.0

func (p *GitLabReleaseProvider) Login(ctx context.Context, prompter forge.Prompter) (string, error)

Login implements the optional forge.Authenticator capability via GitLab's OAuth device flow (RFC 8628): it requests a device code, surfaces it through the forge.Prompter for the user to enter in a browser, then polls for the access token. Presentation — including whether to open a browser at the verification URL — belongs to the Prompter; this adapter speaks only the protocol.

It returns an error wrapping forge.ErrNotSupported when no OAuth client ID is configured (Settings.ClientID or DefaultClientIDEnv), so the caller falls back to manual token entry.

func (*GitLabReleaseProvider) ResolveMergedCommit added in v0.13.0

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

ResolveMergedCommit returns the commit ON THE TARGET BRANCH that this merge request produced.

Why this is not just a field read

GitLab 19.2 rebases automatically before a fast-forward merge and does NOT write the result back to the merge request record. The recorded head SHA stays at the pre-rebase commit, which the rebase orphaned. Reproduced on cicd!233: the record said eed522e6, main carried bdda942, and both merge_commit_sha and squash_commit_sha were null.

Candidate, then confirm

Candidates are tried cheapest first: the merge commit, the squash commit, then the recorded head, 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.

The recorded head is included as a candidate deliberately. It is correct whenever no rebase happened, and confirming it costs one request against a walk that costs many. What makes that safe is that it is never returned unconfirmed — the ordering is an optimisation, the confirmation is the contract.

Returns an error wrapping forge.ErrNotFound when nothing confirms. Never a best guess: "I could not establish what landed" and "nothing landed" call for the same action from a caller, and a plausible wrong SHA becomes a permanent tag.

func (*GitLabReleaseProvider) SearchIssues added in v0.4.0

func (p *GitLabReleaseProvider) SearchIssues(
	ctx context.Context,
	owner, repo string,
	q forge.IssueQuery,
	yield func(forge.Issue) bool,
) error

SearchIssues yields issues matching q.

GitLab searches title and description server-side via `search` + `in`, so the text filter costs nothing extra here — unlike a forge whose search is weaker and has to filter client-side.

func (*GitLabReleaseProvider) Update added in v0.13.0

func (p *GitLabReleaseProvider) 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. GitLab omits a nil pointer and CLEARS on a non-nil pointer to the empty string, so sending both every time is what makes the caller's intent unambiguous.

func (*GitLabReleaseProvider) UpdateRelease added in v0.15.0

func (p *GitLabReleaseProvider) UpdateRelease(
	ctx context.Context, owner, repo, tagName, name, body string,
) error

UpdateRelease amends an existing release's name and notes.

func (*GitLabReleaseProvider) UpdateWikiPage added in v0.19.0

func (p *GitLabReleaseProvider) UpdateWikiPage(
	ctx context.Context, owner, repo string, page forge.WikiPage, message string,
) error

UpdateWikiPage replaces a page's content.

Sending Title is what stops the page MOVING

GitLab re-derives a page's slug from its title on every edit. Omit the title and the slug is recomputed from the content's first heading, so a page at "specs/0079-…" silently becomes a root-level page and every link to it breaks — and the request returns 200, so nothing announces it.

Measured 2026-09-06 on a throwaway project: PUT with content only answered 200 and moved slug "specs/0020-spike-page" to "0020-spike-page", after which the nested slug 404'd. The same PUT carrying the path as its title restored it.

So the title is ALWAYS sent, and it is always the path. This looks like a redundant field and is the reason the capability keeps its promise.

func (*GitLabReleaseProvider) UploadKey added in v0.2.0

func (p *GitLabReleaseProvider) 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 GitLab'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.

It returns an error wrapping forge.ErrNotSupported when no token is configured, since GitLab's key API requires authentication — the caller then instructs the user to add the key manually.

type Settings

type Settings struct {
	// Endpoint addresses this instance. Type is [forge.SourceTypeGitLab]; Host
	// selects the instance, empty meaning gitlab.com; Name selects which
	// configured source this is and scopes the configuration subtree read by
	// [SettingsFromConfig].
	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
	// [DefaultTokenEnv], 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

	// 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 the PRIVATE-TOKEN
	// sensitive-header policy its downloads depend on. 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

	// ClientID is the OAuth app client ID used by the interactive device-flow
	// login ([Authenticator]). Empty falls back to [DefaultClientIDEnv]; when
	// neither is set the provider reports the capability as unsupported.
	ClientID string `json:"client_id" yaml:"client_id"`

	// Scopes overrides the OAuth scopes requested at login. Empty uses a
	// sensible default (GitLab `api`).
	Scopes []string `json:"scopes" yaml:"scopes"`
}

Settings contains the typed configuration needed to construct a GitLab 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) Settings

SettingsFromConfig adapts the gitlab config subtree into typed provider settings. It preserves the existing `url.api` and `auth.client_id` keys.

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. 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. An unnamed endpoint reads `gitlab`; a named one reads `gitlab.<name>`, which is how a self-hosted instance and gitlab.com stop sharing one credential and one url.api.

Jump to

Keyboard shortcuts

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