closes #130 - CHANGELOG.md file for people who can view changelog from github --- `iris changelog` command: `iris changelog [version]` (e.g. iris changelog v0.5.1) `iris changelog -n 3` (see changelog of 3 latest releaseas) => `iris changelog` looks just like CHANGELOG.md, grouped headers and all. It only stops looking that way if we personally overwrite that release's description with something else => With 1-hour local cache (invalidated on updater-channel change), `--refresh` flag to bypass, graceful fallback to stale cache on GitHub rate-limiting --- - New auto update config ```toml # 0 = off (default, notify only), 1 = auto-install, 2 = always confirm first auto-update = 1 ``` - New just debug-changelog and just debug-autoupdate recipes (fully offline, mocked HTTP servers, no real network/config touched)
174 lines
4.5 KiB
Go
174 lines
4.5 KiB
Go
package root
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/versenilvis/iris/internal/config"
|
|
)
|
|
|
|
const installScriptURL = "https://raw.githubusercontent.com/versenilvis/iris/main/scripts/install.sh"
|
|
|
|
func resolveInstallScriptURL() string {
|
|
if url := os.Getenv("IRIS_INSTALL_URL"); url != "" {
|
|
return url
|
|
}
|
|
return installScriptURL
|
|
}
|
|
|
|
// non-interactive output is captured, not streamed - the wrapper's terminal
|
|
// is in raw mode, so streaming installer output would corrupt the display
|
|
func performUpdate(latest string, interactive bool) (output string, err error) {
|
|
ctx := context.Background()
|
|
if !interactive {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(ctx, 5*time.Minute)
|
|
defer cancel()
|
|
}
|
|
|
|
cmdRun := exec.CommandContext(ctx, "sh", "-c", "curl -sSL "+resolveInstallScriptURL()+" | sh")
|
|
if config.Get().Updater.Channel == "nightly" {
|
|
cmdRun.Env = append(os.Environ(), "IRIS_RELEASE_TAG="+latest)
|
|
}
|
|
|
|
if interactive {
|
|
cmdRun.Stdout = os.Stdout
|
|
cmdRun.Stderr = os.Stderr
|
|
cmdRun.Stdin = os.Stdin
|
|
return "", cmdRun.Run()
|
|
}
|
|
|
|
out, runErr := cmdRun.CombinedOutput()
|
|
return string(out), runErr
|
|
}
|
|
|
|
type autoUpdateAction int
|
|
|
|
const (
|
|
autoUpdateNotifyOnly autoUpdateAction = iota
|
|
autoUpdateInstallSilently
|
|
autoUpdateConfirm
|
|
autoUpdateGiveUp
|
|
)
|
|
|
|
// mode 1 escalates per target version: attempt 1 installs silently, attempt
|
|
// 2 asks first, attempt 3+ gives up. mode 2 always asks, never gives up
|
|
func decideAutoUpdateAction(mode int, latest string, st config.UpdaterState) autoUpdateAction {
|
|
if mode == 0 {
|
|
return autoUpdateNotifyOnly
|
|
}
|
|
if latest == st.DeclinedVersion {
|
|
return autoUpdateNotifyOnly
|
|
}
|
|
if mode == 2 {
|
|
return autoUpdateConfirm
|
|
}
|
|
|
|
attempt := st.AutoUpdateAttempt
|
|
if st.AutoUpdateTarget != latest {
|
|
attempt = 0
|
|
}
|
|
switch attempt {
|
|
case 0:
|
|
return autoUpdateInstallSilently
|
|
case 1:
|
|
return autoUpdateConfirm
|
|
default:
|
|
return autoUpdateGiveUp
|
|
}
|
|
}
|
|
|
|
var (
|
|
pendingAutoUpdateConfirm atomic.Bool
|
|
pendingAutoUpdateVersion atomic.Value
|
|
)
|
|
|
|
func armAutoUpdateConfirm(version string) {
|
|
pendingAutoUpdateVersion.Store(version)
|
|
pendingAutoUpdateConfirm.Store(true)
|
|
}
|
|
|
|
// reports whether the byte was consumed - callers must not fall through
|
|
// to normal key handling when true
|
|
func handleAutoUpdateConfirmKey(b byte) bool {
|
|
if !pendingAutoUpdateConfirm.Load() {
|
|
return false
|
|
}
|
|
|
|
switch b {
|
|
case 'y', 'Y':
|
|
pendingAutoUpdateConfirm.Store(false)
|
|
version, _ := pendingAutoUpdateVersion.Load().(string)
|
|
go runConfirmedAutoUpdate(version)
|
|
case 'n', 'N', 0x1b, 0x03: // n, N, Esc, Ctrl+C
|
|
pendingAutoUpdateConfirm.Store(false)
|
|
version, _ := pendingAutoUpdateVersion.Load().(string)
|
|
declineAutoUpdate(version)
|
|
}
|
|
return true
|
|
}
|
|
|
|
func runConfirmedAutoUpdate(version string) {
|
|
if version == "" {
|
|
return
|
|
}
|
|
writeStdout([]byte("\r\033[K\033[36m[IRIS] updating...\033[0m\n"))
|
|
|
|
if _, err := performUpdate(version, false); err != nil {
|
|
writeStdout(fmt.Appendf(nil, "\033[31m[IRIS] update failed: %v\033[0m\n", err))
|
|
return
|
|
}
|
|
|
|
state := config.LoadState()
|
|
state.Updater.AutoUpdateTarget = ""
|
|
state.Updater.AutoUpdateAttempt = 0
|
|
state.Updater.SeenVersion = ""
|
|
_ = config.SaveState(state)
|
|
|
|
writeStdout(fmt.Appendf(nil,
|
|
"\033[32m[IRIS] updated to %s, restart your terminal to use it\033[0m\n", version,
|
|
))
|
|
}
|
|
|
|
func declineAutoUpdate(version string) {
|
|
if version == "" {
|
|
return
|
|
}
|
|
state := config.LoadState()
|
|
state.Updater.DeclinedVersion = version
|
|
state.Updater.AutoUpdateTarget = ""
|
|
state.Updater.AutoUpdateAttempt = 0
|
|
_ = config.SaveState(state)
|
|
|
|
writeStdout([]byte("\r\033[K\033[33m[IRIS] update declined, run `iris update` any time to install manually\033[0m\n"))
|
|
}
|
|
|
|
func printAutoUpdateInstalledNotice(latest string) {
|
|
writeStdout(fmt.Appendf(nil,
|
|
"\r\033[K\033[32m[IRIS] auto-updated %s → %s\033[0m\n\033[32m[IRIS] restart your terminal to use the new version\033[0m\n",
|
|
Version, latest,
|
|
))
|
|
}
|
|
|
|
func printAutoUpdateConfirmPrompt(latest, notes string) {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "\r\033[K\033[33m[IRIS] new version %s → %s available\033[0m\n", Version, latest)
|
|
for _, line := range changelogSummaryLines(notes, 2) {
|
|
fmt.Fprintf(&b, "\033[33m - %s\033[0m\n", line)
|
|
}
|
|
fmt.Fprint(&b, "\033[33minstall now? [y/N] \033[0m")
|
|
writeStdout([]byte(b.String()))
|
|
}
|
|
|
|
func printAutoUpdateGiveUpNotice(latest string) {
|
|
writeStdout(fmt.Appendf(nil,
|
|
"\r\033[K\033[33m[IRIS] could not auto-update to %s after repeated attempts, run \033[1miris update\033[0m\033[33m to install manually\033[0m\n",
|
|
latest,
|
|
))
|
|
}
|