Compare commits
No commits in common. "dev" and "main" have entirely different histories.
10 changed files with 43 additions and 172 deletions
17
cmd.go
17
cmd.go
|
|
@ -45,9 +45,9 @@ Examples:
|
|||
RunE: runUpdate,
|
||||
}
|
||||
|
||||
updateCmd.PersistentFlags().StringVar(&updateChannel, "channel", "stable", "Release channel: stable, beta, alpha, prerelease, or dev")
|
||||
updateCmd.PersistentFlags().StringVar(&updateChannel, "channel", "stable", "Release channel: stable, beta, alpha, or dev")
|
||||
updateCmd.PersistentFlags().BoolVar(&updateForce, "force", false, "Force update even if already on latest version")
|
||||
updateCmd.Flags().BoolVar(&updateCheck, "check", false, "Only check for updates, do not apply")
|
||||
updateCmd.Flags().BoolVar(&updateCheck, "check", false, "Only check for updates, don't apply")
|
||||
updateCmd.Flags().IntVar(&updateWatchPID, "watch-pid", 0, "Internal: watch for parent PID to die then restart")
|
||||
_ = updateCmd.Flags().MarkHidden("watch-pid")
|
||||
|
||||
|
|
@ -55,9 +55,7 @@ Examples:
|
|||
Use: "check",
|
||||
Short: "Check for available updates",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
previousCheck := updateCheck
|
||||
updateCheck = true
|
||||
defer func() { updateCheck = previousCheck }()
|
||||
return runUpdate(cmd, args)
|
||||
},
|
||||
})
|
||||
|
|
@ -72,25 +70,24 @@ func runUpdate(cmd *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
currentVersion := cli.AppVersion
|
||||
normalizedChannel := normaliseGitHubChannel(updateChannel)
|
||||
|
||||
cli.Print("%s %s\n", cli.DimStyle.Render("Current version:"), cli.ValueStyle.Render(currentVersion))
|
||||
cli.Print("%s %s/%s\n", cli.DimStyle.Render("Platform:"), runtime.GOOS, runtime.GOARCH)
|
||||
cli.Print("%s %s\n\n", cli.DimStyle.Render("Channel:"), normalizedChannel)
|
||||
cli.Print("%s %s\n\n", cli.DimStyle.Render("Channel:"), updateChannel)
|
||||
|
||||
// Handle dev channel specially - it's a prerelease tag, not a semver channel
|
||||
if normalizedChannel == "dev" {
|
||||
if updateChannel == "dev" {
|
||||
return handleDevUpdate(currentVersion)
|
||||
}
|
||||
|
||||
// Check for newer version
|
||||
release, updateAvailable, err := CheckForNewerVersion(repoOwner, repoName, normalizedChannel, true)
|
||||
release, updateAvailable, err := CheckForNewerVersion(repoOwner, repoName, updateChannel, true)
|
||||
if err != nil {
|
||||
return cli.Wrap(err, "failed to check for updates")
|
||||
}
|
||||
|
||||
if release == nil {
|
||||
cli.Print("%s No releases found in %s channel\n", cli.WarningStyle.Render("!"), normalizedChannel)
|
||||
cli.Print("%s No releases found in %s channel\n", cli.WarningStyle.Render("!"), updateChannel)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +140,7 @@ func handleDevUpdate(currentVersion string) error {
|
|||
client := NewGithubClient()
|
||||
|
||||
// Fetch the dev release directly by tag
|
||||
release, err := client.GetLatestRelease(context.Background(), repoOwner, repoName, "beta")
|
||||
release, err := client.GetLatestRelease(context.TODO(), repoOwner, repoName, "beta")
|
||||
if err != nil {
|
||||
// Try fetching the "dev" tag directly
|
||||
return handleDevTagUpdate(currentVersion)
|
||||
|
|
|
|||
|
|
@ -26,7 +26,9 @@ The `CheckOnStartup` field can take one of the following values:
|
|||
|
||||
If you are using the example CLI provided in `cmd/updater`, the following flags are available:
|
||||
|
||||
* `--check`: Check for new updates without applying them.
|
||||
* `--channel`: Set the update channel (e.g., stable, beta, alpha). Defaults to `stable`.
|
||||
* `--force`: Force update even when already on latest.
|
||||
* `--watch-pid`: Internal flag used during restart after update.
|
||||
* `--check-update`: Check for new updates without applying them.
|
||||
* `--do-update`: Perform an update if available.
|
||||
* `--channel`: Set the update channel (e.g., stable, beta, alpha). If not set, it's determined from the current version tag.
|
||||
* `--force-semver-prefix`: Force 'v' prefix on semver tags (default `true`).
|
||||
* `--release-url-format`: A URL format for release assets.
|
||||
* `--pull-request`: Update to a specific pull request (integer ID).
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@ package updater
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
coreerr "forge.lthn.ai/core/go-log"
|
||||
)
|
||||
|
|
@ -33,16 +31,10 @@ func GetLatestUpdateFromURL(baseURL string) (*GenericUpdateInfo, error) {
|
|||
if err != nil {
|
||||
return nil, coreerr.E("GetLatestUpdateFromURL", "invalid base URL", err)
|
||||
}
|
||||
|
||||
// Append latest.json to the path
|
||||
u.Path = strings.TrimSuffix(u.Path, "/") + "/latest.json"
|
||||
u.Path += "/latest.json"
|
||||
|
||||
req, err := newAgentRequest(context.Background(), "GET", u.String())
|
||||
if err != nil {
|
||||
return nil, coreerr.E("GetLatestUpdateFromURL", "failed to create update check request", err)
|
||||
}
|
||||
|
||||
resp, err := NewHTTPClient().Do(req)
|
||||
resp, err := http.Get(u.String())
|
||||
if err != nil {
|
||||
return nil, coreerr.E("GetLatestUpdateFromURL", "failed to fetch latest.json", err)
|
||||
}
|
||||
|
|
|
|||
17
github.go
17
github.go
|
|
@ -52,13 +52,10 @@ var NewAuthenticatedClient = func(ctx context.Context) *http.Client {
|
|||
if token == "" {
|
||||
return http.DefaultClient
|
||||
}
|
||||
|
||||
ts := oauth2.StaticTokenSource(
|
||||
&oauth2.Token{AccessToken: token},
|
||||
)
|
||||
client := oauth2.NewClient(ctx, ts)
|
||||
client.Timeout = defaultHTTPTimeout
|
||||
return client
|
||||
return oauth2.NewClient(ctx, ts)
|
||||
}
|
||||
|
||||
func (g *githubClient) GetPublicRepos(ctx context.Context, userOrOrg string) ([]string, error) {
|
||||
|
|
@ -74,10 +71,11 @@ func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, use
|
|||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := newAgentRequest(ctx, "GET", url)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -87,10 +85,11 @@ func (g *githubClient) getPublicReposWithAPIURL(ctx context.Context, apiURL, use
|
|||
_ = resp.Body.Close()
|
||||
// Try organization endpoint
|
||||
url = fmt.Sprintf("%s/orgs/%s/repos", apiURL, userOrOrg)
|
||||
req, err = newAgentRequest(ctx, "GET", url)
|
||||
req, err = http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
resp, err = client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -144,10 +143,11 @@ func (g *githubClient) GetLatestRelease(ctx context.Context, owner, repo, channe
|
|||
client := NewAuthenticatedClient(ctx)
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
|
||||
|
||||
req, err := newAgentRequest(ctx, "GET", url)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
@ -198,10 +198,11 @@ func (g *githubClient) GetReleaseByPullRequest(ctx context.Context, owner, repo
|
|||
client := NewAuthenticatedClient(ctx)
|
||||
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases", owner, repo)
|
||||
|
||||
req, err := newAgentRequest(ctx, "GET", url)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("User-Agent", "Borg-Data-Collector")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -82,54 +82,6 @@ func TestDetermineChannel_Good(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCheckForUpdatesByTag_UsesCurrentVersionChannel(t *testing.T) {
|
||||
originalVersion := Version
|
||||
originalCheckForUpdates := CheckForUpdates
|
||||
defer func() {
|
||||
Version = originalVersion
|
||||
CheckForUpdates = originalCheckForUpdates
|
||||
}()
|
||||
|
||||
var gotChannel string
|
||||
CheckForUpdates = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
gotChannel = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
Version = "v2.0.0-rc.1"
|
||||
if err := CheckForUpdatesByTag("owner", "repo"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if gotChannel != "beta" {
|
||||
t.Fatalf("expected beta channel, got %q", gotChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckOnlyByTag_UsesCurrentVersionChannel(t *testing.T) {
|
||||
originalVersion := Version
|
||||
originalCheckOnly := CheckOnly
|
||||
defer func() {
|
||||
Version = originalVersion
|
||||
CheckOnly = originalCheckOnly
|
||||
}()
|
||||
|
||||
var gotChannel string
|
||||
CheckOnly = func(owner, repo, channel string, forceSemVerPrefix bool, releaseURLFormat string) error {
|
||||
gotChannel = channel
|
||||
return nil
|
||||
}
|
||||
|
||||
Version = "v2.0.0-alpha.1"
|
||||
if err := CheckOnlyByTag("owner", "repo"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if gotChannel != "alpha" {
|
||||
t.Fatalf("expected alpha channel, got %q", gotChannel)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDownloadURL_Good(t *testing.T) {
|
||||
osName := runtime.GOOS
|
||||
archName := runtime.GOARCH
|
||||
|
|
|
|||
14
go.mod
14
go.mod
|
|
@ -1,11 +1,11 @@
|
|||
module dappco.re/go/core/update
|
||||
module forge.lthn.ai/core/go-update
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
dappco.re/go/core/cli v0.3.6
|
||||
dappco.re/go/core/io v0.1.5
|
||||
dappco.re/go/core/log v0.0.4
|
||||
forge.lthn.ai/core/cli v0.3.6
|
||||
forge.lthn.ai/core/go-io v0.1.5
|
||||
forge.lthn.ai/core/go-log v0.0.4
|
||||
github.com/Snider/Borg v0.2.0
|
||||
github.com/minio/selfupdate v0.6.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
|
|
@ -16,9 +16,9 @@ require (
|
|||
require (
|
||||
aead.dev/minisign v0.3.0 // indirect
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
dappco.re/go/core v0.3.1 // indirect
|
||||
dappco.re/go/core/i18n v0.1.6 // indirect
|
||||
dappco.re/go/core/inference v0.1.5 // indirect
|
||||
forge.lthn.ai/core/go v0.3.1 // indirect
|
||||
forge.lthn.ai/core/go-i18n v0.1.6 // indirect
|
||||
forge.lthn.ai/core/go-inference v0.1.5 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.4.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultHTTPTimeout = 30 * time.Second
|
||||
|
||||
var NewHTTPClient = func() *http.Client {
|
||||
return &http.Client{Timeout: defaultHTTPTimeout}
|
||||
}
|
||||
|
||||
func newAgentRequest(ctx context.Context, method, url string) (*http.Request, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("User-Agent", updaterUserAgent())
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func updaterUserAgent() string {
|
||||
version := formatVersionForDisplay(Version, true)
|
||||
if version == "" {
|
||||
version = "unknown"
|
||||
}
|
||||
return fmt.Sprintf("agent-go-update/%s", version)
|
||||
}
|
||||
15
service.go
15
service.go
|
|
@ -30,8 +30,7 @@ type UpdateServiceConfig struct {
|
|||
// 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", "beta", or "prerelease").
|
||||
// "prerelease" is normalised to "beta" to match the GitHub release filter.
|
||||
// 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.
|
||||
|
|
@ -64,7 +63,6 @@ func NewUpdateService(config UpdateServiceConfig) (*UpdateService, error) {
|
|||
var err error
|
||||
|
||||
if isGitHub {
|
||||
config.Channel = normaliseGitHubChannel(config.Channel)
|
||||
owner, repo, err = ParseRepoURL(config.RepoURL)
|
||||
if err != nil {
|
||||
return nil, coreerr.E("NewUpdateService", "failed to parse GitHub repo URL", err)
|
||||
|
|
@ -129,14 +127,3 @@ func ParseRepoURL(repoURL string) (owner string, repo string, err error) {
|
|||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
func normaliseGitHubChannel(channel string) string {
|
||||
channel = strings.ToLower(strings.TrimSpace(channel))
|
||||
if channel == "" {
|
||||
return "stable"
|
||||
}
|
||||
if channel == "prerelease" {
|
||||
return "beta"
|
||||
}
|
||||
return channel
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,15 +12,13 @@ func TestNewUpdateService(t *testing.T) {
|
|||
config UpdateServiceConfig
|
||||
expectError bool
|
||||
isGitHub bool
|
||||
wantChannel string
|
||||
}{
|
||||
{
|
||||
name: "Valid GitHub URL",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
},
|
||||
isGitHub: true,
|
||||
wantChannel: "stable",
|
||||
isGitHub: true,
|
||||
},
|
||||
{
|
||||
name: "Valid non-GitHub URL",
|
||||
|
|
@ -29,24 +27,6 @@ func TestNewUpdateService(t *testing.T) {
|
|||
},
|
||||
isGitHub: false,
|
||||
},
|
||||
{
|
||||
name: "GitHub channel is normalised",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
Channel: " Beta ",
|
||||
},
|
||||
isGitHub: true,
|
||||
wantChannel: "beta",
|
||||
},
|
||||
{
|
||||
name: "GitHub prerelease channel maps to beta",
|
||||
config: UpdateServiceConfig{
|
||||
RepoURL: "https://github.com/owner/repo",
|
||||
Channel: " prerelease ",
|
||||
},
|
||||
isGitHub: true,
|
||||
wantChannel: "beta",
|
||||
},
|
||||
{
|
||||
name: "Invalid GitHub URL",
|
||||
config: UpdateServiceConfig{
|
||||
|
|
@ -65,9 +45,6 @@ func TestNewUpdateService(t *testing.T) {
|
|||
if err == nil && service.isGitHub != tc.isGitHub {
|
||||
t.Errorf("Expected isGitHub: %v, got: %v", tc.isGitHub, service.isGitHub)
|
||||
}
|
||||
if err == nil && tc.wantChannel != "" && service.config.Channel != tc.wantChannel {
|
||||
t.Errorf("Expected GitHub channel %q, got %q", tc.wantChannel, service.config.Channel)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
updater.go
25
updater.go
|
|
@ -31,24 +31,17 @@ var NewGithubClient = func() GithubClient {
|
|||
// DoUpdate is a variable that holds the function to perform the actual update.
|
||||
// This can be replaced in tests to prevent actual updates.
|
||||
var DoUpdate = func(url string) error {
|
||||
client := NewHTTPClient()
|
||||
req, err := newAgentRequest(context.Background(), "GET", url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return coreerr.E("DoUpdate", "failed to create update request", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return coreerr.E("DoUpdate", "failed to download update", err)
|
||||
return err
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
err := Body.Close()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to close response body: %v\n", err)
|
||||
}
|
||||
}(resp.Body)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return coreerr.E("DoUpdate", fmt.Sprintf("failed to download update: %s", resp.Status), nil)
|
||||
}
|
||||
|
||||
err = selfupdate.Apply(resp.Body, selfupdate.Options{})
|
||||
if err != nil {
|
||||
if rerr := selfupdate.RollbackError(err); rerr != nil {
|
||||
|
|
@ -56,6 +49,8 @@ var DoUpdate = func(url string) error {
|
|||
}
|
||||
return coreerr.E("DoUpdate", "update failed", err)
|
||||
}
|
||||
|
||||
fmt.Println("Update applied successfully.")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -145,14 +140,14 @@ var CheckOnly = func(owner, repo, channel string, forceSemVerPrefix bool, releas
|
|||
// 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 CheckForUpdatesByTag = func(owner, repo string) error {
|
||||
channel := determineChannel(Version, semver.Prerelease(formatVersionForComparison(Version)) != "")
|
||||
channel := determineChannel(Version, false) // isPreRelease is false for current version
|
||||
return CheckForUpdates(owner, repo, channel, true, "")
|
||||
}
|
||||
|
||||
// CheckOnlyByTag checks for updates from GitHub based on the channel determined by the
|
||||
// current version tag, without applying them.
|
||||
var CheckOnlyByTag = func(owner, repo string) error {
|
||||
channel := determineChannel(Version, semver.Prerelease(formatVersionForComparison(Version)) != "")
|
||||
channel := determineChannel(Version, false) // isPreRelease is false for current version
|
||||
return CheckOnly(owner, repo, channel, true, "")
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue