DEV Community

Cover image for gopin - Automate Version Pinning for Go Install Commands
kuro
kuro

Posted on • Edited on

gopin - Automate Version Pinning for Go Install Commands

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
Enter fullscreen mode Exit fullscreen mode

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 updating all go install commands to the latest specific semantic versions. This includes @latest, already-pinned versions, and commands without version specifiers.

# 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
)
Enter fullscreen mode Exit fullscreen mode

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"
}
Enter fullscreen mode Exit fullscreen mode

Key Design: Resolver Chain Pattern

Resolvers are composed using the decorator pattern:

CachedResolver
    → FallbackResolver
        → ProxyResolver (primary)
        → GoListResolver (fallback)
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Repository: github.com/nnnkkk7/gopin

Top comments (0)