Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,29 @@ import "github.com/pilot-protocol/updater"

```go
u := updater.New(updater.Config{
Repo: "TeoSlayer/pilotprotocol",
CurrentVer: "v1.10.5",
BinaryNames: []string{"pilot-daemon", "pilotctl"},
Repo: "TeoSlayer/pilotprotocol",
InstallDir: "/home/user/.pilot/bin",
Version: "v1.10.5",
CheckInterval: 1 * time.Hour,
})
u.Run(ctx)
u.Start()
```

### Pinning a version

Set `PinnedVersion` to lock the updater to a specific release tag. When
set, the updater fetches the exact release (via
`/releases/tags/{tag}`), applies it if it differs from the current
install, then idles — it will **not** chase the latest release. Clear
`PinnedVersion` (set to `""`) to resume auto-updating.

```go
u := updater.New(updater.Config{
Repo: "TeoSlayer/pilotprotocol",
InstallDir: "/home/user/.pilot/bin",
PinnedVersion: "v1.10.5", // stay on this version
})
u.Start()
```

The in-process `Service` adapter is used when embedding into the
Expand Down
82 changes: 81 additions & 1 deletion updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,15 @@ type Config struct {
Repo string // "owner/repo"
InstallDir string
Version string // updater's own version (used for user-agent)

// PinnedVersion locks the updater to a specific release tag
// (e.g. "v1.10.5"). When set, the updater installs exactly that
// version — regardless of whether it is newer, older, or already
// current — and will not chase the latest release. An empty
// string (default) preserves the existing "always follow latest"
// behaviour. Set to an empty string to un-pin and resume
// auto-updating to the latest stable.
PinnedVersion string
}

// Updater periodically checks GitHub Releases for new versions and optionally applies them.
Expand Down Expand Up @@ -125,6 +134,16 @@ func (u *Updater) checkLoop() {
func (u *Updater) checkOnce() {
slog.Debug("checking for updates")

// Pinned-version path: install a specific version regardless of
// whether it is newer or older than the current install. Once the
// pinned version is installed, subsequent ticks are no-ops until
// the pin is changed or cleared.
if u.config.PinnedVersion != "" {
u.checkPinnedVersion()
return
}

// Default path: follow the latest release.
release, err := u.fetchLatestRelease()
if err != nil {
slog.Error("failed to fetch latest release", "error", err)
Expand Down Expand Up @@ -161,8 +180,69 @@ func (u *Updater) checkOnce() {
u.touchRestartRecord()
}

// checkPinnedVersion installs the exact release specified by
// Config.PinnedVersion if it is not already installed. Unlike the
// default latest-following path, it does not compare versions — it
// fetches the named release and applies it unconditionally when the
// current install differs from the pin.
func (u *Updater) checkPinnedVersion() {
pinned, err := ParseSemver(u.config.PinnedVersion)
if err != nil {
slog.Error("invalid pinned version", "version", u.config.PinnedVersion, "error", err)
return
}

current, err := u.currentVersion()
if err != nil {
slog.Error("failed to get current version", "error", err)
return
}

if current == pinned {
slog.Info("pinned version already installed", "version", pinned.String())
return
}

slog.Info("pinned version requested, installing",
"current", current.String(),
"pinned", pinned.String(),
)

release, err := u.fetchReleaseByTag(u.config.PinnedVersion)
if err != nil {
slog.Error("failed to fetch pinned release", "tag", u.config.PinnedVersion, "error", err)
return
}

if err := u.applyUpdate(release); err != nil {
slog.Error("failed to apply pinned update", "error", err)
return
}

slog.Info("pinned version installed", "version", pinned.String())
u.touchRestartRecord()
}

func (u *Updater) fetchLatestRelease() (*GitHubRelease, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", u.config.Repo)
return u.fetchRelease("")
}

// fetchReleaseByTag fetches a specific release by its Git tag.
// Example tag: "v1.10.5".
func (u *Updater) fetchReleaseByTag(tag string) (*GitHubRelease, error) {
return u.fetchRelease(tag)
}

// fetchRelease returns the GitHub release for the given tag. If tag is
// empty it fetches the latest release.
func (u *Updater) fetchRelease(tag string) (*GitHubRelease, error) {
var url string
if tag == "" {
url = fmt.Sprintf("https://api.github.com/repos/%s/releases/latest", u.config.Repo)
} else {
url = fmt.Sprintf("https://api.github.com/repos/%s/releases/tags/%s", u.config.Repo, tag)
}

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
Expand Down
Loading
Loading