Skip to content

Commit

Permalink
chore: improve version package
Browse files Browse the repository at this point in the history
ravisuhag committed Nov 1, 2024
1 parent 840834c commit f246388
Showing 4 changed files with 250 additions and 124 deletions.
6 changes: 3 additions & 3 deletions terminal/README.md
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ The `terminal` package provides a collection of utilities to manage terminal int
- **CI Detection**: Identify if the program is running in a Continuous Integration (CI) environment.
- **Homebrew Utilities**: Check for Homebrew installation and verify binary paths.
- **Browser Launching**: Open URLs in the default web browser, with cross-platform support.
-

## Installation

To include this package in your Go project, use:
@@ -95,7 +95,7 @@ if terminal.IsCI() {
Use `IsCI` to check if the program is running in a CI environment.

```go
if termutil.IsCI() {
if terminal.IsCI() {
fmt.Println("Running in a Continuous Integration environment.")
} else {
fmt.Println("Not running in a CI environment.")
@@ -107,7 +107,7 @@ if termutil.IsCI() {
Use `HasHomebrew` to check if Homebrew is installed on the system.

```go
if termuinal.HasHomebrew() {
if terminal.HasHomebrew() {
fmt.Println("Homebrew is installed!")
} else {
fmt.Println("Homebrew is not installed.")
100 changes: 100 additions & 0 deletions version/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Version

The `version` package provides utilities to fetch and compare software version information from GitHub releases. It helps check if a newer version is available and generates update notices.

## Features

- **Fetch Release Information**: Retrieve the latest release details from a GitHub repository.
- **Version Comparison**: Compare semantic versions to determine if an update is needed.
- **Update Notifications**: Generate user-friendly messages if a newer version is available.

## Installation

To include this package in your Go project, use:

```bash
go get github.com/raystack/salt/version
```

## Usage

### 1. Fetching Release Information

You can use the `ReleaseInfo` function to fetch the latest release details from a GitHub repository.

```go
package main

import (
"fmt"
"github.com/raystack/salt/version"
)

func main() {
releaseURL := "https://api.github.com/repos/raystack/optimus/releases/latest"
info, err := version.ReleaseInfo(releaseURL)
if err != nil {
fmt.Println("Error fetching release info:", err)
return
}
fmt.Printf("Latest Version: %s\nDownload URL: %s\n", info.Version, info.TarURL)
}
```

### 2. Comparing Versions

Use `IsCurrentLatest` to check if the current version is up-to-date with the latest release.

```go
currVersion := "1.2.3"
latestVersion := "1.2.4"
isLatest, err := version.IsCurrentLatest(currVersion, latestVersion)
if err != nil {
fmt.Println("Error comparing versions:", err)
} else if isLatest {
fmt.Println("You are using the latest version!")
} else {
fmt.Println("A newer version is available.")
}
```

### 3. Generating Update Notices

`UpdateNotice` generates a message prompting the user to update if a newer version is available.

```go
notice := version.UpdateNotice("1.0.0", "raystack/optimus")
if notice != "" {
fmt.Println(notice)
} else {
fmt.Println("You are up-to-date!")
}
```

## API Reference

### Functions

- `ReleaseInfo(releaseURL string) (*Info, error)`: Fetches the latest release information from the given GitHub API URL.
- `IsCurrentLatest(currVersion, latestVersion string) (bool, error)`: Compares the current version with the latest version using semantic versioning.
- `UpdateNotice(currentVersion, githubRepo string) string`: Returns an update notice if a newer version is available, or an empty string if up-to-date.

### Structs

- `type Info`: Contains details about a release.
- `Version`: The version string (e.g., "v1.2.3").
- `TarURL`: The tarball URL for downloading the release.

## Environment Variables

- The `User-Agent` header in HTTP requests is set to `raystack/salt` to comply with GitHub's API requirements.

## Error Handling

- Uses `github.com/pkg/errors` to wrap errors for better error context.
- Returns errors when HTTP requests fail, or when JSON parsing or version comparison fails.

## Dependencies

- `github.com/hashicorp/go-version`: For semantic version comparison.
- `github.com/pkg/errors`: For enhanced error wrapping.
89 changes: 55 additions & 34 deletions version/release.go
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ package version
import (
"encoding/json"
"fmt"
"io/ioutil"
"io"
"net/http"
"time"

@@ -12,88 +12,109 @@ import (
)

var (
// ReleaseInfoTimeout sets the HTTP client timeout for fetching release info.
ReleaseInfoTimeout = time.Second * 1
Release = "https://api.github.com/repos/%s/releases/latest"

// Release is the GitHub API URL template to fetch the latest release of a repository.
Release = "https://api.github.com/repos/%s/releases/latest"
)

// Info holds information about a software release.
type Info struct {
Version string
TarURL string
Version string // Version of the release
TarURL string // Tarball URL of the release
}

// ReleaseInfo fetches details related to provided release URL
// releaseURL should point to a specific version
// for example: https://api.github.com/repos/raystack/optimus/releases/latest
// ReleaseInfo fetches details related to the latest release from the provided URL.
//
// Parameters:
// - releaseURL: The URL to fetch the latest release information from.
// Example: "https://api.github.com/repos/raystack/optimus/releases/latest"
//
// Returns:
// - An *Info struct containing the version and tarball URL.
// - An error if the HTTP request or response parsing fails.
func ReleaseInfo(releaseURL string) (*Info, error) {
httpClient := http.Client{
Timeout: ReleaseInfoTimeout,
}
req, err := http.NewRequest(http.MethodGet, releaseURL, nil)
if err != nil {
return nil, errors.Wrapf(err, "failed to create request")
return nil, errors.Wrap(err, "failed to create request")
}
req.Header.Set("User-Agent", "raystack/salt")

resp, err := httpClient.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "failed to reach releaseURL: %s", releaseURL)
}
defer func() {
if resp.Body != nil {
resp.Body.Close()
}
}()
if resp.StatusCode != http.StatusOK {
return nil, errors.Wrapf(err, "failed to reach releaseURL: %s, returned: %d", releaseURL, resp.StatusCode)
}
if resp.Body != nil {
defer resp.Body.Close()
return nil, errors.Wrapf(err, "failed to reach releaseURL: %s, status code: %d", releaseURL, resp.StatusCode)
}

body, err := ioutil.ReadAll(resp.Body)
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "failed to read response body")
return nil, errors.Wrap(err, "failed to read response body")
}

var releaseBody struct {
var releaseData struct {
TagName string `json:"tag_name"`
Tarball string `json:"tarball_url"`
}
if err = json.Unmarshal(body, &releaseBody); err != nil {
return nil, errors.Wrapf(err, "failed to parse: %s", string(body))
if err = json.Unmarshal(body, &releaseData); err != nil {
return nil, errors.Wrapf(err, "failed to parse JSON response: %s", string(body))
}

return &Info{
Version: releaseBody.TagName,
TarURL: releaseBody.Tarball,
Version: releaseData.TagName,
TarURL: releaseData.Tarball,
}, nil
}

// IsCurrentLatest returns true if the current version string is greater than
// or equal to latestVersion as per semantic versioning
// IsCurrentLatest compares the current version with the latest version.
//
// Parameters:
// - currVersion: The current version string.
// - latestVersion: The latest version string.
//
// Returns:
// - true if the current version is greater than or equal to the latest version.
// - An error if version parsing fails.
func IsCurrentLatest(currVersion, latestVersion string) (bool, error) {
currentV, err := version.NewVersion(currVersion)
if err != nil {
return false, errors.Wrapf(err, "failed to parse current version")
return false, errors.Wrap(err, "failed to parse current version")
}
latestV, err := version.NewVersion(latestVersion)
if err != nil {
return false, errors.Wrapf(err, "failed to parse latest version")
}
if currentV.GreaterThanOrEqual(latestV) {
return true, nil
return false, errors.Wrap(err, "failed to parse latest version")
}
return false, nil
return currentV.GreaterThanOrEqual(latestV), nil
}

// UpdateNotice returns a notice message if there is a newer version available
// Note: all errors are ignored
// UpdateNotice generates a notice message if a newer version is available.
//
// Parameters:
// - currentVersion: The current version string.
// - githubRepo: The GitHub repository in the format "owner/repo".
//
// Returns:
// - A string message prompting the user to update if a newer version is available.
// - An empty string if there are no updates or if any errors occur.
func UpdateNotice(currentVersion, githubRepo string) string {
info, err := ReleaseInfo(fmt.Sprintf(Release, githubRepo))
if err != nil {
return ""
}
latestVersion := info.Version
isCurrentLatest, err := IsCurrentLatest(currentVersion, latestVersion)
if err != nil {
return ""
}
if isCurrentLatest {
if err != nil || isCurrentLatest {
return ""
}
return fmt.Sprintf("A new release (%s) is available, consider updating the client.", info.Version)
return fmt.Sprintf("A new release (%s) is available, consider updating the client.", latestVersion)
}
Loading

0 comments on commit f246388

Please sign in to comment.