From 06551afdb88306facd538af26a59ec62a007c210 Mon Sep 17 00:00:00 2001 From: RW Date: Sat, 29 Nov 2025 16:54:46 +0100 Subject: [PATCH 1/2] Add swagger contrib migration --- cmd/internal/migrations/lists.go | 1 + .../migrations/v3/contrib_versions.go | 92 +++++++++ .../migrations/v3/swagger_packages.go | 192 ++++++++++++++++++ .../migrations/v3/swagger_packages_test.go | 160 +++++++++++++++ 4 files changed, 445 insertions(+) create mode 100644 cmd/internal/migrations/v3/contrib_versions.go create mode 100644 cmd/internal/migrations/v3/swagger_packages.go create mode 100644 cmd/internal/migrations/v3/swagger_packages_test.go diff --git a/cmd/internal/migrations/lists.go b/cmd/internal/migrations/lists.go index 864a77e..4394afb 100644 --- a/cmd/internal/migrations/lists.go +++ b/cmd/internal/migrations/lists.go @@ -55,6 +55,7 @@ var Migrations = []Migration{ v3migrations.MigrateCORSConfig, v3migrations.MigrateCSRFConfig, v3migrations.MigrateMonitorImport, + v3migrations.MigrateSwaggerPackages, v3migrations.MigrateContribPackages, v3migrations.MigrateUtilsImport, v3migrations.MigrateHealthcheckConfig, diff --git a/cmd/internal/migrations/v3/contrib_versions.go b/cmd/internal/migrations/v3/contrib_versions.go new file mode 100644 index 0000000..1aca3c8 --- /dev/null +++ b/cmd/internal/migrations/v3/contrib_versions.go @@ -0,0 +1,92 @@ +package v3 + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "sync" + "time" +) + +const contribV3ProxyPrefix = "https://proxy.golang.org/github.com/gofiber/contrib/v3/" + +var ( + contribV3VersionMu sync.Mutex + contribV3VersionCache = make(map[string]string) + contribV3VersionFetcher = fetchContribV3Version +) + +func contribV3Version(module string) (string, error) { + contribV3VersionMu.Lock() + if v, ok := contribV3VersionCache[module]; ok { + contribV3VersionMu.Unlock() + return v, nil + } + fetcher := contribV3VersionFetcher + contribV3VersionMu.Unlock() + + v, err := fetcher(module) + if err != nil { + return "", err + } + + contribV3VersionMu.Lock() + contribV3VersionCache[module] = v + contribV3VersionMu.Unlock() + return v, nil +} + +func fetchContribV3Version(module string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + url := contribV3ProxyPrefix + module + "/@latest" + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return "", fmt.Errorf("create request: %w", err) + } + + client := &http.Client{} + res, err := client.Do(req) + if err != nil { + return "", fmt.Errorf("fetch latest version: %w", err) + } + defer func() { + if cerr := res.Body.Close(); cerr != nil && err == nil { + err = cerr + } + }() + + if res.StatusCode != http.StatusOK { + return "", fmt.Errorf("fetch latest version: unexpected status %d", res.StatusCode) + } + + var data struct { + Version string `json:"Version"` //nolint:tagliatelle // field name defined by proxy + } + if err := json.NewDecoder(res.Body).Decode(&data); err != nil { + return "", fmt.Errorf("parse latest version: %w", err) + } + if data.Version == "" { + return "", fmt.Errorf("latest version not found for %s", module) + } + + return data.Version, nil +} + +// SetContribV3VersionFetcher overrides the function used to fetch contrib module versions. +// It resets the cached versions and returns a restore function to revert the change. +func SetContribV3VersionFetcher(fn func(string) (string, error)) func() { + contribV3VersionMu.Lock() + prev := contribV3VersionFetcher + contribV3VersionFetcher = fn + contribV3VersionCache = make(map[string]string) + contribV3VersionMu.Unlock() + return func() { + contribV3VersionMu.Lock() + contribV3VersionFetcher = prev + contribV3VersionCache = make(map[string]string) + contribV3VersionMu.Unlock() + } +} diff --git a/cmd/internal/migrations/v3/swagger_packages.go b/cmd/internal/migrations/v3/swagger_packages.go new file mode 100644 index 0000000..e5a0c13 --- /dev/null +++ b/cmd/internal/migrations/v3/swagger_packages.go @@ -0,0 +1,192 @@ +package v3 + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io/fs" + "os" + "path/filepath" + "regexp" + "strings" + + semver "github.com/Masterminds/semver/v3" + "github.com/spf13/cobra" + + "github.com/gofiber/cli/cmd/internal" +) + +const ( + contribSwaggerOld = "github.com/gofiber/contrib/swagger" + contribSwaggerNew = "github.com/gofiber/contrib/v3/swaggo" + fiberSwaggerOld = "github.com/gofiber/swagger" + fiberSwaggerNew = "github.com/gofiber/contrib/v3/swaggerui" + goModVersionPattern = `v[\w.+-]+` +) + +func MigrateSwaggerPackages(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { + changedImports, err := internal.ChangeFileContent(cwd, func(content string) string { + updated, changed := rewriteSwaggerImports(content) + if changed { + return updated + } + return content + }) + if err != nil { + return fmt.Errorf("failed to migrate swagger imports: %w", err) + } + + modChanged, err := migrateSwaggerModules(cwd) + if err != nil { + return err + } + + if !changedImports && !modChanged { + return nil + } + + cmd.Println("Migrating swagger packages") + return nil +} + +func migrateSwaggerModules(cwd string) (bool, error) { + modChanged := false + var swaggoVersion, swaggerUIVersion string + + walkErr := filepath.WalkDir(cwd, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if d.IsDir() { + if d.Name() == "vendor" { + return filepath.SkipDir + } + return nil + } + if d.Name() != "go.mod" { + return nil + } + + info, err := d.Info() + if err != nil { + return fmt.Errorf("stat %s: %w", path, err) + } + + b, err := os.ReadFile(path) // #nosec G304 -- reading module files + if err != nil { + return fmt.Errorf("read %s: %w", path, err) + } + content := string(b) + + needsSwaggo := strings.Contains(content, contribSwaggerOld) || strings.Contains(content, contribSwaggerNew) + needsSwaggerUI := strings.Contains(content, fiberSwaggerOld) || strings.Contains(content, fiberSwaggerNew) + if !needsSwaggo && !needsSwaggerUI { + return nil + } + + if needsSwaggo && swaggoVersion == "" { + swaggoVersion, err = contribV3Version("swaggo") + if err != nil { + return fmt.Errorf("fetch swaggo version: %w", err) + } + } + if needsSwaggerUI && swaggerUIVersion == "" { + swaggerUIVersion, err = contribV3Version("swaggerui") + if err != nil { + return fmt.Errorf("fetch swaggerui version: %w", err) + } + } + + updated := content + if needsSwaggo { + updated = updateGoModModule(updated, contribSwaggerOld, contribSwaggerNew, swaggoVersion) + } + if needsSwaggerUI { + updated = updateGoModModule(updated, fiberSwaggerOld, fiberSwaggerNew, swaggerUIVersion) + } + + if updated == content { + return nil + } + + if err := os.WriteFile(path, []byte(updated), info.Mode().Perm()); err != nil { + return fmt.Errorf("write %s: %w", path, err) + } + modChanged = true + return nil + }) + if walkErr != nil { + return false, fmt.Errorf("failed to migrate swagger go.mod entries: %w", walkErr) + } + + return modChanged, nil +} + +func rewriteSwaggerImports(content string) (string, bool) { + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, "", content, parser.ParseComments) + if err != nil { + return content, false + } + + changed := false + for _, imp := range f.Imports { + path := strings.Trim(imp.Path.Value, "\"`") + newPath := "" + + switch path { + case contribSwaggerOld: + newPath = contribSwaggerNew + case fiberSwaggerOld: + newPath = fiberSwaggerNew + case contribSwaggerNew, fiberSwaggerNew: + newPath = path + default: + continue + } + + if path != newPath { + imp.Path.Value = fmt.Sprintf("%q", newPath) + changed = true + } + + if imp.Name == nil || imp.Name.Name == "" { + imp.Name = ast.NewIdent("swagger") + changed = true + } + } + + if !changed { + return content, false + } + + var buf bytes.Buffer + if err := format.Node(&buf, fset, f); err != nil { + return content, false + } + + return buf.String(), true +} + +func updateGoModModule(content, oldPath, newPath, version string) string { + if version == "" { + return content + } + + reRequireOld := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)%s\s+%s`, regexp.QuoteMeta(oldPath), goModVersionPattern)) + content = reRequireOld.ReplaceAllString(content, fmt.Sprintf(`${1}%s %s`, newPath, version)) + + reRequireNew := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*(?:require\s+)?)%s\s+%s`, regexp.QuoteMeta(newPath), goModVersionPattern)) + content = reRequireNew.ReplaceAllString(content, fmt.Sprintf(`${1}%s %s`, newPath, version)) + + reReplaceOld := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*replace\s+)%s(\s+%s)?(\s+=>\s+)`, regexp.QuoteMeta(oldPath), goModVersionPattern)) + content = reReplaceOld.ReplaceAllString(content, fmt.Sprintf(`${1}%s${2}${3}`, newPath)) + + reReplaceNew := regexp.MustCompile(fmt.Sprintf(`(?m)^(\s*replace\s+)%s(\s+%s)?(\s+=>\s+)`, regexp.QuoteMeta(newPath), goModVersionPattern)) + content = reReplaceNew.ReplaceAllString(content, fmt.Sprintf(`${1}%s${2}${3}`, newPath)) + + return content +} diff --git a/cmd/internal/migrations/v3/swagger_packages_test.go b/cmd/internal/migrations/v3/swagger_packages_test.go new file mode 100644 index 0000000..48e754c --- /dev/null +++ b/cmd/internal/migrations/v3/swagger_packages_test.go @@ -0,0 +1,160 @@ +package v3_test + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/gofiber/cli/cmd/internal/migrations/v3" +) + +const ( + swaggoModule = "swaggo" + swaggerUIModule = "swaggerui" +) + +func Test_MigrateSwaggerPackages(t *testing.T) { + restore := v3.SetContribV3VersionFetcher(func(module string) (string, error) { + switch module { + case swaggoModule: + return "v3.0.1", nil + case swaggerUIModule: + return "v3.1.0", nil + default: + return "", fmt.Errorf("unexpected module %s", module) + } + }) + t.Cleanup(restore) + + dir := t.TempDir() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/contrib/swagger" + +func main() { + _ = swagger.Config{} +}`) + + modContent := `module example + +go 1.22 + +require github.com/gofiber/contrib/swagger v1.0.0 + +replace github.com/gofiber/contrib/swagger => ../local` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateSwaggerPackages(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `swagger "github.com/gofiber/contrib/v3/swaggo"`) + + mod := readFile(t, filepath.Join(dir, "go.mod")) + assert.Contains(t, mod, "github.com/gofiber/contrib/v3/swaggo v3.0.1") + assert.Contains(t, mod, "replace github.com/gofiber/contrib/v3/swaggo => ../local") + + assert.Contains(t, buf.String(), "Migrating swagger packages") +} + +func Test_MigrateSwaggerPackages_FiberSwagger(t *testing.T) { + restore := v3.SetContribV3VersionFetcher(func(module string) (string, error) { + switch module { + case swaggoModule: + return "v3.0.0", nil + case swaggerUIModule: + return "v3.2.0", nil + default: + return "", fmt.Errorf("unexpected module %s", module) + } + }) + t.Cleanup(restore) + + dir := t.TempDir() + + file := writeTempFile(t, dir, `package main +import ( + "fmt" + "github.com/gofiber/swagger" +) + +func main() { + fmt.Println(swagger.Config{}) +}`) + + modContent := `module example + +go 1.22 + +require github.com/gofiber/swagger v1.1.0 +` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateSwaggerPackages(cmd, dir, nil, nil)) + + content := readFile(t, file) + assert.Contains(t, content, `swagger "github.com/gofiber/contrib/v3/swaggerui"`) + + mod := readFile(t, filepath.Join(dir, "go.mod")) + assert.Contains(t, mod, "github.com/gofiber/contrib/v3/swaggerui v3.2.0") + + assert.Contains(t, buf.String(), "Migrating swagger packages") +} + +func Test_MigrateSwaggerPackages_Idempotent(t *testing.T) { + calls := 0 + restore := v3.SetContribV3VersionFetcher(func(module string) (string, error) { + calls++ + switch module { + case swaggoModule: + return "v3.0.1", nil + case swaggerUIModule: + return "v3.2.1", nil + default: + return "", fmt.Errorf("unexpected module %s", module) + } + }) + t.Cleanup(restore) + + dir := t.TempDir() + + file := writeTempFile(t, dir, `package main +import swagger "github.com/gofiber/contrib/v3/swaggo" + +func main() { + _ = swagger.Config{} +}`) + + modContent := `module example + +go 1.22 + +require ( + github.com/gofiber/contrib/v3/swaggo v3.0.1 + github.com/gofiber/contrib/v3/swaggerui v3.2.1 +)` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateSwaggerPackages(cmd, dir, nil, nil)) + firstContent := readFile(t, file) + firstMod := readFile(t, filepath.Join(dir, "go.mod")) + + require.NoError(t, v3.MigrateSwaggerPackages(cmd, dir, nil, nil)) + secondContent := readFile(t, file) + secondMod := readFile(t, filepath.Join(dir, "go.mod")) + + assert.Equal(t, firstContent, secondContent) + assert.Equal(t, firstMod, secondMod) + assert.Empty(t, buf.String()) + assert.Equal(t, 2, calls) +} From 8d4e0c10da5dce7b2e39a55d60269444c3699ac1 Mon Sep 17 00:00:00 2001 From: RW Date: Sun, 30 Nov 2025 11:56:00 +0100 Subject: [PATCH 2/2] Improve swagger migration stability --- .../migrations/v3/contrib_versions.go | 31 +++++++++---- .../migrations/v3/swagger_packages.go | 11 +++-- .../migrations/v3/swagger_packages_test.go | 45 +++++++++++++++++++ go.mod | 2 +- go.sum | 2 + 5 files changed, 79 insertions(+), 12 deletions(-) diff --git a/cmd/internal/migrations/v3/contrib_versions.go b/cmd/internal/migrations/v3/contrib_versions.go index 1aca3c8..feb467d 100644 --- a/cmd/internal/migrations/v3/contrib_versions.go +++ b/cmd/internal/migrations/v3/contrib_versions.go @@ -7,6 +7,8 @@ import ( "net/http" "sync" "time" + + "golang.org/x/sync/singleflight" ) const contribV3ProxyPrefix = "https://proxy.golang.org/github.com/gofiber/contrib/v3/" @@ -15,6 +17,8 @@ var ( contribV3VersionMu sync.Mutex contribV3VersionCache = make(map[string]string) contribV3VersionFetcher = fetchContribV3Version + contribV3VersionGroup singleflight.Group + contribHTTPClient = &http.Client{} ) func contribV3Version(module string) (string, error) { @@ -26,18 +30,30 @@ func contribV3Version(module string) (string, error) { fetcher := contribV3VersionFetcher contribV3VersionMu.Unlock() - v, err := fetcher(module) + res, err, _ := contribV3VersionGroup.Do(module, func() (any, error) { + v, fetchErr := fetcher(module) + if fetchErr != nil { + return "", fetchErr + } + + contribV3VersionMu.Lock() + contribV3VersionCache[module] = v + contribV3VersionMu.Unlock() + return v, nil + }) if err != nil { - return "", err + return "", fmt.Errorf("fetch contrib version: %w", err) + } + + v, ok := res.(string) + if !ok { + return "", fmt.Errorf("unexpected contrib version type %T", res) } - contribV3VersionMu.Lock() - contribV3VersionCache[module] = v - contribV3VersionMu.Unlock() return v, nil } -func fetchContribV3Version(module string) (string, error) { +func fetchContribV3Version(module string) (version string, err error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() @@ -47,8 +63,7 @@ func fetchContribV3Version(module string) (string, error) { return "", fmt.Errorf("create request: %w", err) } - client := &http.Client{} - res, err := client.Do(req) + res, err := contribHTTPClient.Do(req) if err != nil { return "", fmt.Errorf("fetch latest version: %w", err) } diff --git a/cmd/internal/migrations/v3/swagger_packages.go b/cmd/internal/migrations/v3/swagger_packages.go index e5a0c13..1adfb63 100644 --- a/cmd/internal/migrations/v3/swagger_packages.go +++ b/cmd/internal/migrations/v3/swagger_packages.go @@ -24,7 +24,7 @@ const ( contribSwaggerNew = "github.com/gofiber/contrib/v3/swaggo" fiberSwaggerOld = "github.com/gofiber/swagger" fiberSwaggerNew = "github.com/gofiber/contrib/v3/swaggerui" - goModVersionPattern = `v[\w.+-]+` + goModVersionPattern = `v[a-zA-Z0-9.+-]+` ) func MigrateSwaggerPackages(cmd *cobra.Command, cwd string, _, _ *semver.Version) error { @@ -135,13 +135,18 @@ func rewriteSwaggerImports(content string) (string, bool) { changed := false for _, imp := range f.Imports { path := strings.Trim(imp.Path.Value, "\"`") - newPath := "" + var ( + newPath string + wasMigrated bool + ) switch path { case contribSwaggerOld: newPath = contribSwaggerNew + wasMigrated = true case fiberSwaggerOld: newPath = fiberSwaggerNew + wasMigrated = true case contribSwaggerNew, fiberSwaggerNew: newPath = path default: @@ -153,7 +158,7 @@ func rewriteSwaggerImports(content string) (string, bool) { changed = true } - if imp.Name == nil || imp.Name.Name == "" { + if wasMigrated && (imp.Name == nil || imp.Name.Name == "") { imp.Name = ast.NewIdent("swagger") changed = true } diff --git a/cmd/internal/migrations/v3/swagger_packages_test.go b/cmd/internal/migrations/v3/swagger_packages_test.go index 48e754c..312dc1f 100644 --- a/cmd/internal/migrations/v3/swagger_packages_test.go +++ b/cmd/internal/migrations/v3/swagger_packages_test.go @@ -158,3 +158,48 @@ require ( assert.Empty(t, buf.String()) assert.Equal(t, 2, calls) } + +func Test_MigrateSwaggerPackages_PreservesExistingImports(t *testing.T) { + restore := v3.SetContribV3VersionFetcher(func(module string) (string, error) { + switch module { + case swaggoModule: + return "v3.4.0", nil + case swaggerUIModule: + return "v3.2.1", nil + default: + return "", fmt.Errorf("unexpected module %s", module) + } + }) + t.Cleanup(restore) + + dir := t.TempDir() + + file := writeTempFile(t, dir, `package main +import "github.com/gofiber/contrib/v3/swaggo" + +func main() { + _ = swaggo.Config{} +}`) + + modContent := `module example + +go 1.22 + +require github.com/gofiber/contrib/v3/swaggo v3.4.0 +` + require.NoError(t, os.WriteFile(filepath.Join(dir, "go.mod"), []byte(modContent), 0o600)) + + var buf bytes.Buffer + cmd := newCmd(&buf) + require.NoError(t, v3.MigrateSwaggerPackages(cmd, dir, nil, nil)) + + assert.Equal(t, `package main +import "github.com/gofiber/contrib/v3/swaggo" + +func main() { + _ = swaggo.Config{} +}`, readFile(t, file)) + + assert.Equal(t, modContent, readFile(t, filepath.Join(dir, "go.mod"))) + assert.Empty(t, buf.String()) +} diff --git a/go.mod b/go.mod index f748169..8aeb521 100644 --- a/go.mod +++ b/go.mod @@ -53,7 +53,7 @@ require ( github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect golang.org/x/crypto v0.42.0 // indirect golang.org/x/net v0.44.0 // indirect - golang.org/x/sync v0.17.0 // indirect + golang.org/x/sync v0.18.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 6905e10..6dcc88e 100644 --- a/go.sum +++ b/go.sum @@ -113,6 +113,8 @@ golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=