Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 73 additions & 31 deletions pkg/cli/update_extension_check.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ import (
"os"
"strings"

"github.com/cli/go-gh/v2/pkg/api"
"github.com/github/gh-aw/pkg/console"
"github.com/github/gh-aw/pkg/logger"
"github.com/github/gh-aw/pkg/workflow"
)

var updateExtensionCheckLog = logger.New("cli:update_extension_check")

// checkExtensionUpdate checks if a newer version of gh-aw is available
func checkExtensionUpdate(verbose bool) error {
if verbose {
Expand Down Expand Up @@ -61,56 +65,94 @@ func isAuthenticationError(output string) bool {
strings.Contains(lowerOutput, "invalid token")
}

// ensureLatestExtensionVersion checks if a newer version of gh-aw is available
// and returns an error if an update is needed. This is used by the upgrade command
// to ensure users are on the latest version before upgrading workflows.
// ensureLatestExtensionVersion checks if the current release matches the latest release
// and issues a warning if an update is needed. This function fails silently if the
// release URL is not available or blocked.
func ensureLatestExtensionVersion(verbose bool) error {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage("Checking for gh-aw extension updates..."))
}

// Run gh extension upgrade --dry-run to check for updates
output, err := workflow.RunGHCombined("Checking for extension updates...", "extension", "upgrade", "github/gh-aw", "--dry-run")
outputStr := strings.TrimSpace(string(output))
// Get current version
currentVersion := GetVersion()
updateExtensionCheckLog.Printf("Current version: %s", currentVersion)

// Check for authentication errors (missing or invalid token)
if err != nil || isAuthenticationError(outputStr) {
// Skip check for non-release versions (dev builds)
if !workflow.IsReleasedVersion(currentVersion) {
updateExtensionCheckLog.Print("Not a released version, skipping update check")
if verbose {
if err != nil {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Failed to check for extension updates: %v", err)))
} else {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage("Authentication required to check for updates"))
}
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Skipping version check (development build)"))
}
// If we can't check for updates due to auth issues, allow the upgrade to proceed
return nil
}

if verbose {
fmt.Fprintln(os.Stderr, console.FormatVerboseMessage(fmt.Sprintf("Extension update check output: %s", outputStr)))
// Query GitHub API for latest release
latestVersion, err := getLatestReleaseVersion(verbose)
if err != nil {
// Fail silently - don't block upgrade if we can't check for updates
updateExtensionCheckLog.Printf("Failed to check for updates (silently ignoring): %v", err)
if verbose {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("Could not check for updates: %v", err)))
}
return nil
}

// Parse the output to see if an update is available
// Expected format: "[agentics]: would have upgraded from v0.14.0 to v0.18.1"
lines := strings.Split(outputStr, "\n")
for _, line := range lines {
if strings.Contains(line, "[agentics]: would have upgraded from") {
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatErrorMessage("gh-aw extension is not on the latest version"))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(line))
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Please upgrade the gh extension first:"))
fmt.Fprintln(os.Stderr, console.FormatCommandMessage(" gh extension upgrade github/gh-aw"))
fmt.Fprintln(os.Stderr, "")
return fmt.Errorf("gh-aw extension must be upgraded before running this command")
}
if latestVersion == "" {
updateExtensionCheckLog.Print("Could not determine latest version")
return nil
}

if strings.Contains(outputStr, "✓ Successfully checked extension upgrades") {
updateExtensionCheckLog.Printf("Latest version: %s", latestVersion)

// Normalize versions for comparison (remove 'v' prefix)
currentVersionNormalized := strings.TrimPrefix(currentVersion, "v")
latestVersionNormalized := strings.TrimPrefix(latestVersion, "v")

// Compare versions
if currentVersionNormalized == latestVersionNormalized {
if verbose {
fmt.Fprintln(os.Stderr, console.FormatSuccessMessage("✓ gh-aw extension is up to date"))
}
updateExtensionCheckLog.Print("Extension is up to date")
return nil
}

// Check if we're on a newer version (development/prerelease)
if currentVersionNormalized > latestVersionNormalized {
updateExtensionCheckLog.Printf("Current version (%s) appears newer than latest release (%s)", currentVersion, latestVersion)
if verbose {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Running a development or pre-release version"))
}
return nil
}

// A newer version is available - display warning message (not error)
updateExtensionCheckLog.Printf("Newer version available: %s (current: %s)", latestVersion, currentVersion)
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(fmt.Sprintf("A newer version of gh-aw is available: %s (current: %s)", latestVersion, currentVersion)))
fmt.Fprintln(os.Stderr, console.FormatInfoMessage("Consider upgrading with: gh extension upgrade github/gh-aw"))
fmt.Fprintln(os.Stderr, "")

return nil
}

// getLatestReleaseVersion queries GitHub API for the latest release version of gh-aw
func getLatestReleaseVersion(verbose bool) (string, error) {
updateExtensionCheckLog.Print("Querying GitHub API for latest release...")

// Create GitHub REST client using go-gh
client, err := api.NewRESTClient(api.ClientOptions{})
if err != nil {
return "", fmt.Errorf("failed to create GitHub client: %w", err)
}

// Query the latest release
var release Release
err = client.Get("repos/github/gh-aw/releases/latest", &release)
if err != nil {
return "", fmt.Errorf("failed to query latest release: %w", err)
}

updateExtensionCheckLog.Printf("Latest release: %s", release.TagName)
return release.TagName, nil
}
30 changes: 30 additions & 0 deletions pkg/cli/update_extension_check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestIsAuthenticationError(t *testing.T) {
Expand Down Expand Up @@ -58,3 +59,32 @@ func TestIsAuthenticationError(t *testing.T) {
})
}
}

func TestEnsureLatestExtensionVersion_DevBuild(t *testing.T) {
// Save original version and restore after test
originalVersion := GetVersion()
defer SetVersionInfo(originalVersion)

// Set a dev version
SetVersionInfo("dev")

// Should return nil without error for dev builds
err := ensureLatestExtensionVersion(false)
require.NoError(t, err, "Should not return error for dev builds")
}

func TestEnsureLatestExtensionVersion_SilentFailure(t *testing.T) {
// This test verifies that network/API errors are handled silently
// The actual API call will fail in the test environment but should not return an error

// Save original version and restore after test
originalVersion := GetVersion()
defer SetVersionInfo(originalVersion)

// Set a valid release version
SetVersionInfo("v0.1.0")

// Should return nil even if API call fails (fails silently)
err := ensureLatestExtensionVersion(false)
require.NoError(t, err, "Should fail silently on API errors")
}
Loading