Add SetIdentity for embedder User-Agent attribution, derive default version from build info · Entire

Add SetIdentity for embedder User-Agent attribution, derive default version from build info

27cf094→main·

toothbrush·1w ago·7 files·+196 added/-12 removed

Deployed embedders (mirror-pipeline's worker) have been pushing with "git-sync/dev go-git/6.x" in production: the advertised version lives in an internal package whose doc comment claimed "SDK consumers may overwrite it" — impossible from outside the module — and gitsync.Options exposes no identity knob, so there was no way for a service to name itself in the User-Agent at all.

Two changes:

Changes

Added

Changed

Removed

func main() {
    useragent.Version = versioninfo.Version
    // Prefer the goreleaser-stamped version; a plain `go build` leaves
    // versioninfo at "dev", and overwriting would clobber the version
    // useragent already resolved from the binary's build info (which is
    // the real one for a `go install ...@version` build).
    if versioninfo.Version != "dev" {
        useragent.Version = versioninfo.Version
    }
    err := run(context.Background(), os.Args[1:])
    if err == nil {
        return
    }
}
package gitsync

import (
    "strings"

"entire.io/entire/git-sync/internal/useragent"
)

// SetIdentity names the embedding service in every request git-sync makes:
// the HTTP User-Agent and the git-protocol "agent=" capability become
// "<service>/<version> git-sync/<git-sync-version> go-git/<go-git-version>"
// (non-git provider requests carry the same string without the go-git
// token). git-sync's own version is reported regardless; SetIdentity adds
// the service's identity in front so server operators can attribute traffic
// to the service, not just the library.
//
// The identity is process-wide and read on every request: call SetIdentity
// once at startup, before issuing requests from any Client. An empty
// service removes the identity; an empty version advertises the bare
// service name. Whitespace within either value is collapsed to "-" so the
// result stays a single User-Agent product token.
func SetIdentity(service, version string) {
    service = sanitizeToken(service)
    if service == "" {
        useragent.Identity = ""
        return
    }
    if version = sanitizeToken(version); version != "" {
        service += "/" + version
    }
    useragent.Identity = service
}

func sanitizeToken(s string) string {
    return strings.Join(strings.Fields(s), "-")
}
package gitsync

import (
    "testing"

"entire.io/entire/git-sync/internal/useragent"

"github.com/go-git/go-git/v6/plumbing/protocol/capability"
)

func TestSetIdentity(t *testing.T) {
    // Not parallel — mutates process-wide user-agent state.
    orig := useragent.Identity
    t.Cleanup(func() { useragent.Identity = orig })

tests := []struct {
        name             string
        service, version string
        wantIdentity     string
    }{
        {"service and version", "mirror-worker", "abc1234", "mirror-worker/abc1234"},
        {"service only", "mirror-worker", "", "mirror-worker"},
        {"empty service clears", "", "abc1234", ""},
        {"whitespace collapsed", "mirror worker", "v1 2", "mirror-worker/v1-2"},
        {"blank service clears", "   ", "abc1234", ""},
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            SetIdentity(tt.service, tt.version)
            if useragent.Identity != tt.wantIdentity {
                t.Errorf("SetIdentity(%q, %q) → Identity = %q, want %q",
                        tt.service, tt.version, useragent.Identity, tt.wantIdentity)
            }
        })
    }
}

func TestSetIdentityUserAgentShape(t *testing.T) {
    // Not parallel — mutates process-wide user-agent state.
    origIdentity, origVersion := useragent.Identity, useragent.Version
    t.Cleanup(func() { useragent.Identity, useragent.Version = origIdentity, origVersion })

useragent.Version = "0.7.1"
    SetIdentity("mirror-worker", "abc1234")

want := "mirror-worker/abc1234 git-sync/0.7.1 " + capability.DefaultAgent()
    if got := useragent.GoGit(); got != want {
        t.Errorf("GoGit() after SetIdentity = %q, want %q", got, want)
    }
    if got, want := useragent.Plain(), "mirror-worker/abc1234 git-sync/0.7.1"; got != want {
        t.Errorf("Plain() after SetIdentity = %q, want %q", got, want)
    }
}
// metadata APIs.
package useragent

import "github.com/go-git/go-git/v6/plumbing/protocol/capability"
import (
    "runtime/debug"
    "strings"

// Version is the git-sync version advertised in User-Agent strings.
// CLI builds set this from versioninfo.Version; SDK consumers may
// overwrite it before issuing any requests if they want to identify a
// different version.
var Version = "dev"
)

// Identity is an optional "<service>/<version>" product token advertised
// ahead of git-sync's own token, so servers can attribute traffic to the
// embedding service rather than to the library. Empty means no prefix.
// Set it via gitsync.SetIdentity — this package is internal.
var Identity = ""

// Version is the git-sync version advertised in User-Agent strings. It
// defaults to the git-sync module version recorded in the running binary's
// build info ("dev" when that is unavailable, e.g. a plain `go build` of
// this repo). The CLI overrides it with the goreleaser-stamped version.
// Embedders don't touch this — they identify themselves with
// gitsync.SetIdentity, which prefixes the User-Agent instead of masking
// git-sync's own version.
var Version = moduleVersion()

// fallbackVersion is advertised when the build carries no usable version
// (a plain `go build` of this repo, or missing build info).
const fallbackVersion = "dev"

// GoGit returns the User-Agent for git wire-protocol traffic. Format:
// "git-sync/<version> go-git/<go-git-version>". The go-git suffix is
// preserved because servers and operators commonly key off it.
// "[<identity> ]git-sync/<version> go-git/<go-git-version>". The go-git
// suffix is preserved because servers and operators commonly key off it.
func GoGit() string {
    return "git-sync/" + Version + " " + capability.DefaultAgent()
}

// Plain returns the User-Agent for non-git HTTP requests (e.g. provider
// REST APIs). Format: "git-sync/<version>".
func Plain() string {
    return "git-sync/" + Version
}

func identityPrefix() string {
    if Identity == "" {
        return ""
    }
    return Identity + " "
}

// moduleVersion resolves git-sync's own version from the binary's embedded
// build info: the dependency entry when git-sync is embedded as a module,
// or the main-module version when the CLI was `go install`ed at a version.
// Locally built binaries carry no usable version ("(devel)") → "dev".
func moduleVersion() string {
    const modulePath = "entire.io/entire/git-sync"
    bi, ok := debug.ReadBuildInfo()
    if !ok {
        return fallbackVersion
    }
    mods := append([]*debug.Module{&bi.Main}, bi.Deps...)
    for _, m := range mods {
        if m.Path != modulePath {
            continue
        }
        if m.Replace != nil {
            m = m.Replace
        }
        if v := strings.TrimPrefix(m.Version, "v"); v != "" && v != "(devel)" {
            return v
        }
    }
    return fallbackVersion
}

Testing

func TestIdentityPrefix(t *testing.T) {
    // Not parallel — mutates package-level Identity and Version.
    origIdentity, origVersion := Identity, Version
    t.Cleanup(func() { Identity, Version = origIdentity, origVersion })

Identity = "mirror-worker/abc1234"
    Version = "1.2.3"
    if got, want := Plain(), "mirror-worker/abc1234 git-sync/1.2.3"; got != want {
        t.Errorf("Plain() with Identity = %q, want %q", got, want)
    }
    wantGoGit := "mirror-worker/abc1234 git-sync/1.2.3 " + capability.DefaultAgent()
    if got := GoGit(); got != wantGoGit {
        t.Errorf("GoGit() with Identity = %q, want %q", got, wantGoGit)
    }

Identity = ""
    if got, want := Plain(), "git-sync/1.2.3"; got != want {
        t.Errorf("Plain() with empty Identity = %q, want %q", got, want)
    }
}

func TestModuleVersionInTestBinary(t *testing.T) {
    t.Parallel()

// In this repo's own test binary git-sync is the main module with no
    // stamped version, so the build-info lookup must fall back to "dev"
    // rather than leaking "(devel)" or an empty string into the UA.
    if got := moduleVersion(); got != "dev" {
        t.Errorf("moduleVersion() in test binary = %q, want %q", got, "dev")
    }
}