gopin – Automate Version Pinning for Go Install Commands



This content originally appeared on DEV Community and was authored by kuro

Have you ever wondered why your CI pipeline suddenly broke even though you didn’t change any code? Or why a teammate’s local build produces different results than yours? The culprit might be lurking in your go install commands with @latest tags.

gopin is a CLI tool that automatically pins versions of go install commands in your codebase, ensuring reproducible builds and enhanced security.

The Problem with @latest

Using @latest in go install commands creates several issues:

Reproducibility Issues

.PHONY: lint
lint:
    go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
    golangci-lint run

Today it installs v2.6.0, tomorrow it might install v2.6.2. Builds become non-deterministic.

Security Risk

Unpinned versions increase supply chain attack risk. A compromised version could be installed without your knowledge.

CI/CD Instability

Different runners may install different versions, causing inconsistent test results and build failures.

Debugging Difficulty

Reproducing the exact environment from weeks or months ago becomes impossible with @latest.

Introducing gopin

gopin is a CLI tool that solves these problems by automatically converting @latest to specific semantic versions.

# Before
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest

# After running gopin
go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2

Architecture: A Three-Stage Pipeline

gopin follows a clean, modular architecture with three core stages:

┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  Detector   │ ───> │  Resolver   │ ───> │  Rewriter   │
└─────────────┘      └─────────────┘      └─────────────┘
     │                     │                     │
     │                     │                     │
  Scan files         Query versions         Replace text
  with regex         from proxy.golang.org  in-place

1. Detection Phase

The detector scans files for go install patterns using regex:

// From pkg/detector/detector.go
var GoInstallPattern = regexp.MustCompile(
    `go\s+install\s+` +                  // go install
    `(?:-[a-zA-Z0-9_=,]+\s+)*` +         // Optional flags
    `([^\s@#]+)` +                       // Module path
    `(?:@([^\s#]+))?`,                   // Version (optional)
)

This pattern handles various edge cases:

  • Flags: go install -v -trimpath github.com/tool@latest
  • Subpackages: github.com/org/repo/cmd/tool@latest
  • No version: go install github.com/tool (implicitly @latest)
  • Special characters: gopkg.in/yaml.v3@latest

2. Resolution Phase

The resolver queries proxy.golang.org for the latest version:

GET https://proxy.golang.org/github.com/golangci/golangci-lint/v2/@latest

Response:
{
  "Version": "v2.6.2",
  "Time": "2024-12-01T10:30:00Z"
}

Key Design: Resolver Chain Pattern

Resolvers are composed using the decorator pattern:

CachedResolver
    → FallbackResolver
        → ProxyResolver (primary)
        → GoListResolver (fallback)

This design provides:

  • Caching for repeated module lookups
  • Fallback to go list for private modules
  • Flexibility to add new resolution strategies
// From pkg/cli/app.go
func createResolver(cfg *config.Config) resolver.Resolver {
    var res resolver.Resolver

    switch cfg.Resolver.Type {
    case "golist":
        res = resolver.NewGoListResolver()
    default:
        res = resolver.NewProxyResolver(cfg.Resolver.ProxyURL, cfg.Resolver.Timeout)
    }

    if cfg.Resolver.Fallback {
        res = resolver.NewFallbackResolver(res, resolver.NewGoListResolver())
    }

    return resolver.NewCachedResolver(res)
}

3. Rewriting Phase

The rewriter replaces version strings in-place with change tracking.

Key Design: Backward Processing

Matches are processed in reverse order (last line first, rightmost column first) to prevent offset shifts:

// From pkg/rewriter/rewriter.go
sort.Slice(matches, func(i, j int) bool {
    if matches[i].Line != matches[j].Line {
        return matches[i].Line > matches[j].Line  // Descending
    }
    return matches[i].StartColumn > matches[j].StartColumn
})

This prevents offset shifts – modifying line 5 doesn’t affect line 10’s position. Forward processing would require recalculating offsets after each change.

Real-World Example

Let’s see gopin in action with a typical Makefile:

Before:

.PHONY: install-tools
install-tools:
    go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
    go install golang.org/x/tools/cmd/goimports@latest
    go install honnef.co/go/tools/cmd/staticcheck@latest
    go install github.com/securego/gosec/v2/cmd/gosec@latest

Running gopin:

$ gopin run --diff

--- Makefile
+++ Makefile
- go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
+ go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2
- go install golang.org/x/tools/cmd/goimports@latest
+ go install golang.org/x/tools/cmd/goimports@v0.39.0

After:

.PHONY: install-tools
install-tools:
    go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.6.2
    go install golang.org/x/tools/cmd/goimports@v0.39.0
    go install honnef.co/go/tools/cmd/staticcheck@v0.5.1
    go install github.com/securego/gosec/v2/cmd/gosec@v2.22.0

Default Target Files

By default, gopin scans these file patterns:

  • .github/**/*.yml and .github/**/*.yaml – GitHub Actions workflows
  • Makefile, makefile, GNUmakefile – Make build files
  • *.mk – Make include files

You can customize target files using a .gopin.yaml configuration file.

Conclusion

Version pinning helps ensure reproducible builds and reduces security risks. gopin automates this process for go install commands across your codebase using a clean three-stage architecture: detection, resolution, and rewriting.

Get started:

go install github.com/nnnkkk7/gopin/cmd/gopin@latest
cd your-project
gopin run --dry-run  # Preview changes
gopin run            # Apply changes

Repository: github.com/nnnkkk7/gopin


This content originally appeared on DEV Community and was authored by kuro