# Go Version Management Strategy

**Status:** Research & Recommendations
**Date:** 2025-02-12
**Context:** Addressing Go version compatibility issues with development tools after automated Renovate upgrades

---

## Problem Statement

### Current Situation

- **Go Upgrade Flow:** Renovate automatically updates Go versions in `go.work`, `Dockerfile`, and GitHub Actions workflows
- **Tool Compatibility Issue:** Development tools (e.g., golangci-lint) built with older Go versions break when the project moves to a newer Go version
- **Recent Failure:** golangci-lint v2.8.0 built with Go 1.25.5 failed pre-commit checks after Go 1.26.0 upgrade

### Current Installation State

```bash
$ go version
go version go1.26.0 linux/amd64

$ golangci-lint version
golangci-lint has version 2.8.0 built with go1.25.5 from e2e40021 on 2026-01-07T21:29:47Z

$ ls -la ~/sdk/
drwxr-xr-x 10 root root 4096 Dec  6 05:34 go1.25.5
drwxr-xr-x 10 root root 4096 Jan 21 18:34 go1.25.6
```

**The Mismatch:** Tools built with Go 1.25.5 encountering Go 1.26.0 runtime/stdlib changes.

### Current Update Mechanism

1. **Skill Runner** (`.github/skills/utility-update-go-version-scripts/run.sh`):
   - Uses `golang.org/dl/goX.Y.Z@latest` to download specific versions
   - Installs to `~/sdk/goX.Y.Z/`
   - Updates symlink: `/usr/local/go/bin/go -> ~/sdk/goX.Y.Z/bin/go`
   - **Preserves old versions** in `~/sdk/`

2. **Install Scripts** (`scripts/install-go-*.sh`):
   - Downloads Go tarball from go.dev
   - **Removes** existing `/usr/local/go` completely
   - Extracts fresh installation
   - **Does not preserve** old versions

---

## Research Findings

### 1. Official Go Position on Version Management

**Source:** [Go Downloads](https://go.dev/doc/manage-install) and [golang.org/dl](https://pkg.go.dev/golang.org/dl)

#### Key Points:
- **Multi-version support:** Go officially supports running multiple versions via `golang.org/dl`
- **SDK isolation:** Each version installs to `$HOME/sdk/goX.Y.Z/`
- **Version-specific binaries:** Run as `go1.25.6`, `go1.26.0`, etc.
- **No interference:** Old versions don't conflict with new installations
- **Official recommendation:** Use `go install golang.org/dl/goX.Y.Z@latest && goX.Y.Z download`

#### Example Workflow:
```bash
# Install multiple versions
go install golang.org/dl/go1.25.6@latest
go1.25.6 download

go install golang.org/dl/go1.26.0@latest
go1.26.0 download

# Switch between versions
go1.25.6 version  # go version go1.25.6 linux/amd64
go1.26.0 version  # go version go1.26.0 linux/amd64
```

**Verdict:** ✅ Official support for multi-version. No third-party manager needed.

---

### 2. Version Managers (gvm, goenv, asdf)

#### GVM (Go Version Manager)
- **Status:** Last updated 2019, limited Go 1.13+ support
- **Issues:** Not actively maintained, compatibility problems with modern Go
- **Verdict:** ❌ Not recommended

#### goenv
- **Status:** Active, mimics rbenv/pyenv model
- **Pros:** Automatic version switching via `.go-version` files
- **Cons:** Wrapper around `golang.org/dl`, adds complexity
- **Use Case:** Multi-project environments with different Go versions per project
- **Verdict:** ⚠️ Overkill for single-project workflows

#### asdf (via asdf-golang plugin)
- **Status:** Active, part of asdf ecosystem
- **Pros:** Unified tool for managing multiple language runtimes
- **Cons:** Requires learning asdf, heavy for Go-only use
- **Use Case:** Polyglot teams managing Node, Python, Ruby, Go, etc.
- **Verdict:** ⚠️ Good for polyglot environments, unnecessary for Go-focused projects

**Conclusion:** For single-project Go development, `golang.org/dl` is simpler and officially supported.

---

### 3. Industry Best Practices

#### Kubernetes Project
**Source:** [kubernetes/kubernetes](https://github.com/kubernetes/kubernetes)

- **Approach:** Pin Go version in `.go-version`, `Dockerfile`, and CI
- **Multi-version:** No—upgrades are coordinated across all tools simultaneously
- **Tool management:** Tools rebuilt on every Go upgrade
- **Build reproducibility:** Docker builds use specific `golang:X.Y.Z` images
- **Verdict:** Single version, strict coordination, CI enforcement

#### Docker CLI
**Source:** [docker/cli](https://github.com/docker/cli)

- **Approach:** Single Go version in `Dockerfile`, `go.mod`, and CI
- **Tool management:** CI installs tools fresh on every run (no version conflicts)
- **Upgrade strategy:** Update Go version → rebuild all tools → test → merge
- **Verdict:** Single version, ephemeral CI environments

#### HashiCorp Projects (Terraform, Vault)
**Source:** [hashicorp/terraform](https://github.com/hashicorp/terraform)

- **Approach:** Pin Go version in `go.mod` and `.go-version`
- **Multi-version:** No—single version enforced across team
- **Tool management:** Hermetic builds in Docker, tools installed per-build
- **Upgrade strategy:** Go upgrades blocked until all tools compatible
- **Verdict:** Single version, strict enforcement

#### Common Patterns Across Major OSS Projects:
1. **Single Go version** per project (no multi-version maintenance)
2. **CI installs tools fresh** on every run (avoids stale tool issues)
3. **Docker builds** ensure reproducibility
4. **Local development:** Developers expected to match project Go version
5. **Tool compatibility:** Tools updated/rebuilt alongside Go upgrades

---

### 4. Tool Dependency Management

#### Problem: Pre-built Binaries vs. Source Builds

##### Pre-built Binary (Current Issue):
```bash
$ golangci-lint version
golangci-lint has version 2.8.0 built with go1.25.5
```
- **Problem:** Binary built with Go 1.25.5, incompatible with Go 1.26.0 stdlib
- **Root cause:** Go 1.26 introduced stdlib changes that break tools compiled with 1.25

##### Solution: Rebuild Tools After Go Upgrade
```bash
# Rebuild golangci-lint with current Go version
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

# Verify
$ golangci-lint version
golangci-lint has version 2.8.1 built with go1.26.0
```

#### Best Practice: Go Tools Should Use `go install`
- **Why:** `go install` compiles with the current Go toolchain
- **Result:** Tools always match the active Go version
- **Trade-off:** Requires source compilation (slower) vs. downloading binary (faster)

---

### 5. GitHub Actions Best Practices

#### actions/setup-go Caching
```yaml
- uses: actions/setup-go@v6
  with:
    go-version: ${{ env.GO_VERSION }}
    cache: true  # Caches Go modules and build artifacts
```

**Implications:**
- Cache keyed by Go version and `go.sum`
- Go version change → new cache entry (no stale artifacts)
- Tools installed in CI are ephemeral → rebuilt every run
- **Verdict:** ✅ No version conflict issues in CI

---

## Recommendations

### Strategy: Single Go Version + Tool Rebuild Protocol

**Rationale:**
1. Official Go tooling (`golang.org/dl`) already supports multi-version
2. Industry standard is **single version per project**
3. CI environments are ephemeral (no tool compatibility issues)
4. Local development should match CI
5. Renovate already updates Go version in sync across all locations

### Implementation Plan

#### Phase 1: Pre-Upgrade Tool Verification (Immediate)

**Objective:** Prevent pre-commit failures after Go upgrades

**Changes:**

1. **Update Pre-commit Hook** (`scripts/pre-commit-hooks/golangci-lint-fast.sh`):
   ```bash
   # Add version check before running
   LINT_GO_VERSION=$(golangci-lint version | grep -oP 'go\K[0-9]+\.[0-9]+(?:\.[0-9]+)?')
   SYSTEM_GO_VERSION=$(go version | grep -oP 'go\K[0-9]+\.[0-9]+(?:\.[0-9]+)?')

   if [ "$LINT_GO_VERSION" != "$SYSTEM_GO_VERSION" ]; then
       echo "⚠️  golangci-lint Go version mismatch:"
       echo "   golangci-lint: $LINT_GO_VERSION"
       echo "   system Go:     $SYSTEM_GO_VERSION"
       echo ""
       echo "🔧 Rebuilding golangci-lint with current Go version..."
       go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
   fi
   ```

2. **Update Go Installation Skill** (`.github/skills/utility-update-go-version-scripts/run.sh`):
   ```bash
   # After Go version update, rebuild critical tools
   echo "🔧 Rebuilding development tools with Go $REQUIRED_VERSION..."

   # List of tools to rebuild
   TOOLS=(
       "github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
       "golang.org/x/tools/gopls@latest"
       "golang.org/x/vuln/cmd/govulncheck@latest"
   )

   for tool in "${TOOLS[@]}"; do
       echo "  - Installing $tool"
       go install "$tool" || echo "⚠️  Failed to install $tool"
   done
   ```

3. **Create Tool Rebuild Script** (`scripts/rebuild-go-tools.sh`):
   ```bash
   #!/usr/bin/env bash
   set -euo pipefail

   echo "🔧 Rebuilding Go development tools..."
   echo "Current Go version: $(go version)"
   echo ""

   # Core development tools
   declare -A TOOLS=(
       ["golangci-lint"]="github.com/golangci/golangci-lint/cmd/golangci-lint@latest"
       ["gopls"]="golang.org/x/tools/gopls@latest"
       ["govulncheck"]="golang.org/x/vuln/cmd/govulncheck@latest"
       ["dlv"]="github.com/go-delve/delve/cmd/dlv@latest"
   )

   for tool_name in "${!TOOLS[@]}"; do
       tool_path="${TOOLS[$tool_name]}"
       echo "Installing $tool_name..."
       if go install "$tool_path"; then
           echo "✅ $tool_name installed successfully"
       else
           echo "❌ Failed to install $tool_name"
       fi
   done

   echo ""
   echo "✅ Tool rebuild complete"
   echo ""
   echo "Installed versions:"
   golangci-lint version 2>/dev/null || echo "  golangci-lint: not found"
   gopls version 2>/dev/null || echo "  gopls: $(gopls version)"
   govulncheck -version 2>/dev/null || echo "  govulncheck: not found"
   dlv version 2>/dev/null || echo "  dlv: $(dlv version)"
   ```

#### Phase 2: Documentation Updates

**Files to Update:**

1. **CONTRIBUTING.md**:
   ```markdown
   ### Go Version Updates

   When Renovate updates the Go version:
   1. Pull the latest changes
   2. Run the Go update skill: `.github/skills/scripts/skill-runner.sh utility-update-go-version`
   3. Rebuild development tools: `./scripts/rebuild-go-tools.sh`
   4. Restart your IDE's Go language server

   **Why?** Development tools (golangci-lint, gopls) are compiled binaries.
   After a Go version upgrade, these tools must be rebuilt with the new Go version
   to avoid compatibility issues.
   ```

2. **README.md** (Development Setup):
   ```markdown
   ### Keeping Go Tools Up-to-Date

   After pulling a Go version update from Renovate:
   ```bash
   # Rebuild all Go development tools
   ./scripts/rebuild-go-tools.sh
   ```

   This ensures tools like golangci-lint are compiled with the same Go version
   as your project.
   ```

3. **New File: `docs/development/go_version_upgrades.md`**:
   - Detailed explanation of Go version management
   - Step-by-step upgrade procedure
   - Troubleshooting common issues
   - FAQ section

#### Phase 3: Pre-commit Enhancement (Optional)

**Auto-fix Tool Mismatches:**

Add to `.pre-commit-config.yaml`:
```yaml
repos:
  - repo: local
    hooks:
      - id: go-tool-version-check
        name: Go Tool Version Check
        entry: scripts/pre-commit-hooks/check-go-tool-versions.sh
        language: system
        pass_filenames: false
        stages: [pre-commit]
```

Script automatically detects and fixes tool version mismatches before linting runs.

---

### Not Recommended: Multiple Go Versions Locally

#### Why Not Keep Multiple Versions?

**Cons:**
1. **Complexity:** Switching between versions manually is error-prone
2. **Confusion:** Which version is active? Are tools built with the right one?
3. **Disk space:** Each Go version is ~400MB
4. **Maintenance burden:** Tracking which tool needs which version
5. **CI mismatch:** CI uses single version; local multi-version creates drift

**Exception:** If actively developing multiple Go projects targeting different versions
- **Solution:** Use `goenv` or `asdf` for automatic version switching per project
- **Charon Context:** Single project, single Go version is simpler

**Verdict:** ❌ Multiple versions add complexity without clear benefit for single-project development

---

### Multi-Version Backward Compatibility Analysis

#### Should We Maintain N-1 Compatibility?

**Context:** Some projects maintain compatibility with previous major Go releases (e.g., supporting both Go 1.25 and 1.26)

**Analysis:**
- **Charon is a self-hosted application**, not a library
- Users run pre-built Docker images (Go version is locked in image)
- Developers should match the project's Go version
- No need to support multiple client Go versions

**Recommendation:** ❌ No backward compatibility requirement

**If library:** ✅ Would recommend supporting N-1 (e.g., Go 1.25 and 1.26) using:
```go
//go:build go1.25
// +build go1.25
```

---

## Implementation Timeline

### Week 1: Critical Fixes (High Priority)
- [x] **Document the problem** (this document)
- [ ] **Implement pre-commit tool version check** with auto-rebuild
- [ ] **Update Go version update skill** to rebuild tools
- [ ] **Create `rebuild-go-tools.sh` script**

### Week 2: Documentation & Education
- [ ] **Update CONTRIBUTING.md** with Go upgrade procedure
- [ ] **Update README.md** with tool rebuild instructions
- [ ] **Create `docs/development/go_version_upgrades.md`**
- [ ] **Add troubleshooting section** to copilot instructions

### Week 3: Testing & Validation
- [ ] **Test upgrade flow** with next Renovate Go update
- [ ] **Verify pre-commit auto-rebuild** works correctly
- [ ] **Document tool rebuild performance** (time cost)

---

## Decision Matrix

| Approach | Pros | Cons | Verdict |
|----------|------|------|---------|
| **Keep multiple Go versions** | Can build tools with older Go | Complexity, disk space, confusion | ❌ Not recommended |
| **Use version manager (goenv/asdf)** | Auto-switching, nice UX | Overhead, learning curve | ⚠️ Optional for polyglot teams |
| **Single version + rebuild tools** | Simple, matches CI, industry standard | Tools rebuild takes time (~30s) | ✅ **Recommended** |
| **Pin tool versions to old Go** | Tools don't break | Misses tool updates, security issues | ❌ Not recommended |
| **Delay Go upgrades until tools compatible** | No breakage | Blocks security patches, slows updates | ❌ Not recommended |

---

## FAQ

### Q1: Why did golangci-lint break after Go 1.26 upgrade?

**A:** golangci-lint was compiled with Go 1.25.5, but Go 1.26.0 introduced stdlib changes. Pre-built binaries from older Go versions can be incompatible with newer Go runtimes.

**Solution:** Rebuild golangci-lint with Go 1.26: `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest`

### Q2: Should I keep old Go versions "just in case"?

**A:** No, unless actively developing multiple projects with different Go requirements.

**Why not:** Complexity, confusion, and disk space without clear benefit. If you need older Go, the official `golang.org/dl` mechanism makes it easy to reinstall:
```bash
go install golang.org/dl/go1.25.6@latest
go1.25.6 download
```

### Q3: Will Renovate break my tools every time it updates Go?

**A:** Not with the recommended approach. The pre-commit hook will detect version mismatches and automatically rebuild tools before running linters.

**Manual rebuild:** `./scripts/rebuild-go-tools.sh` (takes ~30 seconds)

### Q4: How often does Go release new versions?

**A:** Go releases **major versions every 6 months** (February and August). Patch releases are more frequent for security/bug fixes.

**Implication:** Expect Go version updates 2-3 times per year from Renovate.

### Q5: What if I want to test my code against multiple Go versions?

**A:** Use CI matrix testing:
```yaml
strategy:
  matrix:
    go-version: ['1.25', '1.26', '1.27']
```

For local testing, use `golang.org/dl`:
```bash
go1.25.6 test ./...
go1.26.0 test ./...
```

### Q6: Should I pin golangci-lint to a specific version?

**A:** Yes, but via Renovate-managed version pin:
```bash
# In Makefile or script
GOLANGCI_LINT_VERSION := v1.62.2
go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
```

Renovate can update the version pin, and CI will use the pinned version consistently.

### Q7: Why doesn't CI have this problem?

**A:** CI environments are ephemeral. Every workflow run:
1. Installs Go from scratch (via `actions/setup-go`)
2. Installs tools with `go install` (compiled with CI's Go version)
3. Runs tests/lints
4. Discards everything

**Local development** has persistent tool installations that can become stale.

---

## Risk Assessment

### Risk: Developers Forget to Rebuild Tools

**Likelihood:** High (after automated Renovate updates)
**Impact:** Medium (pre-commit failures, frustration)
**Mitigation:**
1. Pre-commit hook auto-detects and rebuilds tools (Phase 1)
2. Clear documentation in CONTRIBUTING.md (Phase 2)
3. Error messages include rebuild instructions

### Risk: Tool Rebuild Takes Too Long

**Likelihood:** Medium (depends on tool size)
**Impact:** Low (one-time cost per Go upgrade)
**Measurement:**
```bash
$ time go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
real    0m28.341s  # ~30 seconds
```

**Mitigation:** Acceptable for infrequent upgrades (2-3 times/year)

### Risk: CI and Local Go Versions Drift

**Likelihood:** Low (both managed by Renovate)
**Impact:** High (false positives/negatives in CI)
**Mitigation:**
1. Renovate updates `go.work` and GitHub Actions `GO_VERSION` in sync
2. CI verifies Go version matches `go.work`

---

## References

### Official Go Documentation
- [Managing Go Installations](https://go.dev/doc/manage-install)
- [golang.org/dl](https://pkg.go.dev/golang.org/dl)
- [Go Release Policy](https://go.dev/doc/devel/release)

### Industry Examples
- [Kubernetes: .go-version](https://github.com/kubernetes/kubernetes/blob/master/.go-version)
- [Docker CLI: Dockerfile](https://github.com/docker/cli/blob/master/Dockerfile)
- [HashiCorp Terraform: go.mod](https://github.com/hashicorp/terraform/blob/main/go.mod)

### Version Managers
- [goenv](https://github.com/go-nv/goenv)
- [asdf-golang](https://github.com/kennyp/asdf-golang)
- [gvm](https://github.com/moovweb/gvm) (archived, not recommended)

### Related Resources
- [golangci-lint Installation](https://golangci-lint.run/usage/install/)
- [actions/setup-go Caching](https://github.com/actions/setup-go#caching-dependency-files-and-build-outputs)
- [Go Modules Reference](https://go.dev/ref/mod)

---

## Next Steps

1. **Review this document** with team/maintainers
2. **Approve strategy:** Single Go version + tool rebuild protocol
3. **Implement Phase 1** (pre-commit tool version check)
4. **Test with next Renovate Go update** (validate flow)
5. **Document lessons learned** and adjust as needed

---

## Appendix: Tool Inventory

| Tool | Purpose | Installation | Version Check |
|------|---------|--------------|---------------|
| **golangci-lint** | Pre-commit linting | `go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest` | `golangci-lint version` |
| **gopls** | Go language server (IDE) | `go install golang.org/x/tools/gopls@latest` | `gopls version` |
| **govulncheck** | Security scanning | `go install golang.org/x/vuln/cmd/govulncheck@latest` | `govulncheck -version` |
| **dlv** (Delve) | Debugger | `go install github.com/go-delve/delve/cmd/dlv@latest` | `dlv version` |
| **gotestsum** | Test runner | `go install gotest.tools/gotestsum@latest` | `gotestsum --version` |

**Critical Tools:** golangci-lint, gopls (priority for rebuild)
**Optional Tools:** dlv, gotestsum (rebuild as needed)

---

**Status:** ✅ Research complete, ready for implementation
**Recommendation:** **Single Go version + tool rebuild protocol**
**Next Action:** Implement Phase 1 (pre-commit tool version check)
