Skip to content

Use semver-aware update notices for newer pinned pre-releases#35588

Merged
pelikhan merged 2 commits into
mainfrom
copilot/fix-recommended-upgrade-issue
May 29, 2026
Merged

Use semver-aware update notices for newer pinned pre-releases#35588
pelikhan merged 2 commits into
mainfrom
copilot/fix-recommended-upgrade-issue

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 28, 2026

The CLI update notice was comparing versions lexicographically, which caused newer pinned pre-releases to be treated as outdated and prompted a downgrade recommendation (for example, 0.77.00.76.1). This change makes the background update notifier follow semver ordering instead.

  • Update-notice version comparison

    • Replace the string-based “current > latest” heuristic in pkg/cli/update_check.go with semver-aware comparison.
    • Keep existing handling for exact matches and v-prefix-only differences.
    • Retain the string fallback only for non-semver inputs.
  • Regression coverage

    • Add focused tests for:
      • newer stable vs older stable
      • newer pre-release vs older stable
      • older pre-release vs stable
      • equivalent versions with and without v prefix
  • Result

    • Newer pinned pre-releases no longer trigger an upgrade message pointing at an older stable release.
currentSV := ensureSemverPrefix(currentVersion)
latestSV := ensureSemverPrefix(latestVersion)

if semver.IsValid(currentSV) && semver.IsValid(latestSV) {
	return semver.Compare(currentSV, latestSV) >= 0
}

Co-authored-by: dsyme <7204669+dsyme@users.noreply.github.com>
Copilot AI changed the title [WIP] Fix recommended upgrade issues when pinned to pre-release version Use semver-aware update notices for newer pinned pre-releases May 28, 2026
Copilot finished work on behalf of dsyme May 28, 2026 22:31
Copilot AI requested a review from dsyme May 28, 2026 22:31
@pelikhan pelikhan marked this pull request as ready for review May 29, 2026 00:06
Copilot AI review requested due to automatic review settings May 29, 2026 00:06
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

PR Code Quality Reviewer completed the code quality review.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

No ADR enforcement needed: PR #35588 does not have the 'implementation' label and has only 75 new lines (≤100 threshold) in business logic directories.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes incorrect CLI update notifications by replacing lexicographic version ordering with semver-aware comparisons, ensuring newer pinned pre-releases don’t get flagged as “outdated” vs older stable releases.

Changes:

  • Switch update-notice “current vs latest” ordering logic to use golang.org/x/mod/semver when both versions are valid semver, with a string-compare fallback for non-semver inputs.
  • Preserve existing behavior for exact matches and v-prefix-only differences.
  • Add regression tests covering stable and pre-release ordering and v-prefix equivalence.
Show a summary per file
File Description
pkg/cli/update_check.go Introduces isCurrentVersionAtLeastLatest using semver comparison (with fallback) and wires it into the update notification skip logic.
pkg/cli/update_check_test.go Adds table-driven tests to validate semver ordering and v-prefix equivalence for the new helper.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 2/2 changed files
  • Comments generated: 1

Comment thread pkg/cli/update_check.go
Comment on lines +228 to +232
if semver.IsValid(currentSV) && semver.IsValid(latestSV) {
return semver.Compare(currentSV, latestSV) >= 0
}

return currentVersionNormalized > latestVersionNormalized
@github-actions github-actions Bot mentioned this pull request May 29, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 80/100 — Excellent

Analyzed 1 test function (6 table-driven cases): 1 design test, 0 implementation tests, 0 coding-guideline violations (1 minor style note).

📊 Metrics & Test Classification (1 test analyzed)
Metric Value
New/modified tests analyzed 1
✅ Design tests (behavioral contracts) 1 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 1 (100%)
Duplicate test clusters 0
Test inflation detected ⚠️ Yes (52 test lines / 23 production lines = 2.26:1)
🚨 Coding-guideline violations 0 (build tags ✅, no mocks ✅)

Test Classification Details

Test File Classification Issues Detected
TestIsCurrentVersionAtLeastLatest pkg/cli/update_check_test.go:391 ✅ Design Minor: assertion messages missing; slight inflation ratio

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 1 test — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests
⚠️ Flagged Tests — Style Notes (1 minor item)

i️ TestIsCurrentVersionAtLeastLatest (pkg/cli/update_check_test.go:391)

Classification: Design test ✅
Issue (style): The assertion assert.Equal(t, tt.want, isCurrentVersionAtLeastLatest(...)) is missing a descriptive message argument. The project guideline requires a context message on every assertion call (e.g., assert.Equal(t, tt.want, got, "case: %s", tt.name)).
What design invariant does this test enforce? Verifies the semver-aware comparison logic that determines whether the current version is at least as new as the latest published release — including prerelease vs. stable ordering, v-prefix normalization, and boundary equality.
What would break if deleted? Silent regressions in the update-notice logic — for example, showing an update prompt to users on a newer pre-release build or failing to show it when they are on an older pre-release.
Suggested improvement: Add tt.name as the assertion message: assert.Equal(t, tt.want, isCurrentVersionAtLeastLatest(tt.currentVersion, tt.latestVersion), tt.name)

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The single table-driven test covers a good set of behavioral scenarios including prerelease vs. stable boundary cases. Minor: add assertion messages per project guidelines, and the slight test-to-production line ratio (2.26:1) is expected here given the combinatorial nature of semver comparison cases.

📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · sonnet46 937.6K ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Test Quality Sentinel: 80/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). One table-driven design test covering 6 semver comparison scenarios including prerelease edge cases. Minor style note: add assertion messages per project guidelines.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills-Based Review 🧠

Applied /tdd and /diagnose — approving with minor suggestions on test coverage gaps.

📋 Key Themes & Highlights

Key Themes

  • Missing edge case test: v0.77.0-beta.1 vs v0.77.0 (pre-release of same version vs its stable release) — the most subtle semver boundary, not yet covered.
  • Fallback path untested: the lexicographic fallback for non-semver inputs (e.g. dev, nightly) has no test cases.
  • Assertion messages absent: project convention requires descriptive messages on assert.* calls.
  • Redundant early-returns in helper: the function duplicates equality checks already performed by the caller — harmless, but worth a doc comment.

Positive Highlights

  • ✅ Correct use of golang.org/x/mod/semver — the standard Go module for semver, already in the module graph.
  • ✅ Clean extraction of isCurrentVersionAtLeastLatest makes the logic independently testable.
  • ✅ Good regression test table covering the previously broken older-prerelease-vs-stable boundary.
  • ✅ Graceful fallback to string comparison for non-semver inputs preserves existing behaviour.

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1M


for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isCurrentVersionAtLeastLatest(tt.currentVersion, tt.latestVersion))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Missing edge case: v0.77.0-beta.1 vs v0.77.0 — a pre-release of a version should still prompt the user to upgrade to the stable release of the same version.

💡 Suggested test case
{
    name:           "prerelease of same version as latest stable should prompt upgrade",
    currentVersion: "v0.77.0-beta.1",
    latestVersion:  "v0.77.0",
    want:           false, // pre-release < stable of same version
},

Per semver, v0.77.0-beta.1 < v0.77.0, so a user on a beta of the same version should receive an update notice pointing at the stable release. Without this test, the boundary between "older pre-release of same version" and "newer pre-release of older version" is not covered.


for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, isCurrentVersionAtLeastLatest(tt.currentVersion, tt.latestVersion))
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No test for the non-semver fallback path. The string-comparison fallback (currentVersionNormalized > latestVersionNormalized) is reachable when one or both versions are non-semver (e.g. dev, main, nightly).

💡 Suggested test cases
{
    name:           "non-semver current is lexicographically greater",
    currentVersion: "nightly",
    latestVersion:  "v0.76.1",
    want:           false, // "nightly" < "v0.76.1" lexicographically after TrimPrefix
},
{
    name:           "non-semver both versions equal",
    currentVersion: "dev",
    latestVersion:  "dev",
    want:           true,
},

Document in the test (or a comment) that the fallback intentionally uses lexicographic ordering so future maintainers don't remove it thinking it's dead code.

latestVersion: "v0.76.1",
want: false,
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] Assertion messages are missing — project convention (AGENTS.md) requires descriptive messages on every assert.* call so failures are self-explanatory without re-reading the test table.

💡 Quick fix
assert.Equal(t, tt.want, isCurrentVersionAtLeastLatest(tt.currentVersion, tt.latestVersion),
    "isCurrentVersionAtLeastLatest(%q, %q)", tt.currentVersion, tt.latestVersion)

This makes failure output read: isCurrentVersionAtLeastLatest("v0.77.0-beta.1", "v0.76.1"): expected true, got false instead of just expected true, got false.

Comment thread pkg/cli/update_check.go
fmt.Fprintln(os.Stderr, "")
}

func isCurrentVersionAtLeastLatest(currentVersion, latestVersion string) bool {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/diagnose] The early-return equality checks inside isCurrentVersionAtLeastLatest (lines 214–224) are unreachable when called from checkForUpdates, which already performs those exact checks before calling this helper. This is harmless for correctness but can create confusion about whether the function is intended to be a general-purpose utility or a narrow helper.

💡 Options

Option A — keep as-is and add a doc comment clarifying the function is self-contained by design (preferred if you want to keep it easily testable and reusable):

// isCurrentVersionAtLeastLatest reports whether currentVersion is >= latestVersion
// using semver comparison. It is self-contained and can be called independently
// of the equality checks in checkForUpdates.
func isCurrentVersionAtLeastLatest(...) bool {

Option B — remove the redundant checks if this is intended purely as an internal helper, and rely on the caller to handle equality before calling.

Option A is the safer choice since the function is independently tested.

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 1.1M

Comment thread pkg/cli/update_check.go
@@ -215,6 +211,27 @@ func checkForUpdates(noCheckUpdate bool, verbose bool) {
fmt.Fprintln(os.Stderr, "")
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant equality guards: the two early-return checks inside isCurrentVersionAtLeastLatest (exact-match and v-prefix-only match) duplicate conditions that checkForUpdates already exits on before reaching this call.

💡 Detail

By the time checkForUpdates calls isCurrentVersionAtLeastLatest, it has already returned for both the latestVersion == currentVersion case (line ~185) and the currentVersionNormalized == latestVersionNormalized case (line ~193). So the helper's first two branches are permanently dead code in production.

This isn't a logic bug today, but it makes the helper look like a general-purpose utility (handling all cases including equality) while actually being tightly coupled to a specific call-site contract. Future maintainers who change the early-exit ordering in checkForUpdates may not realise the helper silently duplicates those checks, leading to subtle divergence.

Since the equality cases are already handled upstream, the helper could be simplified to just the semver-compare + string-fallback branches:

func isCurrentVersionNewer(currentVersion, latestVersion string) bool {
    currentSV := ensureSemverPrefix(currentVersion)
    latestSV := ensureSemverPrefix(latestVersion)
    if semver.IsValid(currentSV) && semver.IsValid(latestSV) {
        return semver.Compare(currentSV, latestSV) > 0
    }
    currentNorm := strings.TrimPrefix(currentVersion, "v")
    latestNorm := strings.TrimPrefix(latestVersion, "v")
    return currentNorm > latestNorm
}

Renaming from isCurrentVersionAtLeastLatestisCurrentVersionNewer (using > 0 instead of >= 0) would also make the intent at the call site clearer and eliminate the equality ambiguity.

@pelikhan pelikhan merged commit 841e9e1 into main May 29, 2026
81 of 91 checks passed
@pelikhan pelikhan deleted the copilot/fix-recommended-upgrade-issue branch May 29, 2026 00:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Previous version is recommended as upgrade when pinned to more recent pre-release version

4 participants