diff --git a/cmd/go_mod.go b/cmd/go_mod.go new file mode 100644 index 0000000..e8888b4 --- /dev/null +++ b/cmd/go_mod.go @@ -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 { + 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 +} diff --git a/cmd/internal/migrations/go_version.go b/cmd/internal/migrations/go_version.go new file mode 100644 index 0000000..a893d29 --- /dev/null +++ b/cmd/internal/migrations/go_version.go @@ -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) + 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 +} diff --git a/cmd/internal/migrations/go_version_test.go b/cmd/internal/migrations/go_version_test.go new file mode 100644 index 0000000..3684cd8 --- /dev/null +++ b/cmd/internal/migrations/go_version_test.go @@ -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") +} diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index 0bedcfa..b16c38e 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -55,6 +55,7 @@ var Migrations = []Migration{ v3migrations.MigrateEnvVarConfig, v3migrations.MigrateSessionConfig, v3migrations.MigrateReqHeaderParser, + MigrateGoVersion("1.24"), }, }, } diff --git a/cmd/internal/migrations/v3/common_test.go b/cmd/internal/migrations/v3/common_test.go index 7b4d7ca..12f0862 100644 --- a/cmd/internal/migrations/v3/common_test.go +++ b/cmd/internal/migrations/v3/common_test.go @@ -1,4 +1,4 @@ -package v3 +package v3_test import ( "bytes" @@ -7,6 +7,7 @@ import ( "strings" "testing" + v3 "github.com/gofiber/cli/cmd/internal/migrations/v3" //nolint:revive // alias required "github.com/spf13/cobra" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -48,7 +49,7 @@ func handler(c *fiber.Ctx) error { return nil } var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateHandlerSignatures(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateHandlerSignatures(cmd, dir, nil, nil)) content := readFile(t, file) assert.NotContains(t, content, "*fiber.Ctx") @@ -78,7 +79,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateParserMethods(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateParserMethods(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, ".Bind().Body(&v)") @@ -107,7 +108,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateRedirectMethods(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateRedirectMethods(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, ".Redirect().To(\"/foo\")") @@ -136,7 +137,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateGenericHelpers(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateGenericHelpers(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "fiber.Params[int](c, \"id\"") @@ -166,7 +167,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateContextMethods(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateContextMethods(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, ".RequestCtx()") @@ -190,7 +191,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateViewBind(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateViewBind(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, ".ViewBind(") @@ -215,7 +216,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateMount(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateMount(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, ".Use(\"/api\", api)") @@ -239,7 +240,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateAddMethod(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateAddMethod(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `Add([]string{fiber.MethodGet}, "/foo"`) @@ -260,7 +261,7 @@ const mime = fiber.MIMEApplicationJavaScript var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateMimeConstants(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateMimeConstants(cmd, dir, nil, nil)) content := readFile(t, file) assert.NotContains(t, content, "MIMEApplicationJavaScript") @@ -284,7 +285,7 @@ var _ = logger.TagHeader var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateLoggerTags(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateLoggerTags(cmd, dir, nil, nil)) content := readFile(t, file) assert.NotContains(t, content, "TagHeader") @@ -309,7 +310,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateStaticRoutes(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateStaticRoutes(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `.Get("/*", static.New("./public"))`) @@ -336,7 +337,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateTrustedProxyConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateTrustedProxyConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "TrustProxy: true") @@ -362,7 +363,7 @@ var _ = cors.New(cors.Config{ var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateCORSConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateCORSConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `AllowOrigins: []string{"https://a.com", "https://b.com"}`) @@ -391,7 +392,7 @@ var _ = csrf.New(csrf.Config{ var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateCSRFConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateCSRFConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "IdleTimeout:") @@ -413,7 +414,7 @@ var _ = monitor.New()`) var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateMonitorImport(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateMonitorImport(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "github.com/gofiber/contrib/monitor") @@ -439,7 +440,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateProxyTLSConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateProxyTLSConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "proxy.WithClient(&fasthttp.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}})") @@ -465,7 +466,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateConfigListenerFields(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateConfigListenerFields(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "EnablePrefork: true") @@ -499,7 +500,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateListenerCallbacks(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateListenerCallbacks(cmd, dir, nil, nil)) content := readFile(t, file) assert.NotContains(t, content, "OnShutdownError") @@ -530,7 +531,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateListenMethods(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateListenMethods(cmd, dir, nil, nil)) content := readFile(t, file) assert.NotContains(t, content, "ListenTLS(") @@ -562,7 +563,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateFilesystemMiddleware(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateFilesystemMiddleware(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `static.New("", static.Config{`) @@ -586,7 +587,7 @@ var _ = envvar.New(envvar.Config{ var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateEnvVarConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateEnvVarConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.NotContains(t, content, "ExcludeVars") @@ -613,7 +614,7 @@ var _ = limiter.New(limiter.Config{ var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateLimiterConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateLimiterConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "Expiration:") @@ -640,7 +641,7 @@ var _ = healthcheck.New(healthcheck.Config{ var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateHealthcheckConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateHealthcheckConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "Probe:") @@ -672,7 +673,7 @@ func main() { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateAppTestConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateAppTestConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `app.Test(req, fiber.TestConfig{Timeout: 2*time.Second})`) @@ -696,7 +697,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateMiddlewareLocals(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateMiddlewareLocals(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `requestid.FromContext(c)`) @@ -720,7 +721,7 @@ func handler(c fiber.Ctx) error { var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateReqHeaderParser(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateReqHeaderParser(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, `.Bind().Header(&v)`) @@ -745,7 +746,7 @@ var _ = session.New(session.Config{ var buf bytes.Buffer cmd := newCmd(&buf) - require.NoError(t, MigrateSessionConfig(cmd, dir, nil, nil)) + require.NoError(t, v3.MigrateSessionConfig(cmd, dir, nil, nil)) content := readFile(t, file) assert.Contains(t, content, "IdleTimeout:") diff --git a/cmd/migrate.go b/cmd/migrate.go index 6d1872f..2f62763 100644 --- a/cmd/migrate.go +++ b/cmd/migrate.go @@ -67,6 +67,10 @@ func migrateRunE(cmd *cobra.Command, currentVersionFile, targetVersionS string) return fmt.Errorf("migration failed %w", err) } + if err := runGoMod(wd); err != nil { + return fmt.Errorf("go mod: %w", err) + } + msg := fmt.Sprintf("Migration from Fiber %s to %s", currentVersionS, targetVersionS) cmd.Println(termenv.String(msg). Foreground(termenv.ANSIBrightBlue)) diff --git a/cmd/migrate_test.go b/cmd/migrate_test.go index 9df2761..4f97694 100644 --- a/cmd/migrate_test.go +++ b/cmd/migrate_test.go @@ -2,6 +2,7 @@ package cmd import ( "os" + "os/exec" "path/filepath" "testing" @@ -69,6 +70,8 @@ func main() { defer func() { require.NoError(t, os.Chdir(cwd)) }() cmd := newMigrateCmd("go.mod") + setupCmd() + defer teardownCmd() out, err := runCobraCmd(cmd, "-t=3.0.0") require.NoError(t, err) @@ -94,3 +97,44 @@ func main() { at.Contains(out, "Migrating Go packages") at.Contains(out, "Migrating handler signatures") } + +func Test_RunGoMod(t *testing.T) { + dir := t.TempDir() + + modContent := `module example + +require github.com/gofiber/fiber/v2 v2.0.0` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 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"), 0o600)) + + origExec := execCommand + var cmds []*exec.Cmd + execCommand = func(name string, args ...string) *exec.Cmd { + cs := append([]string{"-test.run=TestHelperProcess", "--", name}, args...) + cmd := exec.Command(os.Args[0], cs...) // #nosec G204 -- safe for test + env := []string{"GO_WANT_HELPER_PROCESS=1"} + if needError { + env = append(env, "GO_WANT_HELPER_NEED_ERR=1") + } + cmd.Env = env + cmds = append(cmds, cmd) + return cmd + } + defer func() { + execCommand = origExec + needError = false + }() + + require.NoError(t, runGoMod(dir)) + assert.Len(t, cmds, 3) + for _, c := range cmds { + assert.Equal(t, dir, c.Dir) + } + + cmds = nil + needError = true + assert.Error(t, runGoMod(dir)) +} diff --git a/go.mod b/go.mod index ad22504..6c3fee4 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/muesli/termenv v0.16.0 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 + golang.org/x/mod v0.25.0 ) require ( diff --git a/go.sum b/go.sum index 4991c07..4b5e675 100644 --- a/go.sum +++ b/go.sum @@ -70,6 +70,8 @@ github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavM github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=