Documentation
¶
Overview ¶
Package updater provides functionality for self-updating Go applications. It supports updates from GitHub releases and generic HTTP endpoints.
Index ¶
- Constants
- Variables
- func AddUpdateCommands(root *cobra.Command)
- func GetDownloadURL(release *Release, releaseURLFormat string) (string, error)
- func ParseRepoURL(repoURL string) (owner string, repo string, err error)
- type GenericUpdateInfo
- type GithubClient
- type Release
- type ReleaseAsset
- type Repo
- type StartupCheckMode
- type UpdateService
- type UpdateServiceConfig
Examples ¶
Constants ¶
const PkgVersion = "1.2.3"
Variables ¶
var CheckForNewerVersion = func(owner, repo, channel string, forceSemVerPrefix bool) (*Release, bool, error) { client := NewGithubClient() ctx := context.Background() release, err := client.GetLatestRelease(ctx, owner, repo, channel) if err != nil { return nil, false, fmt.Errorf("error fetching latest release: %w", err) } if release == nil { return nil, false, nil } vCurrent := formatVersionForComparison(Version) vLatest := formatVersionForComparison(release.TagName) if semver.Compare(vCurrent, vLatest) >= 0 { return release, false, nil } return release, true, nil }
CheckForNewerVersion checks if a newer version of the application is available on GitHub. It fetches the latest release for the given owner, repository, and channel, and compares its tag with the current application version.
var CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error { release, updateAvailable, err := CheckForNewerVersion(owner, repo, channel, forceSemVerPrefix) if err != nil { return err } if !updateAvailable { if release != nil { fmt.Printf("Current version %s is up-to-date with latest release %s.\n", formatVersionForDisplay(Version, forceSemVerPrefix), formatVersionForDisplay(release.TagName, forceSemVerPrefix)) } else { fmt.Println("No releases found.") } return nil } fmt.Printf("Newer version %s found (current: %s). Applying update...\n", formatVersionForDisplay(release.TagName, forceSemVerPrefix), formatVersionForDisplay(Version, forceSemVerPrefix)) downloadURL, err := GetDownloadURL(release, releaseURLFormat) if err != nil { return fmt.Errorf("error getting download URL: %w", err) } return DoUpdate(downloadURL) }
CheckForUpdates checks for new updates on GitHub and applies them if a newer version is found. It uses the provided owner, repository, and channel to find the latest release.
var CheckForUpdatesByPullRequest = func(owner, repo string, prNumber int, releaseURLFormat string) error { client := NewGithubClient() ctx := context.Background() release, err := client.GetReleaseByPullRequest(ctx, owner, repo, prNumber) if err != nil { return fmt.Errorf("error fetching release for pull request: %w", err) } if release == nil { fmt.Printf("No release found for PR #%d.\n", prNumber) return nil } fmt.Printf("Release %s found for PR #%d. Applying update...\n", release.TagName, prNumber) downloadURL, err := GetDownloadURL(release, releaseURLFormat) if err != nil { return fmt.Errorf("error getting download URL: %w", err) } return DoUpdate(downloadURL) }
CheckForUpdatesByPullRequest finds a release associated with a specific pull request number on GitHub and applies the update.
var CheckForUpdatesByTag = func(owner, repo string) error { channel := determineChannel(Version, false) return CheckForUpdates(owner, repo, channel, true, "") }
CheckForUpdatesByTag checks for and applies updates from GitHub based on the channel determined by the current application's version tag (e.g., 'stable' or 'prerelease').
var CheckForUpdatesHTTP = func(baseURL string) error { info, err := GetLatestUpdateFromURL(baseURL) if err != nil { return err } vCurrent := formatVersionForComparison(Version) vLatest := formatVersionForComparison(info.Version) if semver.Compare(vCurrent, vLatest) >= 0 { fmt.Printf("Current version %s is up-to-date with latest release %s.\n", Version, info.Version) return nil } fmt.Printf("Newer version %s found (current: %s). Applying update...\n", info.Version, Version) return DoUpdate(info.URL) }
CheckForUpdatesHTTP checks for and applies updates from a generic HTTP endpoint. The endpoint is expected to provide update information in a structured format.
var CheckOnly = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error { release, updateAvailable, err := CheckForNewerVersion(owner, repo, channel, forceSemVerPrefix) if err != nil { return err } if !updateAvailable { if release != nil { fmt.Printf("Current version %s is up-to-date with latest release %s.\n", formatVersionForDisplay(Version, forceSemVerPrefix), formatVersionForDisplay(release.TagName, forceSemVerPrefix)) } else { fmt.Println("No new release found.") } return nil } fmt.Printf("New release found: %s (current version: %s)\n", formatVersionForDisplay(release.TagName, forceSemVerPrefix), formatVersionForDisplay(Version, forceSemVerPrefix)) return nil }
CheckOnly checks for new updates on GitHub without applying them. It prints a message indicating if a new release is available.
var CheckOnlyByTag = func(owner, repo string) error { channel := determineChannel(Version, false) return CheckOnly(owner, repo, channel, true, "") }
CheckOnlyByTag checks for updates from GitHub based on the channel determined by the current version tag, without applying them.
var CheckOnlyHTTP = func(baseURL string) error { info, err := GetLatestUpdateFromURL(baseURL) if err != nil { return err } vCurrent := formatVersionForComparison(Version) vLatest := formatVersionForComparison(info.Version) if semver.Compare(vCurrent, vLatest) >= 0 { fmt.Printf("Current version %s is up-to-date with latest release %s.\n", Version, info.Version) return nil } fmt.Printf("New release found: %s (current version: %s)\n", info.Version, Version) return nil }
CheckOnlyHTTP checks for updates from a generic HTTP endpoint without applying them. It prints a message if a new version is available.
var DoUpdate = func(url string) error { resp, err := http.Get(url) if err != nil { return err } defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { fmt.Printf("failed to close response body: %v\n", err) } }(resp.Body) err = selfupdate.Apply(resp.Body, selfupdate.Options{}) if err != nil { if rerr := selfupdate.RollbackError(err); rerr != nil { return fmt.Errorf("failed to rollback from failed update: %v", rerr) } return fmt.Errorf("update failed: %v", err) } fmt.Println("Update applied successfully.") return nil }
DoUpdate is a variable that holds the function to perform the actual update. This can be replaced in tests to prevent actual updates.
var NewAuthenticatedClient = func(ctx context.Context) *http.Client { token := os.Getenv("GITHUB_TOKEN") if token == "" { return http.DefaultClient } ts := oauth2.StaticTokenSource( &oauth2.Token{AccessToken: token}, ) return oauth2.NewClient(ctx, ts) }
NewAuthenticatedClient creates a new HTTP client that authenticates with the GitHub API. It uses the GITHUB_TOKEN environment variable for authentication. If the token is not set, it returns the default HTTP client.
var NewGithubClient = func() GithubClient {
return &githubClient{}
}
NewGithubClient is a variable that holds a function to create a new GithubClient. This can be replaced in tests to inject a mock client.
Example:
updater.NewGithubClient = func() updater.GithubClient {
return &mockClient{} // or your mock implementation
}
var Version = PkgVersion
Version holds the current version of the application. It is set at build time via ldflags or fallback to the version in package.json.
Functions ¶
func AddUpdateCommands ¶
AddUpdateCommands registers the update command and subcommands.
func GetDownloadURL ¶
GetDownloadURL finds the appropriate download URL for the current operating system and architecture.
It supports two modes of operation:
- Using a 'releaseURLFormat' template: If 'releaseURLFormat' is provided, it will be used to construct the download URL. The template can contain placeholders for the release tag '{tag}', operating system '{os}', and architecture '{arch}'.
- Automatic detection: If 'releaseURLFormat' is empty, the function will inspect the assets of the release to find a suitable download URL. It searches for an asset name that contains both the current OS and architecture (e.g., "my-app-linux-amd64"). If no match is found, it falls back to matching only the OS.
Example with releaseURLFormat:
release := &updater.Release{TagName: "v1.2.3"}
url, err := updater.GetDownloadURL(release, "https://example.com/downloads/{tag}/{os}/{arch}")
if err != nil {
// handle error
}
fmt.Println(url) // "https://example.com/downloads/v1.2.3/linux/amd64" (on a Linux AMD64 system)
Example with automatic detection:
release := &updater.Release{
Assets: []updater.ReleaseAsset{
{Name: "my-app-linux-amd64", DownloadURL: "https://example.com/download/linux-amd64"},
{Name: "my-app-windows-amd64", DownloadURL: "https://example.com/download/windows-amd64"},
},
}
url, err := updater.GetDownloadURL(release, "")
if err != nil {
// handle error
}
fmt.Println(url) // "https://example.com/download/linux-amd64" (on a Linux AMD64 system)
func ParseRepoURL ¶
ParseRepoURL extracts the owner and repository name from a GitHub URL. It handles standard GitHub URL formats.
Example ¶
package main
import (
"fmt"
"log"
"github.com/host-uk/core/internal/cmd/updater"
)
func main() {
owner, repo, err := updater.ParseRepoURL("https://github.com/owner/repo")
if err != nil {
log.Fatalf("Failed to parse repo URL: %v", err)
}
fmt.Printf("Owner: %s, Repo: %s", owner, repo)
}
Output: Owner: owner, Repo: repo
Types ¶
type GenericUpdateInfo ¶
type GenericUpdateInfo struct {
Version string `json:"version"` // The version number of the update.
URL string `json:"url"` // The URL to download the update from.
}
GenericUpdateInfo holds the information from a latest.json file. This file is expected to be at the root of a generic HTTP update server.
func GetLatestUpdateFromURL ¶
func GetLatestUpdateFromURL(baseURL string) (*GenericUpdateInfo, error)
GetLatestUpdateFromURL fetches and parses a latest.json file from a base URL. The server at the baseURL should host a 'latest.json' file that contains the version and download URL for the latest update.
Example of latest.json:
{
"version": "1.2.3",
"url": "https://your-server.com/path/to/release-asset"
}
type GithubClient ¶
type GithubClient interface {
// GetPublicRepos fetches the public repositories for a user or organization.
GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error)
// GetLatestRelease fetches the latest release for a given repository and channel.
GetLatestRelease(ctx context.Context, owner, repo, channel string) (*Release, error)
// GetReleaseByPullRequest fetches a release associated with a specific pull request number.
GetReleaseByPullRequest(ctx context.Context, owner, repo string, prNumber int) (*Release, error)
}
GithubClient defines the interface for interacting with the GitHub API. This allows for mocking the client in tests.
type Release ¶
type Release struct {
TagName string `json:"tag_name"` // The name of the tag for the release.
PreRelease bool `json:"prerelease"` // Indicates if the release is a pre-release.
Assets []ReleaseAsset `json:"assets"` // A list of assets associated with the release.
}
Release represents a GitHub release.
type ReleaseAsset ¶
type ReleaseAsset struct {
Name string `json:"name"` // The name of the asset.
DownloadURL string `json:"browser_download_url"` // The URL to download the asset.
}
ReleaseAsset represents a single asset from a GitHub release.
type Repo ¶
type Repo struct {
CloneURL string `json:"clone_url"` // The URL to clone the repository.
}
Repo represents a repository from the GitHub API.
type StartupCheckMode ¶
type StartupCheckMode int
StartupCheckMode defines the updater's behavior on startup.
const ( // NoCheck disables any checks on startup. NoCheck StartupCheckMode = iota // CheckOnStartup checks for updates on startup but does not apply them. CheckOnStartup // CheckAndUpdateOnStartup checks for and applies updates on startup. CheckAndUpdateOnStartup )
type UpdateService ¶
type UpdateService struct {
// contains filtered or unexported fields
}
UpdateService provides a configurable interface for handling application updates. It can be configured to check for updates on startup and, if desired, apply them automatically. The service can handle updates from both GitHub releases and generic HTTP servers.
func NewUpdateService ¶
func NewUpdateService(config UpdateServiceConfig) (*UpdateService, error)
NewUpdateService creates and configures a new UpdateService. It parses the repository URL to determine if it's a GitHub repository and extracts the owner and repo name.
Example ¶
package main
import (
"fmt"
"log"
"github.com/host-uk/core/internal/cmd/updater"
)
func main() {
// Mock the update check functions to prevent actual updates during tests
updater.CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
fmt.Println("CheckForUpdates called")
return nil
}
defer func() {
updater.CheckForUpdates = nil // Restore original function
}()
config := updater.UpdateServiceConfig{
RepoURL: "https://github.com/owner/repo",
Channel: "stable",
CheckOnStartup: updater.CheckAndUpdateOnStartup,
}
updateService, err := updater.NewUpdateService(config)
if err != nil {
log.Fatalf("Failed to create update service: %v", err)
}
if err := updateService.Start(); err != nil {
log.Printf("Update check failed: %v", err)
}
}
Output: CheckForUpdates called
func (*UpdateService) Start ¶
func (s *UpdateService) Start() error
Start initiates the update check based on the service configuration. It determines whether to perform a GitHub or HTTP-based update check based on the RepoURL. The behavior of the check is controlled by the CheckOnStartup setting in the configuration.
type UpdateServiceConfig ¶
type UpdateServiceConfig struct {
// RepoURL is the URL to the repository for updates. It can be a GitHub
// repository URL (e.g., "https://github.com/owner/repo") or a base URL
// for a generic HTTP update server.
RepoURL string
// Channel specifies the release channel to track (e.g., "stable", "prerelease").
// This is only used for GitHub-based updates.
Channel string
// CheckOnStartup determines the update behavior when the service starts.
CheckOnStartup StartupCheckMode
// ForceSemVerPrefix toggles whether to enforce a 'v' prefix on version tags for display.
// If true, a 'v' prefix is added if missing. If false, it's removed if present.
ForceSemVerPrefix bool
// ReleaseURLFormat provides a template for constructing the download URL for a
// release asset. The placeholder {tag} will be replaced with the release tag.
ReleaseURLFormat string
}
UpdateServiceConfig holds the configuration for the UpdateService.