Release Provider¶
Package: pkg/vcs/release
Defines the three interfaces that abstract over platform-specific release APIs, and the provider registry that lets consuming code (and downstream tools) work with any backend without importing platform packages directly.
Interfaces¶
Provider¶
type Provider interface {
GetLatestRelease(ctx context.Context, owner, repo string) (Release, error)
GetReleaseByTag(ctx context.Context, owner, repo, tag string) (Release, error)
ListReleases(ctx context.Context, owner, repo string, limit int) ([]Release, error)
DownloadReleaseAsset(ctx context.Context, owner, repo string, asset ReleaseAsset) (io.ReadCloser, string, error)
}
DownloadReleaseAsset returns (io.ReadCloser, redirectURL string, error). The redirect URL is populated by the GitHub implementation when the API redirects to a CDN; all other providers return an empty string.
Not all providers support every method
GetReleaseByTag and ListReleases return ErrNotSupported for providers whose platform has no versioned-release concept (Bitbucket, Direct). Check for this sentinel before treating it as a fatal error.
Release¶
type Release interface {
GetName() string
GetTagName() string
GetBody() string
GetDraft() bool
GetAssets() []ReleaseAsset
}
ReleaseAsset¶
ChecksumProvider (Optional Interface)¶
ChecksumProvider is an opt-in interface โ providers implement it when they can retrieve a checksums manifest by a route other than a standard release-asset download. The update flow does a runtime type assertion; providers that don't implement it fall back to locating checksums.txt by filename within the release's asset list.
type ChecksumProvider interface {
DownloadChecksumManifest(ctx context.Context, rel Release, maxBytes int64) ([]byte, error)
}
Implementations must:
- Return
release.ErrNotSupportedwhen the current configuration disables retrieval (e.g. Direct'schecksum_url_templateunset, Bitbucket's downloads list has nochecksums.txt). The caller treats this identically to "not implemented" and falls back. - Cap the response at
maxBytesto protect against a hostile server streaming indefinitely. - Return a wrapped error for transport failures. The caller respects
update.require_checksumto decide fail-open vs fail-closed.
Built-in implementations:
| Provider | Source | When ErrNotSupported is returned |
|---|---|---|
direct |
Expands checksum_url_template against the release version and HTTP-fetches it. |
checksum_url_template is empty. |
bitbucket |
Looks up checksums.txt by exact filename in the repository's downloads list. |
No checksums.txt download exists. |
github, gitlab, gitea, and codeberg do not implement ChecksumProvider: they rely on the default asset-list lookup, which works cleanly because those platforms expose the GoReleaser-produced checksums.txt as an ordinary release asset.
Sentinel Errors¶
var (
// ErrProviderNotFound is returned by Lookup when no factory is registered
// for the requested source type.
ErrProviderNotFound = errors.New("no release provider registered for source type")
// ErrNotSupported is returned by provider methods not applicable to the
// underlying platform (e.g. ListReleases on Bitbucket).
ErrNotSupported = errors.New("operation not supported by this release provider")
// ErrVersionUnknown is returned by the direct provider when neither
// version_url nor pinned_version is configured.
ErrVersionUnknown = errors.New("cannot determine latest version: configure version_url or pinned_version in Params")
)
Provider Registry¶
The registry maps source type strings to factory functions. All built-in providers register themselves at package init via blank imports in pkg/setup/providers.go โ no manual wiring is needed.
Built-in source type constants¶
const (
SourceTypeGitHub = "github"
SourceTypeGitLab = "gitlab"
SourceTypeBitbucket = "bitbucket"
SourceTypeGitea = "gitea"
SourceTypeCodeberg = "codeberg"
SourceTypeDirect = "direct"
)
Registering a custom provider¶
Call release.Register in your main() before any update operations. The registry is backed by a sync.RWMutex and is safe to call concurrently.
import (
"gitlab.com/phpboyscout/go-tool-base/pkg/vcs/release"
"gitlab.com/phpboyscout/go-tool-base/pkg/config"
)
func main() {
release.Register("s3", func(src release.ReleaseSourceConfig, cfg config.Containable) (release.Provider, error) {
return myS3Provider(src, cfg)
})
// ... build and run the Cobra root command
}
ReleaseSourceConfig¶
Passed to every ProviderFactory. Populated from props.ReleaseSource by NewUpdater.
type ReleaseSourceConfig struct {
Type string
Host string // provider base URL (Gitea, GitLab, direct)
Owner string // org / workspace / username
Repo string // repository slug
Private bool // require authentication
Params map[string]string // provider-specific key/value pairs (snake_case keys)
}
Querying registered types¶
Built-in Providers¶
GitHub โ pkg/vcs/github¶
Uses the go-github SDK. Supports GitHub Enterprise via ReleaseSource.Host.
Authentication: GITHUB_TOKEN env var or github.auth.value / github.auth.env config keys.
GitLab โ pkg/vcs/gitlab¶
Uses the gitlab-org/api/client-go SDK. Supports self-hosted GitLab via ReleaseSource.Host (defaults to https://gitlab.com/api/v4).
Authentication: GITLAB_TOKEN or gitlab.auth.* config keys.
Bitbucket Cloud โ pkg/vcs/bitbucket¶
Uses the Bitbucket Downloads API (/2.0/repositories/{workspace}/{repo}/downloads). Bitbucket has no native "Releases" concept โ version information is inferred from asset filenames using a configurable regular expression.
Authentication: HTTP Basic auth. Credentials are read in order:
bitbucket.username+bitbucket.app_passwordconfig keysBITBUCKET_USERNAME+BITBUCKET_APP_PASSWORDenvironment variables
Params keys:
| Key | Description | Default |
|---|---|---|
filename_pattern |
Go regex for asset matching. Capture group 1 = version string. | GoReleaser convention (see below) |
workspace |
Bitbucket workspace slug, if different from Owner |
same as Owner |
Default filename pattern matches GoReleaser output:
tool_v1.2.3_Linux_x86_64.tar.gz โ version = "v1.2.3"
tool_Linux_x86_64.tar.gz โ version = RFC3339 upload timestamp
The most recently uploaded set of matching assets is returned as the "latest release". GetReleaseByTag and ListReleases return ErrNotSupported.
Example configuration:
props.Tool{
ReleaseSource: props.ReleaseSource{
Type: "bitbucket",
Owner: "my-workspace",
Repo: "my-tool",
Private: true,
},
}
Gitea / Forgejo โ pkg/vcs/gitea¶
Uses the Gitea REST API v1 ({host}/api/v1/repos/{owner}/{repo}/releases). Compatible with any Gitea or Forgejo instance.
Authentication: GITEA_TOKEN or gitea.auth.* config keys. Token is sent as Authorization: token <value>.
Params keys:
| Key | Description | Default |
|---|---|---|
api_version |
API path version segment | v1 |
ReleaseSource.Host is required and must be the full base URL of the instance (e.g. https://git.example.com).
Example configuration:
props.Tool{
ReleaseSource: props.ReleaseSource{
Type: "gitea",
Host: "https://git.example.com",
Owner: "my-org",
Repo: "my-tool",
},
}
Codeberg โ pkg/vcs/gitea¶
Codeberg (https://codeberg.org) runs Forgejo and is registered as a first-class source type. The Host field defaults to https://codeberg.org โ no extra configuration is needed.
Authentication: CODEBERG_TOKEN or codeberg.auth.* config keys.
Example configuration:
props.Tool{
ReleaseSource: props.ReleaseSource{
Type: "codeberg",
Owner: "my-org",
Repo: "my-tool",
},
}
Direct HTTP โ pkg/vcs/direct¶
For tools distributed via arbitrary HTTP servers โ S3, GCS, Artifactory, Nexus, static web servers, internal CDNs. Asset download URLs are constructed from a configurable template; version detection is optional.
Authentication: DIRECT_TOKEN env var or direct.token config key. Sent as Authorization: Bearer <value>.
Params keys:
| Key | Required | Description |
|---|---|---|
url_template |
Yes | Download URL template. See placeholders below. |
version_url |
No | URL returning the latest version string. |
version_format |
No | Override format detection: text, json, yaml, or xml. |
version_key |
No | Field name to extract from structured responses. Tries tag_name then version by default. |
pinned_version |
No | Static version string. Disables all network version checks. |
checksum_url_template |
No | Template for the SHA-256 checksums manifest URL. Same placeholders as url_template. Activates checksum verification on Update() โ the Direct provider implements release.ChecksumProvider and fetches the manifest from the expanded URL. |
URL template placeholders:
| Placeholder | Example value |
|---|---|
{version} |
v1.2.3 |
{version_bare} |
1.2.3 (no leading v) |
{os} |
Linux, Darwin, Windows |
{arch} |
x86_64, arm64 |
{tool} |
value of ReleaseSource.Repo |
{ext} |
tar.gz |
Version endpoint formats โ auto-detected from Content-Type, overridable via version_format:
# Plain text (text/plain)
v1.2.3
# JSON (application/json)
{"tag_name": "v1.2.3", "prerelease": false}
# YAML (application/yaml)
version: v1.2.3
# XML (application/xml)
<release><version>v1.2.3</version></release>
Example configuration:
props.Tool{
ReleaseSource: props.ReleaseSource{
Type: "direct",
Repo: "mytool",
Params: map[string]string{
"url_template": "https://releases.example.com/{tool}/{version}/{tool}_{os}_{arch}.{ext}",
"version_url": "https://releases.example.com/latest.json",
"version_key": "tag_name",
},
},
}
Usage¶
Via NewUpdater (recommended)¶
pkg/setup.NewUpdater handles provider lookup automatically using props.Tool.ReleaseSource.Type. Simply set the type and call NewUpdater โ no provider import needed. The context is forwarded through private-repo token resolution, so remote-store credential backends (Vault, AWS SSM) honour the caller's deadline.
Direct provider construction¶
For use cases outside the update command:
import (
"gitlab.com/phpboyscout/go-tool-base/pkg/vcs/release"
)
factory, err := release.Lookup("gitea")
if err != nil {
return err
}
src := release.ReleaseSourceConfig{
Host: "https://git.example.com",
Owner: "my-org",
Repo: "my-tool",
}
provider, err := factory(src, props.Config)
Getting the latest release¶
rel, err := provider.GetLatestRelease(ctx, "my-org", "my-repo")
if err != nil {
return err
}
fmt.Println(rel.GetTagName(), rel.GetName())
for _, asset := range rel.GetAssets() {
fmt.Println(" -", asset.GetName(), asset.GetBrowserDownloadURL())
}
Downloading an asset¶
rc, _, err := provider.DownloadReleaseAsset(ctx, "my-org", "my-repo", asset)
if err != nil {
return err
}
defer rc.Close()
outFile, _ := props.FS.Create("/tmp/mytool.tar.gz")
defer outFile.Close()
io.Copy(outFile, rc)
Testing¶
Mocks for all three interfaces are generated by mockery:
import (
"testing"
mock_release "gitlab.com/phpboyscout/go-tool-base/mocks/pkg/vcs/release"
)
func TestAutoUpdate(t *testing.T) {
mockRel := mock_release.NewMockRelease(t)
mockRel.EXPECT().GetTagName().Return("v2.0.0")
mockRel.EXPECT().GetDraft().Return(false)
mockProvider := mock_release.NewMockProvider(t)
mockProvider.EXPECT().
GetLatestRelease(mock.Anything, "my-org", "my-repo").
Return(mockRel, nil)
// Pass mockProvider wherever release.Provider is required
}
For HTTP-based providers (Gitea, Bitbucket, Direct), unit tests use httptest.NewServer to serve mock responses without any network access.
Related Documentation¶
- GitHub โ
github.NewReleaseProviderimplementation - GitLab โ
gitlab.NewReleaseProviderimplementation - Setup โ how
NewUpdaterselects and constructs providers - Auto-Update Lifecycle โ how
release.Providerdrives version checks