Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions cmd/go_mod.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package cmd

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

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

// runGoMod executes `go mod tidy`, `go mod download` and `go mod vendor`
// inside every directory under root that contains a go.mod file referencing
// github.com/gofiber/fiber. Directories named `vendor` are skipped.
func runGoMod(root string) error {
dirs, err := fiberModuleDirs(root)
if err != nil {
return fmt.Errorf("find modules: %w", err)
}
commands := [][]string{
{"go", "mod", "tidy"},
{"go", "mod", "download"},
{"go", "mod", "vendor"},
}
for _, dir := range dirs {
for _, args := range commands {
cmd := execCommand(args[0], args[1:]...) // #nosec G204 -- commands are controlled
cmd.Dir = dir
if err := runCmd(cmd); err != nil {
Comment thread
ReneWerner87 marked this conversation as resolved.
return fmt.Errorf("in %s: %w", dir, err)
}
}
}
return nil
}

// fiberModuleDirs returns directories under root containing a go.mod file that
// requires github.com/gofiber/fiber. vendor directories are skipped.
func fiberModuleDirs(root string) ([]string, error) {
var dirs []string
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == "vendor" {
return filepath.SkipDir
}
if !d.IsDir() && d.Name() == "go.mod" {
b, err := os.ReadFile(path) // #nosec G304 -- reading module file
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
mf, err := modfile.Parse(path, b, nil)
if err != nil {
return fmt.Errorf("parse %s: %w", path, err)
}
for _, r := range mf.Require {
if strings.HasPrefix(r.Mod.Path, "github.com/gofiber/fiber") {
dirs = append(dirs, filepath.Dir(path))
break
}
}
}
return nil
})
if walkErr != nil {
return nil, fmt.Errorf("walk %s: %w", root, walkErr)
}
return dirs, nil
}
91 changes: 91 additions & 0 deletions cmd/internal/migrations/go_version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package migrations

import (
"fmt"
"io/fs"
"os"
"path/filepath"
"strings"

"golang.org/x/mod/modfile"

semver "github.com/Masterminds/semver/v3"
"github.com/spf13/cobra"
)

// MigrateGoVersion ensures that all go.mod files referencing Fiber declare at
// least the provided Go version. Vendor directories are skipped.
func MigrateGoVersion(minVersion string) func(*cobra.Command, string, *semver.Version, *semver.Version) error {
minVer := semver.MustParse(minVersion)
Comment on lines +18 to +19
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace semver.MustParse with semver.NewVersion to avoid potential panics.

Using MustParse will panic if an invalid version is provided. Since this is a public API, it's safer to handle the error gracefully.

func MigrateGoVersion(minVersion string) func(*cobra.Command, string, *semver.Version, *semver.Version) error {
-	minVer := semver.MustParse(minVersion)
+	minVer, err := semver.NewVersion(minVersion)
+	if err != nil {
+		panic(fmt.Sprintf("invalid minimum version %q: %v", minVersion, err))
+	}
	return func(cmd *cobra.Command, cwd string, _, _ *semver.Version) error {

Alternatively, for even better error handling, validate the version when the migration function is called:

func MigrateGoVersion(minVersion string) func(*cobra.Command, string, *semver.Version, *semver.Version) error {
	return func(cmd *cobra.Command, cwd string, _, _ *semver.Version) error {
+		minVer, err := semver.NewVersion(minVersion)
+		if err != nil {
+			return fmt.Errorf("invalid minimum version %q: %w", minVersion, err)
+		}
-		minVer := semver.MustParse(minVersion)

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In cmd/internal/migrations/go_version.go around lines 18 to 19, replace
semver.MustParse with semver.NewVersion to avoid panics on invalid version
strings. Change the code to call semver.NewVersion(minVersion) and handle the
error returned instead of panicking. This ensures safer error handling for the
public API by validating the version string when the migration function is
called.

return func(cmd *cobra.Command, cwd string, _, _ *semver.Version) error {
dirs, err := fiberModuleDirs(cwd)
if err != nil {
return fmt.Errorf("find modules: %w", err)
}
for _, dir := range dirs {
modFile := filepath.Join(dir, "go.mod")
b, err := os.ReadFile(modFile) // #nosec G304
if err != nil {
return fmt.Errorf("read %s: %w", modFile, err)
}
lines := strings.Split(string(b), "\n")
changed := false
for i, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "go ") {
currVer, err := semver.NewVersion(strings.TrimSpace(strings.TrimPrefix(line, "go")))
if err != nil {
return fmt.Errorf("parse go version in %s: %w", modFile, err)
}
if currVer.LessThan(minVer) {
lines[i] = "go " + minVer.String()
changed = true
}
break
}
}
if changed {
if err := os.WriteFile(modFile, []byte(strings.Join(lines, "\n")), 0o600); err != nil {
return fmt.Errorf("write %s: %w", modFile, err)
}
}
}
cmd.Printf("Ensuring go version >= %s\n", minVer.String())
return nil
}
}

// fiberModuleDirs returns directories under root containing a go.mod file that
// requires github.com/gofiber/fiber. vendor directories are skipped.
func fiberModuleDirs(root string) ([]string, error) {
var dirs []string
walkErr := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && d.Name() == "vendor" {
return filepath.SkipDir
}
if !d.IsDir() && d.Name() == "go.mod" {
b, err := os.ReadFile(path) // #nosec G304 -- reading module file
if err != nil {
return fmt.Errorf("read %s: %w", path, err)
}
mf, err := modfile.Parse(path, b, nil)
if err != nil {
return fmt.Errorf("parse %s: %w", path, err)
}
for _, r := range mf.Require {
if strings.HasPrefix(r.Mod.Path, "github.com/gofiber/fiber") {
dirs = append(dirs, filepath.Dir(path))
break
}
}
}
return nil
})
if walkErr != nil {
return nil, fmt.Errorf("walk %s: %w", root, walkErr)
}
return dirs, nil
}
58 changes: 58 additions & 0 deletions cmd/internal/migrations/go_version_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package migrations_test

import (
"bytes"
"os"
"path/filepath"
"testing"

"github.com/gofiber/cli/cmd/internal/migrations"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func readFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path) // #nosec G304
require.NoError(t, err)
return string(b)
}

func newCmd(buf *bytes.Buffer) *cobra.Command {
cmd := &cobra.Command{}
cmd.SetOut(buf)
cmd.SetErr(buf)
return cmd
}

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

dir, err := os.MkdirTemp("", "mgover")
require.NoError(t, err)
defer func() { require.NoError(t, os.RemoveAll(dir)) }()

mod := `module example

go 1.21

require github.com/gofiber/fiber/v2 v2.0.0`
require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o600))

vendor := filepath.Join(dir, "vendor")
require.NoError(t, os.Mkdir(vendor, 0o750))
require.NoError(t, os.WriteFile(filepath.Join(vendor, "go.mod"), []byte("module vendor\n\ngo 1.10"), 0o600))

var buf bytes.Buffer
cmd := newCmd(&buf)
fn := migrations.MigrateGoVersion("1.23")
require.NoError(t, fn(cmd, dir, nil, nil))

content := readFile(t, filepath.Join(dir, "go.mod"))
assert.Contains(t, content, "go 1.23")
assert.Contains(t, buf.String(), "1.23")

vendorContent := readFile(t, filepath.Join(vendor, "go.mod"))
assert.Contains(t, vendorContent, "go 1.10")
}
1 change: 1 addition & 0 deletions cmd/internal/migrations/lists.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ var Migrations = []Migration{
v3migrations.MigrateEnvVarConfig,
v3migrations.MigrateSessionConfig,
v3migrations.MigrateReqHeaderParser,
MigrateGoVersion("1.24"),
},
},
}
Expand Down
Loading
Loading