Skip to content
23 changes: 16 additions & 7 deletions Taskfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ tasks:
cmds:
- echo ">> Run tests..."
- toolset run gotestsum
- task: "test:remove"
- task: "test:alias"
- task: "test:runtimes"
- task: "test:ensure"
- task: "test:ensure:gh"
- task: "test:other"

upd:dl:
- rm -rf dl
Expand All @@ -53,7 +59,6 @@ tasks:
dir: temp_tests
cmds:
- "rm .toolset.json .toolset.lock.json || true"
- "rm -rf ./bin"
- "{{.TOOLSET}} init --copy-from git+https://gist.github.com/c92c40d0e4329c1c2fe9372216474cd7.git:/formatters.json"

test:remove:
Expand All @@ -70,23 +75,27 @@ tasks:
dir: temp_tests
cmds:
- task: "init"
- "{{.TOOLSET}} add go github.com/vburenin/ifacemaker iface"
- "{{.TOOLSET}} add go github.com/vburenin/ifacemaker hello"
- "{{.TOOLSET}} sync"
- "{{.TOOLSET}} list"
- ls -lsa bin/tools | grep iface
- "{{.TOOLSET}} list | grep hello"

test:runtimes:
dir: temp_tests
cmds:
- "rm .toolset.json .toolset.lock.json || true"
- "rm -rf ./bin"
- "{{.TOOLSET}} init"
- "{{.TOOLSET}} runtime add go@1.22.10"
- "{{.TOOLSET}} runtime add go@1.24"
- "{{.TOOLSET}} runtime add go@1.25"
- "{{.TOOLSET}} runtime add go"
- "{{.TOOLSET}} runtime add gh"
- "{{.TOOLSET}} runtime list"
- "{{.TOOLSET}} add go@1.22.10 golang.org/x/tools/cmd/goimports"
- "{{.TOOLSET}} ensure go@1.25 golang.org/x/tools/cmd/goimports"
- "{{.TOOLSET}} sync"
- "{{.TOOLSET}} list"
- "{{.TOOLSET}} remove goimports"
- "{{.TOOLSET}} add go golang.org/x/tools/cmd/goimports"
- "{{.TOOLSET}} ensure go@1.24 golang.org/x/tools/cmd/goimports"
- "{{.TOOLSET}} sync"
- "{{.TOOLSET}} remove goimports"
- "{{.TOOLSET}} list"
Expand Down Expand Up @@ -123,7 +132,7 @@ tasks:
- "{{.TOOLSET}} list"
- "{{.TOOLSET}} run golangci-lint version"

test:
test:other:
dir: temp_tests
cmds:
- task: "init"
Expand Down
11 changes: 1 addition & 10 deletions internal/fsh/real_fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,20 +112,11 @@ func TestLocks(t *testing.T) {

fs := fsh.NewRealFS()

// Use a unique temporary file for this test
tmpDir := t.TempDir()
filename := tmpDir + "/test.lock"

// Ensure clean state
_ = os.Remove(filename)
const filename = "/tmp/test.lock"

// Lock file
unlock, err := fs.Lock(ctx, filename)
require.NoError(t, err)
defer func() {
// Cleanup: ensure the lock is released and file is removed
_ = os.Remove(filename)
}()

ch := make(chan struct{})
go func() {
Expand Down
8 changes: 5 additions & 3 deletions internal/version/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,10 @@ func runGo(root string) {

handleSignals()

if err := cmd.Run(); err != nil {
// TODO: return the same exit status maybe.
err := cmd.Run()
if eerr, ok := err.(*exec.ExitError); ok {
os.Exit(eerr.ExitCode())
} else if err != nil {
os.Exit(1)
}
os.Exit(0)
Expand Down Expand Up @@ -534,5 +536,5 @@ func dedupEnv(caseInsensitive bool, env []string) []string {
func handleSignals() {
// Ensure that signals intended for the child process are not handled by
// this process' runtime (e.g. SIGQUIT). See issue #36976.
signal.Notify(make(chan os.Signal), signalsToIgnore...) //nolint:staticcheck
signal.Notify(make(chan os.Signal), signalsToIgnore...)
}
135 changes: 135 additions & 0 deletions internal/version/versions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
package version

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"sort"
"strings"
"time"

"golang.org/x/mod/semver"
)

var (
fullRe = regexp.MustCompile(`^(\d+)\.(\d+)\.(\d+)$`)
partialRe = regexp.MustCompile(`^(\d+)\.(\d+)$`)
)

// ResolvePartialVersion resolves a partial version (e.g., "1.25") to the latest patch version.
// Returns the full version string without "go" prefix (e.g., "1.25.4").
func ResolvePartialVersion(ctx context.Context, partialVer string) (string, error) {
major, minor, _, isPartial, err := parseVersion(partialVer)
if err != nil {
return "", err
}

if !isPartial {
// Already a full version, return as-is
return strings.TrimPrefix(partialVer, "go"), nil
}

versions, err := listAvailableVersions(ctx)
if err != nil {
return "", fmt.Errorf("list versions: %w", err)
}

// Filter versions that match major.minor and are stable
var candidates []string
prefix := fmt.Sprintf("go%d.%d.", major, minor)
for _, v := range versions {
if !v.Stable {
continue
}
if strings.HasPrefix(v.Version, prefix) {
candidates = append(candidates, v.Version)
}
}

if len(candidates) == 0 {
return "", fmt.Errorf("no stable versions found for go%d.%d", major, minor)
}

// Sort versions using semver to find the latest
sort.Slice(candidates, func(i, j int) bool {
return semver.Compare(candidates[i], candidates[j]) > 0
})

latest := candidates[0]
// Remove "go" prefix
return strings.TrimPrefix(latest, "go"), nil
}

// goVersionRec represents a Go release version.
type goVersionRec struct {
Version string `json:"version"` // e.g., "go1.25.4"
Stable bool `json:"stable"`
}

// listAvailableVersions fetches all available Go versions from golang.org.
func listAvailableVersions(ctx context.Context) ([]goVersionRec, error) {
const url = "https://go.dev/dl/?mode=json"

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}

client := &http.Client{
Timeout: 30 * time.Second,
}

resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch versions: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read response: %w", err)
}

var versions []goVersionRec
if err := json.Unmarshal(body, &versions); err != nil {
return nil, fmt.Errorf("parse versions: %w", err)
}

return versions, nil
}

// parseVersion parses a version string and returns whether it's a partial version (major.minor)
// or full version (major.minor.patch).
// Returns: major, minor, patch, isPartial
func parseVersion(ver string) (major, minor, patch int, isPartial bool, err error) {
// Remove "go" prefix if present
ver = strings.TrimPrefix(ver, "go")

// Try to match major.minor.patch
if matches := fullRe.FindStringSubmatch(ver); matches != nil {
var majorPart, minorPart, patchPart int
fmt.Sscanf(matches[1], "%d", &majorPart)
fmt.Sscanf(matches[2], "%d", &minorPart)
fmt.Sscanf(matches[3], "%d", &patchPart)

return majorPart, minorPart, patchPart, false, nil
}

// Try to match major.minor
if matches := partialRe.FindStringSubmatch(ver); matches != nil {
var majorPart, minorPart int
fmt.Sscanf(matches[1], "%d", &majorPart)
fmt.Sscanf(matches[2], "%d", &minorPart)

return majorPart, minorPart, 0, true, nil
}

return 0, 0, 0, false, fmt.Errorf("invalid version format: %s", ver)
}
Loading
Loading