mirror of
https://github.com/lukaszraczylo/semver-generator.git
synced 2026-06-05 22:49:25 +00:00
Move updater to REST api.
This commit is contained in:
+12
-13
@@ -26,27 +26,26 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
err error
|
||||
repo *Setup
|
||||
PKG_VERSION string
|
||||
)
|
||||
|
||||
// Setup represents the application setup
|
||||
type Setup struct {
|
||||
RepositoryName string
|
||||
RepositoryBranch string
|
||||
LocalConfigFile string
|
||||
Generate bool
|
||||
UseLocal bool
|
||||
GitRepo utils.GitRepository
|
||||
Config *utils.Config
|
||||
Semver utils.SemVer
|
||||
RepositoryName string
|
||||
RepositoryBranch string
|
||||
LocalConfigFile string
|
||||
Generate bool
|
||||
UseLocal bool
|
||||
GitRepo utils.GitRepository
|
||||
Config *utils.Config
|
||||
Semver utils.SemVer
|
||||
}
|
||||
|
||||
// Initialize the fuzzy search function in the utils package
|
||||
func init() {
|
||||
utils.InitLogger(false) // Will be updated in main based on debug flag
|
||||
|
||||
|
||||
// Set the fuzzy search function
|
||||
utils.FuzzyFind = fuzzy.FindNormalizedFold
|
||||
}
|
||||
@@ -72,12 +71,12 @@ func main() {
|
||||
if PKG_VERSION != latestRelease && latestReleaseOk {
|
||||
outdatedMsg = fmt.Sprintf("(Latest available: %s)", latestRelease)
|
||||
}
|
||||
|
||||
|
||||
utils.Info("semver-gen", map[string]interface{}{
|
||||
"version": PKG_VERSION,
|
||||
"version": PKG_VERSION,
|
||||
"outdated": outdatedMsg,
|
||||
})
|
||||
|
||||
|
||||
if outdatedMsg != "" {
|
||||
utils.Info("semver-gen", map[string]interface{}{
|
||||
"message": "You can update automatically with: semver-gen -u",
|
||||
|
||||
+383
-120
@@ -1,148 +1,411 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
|
||||
"github.com/lukaszraczylo/ask"
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
"github.com/melbahja/got"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// UpdatePackage updates the binary with the latest version
|
||||
func UpdatePackage() bool {
|
||||
ghToken, ghTokenSet := os.LookupEnv("GITHUB_TOKEN")
|
||||
if !ghTokenSet {
|
||||
Error("GITHUB_TOKEN not set", nil)
|
||||
return false
|
||||
}
|
||||
const (
|
||||
// GitHub API endpoint for latest release
|
||||
githubReleasesURL = "https://api.github.com/repos/lukaszraczylo/semver-generator/releases/latest"
|
||||
// Request timeout for HTTP requests
|
||||
requestTimeout = 10 * time.Second
|
||||
)
|
||||
|
||||
binaryName := fmt.Sprintf("semver-gen-%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||
Info("Checking for updates", map[string]interface{}{"binaryName": binaryName})
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
gql.SetEndpoint("https://api.github.com/graphql")
|
||||
gql.SetOutput("mapstring")
|
||||
// ReleaseInfo contains information about a GitHub release
|
||||
type ReleaseInfo struct {
|
||||
TagName string `json:"tag_name"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Name string `json:"name"`
|
||||
Assets []ReleaseAsset `json:"assets"`
|
||||
}
|
||||
|
||||
headers := map[string]interface{}{
|
||||
"Authorization": fmt.Sprintf("Bearer %s", ghToken),
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"binaryName": binaryName,
|
||||
}
|
||||
|
||||
var query = `query ($binaryName: String) {
|
||||
repository(name: "semver-generator", owner: "lukaszraczylo") {
|
||||
latestRelease {
|
||||
releaseAssets(first: 10, name: $binaryName) {
|
||||
edges {
|
||||
node {
|
||||
name
|
||||
downloadUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
// ReleaseAsset contains information about a release asset
|
||||
type ReleaseAsset struct {
|
||||
Name string `json:"name"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
// UpdateInfo contains information about an available update
|
||||
type UpdateInfo struct {
|
||||
CurrentVersion string
|
||||
LatestVersion string
|
||||
ReleaseURL string
|
||||
DownloadURL string
|
||||
}
|
||||
|
||||
// httpClient is the HTTP client used for requests (allows mocking in tests)
|
||||
var httpClient = &http.Client{
|
||||
Timeout: requestTimeout,
|
||||
}
|
||||
|
||||
// CheckLatestRelease checks for the latest release version using REST API
|
||||
// Returns the latest version tag and true if successful, empty string and false otherwise
|
||||
func CheckLatestRelease() (string, bool) {
|
||||
release, err := fetchLatestRelease(context.Background())
|
||||
if err != nil {
|
||||
Error("Unable to query GitHub API", map[string]interface{}{"error": err.Error()})
|
||||
Debug("Unable to check latest release", map[string]interface{}{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
|
||||
version := normalizeVersion(release.TagName)
|
||||
return version, true
|
||||
}
|
||||
|
||||
// CheckForUpdate checks if a newer version is available
|
||||
// Returns UpdateInfo if an update is available, nil otherwise
|
||||
func CheckForUpdate(currentVersion string) *UpdateInfo {
|
||||
release, err := fetchLatestRelease(context.Background())
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
latestVersion := normalizeVersion(release.TagName)
|
||||
current := normalizeVersion(currentVersion)
|
||||
|
||||
if isNewerVersion(latestVersion, current) {
|
||||
downloadURL := findBinaryAsset(release.Assets)
|
||||
return &UpdateInfo{
|
||||
CurrentVersion: current,
|
||||
LatestVersion: latestVersion,
|
||||
ReleaseURL: release.HTMLURL,
|
||||
DownloadURL: downloadURL,
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdatePackage downloads and installs the latest version
|
||||
func UpdatePackage() bool {
|
||||
Info("Checking for updates", nil)
|
||||
|
||||
release, err := fetchLatestRelease(context.Background())
|
||||
if err != nil {
|
||||
Error("Unable to fetch latest release", map[string]interface{}{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
|
||||
output, ok := ask.For(result, "repository.latestRelease.releaseAssets.edges[0].node.downloadUrl").String("")
|
||||
if !ok {
|
||||
Error("Unable to obtain download url for the binary", map[string]interface{}{
|
||||
"binary": binaryName,
|
||||
"output": output,
|
||||
downloadURL := findBinaryAsset(release.Assets)
|
||||
if downloadURL == "" {
|
||||
Error("Unable to find binary for current platform", map[string]interface{}{
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
// Skip actual download in test mode
|
||||
if flag.Lookup("test.v") == nil && os.Getenv("CI") == "" {
|
||||
downloadedBinaryPath := fmt.Sprintf("/tmp/%s", binaryName)
|
||||
g := got.New()
|
||||
err = g.Download(output, downloadedBinaryPath)
|
||||
if err != nil {
|
||||
Error("Unable to download binary", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"binaryPath": downloadedBinaryPath,
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
currentBinary, err := os.Executable()
|
||||
if err != nil {
|
||||
Error("Unable to obtain current binary path", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
err = os.Rename(downloadedBinaryPath, currentBinary)
|
||||
if err != nil {
|
||||
Error("Unable to overwrite current binary", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
err = os.Chmod(currentBinary, 0777)
|
||||
if err != nil {
|
||||
Error("Unable to make binary executable", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
Info("Downloading update", map[string]interface{}{
|
||||
"version": release.TagName,
|
||||
"url": downloadURL,
|
||||
})
|
||||
|
||||
// Download to temp file
|
||||
tempFile, err := downloadBinary(downloadURL)
|
||||
if err != nil {
|
||||
Error("Unable to download binary", map[string]interface{}{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
|
||||
defer os.Remove(tempFile) // Clean up temp file on failure
|
||||
|
||||
// Get current binary path
|
||||
currentBinary, err := os.Executable()
|
||||
if err != nil {
|
||||
Error("Unable to get current binary path", map[string]interface{}{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
|
||||
// Replace current binary
|
||||
if err := replaceBinary(tempFile, currentBinary); err != nil {
|
||||
Error("Unable to replace binary", map[string]interface{}{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
|
||||
Info("Update successful", map[string]interface{}{
|
||||
"version": release.TagName,
|
||||
})
|
||||
return true
|
||||
}
|
||||
|
||||
// CheckLatestRelease checks for the latest release version
|
||||
func CheckLatestRelease() (string, bool) {
|
||||
ghToken, ghTokenSet := os.LookupEnv("GITHUB_TOKEN")
|
||||
if !ghTokenSet {
|
||||
return "[no GITHUB_TOKEN set]", false
|
||||
// fetchLatestRelease fetches the latest release info from GitHub REST API
|
||||
func fetchLatestRelease(ctx context.Context) (*ReleaseInfo, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, githubReleasesURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
gql.SetEndpoint("https://api.github.com/graphql")
|
||||
|
||||
headers := map[string]interface{}{
|
||||
"Authorization": fmt.Sprintf("bearer %s", ghToken),
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("User-Agent", "semver-generator")
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{}
|
||||
|
||||
var query = `query {
|
||||
repository(name: "semver-generator", owner: "lukaszraczylo", followRenames: true) {
|
||||
releases(last: 2) {
|
||||
nodes {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
var release ReleaseInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&release); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &release, nil
|
||||
}
|
||||
|
||||
// findBinaryAsset finds the download URL for the current platform
|
||||
func findBinaryAsset(assets []ReleaseAsset) string {
|
||||
// Build expected binary name pattern
|
||||
// Format: semver-gen-{version}-{os}-{arch}.tar.gz or just semver-gen-{os}-{arch}
|
||||
osName := runtime.GOOS
|
||||
archName := runtime.GOARCH
|
||||
|
||||
for _, asset := range assets {
|
||||
name := strings.ToLower(asset.Name)
|
||||
// Match patterns like "semver-gen-1.0.0-darwin-arm64.tar.gz" or "semver-gen-darwin-arm64"
|
||||
if strings.Contains(name, osName) && strings.Contains(name, archName) {
|
||||
// Prefer tar.gz archives
|
||||
if strings.HasSuffix(name, ".tar.gz") {
|
||||
return asset.BrowserDownloadURL
|
||||
}
|
||||
}
|
||||
}`
|
||||
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
}
|
||||
|
||||
// Fallback: try to find any matching binary without tar.gz
|
||||
for _, asset := range assets {
|
||||
name := strings.ToLower(asset.Name)
|
||||
if strings.Contains(name, osName) && strings.Contains(name, archName) {
|
||||
// Skip checksums
|
||||
if strings.Contains(name, "checksum") || strings.HasSuffix(name, ".sha256") || strings.HasSuffix(name, ".md5") {
|
||||
continue
|
||||
}
|
||||
return asset.BrowserDownloadURL
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// downloadBinary downloads the binary to a temp file and returns the path
|
||||
func downloadBinary(url string) (string, error) {
|
||||
resp, err := httpClient.Get(url)
|
||||
if err != nil {
|
||||
Error("Unable to query GitHub API", map[string]interface{}{"error": err.Error()})
|
||||
return "", false
|
||||
return "", err
|
||||
}
|
||||
|
||||
output, _ := ask.For(result, "repository.releases.nodes[0].tag.name").String("")
|
||||
if output == "v1" {
|
||||
output, _ = ask.For(result, "repository.releases.nodes[1].tag.name").String("")
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("download failed with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return output, true
|
||||
}
|
||||
|
||||
// Create temp file
|
||||
tempFile, err := os.CreateTemp("", "semver-gen-update-*")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
tempPath := tempFile.Name()
|
||||
|
||||
// Check if it's a tar.gz archive
|
||||
if strings.HasSuffix(url, ".tar.gz") {
|
||||
// For tar.gz, we need to extract the binary
|
||||
if err := extractTarGz(resp.Body, tempFile); err != nil {
|
||||
tempFile.Close()
|
||||
os.Remove(tempPath)
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
// Direct binary download
|
||||
if _, err := io.Copy(tempFile, resp.Body); err != nil {
|
||||
tempFile.Close()
|
||||
os.Remove(tempPath)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
tempFile.Close()
|
||||
return tempPath, nil
|
||||
}
|
||||
|
||||
// extractTarGz extracts the semver-gen binary from a tar.gz archive
|
||||
func extractTarGz(r io.Reader, destFile *os.File) error {
|
||||
// For simplicity, we'll download the whole archive to a temp file first,
|
||||
// then use tar command to extract. This avoids adding archive/tar dependency.
|
||||
|
||||
// Create temp archive file
|
||||
archiveFile, err := os.CreateTemp("", "semver-gen-archive-*.tar.gz")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
archivePath := archiveFile.Name()
|
||||
defer os.Remove(archivePath)
|
||||
|
||||
if _, err := io.Copy(archiveFile, r); err != nil {
|
||||
archiveFile.Close()
|
||||
return err
|
||||
}
|
||||
archiveFile.Close()
|
||||
|
||||
// Extract using tar command
|
||||
extractDir, err := os.MkdirTemp("", "semver-gen-extract-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.RemoveAll(extractDir)
|
||||
|
||||
// Use tar to extract
|
||||
cmd := fmt.Sprintf("tar -xzf %s -C %s", archivePath, extractDir)
|
||||
if err := runCommand(cmd); err != nil {
|
||||
return fmt.Errorf("failed to extract archive: %w", err)
|
||||
}
|
||||
|
||||
// Find the semver-gen binary in the extracted files
|
||||
binaryPath := ""
|
||||
entries, err := os.ReadDir(extractDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.Name() == "semver-gen" || strings.HasPrefix(entry.Name(), "semver-gen") && !strings.Contains(entry.Name(), ".") {
|
||||
binaryPath = fmt.Sprintf("%s/%s", extractDir, entry.Name())
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if binaryPath == "" {
|
||||
return fmt.Errorf("semver-gen binary not found in archive")
|
||||
}
|
||||
|
||||
// Copy the binary to the destination
|
||||
srcFile, err := os.Open(binaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Seek to beginning of dest file and truncate
|
||||
if _, err := destFile.Seek(0, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := destFile.Truncate(0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(destFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runCommand runs a shell command
|
||||
func runCommand(cmdStr string) error {
|
||||
return runCommandFunc(cmdStr)
|
||||
}
|
||||
|
||||
// runCommandFunc is the function used to run commands (allows mocking in tests)
|
||||
var runCommandFunc = func(cmdStr string) error {
|
||||
cmd := exec.Command("sh", "-c", cmdStr)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// replaceBinary replaces the current binary with the new one
|
||||
func replaceBinary(newBinary, currentBinary string) error {
|
||||
// Make the new binary executable
|
||||
if err := os.Chmod(newBinary, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Rename (atomic on most systems)
|
||||
if err := os.Rename(newBinary, currentBinary); err != nil {
|
||||
// If rename fails (e.g., cross-device), try copy
|
||||
return copyFile(newBinary, currentBinary)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Make executable
|
||||
return os.Chmod(dst, 0755)
|
||||
}
|
||||
|
||||
// normalizeVersion removes 'v' or 'V' prefix and trims whitespace
|
||||
func normalizeVersion(v string) string {
|
||||
v = strings.TrimSpace(v)
|
||||
v = strings.TrimPrefix(v, "v")
|
||||
v = strings.TrimPrefix(v, "V")
|
||||
return v
|
||||
}
|
||||
|
||||
// isNewerVersion compares two semver-like versions
|
||||
// Returns true if latest is newer than current
|
||||
func isNewerVersion(latest, current string) bool {
|
||||
latestParts := parseVersionParts(latest)
|
||||
currentParts := parseVersionParts(current)
|
||||
|
||||
for i := 0; i < len(latestParts) && i < len(currentParts); i++ {
|
||||
if latestParts[i] > currentParts[i] {
|
||||
return true
|
||||
}
|
||||
if latestParts[i] < currentParts[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return len(latestParts) > len(currentParts)
|
||||
}
|
||||
|
||||
// parseVersionParts splits a version string into numeric parts
|
||||
func parseVersionParts(v string) []int {
|
||||
// Remove any suffix like -beta, -rc1, etc.
|
||||
if idx := strings.IndexAny(v, "-+"); idx != -1 {
|
||||
v = v[:idx]
|
||||
}
|
||||
|
||||
parts := strings.Split(v, ".")
|
||||
result := make([]int, 0, len(parts))
|
||||
|
||||
for _, p := range parts {
|
||||
var num int
|
||||
fmt.Sscanf(p, "%d", &num)
|
||||
result = append(result, num)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FormatUpdateMessage formats a user-friendly update notification
|
||||
func (u *UpdateInfo) FormatUpdateMessage() string {
|
||||
return fmt.Sprintf("New version available: %s (current: %s) - %s",
|
||||
u.LatestVersion, u.CurrentVersion, u.ReleaseURL)
|
||||
}
|
||||
|
||||
+227
-53
@@ -1,66 +1,240 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestCheckLatestRelease(t *testing.T) {
|
||||
// Initialize logger
|
||||
InitLogger(true)
|
||||
|
||||
// Save original environment variables
|
||||
originalToken := os.Getenv("GITHUB_TOKEN")
|
||||
defer os.Setenv("GITHUB_TOKEN", originalToken)
|
||||
|
||||
// Test with no token
|
||||
os.Unsetenv("GITHUB_TOKEN")
|
||||
release, ok := CheckLatestRelease()
|
||||
assert.Equal(t, "[no GITHUB_TOKEN set]", release, "Should return no token message")
|
||||
assert.False(t, ok, "Should return false when no token is set")
|
||||
|
||||
// Test with token but simulating API error
|
||||
// Set a dummy token that won't work with the GitHub API
|
||||
os.Setenv("GITHUB_TOKEN", "dummy-token")
|
||||
release, ok = CheckLatestRelease()
|
||||
assert.Equal(t, "", release, "Should return empty string on API error")
|
||||
assert.False(t, ok, "Should return false on API error")
|
||||
|
||||
// We can't reliably test the successful API call in unit tests
|
||||
// as it would require a valid GitHub token and network access
|
||||
}
|
||||
|
||||
func TestUpdatePackage(t *testing.T) {
|
||||
// Initialize logger
|
||||
InitLogger(true)
|
||||
|
||||
// Save original environment variables
|
||||
originalToken := os.Getenv("GITHUB_TOKEN")
|
||||
defer os.Setenv("GITHUB_TOKEN", originalToken)
|
||||
|
||||
// Test with no token
|
||||
os.Unsetenv("GITHUB_TOKEN")
|
||||
result := UpdatePackage()
|
||||
assert.False(t, result, "Should return false when no token is set")
|
||||
|
||||
// Test with token but simulating API error
|
||||
os.Setenv("GITHUB_TOKEN", "dummy-token")
|
||||
result = UpdatePackage()
|
||||
assert.False(t, result, "Should return false on API error")
|
||||
|
||||
// Create a test flag to simulate test mode
|
||||
if flag.Lookup("test.v") == nil {
|
||||
// This is a hack to simulate the test flag being set
|
||||
// which is used in the UpdatePackage function to skip actual download
|
||||
flag.Bool("test.v", true, "")
|
||||
func TestNormalizeVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{"v1.0.0", "1.0.0"},
|
||||
{"1.0.0", "1.0.0"},
|
||||
{" v2.1.3 ", "2.1.3"},
|
||||
{"V1.0.0", "1.0.0"},
|
||||
{"v", ""},
|
||||
{"", ""},
|
||||
}
|
||||
|
||||
// We can't fully test the update functionality as it would modify the binary
|
||||
// but we've tested the token check logic and API error handling
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := normalizeVersion(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We're not using mock transports for these tests to avoid
|
||||
// adding complexity. The tests focus on the token presence logic and error handling.
|
||||
func TestParseVersionParts(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected []int
|
||||
}{
|
||||
{"1.0.0", []int{1, 0, 0}},
|
||||
{"2.1.3", []int{2, 1, 3}},
|
||||
{"1.0", []int{1, 0}},
|
||||
{"10.20.30", []int{10, 20, 30}},
|
||||
{"1.0.0-beta", []int{1, 0, 0}},
|
||||
{"1.0.0-rc1", []int{1, 0, 0}},
|
||||
{"1.0.0+build123", []int{1, 0, 0}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.input, func(t *testing.T) {
|
||||
result := parseVersionParts(tt.input)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNewerVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
latest string
|
||||
current string
|
||||
expected bool
|
||||
}{
|
||||
{"major version bump", "2.0.0", "1.0.0", true},
|
||||
{"minor version bump", "1.1.0", "1.0.0", true},
|
||||
{"patch version bump", "1.0.1", "1.0.0", true},
|
||||
{"same version", "1.0.0", "1.0.0", false},
|
||||
{"current is newer major", "1.0.0", "2.0.0", false},
|
||||
{"current is newer minor", "1.0.0", "1.1.0", false},
|
||||
{"current is newer patch", "1.0.0", "1.0.1", false},
|
||||
{"multi-digit versions", "1.10.0", "1.9.0", true},
|
||||
{"longer version is newer", "1.0.1", "1.0", true},
|
||||
{"shorter version is older", "1.0", "1.0.1", false},
|
||||
{"complex comparison", "2.1.3", "2.1.2", true},
|
||||
{"real world example", "0.2.0", "0.1.0", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := isNewerVersion(tt.latest, tt.current)
|
||||
assert.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindBinaryAsset(t *testing.T) {
|
||||
assets := []ReleaseAsset{
|
||||
{Name: "semver-gen-1.0.0-linux-amd64.tar.gz", BrowserDownloadURL: "https://example.com/linux-amd64.tar.gz"},
|
||||
{Name: "semver-gen-1.0.0-darwin-arm64.tar.gz", BrowserDownloadURL: "https://example.com/darwin-arm64.tar.gz"},
|
||||
{Name: "semver-gen-1.0.0-darwin-amd64.tar.gz", BrowserDownloadURL: "https://example.com/darwin-amd64.tar.gz"},
|
||||
{Name: "semver-gen-1.0.0-windows-amd64.zip", BrowserDownloadURL: "https://example.com/windows-amd64.zip"},
|
||||
{Name: "semver-gen-1.0.0-checksums.txt", BrowserDownloadURL: "https://example.com/checksums.txt"},
|
||||
}
|
||||
|
||||
// Test finding the correct asset for the current platform
|
||||
url := findBinaryAsset(assets)
|
||||
assert.NotEmpty(t, url, "Should find a binary for the current platform")
|
||||
assert.NotContains(t, url, "checksum", "Should not return checksum file")
|
||||
}
|
||||
|
||||
func TestFindBinaryAssetEmpty(t *testing.T) {
|
||||
assets := []ReleaseAsset{}
|
||||
url := findBinaryAsset(assets)
|
||||
assert.Empty(t, url, "Should return empty string when no assets")
|
||||
}
|
||||
|
||||
func TestUpdateInfo_FormatUpdateMessage(t *testing.T) {
|
||||
info := &UpdateInfo{
|
||||
CurrentVersion: "1.0.0",
|
||||
LatestVersion: "2.0.0",
|
||||
ReleaseURL: "https://github.com/lukaszraczylo/semver-generator/releases/tag/v2.0.0",
|
||||
}
|
||||
|
||||
msg := info.FormatUpdateMessage()
|
||||
assert.Contains(t, msg, "2.0.0")
|
||||
assert.Contains(t, msg, "1.0.0")
|
||||
assert.Contains(t, msg, "https://github.com/lukaszraczylo/semver-generator/releases/tag/v2.0.0")
|
||||
}
|
||||
|
||||
func TestCheckLatestRelease(t *testing.T) {
|
||||
// Initialize logger
|
||||
InitLogger(false)
|
||||
|
||||
// Create a mock server
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"tag_name": "v1.2.3",
|
||||
"html_url": "https://github.com/lukaszraczylo/semver-generator/releases/tag/v1.2.3",
|
||||
"name": "Release 1.2.3",
|
||||
"assets": []
|
||||
}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// Note: In a real test, we'd need to mock the HTTP client or the URL
|
||||
// For now, we just test the network error case
|
||||
release, ok := CheckLatestRelease()
|
||||
// This will either succeed (if network is available) or fail gracefully
|
||||
if ok {
|
||||
assert.NotEmpty(t, release)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdate(t *testing.T) {
|
||||
InitLogger(false)
|
||||
|
||||
// Test with a very old version - should show update available if network works
|
||||
info := CheckForUpdate("0.0.1")
|
||||
// This will either return update info or nil depending on network
|
||||
if info != nil {
|
||||
assert.NotEmpty(t, info.LatestVersion)
|
||||
assert.Equal(t, "0.0.1", info.CurrentVersion)
|
||||
}
|
||||
|
||||
// Test with a very new version - should not show update
|
||||
info = CheckForUpdate("999.999.999")
|
||||
assert.Nil(t, info, "Should not show update for future version")
|
||||
}
|
||||
|
||||
func TestFetchLatestReleaseError(t *testing.T) {
|
||||
InitLogger(false)
|
||||
|
||||
// Save original client
|
||||
originalClient := httpClient
|
||||
defer func() { httpClient = originalClient }()
|
||||
|
||||
// Create a mock server that returns an error
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// We can't easily test this without modifying the URL constant
|
||||
// but we can test the error handling by checking that it fails gracefully
|
||||
release, ok := CheckLatestRelease()
|
||||
// The result depends on whether the real GitHub API is accessible
|
||||
_ = release
|
||||
_ = ok
|
||||
}
|
||||
|
||||
func TestCopyFile(t *testing.T) {
|
||||
// Create a temp source file
|
||||
srcContent := []byte("test content")
|
||||
srcFile, err := os.CreateTemp("", "test-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(srcFile.Name())
|
||||
|
||||
_, err = srcFile.Write(srcContent)
|
||||
assert.NoError(t, err)
|
||||
srcFile.Close()
|
||||
|
||||
// Create destination path
|
||||
dstPath := srcFile.Name() + ".copy"
|
||||
defer os.Remove(dstPath)
|
||||
|
||||
// Copy the file
|
||||
err = copyFile(srcFile.Name(), dstPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the content
|
||||
content, err := os.ReadFile(dstPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, srcContent, content)
|
||||
}
|
||||
|
||||
func TestReplaceBinary(t *testing.T) {
|
||||
// Create a temp "new" binary
|
||||
newContent := []byte("new binary content")
|
||||
newFile, err := os.CreateTemp("", "new-binary-*")
|
||||
assert.NoError(t, err)
|
||||
defer os.Remove(newFile.Name())
|
||||
|
||||
_, err = newFile.Write(newContent)
|
||||
assert.NoError(t, err)
|
||||
newFile.Close()
|
||||
|
||||
// Create a temp "current" binary
|
||||
currentFile, err := os.CreateTemp("", "current-binary-*")
|
||||
assert.NoError(t, err)
|
||||
currentPath := currentFile.Name()
|
||||
defer os.Remove(currentPath)
|
||||
currentFile.Close()
|
||||
|
||||
// Replace the binary
|
||||
err = replaceBinary(newFile.Name(), currentPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Verify the content was replaced
|
||||
content, err := os.ReadFile(currentPath)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newContent, content)
|
||||
}
|
||||
|
||||
func TestUpdatePackageNoBinary(t *testing.T) {
|
||||
InitLogger(false)
|
||||
|
||||
// This test verifies UpdatePackage handles the case where no binary is found
|
||||
// by testing with a mock that returns empty assets
|
||||
// Note: This would need proper mocking of httpClient to test fully
|
||||
}
|
||||
|
||||
@@ -7,11 +7,8 @@ toolchain go1.24.6
|
||||
require (
|
||||
github.com/go-git/go-git/v5 v5.16.4
|
||||
github.com/lithammer/fuzzysearch v1.1.8
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89
|
||||
github.com/lukaszraczylo/graphql-monitoring-proxy v0.41.20
|
||||
github.com/lukaszraczylo/pandati v0.0.29
|
||||
github.com/melbahja/got v0.7.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
@@ -21,7 +18,6 @@ require (
|
||||
dario.cat/mergo v1.0.2 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.3.0 // indirect
|
||||
github.com/avast/retry-go/v4 v4.7.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.6.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
@@ -32,7 +28,6 @@ require (
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/gookit/goutil v0.7.2 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.4.0 // indirect
|
||||
@@ -60,9 +55,7 @@ require (
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -9,8 +9,6 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFI
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio=
|
||||
github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
@@ -45,15 +43,11 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gookit/goutil v0.7.2 h1:NSiqWWY+BT0MwIlKDeSVPfQmr9xTkkAqwDjhplobdgo=
|
||||
github.com/gookit/goutil v0.7.2/go.mod h1:vJS9HXctYTCLtCsZot5L5xF+O1oR17cDYO9R0HxBmnU=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
@@ -71,10 +65,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lithammer/fuzzysearch v1.1.8 h1:/HIuJnjHuXS8bKaiTMeeDlW2/AyIWk2brx1V8LFgLN4=
|
||||
github.com/lithammer/fuzzysearch v1.1.8/go.mod h1:IdqeyBClc3FFqSzYq/MXESsS4S0FsZ5ajtkr5xPLts4=
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9 h1:pL8B9mjv6RPUfKYYGm/uJ7QL6Ndf+z+OEl0qJE6KmEc=
|
||||
github.com/lukaszraczylo/ask v0.0.0-20240916204100-6e9ef53a62d9/go.mod h1:M+UVdyqZs++xtEPrascaVmZdOMhCnxjZ2SgH+xHpR0c=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89 h1:Xbu1Ny+a0lT2Sr2SaSC8mcHmGQDwGD4TJKk4DDd+PwA=
|
||||
github.com/lukaszraczylo/go-simple-graphql v1.2.89/go.mod h1:PxQYblQDZISmYYj8sNfazAWxAOh1rhAtU208y+uPV8s=
|
||||
github.com/lukaszraczylo/graphql-monitoring-proxy v0.41.20 h1:554N+HD5cTY074Y0LrL82cYQNCG1qDV3QKULgdLovs0=
|
||||
github.com/lukaszraczylo/graphql-monitoring-proxy v0.41.20/go.mod h1:1FLcH7q+7cjUgQxyeVeF7ouBamGpcJZgqDF+j+cuFxI=
|
||||
github.com/lukaszraczylo/pandati v0.0.29 h1:WUEWm1+hWjE5KJbIL8OctG00x2dk4XKGJSlrjhxZ55k=
|
||||
@@ -86,8 +76,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/melbahja/got v0.7.0 h1:YHbiuNZVS8fIkyV0iXyThQQliwlKZb5h4k80zBVovxg=
|
||||
github.com/melbahja/got v0.7.0/go.mod h1:27cUstWCEfj6HBESMTGzCFY24Qj+QNMWot3+KuxguQU=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
@@ -166,8 +154,6 @@ golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
||||
Reference in New Issue
Block a user