feat(duo): lock Duo CLI auto-updates to compatible major version
## Summary
Currently, the Duo CLI auto-update mechanism in `glab` will update across major version boundaries without any compatibility checks. Since major version bumps in Duo CLI can introduce breaking changes that require corresponding `glab` updates, we need a mechanism to prevent older `glab` versions from auto-updating to an incompatible Duo CLI major version.
## Problem
The update logic in `internal/commands/duo/cli/cliutils/binary_manager.go` fetches the single latest package from the GitLab Package Registry and installs it if it is newer than the installed version. There are no constraints on major version boundaries. If Duo CLI ships v9 with breaking changes, users on older `glab` will auto-update and break.
## Proposed Solution
Add a **maximum compatible major version constant** to `binary_manager.go` that acts as a ceiling on auto-updates. `glab` will only auto-update to the latest patch/minor within the approved major version. Crossing a major version boundary requires a deliberate code change in `glab` to validate and approve the new major.
### Implementation
**1. Add a compatibility constant**
```go
// duoMaxCompatibleMajorVersion is the maximum Duo CLI major version this build of glab
// supports. Auto-updates are bounded to this major version. Bumping this value requires
// validating that the new major version is compatible with glab.
const duoMaxCompatibleMajorVersion = 8
```
**2. Fetch more package candidates and filter by major version**
Currently the API call uses `per_page=1` (the absolute latest). Change it to fetch more candidates (e.g. `per_page=20`) and select the latest version within the approved major:
```go
func findLatestCompatible(packages []Package, maxMajor int) (*Package, error) {
var best *version.Version
var bestPkg *Package
for _, pkg := range packages {
v, err := version.NewVersion(pkg.Version)
if err != nil {
continue
}
if v.Segments()[0] != maxMajor {
continue
}
if best == nil || v.GreaterThan(best) {
best = v
bestPkg = &pkg
}
}
if bestPkg == nil {
return nil, fmt.Errorf("no compatible Duo CLI version found for major version %d", maxMajor)
}
return bestPkg, nil
}
```
**3. Surface a clear message when a newer incompatible major is available**
When the absolute latest package exceeds `duoMaxCompatibleMajorVersion`, notify the user rather than silently ignoring it:
```
A new major version of Duo CLI (v9.0.0) is available but requires a newer version of glab.
Run 'glab check-update' or visit https://gitlab.com/gitlab-org/cli to upgrade.
```
This fires as a non-blocking notification during background update checks, and as an explicit message during `glab duo cli --update`.
**4. Optional power-user escape hatch**
For users who want to test a new major before `glab` officially supports it:
```sh
GLAB_DUO_CLI_ALLOW_MAJOR_UPGRADE=true glab duo cli --update
```
This should print a prominent warning before proceeding.
### Release Process
When Duo CLI ships a new major version:
1. Duo team releases the new major and notifies the `glab` team
2. `glab` team validates compatibility and updates `duoMaxCompatibleMajorVersion`
3. New `glab` version ships — users who update `glab` will receive the new Duo major on next update check
4. Users on older `glab` are protected and continue on the latest patch of the previous major
### Files to Change
| File | Change |
|---|---|
| `internal/commands/duo/cli/cliutils/binary_manager.go` | Add `duoMaxCompatibleMajorVersion` constant, update fetch/filter logic, add new-major notification |
| `internal/commands/duo/cli/cliutils/binary_manager_test.go` | Tests for version filtering and cross-major boundary messaging |
| `internal/commands/duo/cli/cli.go` | Wire new-major-available message into background check output |
## Notes
- Users on an older `glab` are **not broken** when a new Duo major ships — they continue receiving updates to the latest patch of the current approved major indefinitely
- This is intentionally a ceiling (maximum), not a floor (minimum). A minimum version (`>= 8`) would not protect against v9 breaking changes since `9 >= 8` would always pass
- A separate `duoMinVersion` constant could be added later if specific `glab` features require a minimum Duo CLI patch version, but that is a distinct concern from this issue
issue