fix: fiber migrate --to v3 resolves to latest matching release instead of 3.0.0#276
fix: fiber migrate --to v3 resolves to latest matching release instead of 3.0.0#276
fiber migrate --to v3 resolves to latest matching release instead of 3.0.0#276Conversation
Co-authored-by: ReneWerner87 <7063188+ReneWerner87@users.noreply.github.com>
fiber migrate --to v3 resolves to latest 3.x instead of 3.0.0
|
@copilot Your solution focuses on the major number. |
…traints Co-authored-by: ReneWerner87 <7063188+ReneWerner87@users.noreply.github.com>
Good point. Updated in adab364 to use semver constraints for the full range of partial/wildcard inputs:
The core piece is a Warning Firewall rules blocked me from connecting to one or more addresses (expand for details)I tried to connect to the following addresses, but was blocked by firewall rules:
If you need me to access, download, or install something from one of these locations, you can either:
|
fiber migrate --to v3 resolves to latest 3.x instead of 3.0.0fiber migrate --to v3 resolves to latest matching release instead of 3.0.0
|
Cool thx |
There was a problem hiding this comment.
Pull request overview
This PR fixes fiber migrate --to handling for partial/wildcard versions (e.g. v3, 3.1, 3.*) so that migrations target the latest stable matching release rather than being coerced to the lowest patch (e.g. 3.0.0).
Changes:
- Add semver constraint-based resolution for partial/wildcard
--toinputs and resolve the latest matching non-prerelease Fiber release via the GitHub releases list API. - Introduce
partialVersionConstraintto convert partial inputs into semver constraint ranges. - Expand migrate tests to cover multiple short/wildcard version forms and add direct tests for
partialVersionConstraint.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| cmd/version.go | Adds LatestFiberVersionForConstraint to scan GitHub releases and return the latest tag matching a semver constraint. |
| cmd/migrate.go | Adds partialVersionConstraint and wires partial version resolution into migrateRunE before semver parsing. |
| cmd/migrate_test.go | Extends migrate short-version coverage via table tests and adds unit tests for partialVersionConstraint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| url := "https://api.github.com/repos/gofiber/fiber/releases?per_page=100" | ||
| b, status, err := cachedGET(ctx, url, nil) | ||
| if err != nil { | ||
| return "", fmt.Errorf("http request failed: %w", err) | ||
| } | ||
| if status != http.StatusOK { | ||
| msg := strings.TrimSpace(string(b)) | ||
| if msg == "" { | ||
| msg = http.StatusText(status) | ||
| } | ||
| return "", fmt.Errorf("http request failed: %s", msg) | ||
| } | ||
|
|
||
| var releases []struct { | ||
| TagName string `json:"tag_name"` | ||
| Prerelease bool `json:"prerelease"` | ||
| Draft bool `json:"draft"` | ||
| } | ||
| if err := json.Unmarshal(b, &releases); err != nil { | ||
| return "", fmt.Errorf("decode response: %w", err) | ||
| } | ||
|
|
||
| for _, r := range releases { | ||
| if r.Draft || r.Prerelease { | ||
| continue | ||
| } | ||
| v, parseErr := semver.NewVersion(r.TagName) | ||
| if parseErr != nil { | ||
| continue | ||
| } | ||
| if constraint.Check(v) { | ||
| return strings.TrimPrefix(r.TagName, "v"), nil | ||
| } |
There was a problem hiding this comment.
LatestFiberVersionForConstraint only fetches the first page of releases (per_page=100) and does not follow pagination. For constraints that match older releases (e.g., targeting older majors/minors), this can incorrectly return "no matching release found" once the matching releases fall beyond the first 100 results. Consider iterating pages (using page= and/or Link headers) until a match is found or results are exhausted.
| url := "https://api.github.com/repos/gofiber/fiber/releases?per_page=100" | |
| b, status, err := cachedGET(ctx, url, nil) | |
| if err != nil { | |
| return "", fmt.Errorf("http request failed: %w", err) | |
| } | |
| if status != http.StatusOK { | |
| msg := strings.TrimSpace(string(b)) | |
| if msg == "" { | |
| msg = http.StatusText(status) | |
| } | |
| return "", fmt.Errorf("http request failed: %s", msg) | |
| } | |
| var releases []struct { | |
| TagName string `json:"tag_name"` | |
| Prerelease bool `json:"prerelease"` | |
| Draft bool `json:"draft"` | |
| } | |
| if err := json.Unmarshal(b, &releases); err != nil { | |
| return "", fmt.Errorf("decode response: %w", err) | |
| } | |
| for _, r := range releases { | |
| if r.Draft || r.Prerelease { | |
| continue | |
| } | |
| v, parseErr := semver.NewVersion(r.TagName) | |
| if parseErr != nil { | |
| continue | |
| } | |
| if constraint.Check(v) { | |
| return strings.TrimPrefix(r.TagName, "v"), nil | |
| } | |
| type release struct { | |
| TagName string `json:"tag_name"` | |
| Prerelease bool `json:"prerelease"` | |
| Draft bool `json:"draft"` | |
| } | |
| for page := 1; ; page++ { | |
| url := fmt.Sprintf("https://api.github.com/repos/gofiber/fiber/releases?per_page=100&page=%d", page) | |
| b, status, err := cachedGET(ctx, url, nil) | |
| if err != nil { | |
| return "", fmt.Errorf("http request failed: %w", err) | |
| } | |
| if status != http.StatusOK { | |
| msg := strings.TrimSpace(string(b)) | |
| if msg == "" { | |
| msg = http.StatusText(status) | |
| } | |
| return "", fmt.Errorf("http request failed: %s", msg) | |
| } | |
| var releases []release | |
| if err := json.Unmarshal(b, &releases); err != nil { | |
| return "", fmt.Errorf("decode response: %w", err) | |
| } | |
| // No more releases; we've exhausted all pages. | |
| if len(releases) == 0 { | |
| break | |
| } | |
| for _, r := range releases { | |
| if r.Draft || r.Prerelease { | |
| continue | |
| } | |
| v, parseErr := semver.NewVersion(r.TagName) | |
| if parseErr != nil { | |
| continue | |
| } | |
| if constraint.Check(v) { | |
| return strings.TrimPrefix(r.TagName, "v"), nil | |
| } | |
| } |
| // latest matching release from GitHub. | ||
| if constraint, parseErr := partialVersionConstraint(opts.TargetVersionS); parseErr == nil { | ||
| resolved, resolveErr := LatestFiberVersionForConstraint(constraint) | ||
| if resolveErr != nil { | ||
| return fmt.Errorf("failed to resolve version %q: %w", opts.TargetVersionS, resolveErr) | ||
| } | ||
| opts.TargetVersionS = resolved |
There was a problem hiding this comment.
Partial/wildcard resolution happens before the --hash pseudo-version logic. If a user combines --to 3 (or 3.1, 3.*, etc.) with --hash, the base version used to construct the pseudo-version will be the resolved latest release, which can produce an incorrect pseudo-version (it should generally be based on the version the user specified, or resolution should be skipped when --hash is provided). Consider bypassing constraint resolution when opts.TargetHash is set, or applying resolution only when no hash is requested.
| // latest matching release from GitHub. | |
| if constraint, parseErr := partialVersionConstraint(opts.TargetVersionS); parseErr == nil { | |
| resolved, resolveErr := LatestFiberVersionForConstraint(constraint) | |
| if resolveErr != nil { | |
| return fmt.Errorf("failed to resolve version %q: %w", opts.TargetVersionS, resolveErr) | |
| } | |
| opts.TargetVersionS = resolved | |
| // latest matching release from GitHub. When a target hash is provided, skip | |
| // this resolution so that the pseudo-version base is derived directly from | |
| // the user-specified version. | |
| if opts.TargetHash == "" { | |
| if constraint, parseErr := partialVersionConstraint(opts.TargetVersionS); parseErr == nil { | |
| resolved, resolveErr := LatestFiberVersionForConstraint(constraint) | |
| if resolveErr != nil { | |
| return fmt.Errorf("failed to resolve version %q: %w", opts.TargetVersionS, resolveErr) | |
| } | |
| opts.TargetVersionS = resolved | |
| } |
When passing a partial or wildcard version like
--to v3,--to 3.1, or--to 3.*, the semver library coerces the input to its lowest patch (e.g."3"→3.0.0), so migrations always targeted the first release rather than the latest stable release matching the user's intent.Explain the details for making this change. What existing problem does the pull request solve?
The fix introduces a
partialVersionConstrainthelper that detects partial/wildcard version strings and converts them into semver constraint ranges. These constraints are then used to query the GitHub releases list API and resolve the latest matching non-prerelease release. Full version strings (e.g.3.0.0,3.0.0-beta.1) continue to be used as-is.Changes
cmd/version.go: ReplacedLatestFiberVersionForMajor(major uint64)withLatestFiberVersionForConstraint(constraint *semver.Constraints)— queries the GitHub releases list API (sorted newest-first), evaluates each non-draft non-prerelease release against the semver constraint, and returns the first matchcmd/migrate.go: AddedpartialVersionConstraint(v string) (*semver.Constraints, error)helper that normalizes trailing wildcard suffixes (.*,.x,.*.*,.x.x), validates all remaining parts are numeric, and builds the appropriate constraint range.migrateRunEcalls this before semver parsing and resolves via API if a constraint is returnedcmd/migrate_test.go: ExpandedTest_Migrate_TargetVersionShortinto a 9-case table test; addedTest_PartialVersionConstraintwith 11 subtests for the helper directlySupported partial/wildcard formats
v3,3,3.*,3.x,3.*.*,3.x.x3.x.x3.1,3.1.*,3.1.x3.1.x3.0.0,3.0.0-beta.1Example
Behavior without
--to(callsreleases/latest) is unchanged.Original prompt
fiber migrate --to v3resolves to3.0.0instead of latest3.x#275✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.