Skip to content

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.

go get gitlab.com/phpboyscout/go/forge

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:

import _ "gitlab.com/phpboyscout/go/forge-gitlab"
factory, err := forge.Lookup("gitlab")

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