Extended Release Sources Specification¶
- Authors
- Matt Cockayne, Claude Sonnet 4.6 (AI drafting assistant)
- Date
- 29 March 2026
- Status
- IMPLEMENTED
Overview¶
GTB tools currently resolve self-update releases from two sources: GitHub (including GitHub Enterprise) and GitLab (including self-hosted instances). The ReleaseSource.Type string is a hard-coded switch inside pkg/setup/update.go, which means adding a new source requires modifying core library code.
This specification:
- Introduces a provider registry โ a compile-time extensibility mechanism in
pkg/vcs/releasethat lets downstream consumers register customrelease.Providerimplementations without forking the library. - Adds Bitbucket Cloud as a built-in provider, using the Bitbucket Downloads API with filename-pattern-based version detection.
- Adds Gitea/Forgejo as a built-in provider, leveraging Gitea's GitHub-compatible releases API.
- Adds Codeberg as a first-class provider โ a distinct
type: codebergbacked by the Gitea provider withcodeberg.orgpre-configured. - Adds a direct HTTP download provider for tools hosted on arbitrary HTTP servers (S3 buckets, internal mirrors, CDNs) where no VCS platform manages releases.
- Extends
ReleaseSourcewith aParamsfield for provider-specific configuration without breaking the existing struct.
Motivation and Context¶
Current Limitations¶
- The
NewUpdaterfunction inpkg/setup/update.gocontains a hardif vcsProvider == "gitlab"branch. Any new provider requires modifying this function. - The
ReleaseSourcestruct has no extensibility point โ provider-specific fields (URL templates, API versions) cannot be expressed. - Tools distributed via Bitbucket, Gitea, Forgejo, Codeberg, internal S3 buckets, or other HTTP hosts cannot use GTB's self-update machinery at all.
Target Audience for New Sources¶
| Provider | Use Case |
|---|---|
| Bitbucket Cloud | Teams using Bitbucket for source control, distributing binaries via Bitbucket Downloads |
| Gitea / Forgejo | Self-hosted Git (popular in corporate environments and the open-source community) |
| Codeberg | Public Forgejo instance at codeberg.org; growing traction in the open-source community |
| Direct HTTP | Teams using S3, GCS, Azure Blob, Artifactory, Nexus, or a static web server as a release host |
Design Decisions¶
Provider registry over interface injection: A global registry (vcs/release) is the simplest extensibility mechanism that does not require changes to Props, constructors, or command wiring. Downstream consumers call release.Register(...) once at startup (in main.go) to add custom providers. The registry uses a sync.RWMutex โ written once at startup (during init() calls and any custom registration in main()), then read-only thereafter. This is idiomatic and avoids the complexity of a sealed/panic-on-late-register approach.
Built-in providers remain zero-configuration: All built-in providers register themselves via init() in their respective packages and are pulled in by blank imports in pkg/setup/providers.go. NewUpdater becomes a uniform registry lookup.
ReleaseSource.Params for provider-specific configuration: A map[string]string field is the lowest-friction extension point. Keys use snake_case throughout, consistent with the existing Viper-based config system. Providers document their recognised param keys. The field is omitempty in JSON and YAML so tools that don't use it produce identical serialised output to today.
vcs.provider config override continues to work: The runtime config key vcs.provider can override ReleaseSource.Type for all providers, including new ones. This is useful for operators who want to redirect a binary to a different host at runtime (e.g. a private mirror) without recompilation.
Bitbucket uses Downloads, not Releases: Bitbucket Cloud has no native "Releases" concept โ only a flat Downloads list. Version is inferred from filename using a configurable regex pattern. The default pattern matches GoReleaser's naming convention ({tool}_{OS}_{Arch}.tar.gz) and extracts a version segment when present (e.g. tool_v1.2.3_Linux_x86_64.tar.gz). Engineers can override the pattern via Params["filename_pattern"]. Assets are sorted by upload date (created_on) descending; the most recent matching set is treated as the latest release. GetReleaseByTag and ListReleases return ErrNotSupported.
Codeberg is a first-class provider: Codeberg runs Forgejo and is at codeberg.org. Rather than expecting users to set type: gitea + host: codeberg.org, a dedicated type: codeberg is registered that pre-configures the Gitea/Forgejo provider with the correct host. This is a distinct registry entry backed by the same GiteaReleaseProvider implementation.
Direct download version endpoint formats: The version_url may serve any of four formats โ plain text, JSON, YAML, or XML. The provider auto-detects based on Content-Type response header; the version_format param can override detection. For JSON, YAML, and XML, a configurable version_key param specifies the field to extract (default: tries tag_name then version). This gives consumers the broadest compatibility with existing version endpoints.
No Bitbucket Server / Bitbucket Data Center in Phase 1: Bitbucket Server has a different API and is largely superseded. Noted as a future consideration.
Public API Changes¶
New Constants in pkg/vcs/release¶
const (
SourceTypeGitHub = "github"
SourceTypeGitLab = "gitlab"
SourceTypeBitbucket = "bitbucket"
SourceTypeGitea = "gitea"
SourceTypeCodeberg = "codeberg"
SourceTypeDirect = "direct"
)
These are informational constants; any string is accepted by the registry. Downstream consumers can define their own.
Provider Registry โ pkg/vcs/release/registry.go¶
// ProviderFactory is a function that constructs a release.Provider from a
// ReleaseSourceConfig and a Viper configuration subtree.
type ProviderFactory func(source ReleaseSourceConfig, cfg config.Containable) (Provider, error)
// Register associates a source type string with a factory function.
// Safe to call concurrently; uses a sync.RWMutex internally.
// Intended to be called from init() or early in main() before any Lookup call.
func Register(sourceType string, factory ProviderFactory)
// Lookup returns the ProviderFactory for the given source type.
// Returns ErrProviderNotFound if no factory is registered for that type.
func Lookup(sourceType string) (ProviderFactory, error)
// RegisteredTypes returns a sorted slice of all registered source type strings.
// Used for generating user-facing error messages.
func RegisteredTypes() []string
ReleaseSourceConfig โ pkg/vcs/release/source_config.go¶
Rather than passing props.ReleaseSource directly (to avoid a circular import between pkg/vcs/release and pkg/props), a lightweight config struct is defined in pkg/vcs/release:
// ReleaseSourceConfig carries the information a ProviderFactory needs to
// construct its client. It is populated from props.ReleaseSource.
type ReleaseSourceConfig struct {
Type string
Host string
Owner string
Repo string
Private bool
Params map[string]string
}
props.ReleaseSource Extension¶
type ReleaseSource struct {
Type string `json:"type" yaml:"type"`
Host string `json:"host" yaml:"host"`
Owner string `json:"owner" yaml:"owner"`
Repo string `json:"repo" yaml:"repo"`
Private bool `json:"private" yaml:"private"`
// Params holds provider-specific configuration key/value pairs.
// Keys use snake_case. Valid keys are documented per provider.
Params map[string]string `json:"params,omitempty" yaml:"params,omitempty"`
}
No existing fields are changed; Params is additive and omitted when empty.
pkg/setup/update.go โ NewUpdater Refactor¶
The hard if/else switch is replaced with a registry lookup:
func NewUpdater(props *props.Props, version string, force bool) (*SelfUpdater, error) {
if props.Config == nil {
return nil, errors.New("configuration is not loaded")
}
vcsProvider, _, _ := props.Tool.GetReleaseSource()
if props.Config.IsSet("vcs.provider") {
vcsProvider = strings.ToLower(props.Config.GetString("vcs.provider"))
}
if props.Tool.ReleaseSource.Private {
if err := requireReleaseToken(vcsProvider, props); err != nil {
return nil, err
}
}
factory, err := release.Lookup(vcsProvider)
if err != nil {
return nil, errors.WithHintf(err,
"Supported release source types: %s. Register a custom provider with release.Register().",
strings.Join(release.RegisteredTypes(), ", "),
)
}
sourceCfg := release.ReleaseSourceConfig{
Type: props.Tool.ReleaseSource.Type,
Host: props.Tool.ReleaseSource.Host,
Owner: props.Tool.ReleaseSource.Owner,
Repo: props.Tool.ReleaseSource.Repo,
Private: props.Tool.ReleaseSource.Private,
Params: props.Tool.ReleaseSource.Params,
}
releaseClient, err := factory(sourceCfg, props.Config)
if err != nil {
return nil, errors.WithStack(err)
}
return &SelfUpdater{
force: force,
version: version,
logger: props.Logger,
Tool: props.Tool,
releaseClient: releaseClient,
CurrentVersion: ver.FormatVersionString(props.Version.GetVersion(), true),
Fs: props.FS,
}, nil
}
New Providers¶
Bitbucket Cloud โ pkg/vcs/bitbucket/¶
API: Bitbucket Downloads API v2 (https://api.bitbucket.org/2.0/repositories/{workspace}/{slug}/downloads)
Bitbucket Cloud has no "Releases" concept. Binary artefacts are uploaded as flat Downloads associated with a repository. Version information is not stored by the platform and must be inferred from filenames.
Authentication: HTTP Basic auth with an App Password (username:app_password). The Private flag in ReleaseSource governs whether credentials are required.
Token resolution (in order of precedence):
1. cfg.bitbucket.username + cfg.bitbucket.app_password (config file)
2. BITBUCKET_USERNAME + BITBUCKET_APP_PASSWORD environment variables
Version detection via filename pattern:
The provider applies a regex to each Download's filename to identify matching artefacts and extract an optional version segment. The default pattern matches GoReleaser's output:
- The version capture group is optional โ artefacts without a version segment (e.g.
tool_Linux_x86_64.tar.gz) are matched but reported with an empty version. - Engineers can override the pattern via
Params["filename_pattern"](a Goregexpstring). The first capture group, if present, is treated as the version. - Matching artefacts are sorted by
created_ondescending. The most recent matching set constitutes the "latest release". GetLatestReleasereturns a syntheticReleasewithTagNameset to the extracted version (or thecreated_ontimestamp in ISO 8601 if no version was captured).GetReleaseByTagandListReleasesreturnErrNotSupported.
Params keys:
| Key | Description | Default |
|---|---|---|
workspace |
Bitbucket workspace slug (if different from Owner) |
same as Owner |
filename_pattern |
Go regex for asset matching. First capture group = version. | default GoReleaser pattern |
Asset matching: The provider returns all Downloads that match the filename pattern for the current platform ({OS}_{Arch}), regardless of version segment.
Gitea / Forgejo โ pkg/vcs/gitea/¶
API: Gitea/Forgejo REST API v1 ({host}/api/v1/repos/{owner}/{repo}/releases)
The Gitea API mirrors GitHub's releases endpoint in structure, but uses different field names and does not issue CDN redirects on asset downloads. A dedicated implementation avoids coupling to the GitHub client.
Authentication: Bearer token via Authorization: token <value> header.
Token resolution (in order of precedence):
1. cfg.gitea.token (config file)
2. GITEA_TOKEN environment variable
Host field: Required โ Gitea/Forgejo instances have no shared public host. The Host value is the full base URL (e.g. https://git.example.com).
Params keys:
| Key | Description | Default |
|---|---|---|
api_version |
API path version segment | v1 |
Codeberg โ first-class type backed by pkg/vcs/gitea/¶
Codeberg (https://codeberg.org) is a public Forgejo instance with growing adoption in the open-source community. GTB registers SourceTypeCodeberg = "codeberg" as a distinct provider type. The factory pre-configures GiteaReleaseProvider with Host: "https://codeberg.org" โ no Host field is required in ReleaseSource for Codeberg repositories.
Token resolution:
1. cfg.codeberg.token (config file)
2. CODEBERG_TOKEN environment variable
Params keys: Same as Gitea.
Example configuration:
props.Tool{
ReleaseSource: props.ReleaseSource{
Type: "codeberg",
Owner: "myorg",
Repo: "mytool",
},
}
Direct HTTP Download โ pkg/vcs/direct/¶
For tools distributed via arbitrary HTTP servers. The provider constructs asset download URLs from a configurable template and optionally fetches the latest version from a version endpoint.
Params keys:
| Key | Description | Required |
|---|---|---|
url_template |
Download URL template. Supported placeholders listed below. | Yes |
version_url |
URL that returns the latest version string. | No |
version_format |
Override format detection: text, json, yaml, or xml. |
No (auto-detected) |
version_key |
Field name to extract from structured responses. Tried as-is, then dot-separated path for nested fields. Default: tries tag_name then version. |
No |
pinned_version |
Static version string. Disables update checking (no network call). | No |
checksum_url_template |
Template for the checksum sidecar URL. Same placeholders as url_template. |
No |
URL template placeholders:
| Placeholder | Value | Example |
|---|---|---|
{version} |
Full version string | v1.2.3 |
{version_bare} |
Version without leading v |
1.2.3 |
{os} |
Title-cased OS (GoReleaser convention) | Linux, Darwin, Windows |
{arch} |
Architecture (GoReleaser convention) | x86_64, arm64 |
{tool} |
Tool name from props.Tool.Name |
mytool |
{ext} |
Archive extension | tar.gz |
Example configuration:
release_source:
type: direct
params:
url_template: "https://releases.example.com/{tool}/{version}/{tool}_{os}_{arch}.{ext}"
version_url: "https://releases.example.com/latest.json"
version_key: "version"
checksum_url_template: "https://releases.example.com/{tool}/{version}/{tool}_{os}_{arch}.{ext}.sha256"
Version endpoint response formats:
The provider supports four response formats, auto-detected from the Content-Type header (or overridden via version_format):
| Format | Content-Type |
Detection | Extraction |
|---|---|---|---|
| Plain text | text/plain |
Default if no structured type matches | Entire body, whitespace-trimmed |
| JSON | application/json |
application/json |
Value at version_key (dot-separated path for nested keys) |
| YAML | application/yaml, text/yaml |
Either YAML content-type | Value at version_key |
| XML | application/xml, text/xml |
Either XML content-type | Text content of the element matching version_key |
When a structured format is detected but version_key is not set, the provider tries tag_name then version as fallbacks before returning an error.
Example version endpoint responses:
# Plain text
v1.2.3
# JSON
{"tag_name": "v1.2.3", "prerelease": false}
# YAML
version: v1.2.3
released_at: 2026-03-29
# XML
<release>
<version>v1.2.3</version>
</release>
Behaviour when version is unavailable:
- version_url absent + pinned_version set: IsLatestVersion returns true (no network call).
- Both absent: GetLatestRelease returns ErrVersionUnknown. The update command advises the user to specify --version explicitly.
GetReleaseByTag: Constructs a synthetic release using the provided tag as the version. No network call.
ListReleases: Returns ErrNotSupported.
Authentication: Bearer token for authenticated endpoints.
- cfg.direct.token (config file)
- DIRECT_TOKEN environment variable
Project Structure¶
pkg/vcs/release/
โโโ provider.go โ EXISTING: Release, ReleaseAsset, Provider interfaces
โโโ registry.go โ NEW: Register, Lookup, RegisteredTypes (sync.RWMutex)
โโโ registry_test.go โ NEW: registry unit tests
โโโ source_config.go โ NEW: ReleaseSourceConfig type
โโโ constants.go โ NEW: SourceType* constants (github, gitlab, bitbucket, gitea, codeberg, direct)
pkg/vcs/bitbucket/
โโโ client.go โ NEW: HTTP client with Basic auth
โโโ release.go โ NEW: BitbucketRelease, BitbucketAsset, BitbucketReleaseProvider, filename pattern matching
โโโ release_test.go โ NEW: unit tests with mock HTTP
โโโ release_integration_test.go โ NEW: integration tests (INT_TEST_BITBUCKET=1)
โโโ init.go โ NEW: func init() { release.Register(release.SourceTypeBitbucket, factory) }
pkg/vcs/gitea/
โโโ client.go โ NEW: HTTP client with token auth
โโโ release.go โ NEW: GiteaRelease, GiteaAsset, GiteaReleaseProvider
โโโ release_test.go โ NEW: unit tests with mock HTTP
โโโ release_integration_test.go โ NEW: integration tests (INT_TEST_GITEA=1)
โโโ init.go โ NEW: register "gitea" and "codeberg" factories
pkg/vcs/direct/
โโโ provider.go โ NEW: DirectReleaseProvider, URL template expansion, version endpoint parsing
โโโ version.go โ NEW: version endpoint fetch + format parsing (text/JSON/YAML/XML)
โโโ provider_test.go โ NEW: unit tests
โโโ version_test.go โ NEW: version format parsing tests
โโโ init.go โ NEW: func init() { release.Register(release.SourceTypeDirect, factory) }
pkg/props/
โโโ tool.go โ MODIFIED: add Params field to ReleaseSource
pkg/setup/
โโโ update.go โ MODIFIED: replace if/else with registry lookup; extend requireReleaseToken
โโโ update_test.go โ MODIFIED: registry-driven tests
โโโ providers.go โ NEW: blank imports to register all built-in providers
docs/components/
โโโ setup.md โ MODIFIED: document new providers and Params config
โโโ vcs.md โ MODIFIED: document registry, new packages
Built-in Provider Registration โ pkg/setup/providers.go¶
package setup
import (
_ "gitlab.com/phpboyscout/go-tool-base/pkg/vcs/bitbucket"
_ "gitlab.com/phpboyscout/go-tool-base/pkg/vcs/direct"
_ "gitlab.com/phpboyscout/go-tool-base/pkg/vcs/gitea"
_ "gitlab.com/phpboyscout/go-tool-base/pkg/vcs/github"
_ "gitlab.com/phpboyscout/go-tool-base/pkg/vcs/gitlab"
)
All built-in providers are registered when pkg/setup is imported. Downstream consumers that want to add a custom provider call release.Register(...) in their main() before invoking any setup functions.
Error Handling¶
All new providers use github.com/cockroachdb/errors for all error creation and wrapping.
New sentinel errors in pkg/vcs/release:
var (
// ErrProviderNotFound is returned when Lookup cannot find a factory for the given type.
ErrProviderNotFound = errors.New("no release provider registered for source type")
// ErrNotSupported is returned by provider methods that are not applicable
// for 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 and a version check is requested.
ErrVersionUnknown = errors.New("cannot determine latest version: configure version_url or pinned_version in Params")
)
User-facing hints (via errors.WithHint) are provided for:
- ErrProviderNotFound: lists all registered type strings.
- Authentication failures: names the specific environment variable to set.
- Template expansion failures: includes the template string and the unresolvable placeholder.
- Version format parsing failures: names the detected/configured format and the key attempted.
Testing Strategy¶
Unit Tests¶
All new providers use httptest.NewServer for HTTP interactions. No real network calls in unit tests.
| Test | Package | Scenario |
|---|---|---|
TestRegistry_Register |
vcs/release |
Register factory โ Lookup returns it |
TestRegistry_Lookup_NotFound |
vcs/release |
Lookup unregistered type โ ErrProviderNotFound |
TestRegistry_RegisteredTypes |
vcs/release |
Returns sorted list including all built-in types |
TestRegistry_Concurrent |
vcs/release |
Concurrent Register + Lookup โ no data race |
TestBitbucketProvider_GetLatestRelease_WithVersion |
vcs/bitbucket |
Filename contains version โ version extracted |
TestBitbucketProvider_GetLatestRelease_NoVersion |
vcs/bitbucket |
Filename without version โ TagName is creation timestamp |
TestBitbucketProvider_GetLatestRelease_CustomPattern |
vcs/bitbucket |
Custom filename_pattern in Params โ applied correctly |
TestBitbucketProvider_DownloadAsset |
vcs/bitbucket |
Asset URL โ bytes streamed |
TestBitbucketProvider_GetReleaseByTag |
vcs/bitbucket |
Returns ErrNotSupported |
TestBitbucketProvider_ListReleases |
vcs/bitbucket |
Returns ErrNotSupported |
TestBitbucketProvider_Auth |
vcs/bitbucket |
Basic auth header sent when Private=true |
TestGiteaProvider_GetLatestRelease |
vcs/gitea |
Standard release JSON โ fields mapped correctly |
TestGiteaProvider_GetReleaseByTag |
vcs/gitea |
Tag โ correct endpoint called |
TestGiteaProvider_ListReleases |
vcs/gitea |
Pagination โ all releases returned up to limit |
TestGiteaProvider_DownloadAsset |
vcs/gitea |
Streaming download |
TestGiteaProvider_Codeberg_DefaultHost |
vcs/gitea |
SourceTypeCodeberg โ requests go to codeberg.org |
TestGiteaProvider_Codeberg_TokenEnvVar |
vcs/gitea |
CODEBERG_TOKEN used for auth |
TestDirectProvider_VersionURL_JSON |
vcs/direct |
JSON response + version_key โ correct version |
TestDirectProvider_VersionURL_YAML |
vcs/direct |
YAML response + version_key โ correct version |
TestDirectProvider_VersionURL_XML |
vcs/direct |
XML response + version_key โ correct version |
TestDirectProvider_VersionURL_PlainText |
vcs/direct |
Plain text response โ trimmed version |
TestDirectProvider_VersionURL_AutoDetect |
vcs/direct |
Content-Type drives format selection |
TestDirectProvider_VersionURL_FormatOverride |
vcs/direct |
version_format param overrides Content-Type |
TestDirectProvider_VersionURL_FallbackKey |
vcs/direct |
No version_key โ tries tag_name then version |
TestDirectProvider_Pinned |
vcs/direct |
pinned_version โ no HTTP call, no update |
TestDirectProvider_VersionUnknown |
vcs/direct |
No version config โ ErrVersionUnknown |
TestDirectProvider_GetReleaseByTag |
vcs/direct |
Synthetic release, no network call |
TestDirectProvider_URLTemplate_AllPlaceholders |
vcs/direct |
All placeholders expanded correctly |
TestDirectProvider_DownloadAsset |
vcs/direct |
Template expanded โ asset downloaded |
TestDirectProvider_ChecksumURL |
vcs/direct |
checksum_url_template fetched and verified |
TestNewUpdater_RegistryLookup_Unknown |
setup |
Unknown type โ error with hint listing registered types |
TestNewUpdater_Bitbucket |
setup |
type="bitbucket" โ BitbucketReleaseProvider created |
TestNewUpdater_Gitea |
setup |
type="gitea" โ GiteaReleaseProvider created |
TestNewUpdater_Codeberg |
setup |
type="codeberg" โ GiteaReleaseProvider at codeberg.org |
TestNewUpdater_Direct |
setup |
type="direct" โ DirectReleaseProvider created |
Integration Tests¶
Gated by environment-variable tags following the existing pattern in internal/testutil.
| Tag | Env Var | What is tested |
|---|---|---|
bitbucket |
INT_TEST_BITBUCKET=1 |
Bitbucket Cloud Downloads API: list, match, download |
gitea |
INT_TEST_GITEA=1 |
A real Gitea/Forgejo instance (URL + token in env): releases, download |
E2E BDD¶
The existing update command E2E scenarios cover provider-agnostic behaviour. No new Gherkin scenarios are required โ new providers do not change command interface or user-visible output.
Coverage¶
Target โฅ90% for all new pkg/vcs/* packages and modified paths in pkg/setup.
Migration and Compatibility¶
GitHub and GitLab Providers¶
The existing githubvcs.NewReleaseProvider and gitlabvcs.NewReleaseProvider constructors are preserved as-is. Each gains a new init.go registering a factory wrapper:
// pkg/vcs/github/init.go
func init() {
release.Register(release.SourceTypeGitHub, func(src release.ReleaseSourceConfig, cfg config.Containable) (release.Provider, error) {
client, err := NewGitHubClient(cfg.Sub("github"))
if err != nil {
return nil, err
}
return NewReleaseProvider(client), nil
})
}
NewUpdater Behaviour¶
The refactored NewUpdater is behaviourally identical for type: "github" and type: "gitlab". The config override via vcs.provider continues to work for all provider types.
requireReleaseToken¶
Extended with cases for the new providers:
switch vcsProvider {
case "gitlab":
fallbackEnv = "GITLAB_TOKEN"
case "bitbucket":
// Bitbucket uses two env vars; handled separately in the Bitbucket factory.
return nil // token presence check delegated to the provider
case "gitea":
fallbackEnv = "GITEA_TOKEN"
case "codeberg":
fallbackEnv = "CODEBERG_TOKEN"
case "direct":
fallbackEnv = "DIRECT_TOKEN"
default:
fallbackEnv = "GITHUB_TOKEN"
}
The Bitbucket case delegates credential presence checking to its factory (two separate vars: username + app_password), which returns a structured error if either is missing and Private: true.
props.ReleaseSource Serialisation¶
The Params field uses omitempty. Tools that don't use it produce output identical to today.
Generator Templates¶
internal/generator/ templates that scaffold the Tool initialisation block are updated to include an optional Params comment example. No breaking change to generated code.
Future Considerations¶
- Bitbucket Server / Data Center: Different API (
/rest/api/1.0/projects/{project}/repos/{slug}/archive). Lower priority given declining market share. - OCI / Container Registry: Releasing binaries as OCI artefacts (GHCR, Docker Hub) is an emerging pattern. Warrants a separate spec.
- AWS S3 / GCS / Azure Blob native auth: The direct provider can reach these via pre-signed URLs, but native IAM authentication would improve private bucket ergonomics.
- GitLab Package Registry: Distinct from GitLab Releases; some teams publish binaries there.
- Mirror / fallback chain: A
ReleaseSourcethat tries multiple providers in order (primary VCS, fallback CDN mirror). - Signature verification: GPG/cosign support, applicable to all providers. Tracked in the offline update spec as a future phase.
- GoReleaser
checksums.txt: Multi-platform checksum file as an alternative to per-file.sha256sidecars; relevant for the direct provider. - Bitbucket version in
checksums.txt: If GoReleaser is used to upload to Bitbucket Downloads, achecksums.txtfile could provide version information as a side-channel โ worth exploring.
Implementation Phases¶
Phase 1 โ Provider Registry and GitHub/GitLab Migration¶
- Add
pkg/vcs/release/registry.gowithRegister,Lookup,RegisteredTypes(usingsync.RWMutex). - Add
pkg/vcs/release/source_config.gowithReleaseSourceConfig. - Add
pkg/vcs/release/constants.gowith allSourceType*constants. - Add
init.gotopkg/vcs/githubandpkg/vcs/gitlabregistering their factories. - Add
pkg/setup/providers.gowith blank imports. - Refactor
NewUpdaterto use registry lookup. - Extend
requireReleaseTokenwith stubs for new provider cases. - Tests: registry unit tests (including concurrent access); verify GitHub and GitLab pass end-to-end.
Acceptance criteria: All existing tests pass. go test -race ./... clean. golangci-lint run clean.
Phase 2 โ Gitea / Forgejo / Codeberg Provider¶
- Implement
pkg/vcs/gitea/(client, release wrappers, provider, init). - Register both
"gitea"and"codeberg"factories ininit.go. - Unit tests with
httptest(including Codeberg host pre-configuration). - Integration tests gated by
INT_TEST_GITEA=1. - Complete
requireReleaseTokencases forgiteaandcodeberg.
Phase 3 โ Bitbucket Cloud Provider¶
- Implement
pkg/vcs/bitbucket/(client, filename pattern matching, release wrappers, provider, init). - Default and custom regex pattern support via
Params["filename_pattern"]. - Unit tests with
httptest. - Integration tests gated by
INT_TEST_BITBUCKET=1. - Complete
requireReleaseTokencase forbitbucket.
Phase 4 โ Direct HTTP Download Provider¶
- Implement
pkg/vcs/direct/version.goโ version endpoint fetch with text/JSON/YAML/XML parsing. - Implement
pkg/vcs/direct/provider.goโ URL template expansion,GetLatestRelease,GetReleaseByTag,DownloadReleaseAsset. - Add
Paramsfield toprops.ReleaseSource. - Unit tests covering all format variants and template placeholders.
Phase 5 โ Documentation and Generator¶
- Update
docs/components/setup.mdwith all new providers,Paramsreference tables, and authentication instructions. - Update
docs/components/vcs.mdwith registry documentation and provider extension guide. - Update
docs/concepts/architecture.mdto mention the provider registry pattern. - Update
internal/generator/templates withParamscomment example. - Update
docs/development/integration-testing.mdwithbitbucketandgiteatest tags.
Verification¶
# After Phase 1
go build ./...
go test -race ./pkg/vcs/release/...
go test -race ./pkg/setup/...
go test -race ./pkg/vcs/github/...
go test -race ./pkg/vcs/gitlab/...
golangci-lint run
# After Phase 2
go test -race ./pkg/vcs/gitea/...
INT_TEST_GITEA=1 go test ./pkg/vcs/gitea/... -v
# After Phase 3
go test -race ./pkg/vcs/bitbucket/...
INT_TEST_BITBUCKET=1 go test ./pkg/vcs/bitbucket/... -v
# After Phase 4
go test -race ./pkg/vcs/direct/...
# Full suite
just ci