Getting started¶
Fetch the latest release of a tool from a forge, without importing that forge's SDK.
By the end you will have a program that resolves a provider by name, reads a release through the shared contract, and prints its tag, with no forge SDK in your dependency graph.
Resolve a provider and read a release¶
package main
import (
"context"
"fmt"
"log"
"gitlab.com/phpboyscout/go/forge"
// Blank imports register providers. Import only the forges you support.
_ "gitlab.com/phpboyscout/go/forge/direct"
)
// forge.Config is two methods, so this program needs no config library to
// satisfy it. Yours will be a real one; this is the whole interface.
type tutorialConfig map[string]string
func (c tutorialConfig) GetString(key string) string { return c[key] }
func (c tutorialConfig) Sub(key string) forge.Config {
if key != forge.SourceTypeDirect {
return nil
}
return c
}
func main() {
ctx := context.Background()
factory, err := forge.Lookup("direct")
if err != nil {
log.Fatal(err)
}
// The endpoint addresses the source; the source's own settings live in the
// configuration it reads, under the `direct` subtree.
cfg := tutorialConfig{
"tool_name": "mytool",
"pinned_version": "v1.2.3",
"url_template": "https://dl.example.com/{tool}/{version}/{tool}_{os}_{arch}.{ext}",
}
provider, err := factory(ctx, forge.Endpoint{Type: forge.SourceTypeDirect}, cfg)
if err != nil {
log.Fatal(err)
}
rel, err := provider.GetLatestRelease(ctx, "acme", "mytool")
if err != nil {
log.Fatal(err)
}
fmt.Println(rel.GetTagName())
}
Run it and it prints v1.2.3, the version pinned_version named. Nothing left
your machine: the direct provider composes a release rather than asking an API
for one.
Three things to notice, because they shape everything else:
Your code never named a forge type. factory returns a forge.Provider.
Swapping "direct" for "gitlab" (and the blank import to match) changes
nothing below that line. That is
backend agnosticism, and it is the
point.
No vendor SDK entered your build. The core imports none, and a guard test keeps it that way. You pay only for the providers you blank-import.
The source type is a plain string. "direct" is not an enum member. A forge
this module has never heard of works the same way, provided something registered
it.
Read the factory signature once¶
Every provider is constructed through the same four-parameter factory, so read it once rather than copying it:
type ProviderFactory func(
ctx context.Context,
ep Endpoint,
cfg Config,
opts ...Option,
) (Provider, error)
ctx bounds construction, which is where a credential is resolved, and a source
you supply may reach a keychain or a remote secret store. cfg may be nil, which
is what a config-free public lookup passes. opts carries settings with no home
in a config subtree; forge.WithLogger, forge.WithHTTPTransport and forge.WithHTTPClient are the ones
today.
Add a real forge¶
Provider modules are separate, so you take only what you use:
An unknown type returns ErrProviderNotFound with a hint listing what is
registered, which is usually the real problem: a missing blank import.
See providers for the supported forges, their source types, config keys and capabilities.
Where next¶
- Verify a release before you trust it: the second tutorial, and the one that makes an update path safe
- Providers: supported forges, config keys, capabilities
- Configuration: every key, param and environment variable, with its default
- Author a provider: teach it a new forge, in your own module
- Authenticate to a forge: the credential seam
- Test against a forge: doubles, mocks, and the conformance harness