Compare commits

..

3 Commits

Author SHA1 Message Date
github-actions[bot] 780b3674c7 chore: bump version to v1.2 2025-12-05 12:38:45 +00:00
Andras Bacsai 77adbfaebc Merge pull request #46 from coollabsio/update-check-every-cmd
Check for CLI updates on every command
2025-12-05 13:35:52 +01:00
Andras Bacsai 9215fd537e feat: check for CLI updates on every command
Remove the 10-minute check interval so update notifications appear on every command execution. Silent error handling prevents network issues from interrupting commands. Updated message format is more concise and shows the available version.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-05 13:35:05 +01:00
3 changed files with 305 additions and 64 deletions
+2 -21
View File
@@ -6,7 +6,6 @@ import (
"log"
"os"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/cobra"
"github.com/spf13/viper"
@@ -144,24 +143,6 @@ func initConfig() {
// They are loaded on-demand by getAPIClient() based on --instance or default instance
// This allows --instance flag to work correctly
// Check for updates
latestVersionStr, err := version.CheckLatestVersionOfCli(Debug)
if err != nil {
if Debug {
log.Println("Failed to check for updates:", err)
}
}
// Compare versions properly using semantic versioning
if latestVersionStr != "" {
latestVersion, err := compareVersion.NewVersion(latestVersionStr)
if err == nil {
currentVersion, err := compareVersion.NewVersion(version.GetVersion())
if err == nil && latestVersion.GreaterThan(currentVersion) {
if Debug {
log.Printf("New version of Coolify CLI is available: %s\n", latestVersionStr)
}
}
}
}
// Check for updates (errors are handled silently inside the function)
_, _ = version.CheckLatestVersionOfCli(Debug)
}
+41 -43
View File
@@ -5,92 +5,90 @@ import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"sort"
"time"
compareVersion "github.com/hashicorp/go-version"
"github.com/spf13/viper"
)
// Version variables injected by GoReleaser at build time via ldflags
var (
version = "v1.1"
version = "v1.2"
)
// GitHubAPIURL is the URL for fetching CLI version tags (exported for testing)
var GitHubAPIURL = "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
func GetVersion() string {
return version
}
// CheckInterval for version checking
const CheckInterval = 10 * time.Minute
// Tag represents a git tag for version checking
type Tag struct {
Ref string `json:"ref"`
}
// CheckLatestVersionOfCli checks for CLI updates
func CheckLatestVersionOfCli(debug bool) (string, error) {
lastCheck := viper.GetString("lastupdatechecktime")
if lastCheck != "" {
lastCheckTime, err := time.Parse(time.RFC3339, lastCheck)
if err == nil && lastCheckTime.Add(CheckInterval).After(time.Now()) {
if debug {
log.Println("Skipping update check. Last check was less than 10 minutes ago.")
}
return GetVersion(), nil
}
}
// CheckLatestVersionOfCli checks for CLI updates on every command.
// Errors are handled silently - the function returns without printing anything
// if the GitHub API call fails.
func CheckLatestVersionOfCli(_ bool) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Update check time
viper.Set("lastupdatechecktime", time.Now().Format(time.RFC3339))
if err := viper.WriteConfig(); err != nil {
log.Printf("Failed to write config: %v\n", err)
}
url := "https://api.github.com/repos/coollabsio/coolify-cli/git/refs/tags"
ctx := context.Background()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
req, err := http.NewRequestWithContext(ctx, "GET", GitHubAPIURL, nil)
if err != nil {
return "", err
return "", nil // Silent fail
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
return "", nil // Silent fail
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
if resp.StatusCode != 200 {
return "", nil // Silent fail
}
if resp.StatusCode != 200 {
return "", fmt.Errorf("%d - Failed to fetch data from %s. Error: %s", resp.StatusCode, url, string(body))
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil // Silent fail
}
var tags []Tag
if err := json.Unmarshal(body, &tags); err != nil {
return "", err
return "", nil // Silent fail
}
if len(tags) == 0 {
return "", nil // Silent fail
}
versionsRaw := make([]string, 0, len(tags))
for _, tag := range tags {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
if len(tag.Ref) > 10 {
versionStr := tag.Ref[10:]
versionsRaw = append(versionsRaw, versionStr)
}
}
versions := make([]*compareVersion.Version, len(versionsRaw))
for i, raw := range versionsRaw {
if len(versionsRaw) == 0 {
return "", nil // Silent fail
}
versions := make([]*compareVersion.Version, 0, len(versionsRaw))
for _, raw := range versionsRaw {
v, err := compareVersion.NewVersion(raw)
if err != nil {
return "", err
continue // Skip invalid versions
}
versions[i] = v
versions = append(versions, v)
}
if len(versions) == 0 {
return "", nil // Silent fail
}
sort.Sort(compareVersion.Collection(versions))
@@ -99,11 +97,11 @@ func CheckLatestVersionOfCli(debug bool) (string, error) {
// Compare versions properly using semantic versioning
currentVersion, err := compareVersion.NewVersion(GetVersion())
if err != nil {
return latestVersion.String(), err
return "", nil // Silent fail
}
if latestVersion.GreaterThan(currentVersion) {
fmt.Printf("There is a new version of Coolify CLI available.\nPlease update with 'coolify update'.\n\n")
fmt.Printf("A new version (%s) is available. Update with: coolify update\n", latestVersion.String())
}
return latestVersion.String(), nil
}
+262
View File
@@ -0,0 +1,262 @@
package version
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"os"
"testing"
)
func TestGetVersion(t *testing.T) {
v := GetVersion()
if v == "" {
t.Error("GetVersion() returned empty string")
}
// Version should start with 'v'
if v[0] != 'v' {
t.Errorf("GetVersion() = %q, expected to start with 'v'", v)
}
}
func TestCheckLatestVersionOfCli_UpdateAvailable(t *testing.T) {
// Save original values
originalURL := GitHubAPIURL
originalVersion := version
defer func() {
GitHubAPIURL = originalURL
version = originalVersion
}()
// Set a low version to ensure update is available
version = "v0.0.1"
// Create mock server with newer version
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
// Return tags in GitHub API format
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout to check for update message
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
}
if latestVersion != "2.0.0" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
}
// Should print update message
expectedMsg := "A new version (2.0.0) is available. Update with: coolify update\n"
if output != expectedMsg {
t.Errorf("CheckLatestVersionOfCli() output = %q, want %q", output, expectedMsg)
}
}
func TestCheckLatestVersionOfCli_NoUpdate(t *testing.T) {
// Save original values
originalURL := GitHubAPIURL
originalVersion := version
defer func() {
GitHubAPIURL = originalURL
version = originalVersion
}()
// Set a high version to ensure no update is available
version = "v99.99.99"
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[{"ref":"refs/tags/v1.0.0"},{"ref":"refs/tags/v2.0.0"}]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil", err)
}
// Function returns the latest version from GitHub (2.0.0), not the current version
if latestVersion != "2.0.0" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want %q", latestVersion, "2.0.0")
}
// Should NOT print any message when already on latest (current v99.99.99 > latest v2.0.0)
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything when on latest version, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_APIError_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns error
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusInternalServerError)
_, _ = w.Write([]byte(`{"error": "internal server error"}`))
}))
defer server.Close()
GitHubAPIURL = server.URL
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on API error", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on API error", latestVersion)
}
// Should NOT print anything on error
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything on API error, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_NetworkError_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Use invalid URL to cause network error
GitHubAPIURL = "http://localhost:1" // Port 1 should fail to connect
// Capture stdout
oldStdout := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
latestVersion, err := CheckLatestVersionOfCli(false)
_ = w.Close()
os.Stdout = oldStdout
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
output := buf.String()
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on network error", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on network error", latestVersion)
}
// Should NOT print anything on error
if output != "" {
t.Errorf("CheckLatestVersionOfCli() should not print anything on network error, got: %q", output)
}
}
func TestCheckLatestVersionOfCli_InvalidJSON_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns invalid JSON
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`not valid json`))
}))
defer server.Close()
GitHubAPIURL = server.URL
latestVersion, err := CheckLatestVersionOfCli(false)
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on invalid JSON", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on invalid JSON", latestVersion)
}
}
func TestCheckLatestVersionOfCli_EmptyTags_SilentFail(t *testing.T) {
// Save original URL
originalURL := GitHubAPIURL
defer func() {
GitHubAPIURL = originalURL
}()
// Create mock server that returns empty array
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`[]`))
}))
defer server.Close()
GitHubAPIURL = server.URL
latestVersion, err := CheckLatestVersionOfCli(false)
// Should return empty string and nil error (silent fail)
if err != nil {
t.Errorf("CheckLatestVersionOfCli() error = %v, want nil on empty tags", err)
}
if latestVersion != "" {
t.Errorf("CheckLatestVersionOfCli() latestVersion = %q, want empty string on empty tags", latestVersion)
}
}