fix(binarymgr): sort packages by semver client-side

Summary

The GitLab packages API sorts order_by=version lexicographically, so 8.99.0 ranks above 8.100.0. fetchLatestPackage trusted the API's sort and returned packages[0], which made glab duo cli --update install 8.99.0 even though 8.100.0 was the latest release in the registry.

Reproducer (against the live registry):

$ glab api '/projects/46519181/packages?package_type=generic&package_name=duo-cli&order_by=version&sort=desc&per_page=1' \
    | jq -r '.[].version'
8.99.0

8.100.0 is in the registry — it's just buried by the lex sort.

Fix

Switch fetchLatestPackage to order_by=created_at desc and pick the maximum semver client-side via hashicorp/go-version (already imported and used in CheckForUpdate a few lines up). Non-parseable version strings are skipped — the generic packages API permits any string.

With created_at desc the highest-semver release is always within the first page in practice (duo-cli releases at roughly weekly cadence; per_page=100 is ~2 years of headroom). The client-side scan is still required even with created_at desc — a hotfix on an older minor (e.g. 8.99.1 after 8.100.0) would otherwise mask the true latest.

Scope

binarymgr is shared infrastructure — this fix benefits every glab-managed binary whose minor version crosses 99 → 100 (Duo CLI today; future binaries automatically). No behavior change for binaries where lex and semver order happen to agree.

Deliberately not changed:

  • MaxCompatibleMajor gating: still errors out when the highest semver is past the major cap, same as today — just with the correct highest now.
  • Caching / lastCheckTime: untouched.
  • Pagination: single page of 100 is sufficient; not adding pagination for a scenario that doesn't realistically exist.

Why not fix this upstream

The upstream feature for semver ordering on generic packages is gitlab-org/gitlab#372033 — opened 2022-08-28, still workflow::ready for design, no timeline. Even if/when it lands, self-managed instances on older GitLab versions wouldn't have it. Client-side is the right place regardless.

Test plan

New test TestManager_fetchLatestPackage (5 subtests) covers:

  • Picks highest semver when lex and semver disagree (8.99.0 vs 8.100.0)
  • Picks highest semver when the most recent upload is a hotfix on an older minor (8.99.1 after 8.100.0)
  • Skips unparseable version strings (latest, not-a-version)
  • Errors when registry is empty
  • Errors when no version parses

The existing mock-based tests at manager_test.go:194,215 only handed back whatever the test wanted — they didn't exercise the real sort, which is why this slipped through. The new lex-vs-semver test locks the behavior in.

Verified locally:

  • go build ./...
  • go vet ./...
  • golangci-lint run ./internal/binarymgr/... — 0 issues
  • go test ./internal/binarymgr/... — all passing including the 5 new cases
  • Pre-push hooks (check-generated, build, go-test 147 tests, go-lint) all green

Closes gitlab-org/editor-extensions/gitlab-lsp#2486 (closed)

Merge request reports

Loading