-
Notifications
You must be signed in to change notification settings - Fork 21
migrate: align shared dependencies #183
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,111 @@ | ||
| package migrations | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "strings" | ||
|
|
||
| semver "github.com/Masterminds/semver/v3" | ||
| "github.com/spf13/cobra" | ||
| "golang.org/x/mod/modfile" | ||
| ) | ||
|
|
||
| // ExecCommand is used to run external commands. It can be replaced in tests. | ||
| var ExecCommand = exec.Command | ||
|
|
||
| // MigrateDependencies ensures that dependencies shared with Fiber are at least | ||
| // the versions required by the target Fiber release. | ||
| // | ||
| // It updates go.mod files that already require a dependency also required by | ||
| // Fiber, bumping the version when it is lower than Fiber's requirement. No | ||
| // changes are made if the existing version is equal or higher. | ||
| func MigrateDependencies(cmd *cobra.Command, cwd string, _, target *semver.Version) error { | ||
| fiberModule := fmt.Sprintf("github.com/gofiber/fiber/v%d@v%s", target.Major(), target.String()) | ||
|
|
||
| c := ExecCommand("go", "mod", "download", "-json", fiberModule) | ||
| var out bytes.Buffer | ||
| c.Stdout = &out | ||
| c.Stderr = &out | ||
| if err := c.Run(); err != nil { | ||
| return fmt.Errorf("download fiber module: %w", err) | ||
| } | ||
| var info struct { | ||
| GoMod string `json:"GoMod"` //nolint:tagliatelle // field name defined by go tool output | ||
| } | ||
| if err := json.Unmarshal(out.Bytes(), &info); err != nil { | ||
| return fmt.Errorf("parse download info: %w", err) | ||
| } | ||
|
|
||
| b, err := os.ReadFile(info.GoMod) // #nosec G304 | ||
| if err != nil { | ||
| return fmt.Errorf("read fiber go.mod: %w", err) | ||
| } | ||
| mf, err := modfile.Parse(info.GoMod, b, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("parse fiber go.mod: %w", err) | ||
| } | ||
|
|
||
| deps := make(map[string]*semver.Version, len(mf.Require)) | ||
| for _, r := range mf.Require { | ||
| v, err := semver.NewVersion(strings.TrimPrefix(r.Mod.Version, "v")) | ||
| if err != nil { | ||
| return fmt.Errorf("parse fiber dependency %s version %s: %w", r.Mod.Path, r.Mod.Version, err) | ||
| } | ||
| deps[r.Mod.Path] = v | ||
| } | ||
|
|
||
| dirs, err := fiberModuleDirs(cwd) | ||
| if err != nil { | ||
| return fmt.Errorf("find modules: %w", err) | ||
| } | ||
|
|
||
| anyChanged := false | ||
| 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) | ||
| } | ||
| mf, err := modfile.Parse(modFile, b, nil) | ||
| if err != nil { | ||
| return fmt.Errorf("parse %s: %w", modFile, err) | ||
| } | ||
|
|
||
| changed := false | ||
| for _, r := range mf.Require { | ||
| targetVer, ok := deps[r.Mod.Path] | ||
| if !ok { | ||
| continue | ||
| } | ||
| currVer, err := semver.NewVersion(strings.TrimPrefix(r.Mod.Version, "v")) | ||
| if err != nil { | ||
| return fmt.Errorf("parse %s version in %s: %w", r.Mod.Path, modFile, err) | ||
| } | ||
| if currVer.LessThan(targetVer) { | ||
| r.Mod.Version = "v" + targetVer.String() | ||
| changed = true | ||
| } | ||
| } | ||
|
|
||
| if changed { | ||
| mf.SetRequire(mf.Require) | ||
| formatted, err := mf.Format() | ||
| if err != nil { | ||
| return fmt.Errorf("format %s: %w", modFile, err) | ||
| } | ||
| if err := os.WriteFile(modFile, formatted, 0o600); err != nil { | ||
| return fmt.Errorf("write %s: %w", modFile, err) | ||
| } | ||
| anyChanged = true | ||
| } | ||
| } | ||
|
|
||
| if anyChanged { | ||
| cmd.Println("Updating dependency versions") | ||
| } | ||
| return nil | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| package migrations_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| semver "github.com/Masterminds/semver/v3" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
|
|
||
| "github.com/gofiber/cli/cmd/internal/migrations" | ||
| ) | ||
|
|
||
| func Test_MigrateDependencies(t *testing.T) { | ||
| dir, err := os.MkdirTemp("", "mdeps") | ||
| require.NoError(t, err) | ||
| defer func() { require.NoError(t, os.RemoveAll(dir)) }() | ||
|
|
||
| mod := `module example | ||
|
|
||
| go 1.22 | ||
|
|
||
| require ( | ||
| github.com/gofiber/fiber/v3 v3.0.0 | ||
| github.com/valyala/fasthttp v1.0.0 | ||
| github.com/andybalholm/brotli v1.2.0 | ||
| )` | ||
| require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o600)) | ||
|
|
||
| fiberMod := `module github.com/gofiber/fiber/v3 | ||
|
|
||
| go 1.22 | ||
|
|
||
| require ( | ||
| github.com/valyala/fasthttp v1.10.0 | ||
| github.com/andybalholm/brotli v1.0.0 | ||
| )` | ||
| fiberGoMod := filepath.Join(dir, "fiber.mod") | ||
| require.NoError(t, os.WriteFile(fiberGoMod, []byte(fiberMod), 0o600)) | ||
|
|
||
| restore := stubFiberDownload(t, fiberGoMod) | ||
| defer restore() | ||
|
|
||
| var buf bytes.Buffer | ||
| cmd := newCmd(&buf) | ||
| target := semver.MustParse("3.0.0") | ||
| require.NoError(t, migrations.MigrateDependencies(cmd, dir, nil, target)) | ||
|
|
||
| content := readFile(t, filepath.Join(dir, "go.mod")) | ||
| assert.Contains(t, content, "github.com/valyala/fasthttp v1.10.0") | ||
| assert.Contains(t, content, "github.com/andybalholm/brotli v1.2.0") | ||
| assert.Contains(t, buf.String(), "Updating dependency versions") | ||
| } | ||
|
|
||
| func Test_MigrateDependencies_NoChange(t *testing.T) { | ||
| dir, err := os.MkdirTemp("", "mdeps_nc") | ||
| require.NoError(t, err) | ||
| defer func() { require.NoError(t, os.RemoveAll(dir)) }() | ||
|
|
||
| mod := `module example | ||
|
|
||
| go 1.22 | ||
|
|
||
| require ( | ||
| github.com/gofiber/fiber/v3 v3.0.0 | ||
| github.com/valyala/fasthttp v1.10.0 | ||
| github.com/andybalholm/brotli v1.2.0 | ||
| )` | ||
| require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(mod), 0o600)) | ||
|
|
||
| fiberMod := `module github.com/gofiber/fiber/v3 | ||
|
|
||
| go 1.22 | ||
|
|
||
| require ( | ||
| github.com/valyala/fasthttp v1.10.0 | ||
| github.com/andybalholm/brotli v1.0.0 | ||
| )` | ||
| fiberGoMod := filepath.Join(dir, "fiber.mod") | ||
| require.NoError(t, os.WriteFile(fiberGoMod, []byte(fiberMod), 0o600)) | ||
|
|
||
| restore := stubFiberDownload(t, fiberGoMod) | ||
| defer restore() | ||
|
|
||
| var buf bytes.Buffer | ||
| cmd := newCmd(&buf) | ||
| target := semver.MustParse("3.0.0") | ||
| require.NoError(t, migrations.MigrateDependencies(cmd, dir, nil, target)) | ||
|
|
||
| content := readFile(t, filepath.Join(dir, "go.mod")) | ||
| assert.Contains(t, content, "github.com/valyala/fasthttp v1.10.0") | ||
| assert.Contains(t, content, "github.com/andybalholm/brotli v1.2.0") | ||
| assert.Empty(t, buf.String()) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package migrations_test | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "os" | ||
| "os/exec" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
||
| "github.com/gofiber/cli/cmd/internal/migrations" | ||
| ) | ||
|
|
||
| // stubFiberDownload replaces migrations.ExecCommand with a helper that | ||
| // returns the provided Fiber go.mod path in JSON format. It returns a | ||
| // function to restore the original ExecCommand. | ||
| func stubFiberDownload(t *testing.T, fiberGoMod string) func() { | ||
| t.Helper() | ||
| orig := migrations.ExecCommand | ||
| out := fmt.Sprintf(`{"GoMod":%q}`, filepath.ToSlash(fiberGoMod)) | ||
| migrations.ExecCommand = func(string, ...string) *exec.Cmd { | ||
| cmd := exec.Command(os.Args[0], "-test.run=TestHelperProcess", "--") // #nosec G204 -- test helper | ||
| cmd.Env = []string{ | ||
| "GO_WANT_HELPER_PROCESS=1", | ||
| "GO_HELPER_STDOUT=" + out, | ||
| } | ||
| return cmd | ||
| } | ||
| return func() { migrations.ExecCommand = orig } | ||
| } | ||
|
Comment on lines
+16
to
+29
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chainGlobal override is not parallel-safe Because You can quickly check if any tests in this package use 🏁 Script executed: #!/bin/bash
# Search for parallel tests and stub usage within the migrations tests
rg -nP --type=go -C1 '\bt\.Parallel\(\)' cmd/internal/migrations
rg -nP --type=go -C2 '\bstubFiberDownload\s*\(' cmd/internal/migrationsLength of output: 20720 Protect migrations.ExecCommand Stub from Parallel Execution The • In cmd/internal/migrations/lists_test.go
• In cmd/internal/migrations/dependencies_test.go Addressing this will ensure test isolation and eliminate flakiness from concurrent overrides. 🤖 Prompt for AI Agents |
||
|
|
||
| func TestHelperProcess(t *testing.T) { | ||
| t.Helper() | ||
| if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" { | ||
| return | ||
| } | ||
| if out := os.Getenv("GO_HELPER_STDOUT"); out != "" { | ||
| _, _ = fmt.Fprint(os.Stdout, out) | ||
| } | ||
| os.Exit(0) //nolint:revive // helper process exits intentionally | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,14 +44,22 @@ func Test_DoMigration_Verbose(t *testing.T) { | |
| } | ||
|
|
||
| func Test_DoMigration_Verbose_Run(t *testing.T) { | ||
| t.Parallel() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removing |
||
| curr := semver.MustParse("1.0.0") | ||
| target := semver.MustParse("2.0.0") | ||
|
|
||
| t.Run("no changes", func(t *testing.T) { | ||
| t.Parallel() | ||
| fiberMod := `module github.com/gofiber/fiber/v2 | ||
|
|
||
| go 1.22 | ||
|
|
||
| require github.com/valyala/fasthttp v1.0.0` | ||
| dir := t.TempDir() | ||
| fiberGoMod := filepath.Join(dir, "fiber.mod") | ||
| require.NoError(t, os.WriteFile(fiberGoMod, []byte(fiberMod), 0o600)) | ||
|
|
||
| restore := stubFiberDownload(t, fiberGoMod) | ||
| t.Cleanup(restore) | ||
|
|
||
| require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n\nrequire github.com/gofiber/fiber/v2 v2.0.0\n"), 0o600)) | ||
| var buf bytes.Buffer | ||
| cmd := &cobra.Command{} | ||
|
|
@@ -63,9 +71,18 @@ func Test_DoMigration_Verbose_Run(t *testing.T) { | |
| }) | ||
|
|
||
| t.Run("changes", func(t *testing.T) { | ||
| t.Parallel() | ||
| fiberMod := `module github.com/gofiber/fiber/v1 | ||
|
|
||
| go 1.22 | ||
|
|
||
| require github.com/valyala/fasthttp v1.0.0` | ||
| dir := t.TempDir() | ||
| fiberGoMod := filepath.Join(dir, "fiber.mod") | ||
| require.NoError(t, os.WriteFile(fiberGoMod, []byte(fiberMod), 0o600)) | ||
|
|
||
| restore := stubFiberDownload(t, fiberGoMod) | ||
| t.Cleanup(restore) | ||
|
|
||
| require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module test\n\nrequire github.com/gofiber/fiber/v1 v1.0.0\n"), 0o600)) | ||
| require.NoError(t, os.WriteFile(filepath.Join(dir, "main.go"), []byte("package main\nimport \"github.com/gofiber/fiber/v1\"\n"), 0o600)) | ||
| var buf bytes.Buffer | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Capturing stdout and stderr into the same buffer can lead to issues if the command writes to both streams. For instance, if
go mod downloadwrites an error or warning to stderr while also writing JSON to stdout, the combined output would be invalid JSON, causingjson.Unmarshalto fail. It's safer to capture stderr in a separate buffer to provide clearer error messages.