Skip to content

Commit 81f957c

Browse files
committed
fix: cli auto update
1 parent 3d411fb commit 81f957c

File tree

15 files changed

+773
-6
lines changed

15 files changed

+773
-6
lines changed

.github/workflows/ci.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Go
16+
uses: actions/setup-go@v5
17+
with:
18+
go-version: "1.23"
19+
20+
- name: Build
21+
run: go build -v ./...
22+
23+
- name: Test
24+
run: go test -v ./...
25+
26+
lint:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- name: Set up Go
32+
uses: actions/setup-go@v5
33+
with:
34+
go-version: "1.23"
35+
36+
- name: golangci-lint
37+
uses: golangci/golangci-lint-action@v4
38+
with:
39+
version: latest

.github/workflows/release.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
permissions:
9+
contents: write
10+
11+
jobs:
12+
release:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Set up Go
20+
uses: actions/setup-go@v5
21+
with:
22+
go-version: "1.23"
23+
24+
- name: Run GoReleaser
25+
uses: goreleaser/goreleaser-action@v5
26+
with:
27+
distribution: goreleaser
28+
version: latest
29+
args: release --clean
30+
env:
31+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.goreleaser.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
version: 2
2+
3+
project_name: push-validator
4+
5+
before:
6+
hooks:
7+
- go mod tidy
8+
9+
builds:
10+
- id: push-validator
11+
main: ./cmd/push-validator
12+
binary: push-validator
13+
env:
14+
- CGO_ENABLED=0
15+
goos:
16+
- linux
17+
- darwin
18+
goarch:
19+
- amd64
20+
- arm64
21+
ldflags:
22+
- -s -w
23+
- -X main.Version={{.Version}}
24+
- -X main.Commit={{.ShortCommit}}
25+
- -X main.BuildDate={{.Date}}
26+
27+
archives:
28+
- id: default
29+
format: tar.gz
30+
name_template: >-
31+
{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}
32+
files:
33+
- LICENSE*
34+
- README*
35+
36+
checksum:
37+
name_template: "checksums.txt"
38+
algorithm: sha256
39+
40+
changelog:
41+
sort: asc
42+
filters:
43+
exclude:
44+
- "^docs:"
45+
- "^test:"
46+
- "^chore:"
47+
48+
release:
49+
github:
50+
owner: pushchain
51+
name: push-validator-cli
52+
draft: false
53+
prerelease: auto
54+
name_template: "v{{.Version}}"

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ push-validator balance # Check balance (defaults to validator key)
106106
push-validator reset # Reset chain data (keeps address book)
107107
push-validator full-reset # ⚠️ Complete reset (deletes ALL keys and data)
108108
push-validator backup # Backup config and validator state
109+
push-validator update # Update CLI to latest version
109110
```
110111

111112
## ⚡ Features
@@ -124,6 +125,21 @@ push-validator backup # Backup config and validator state
124125
- **Explorer**: https://donut.push.network
125126

126127

128+
## 🔄 Updates
129+
130+
The CLI automatically checks for updates and notifies you:
131+
- **Dashboard**: Shows notification in header when update available
132+
- **CLI commands**: Shows notification after command completes
133+
134+
### Manual Update
135+
```bash
136+
push-validator update # Update to latest version
137+
push-validator update --check # Check only, don't install
138+
push-validator update --version v1.2.0 # Install specific version
139+
```
140+
141+
Updates download pre-built binaries from GitHub Releases with checksum verification.
142+
127143
## 🔧 Advanced Setup (Optional)
128144

129145
### Setup NGINX with SSL

cmd/push-validator/cmd_dashboard.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ to a static text snapshot.`,
4848
NoColor: flagNoColor,
4949
NoEmoji: flagNoEmoji,
5050
Debug: debugMode,
51+
CLIVersion: Version,
5152
}
5253
opts = normalizeDashboardOptions(opts)
5354

cmd/push-validator/root_cobra.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/pushchain/push-validator-cli/internal/process"
2424
syncmon "github.com/pushchain/push-validator-cli/internal/sync"
2525
ui "github.com/pushchain/push-validator-cli/internal/ui"
26+
"github.com/pushchain/push-validator-cli/internal/update"
2627
"github.com/pushchain/push-validator-cli/internal/validator"
2728
)
2829

@@ -36,6 +37,9 @@ var (
3637
// rootCmd wires the CLI surface using Cobra. Persistent flags are
3738
// applied to a loaded config in loadCfg(). Subcommands implement the
3839
// actual operations (init, start/stop, sync, status, etc.).
40+
// updateCheckResult stores the result of background update check
41+
var updateCheckResult *update.CheckResult
42+
3943
var rootCmd = &cobra.Command{
4044
Use: "push-validator",
4145
Short: "Push Validator",
@@ -51,6 +55,20 @@ var rootCmd = &cobra.Command{
5155
Quiet: flagQuiet,
5256
Debug: flagDebug,
5357
})
58+
59+
// Start background update check (non-blocking)
60+
// Skip for update command itself and help/version commands
61+
cmdName := cmd.Name()
62+
if cmdName != "update" && cmdName != "help" && cmdName != "version" {
63+
go checkForUpdateBackground()
64+
}
65+
},
66+
PersistentPostRun: func(cmd *cobra.Command, args []string) {
67+
// Show update notification if available (after command completes)
68+
// Skip for update command itself
69+
if cmd.Name() != "update" && updateCheckResult != nil && updateCheckResult.UpdateAvailable {
70+
showUpdateNotification(updateCheckResult.LatestVersion)
71+
}
5472
},
5573
}
5674

@@ -850,6 +868,7 @@ func handleDashboard(cfg config.Config) error {
850868
RPCTimeout: 5 * time.Second,
851869
NoColor: flagNoColor,
852870
NoEmoji: flagNoEmoji,
871+
CLIVersion: Version,
853872
Debug: false,
854873
}
855874
return runDashboardInteractive(opts)
@@ -938,3 +957,69 @@ func isTerminalInteractive() bool {
938957
}
939958
return true
940959
}
960+
961+
// checkForUpdateBackground performs a non-blocking update check.
962+
// Uses cache to avoid checking more than once per 24 hours.
963+
// Stores result in updateCheckResult global for use by PersistentPostRun.
964+
func checkForUpdateBackground() {
965+
cfg := loadCfg()
966+
967+
// Check cache first (avoid network calls if recently checked)
968+
cache, err := update.LoadCache(cfg.HomeDir)
969+
if err == nil && update.IsCacheValid(cache) {
970+
// Use cached result
971+
if cache.UpdateAvailable {
972+
updateCheckResult = &update.CheckResult{
973+
CurrentVersion: strings.TrimPrefix(Version, "v"),
974+
LatestVersion: cache.LatestVersion,
975+
UpdateAvailable: true,
976+
}
977+
}
978+
return
979+
}
980+
981+
// Perform network check with timeout
982+
updater, err := update.NewUpdater(Version)
983+
if err != nil {
984+
return // Silently fail - don't disrupt user's command
985+
}
986+
987+
result, err := updater.Check()
988+
if err != nil {
989+
return // Silently fail
990+
}
991+
992+
// Save to cache
993+
_ = update.SaveCache(cfg.HomeDir, &update.CacheEntry{
994+
CheckedAt: time.Now(),
995+
LatestVersion: result.LatestVersion,
996+
UpdateAvailable: result.UpdateAvailable,
997+
})
998+
999+
// Store result for notification
1000+
if result.UpdateAvailable {
1001+
updateCheckResult = result
1002+
}
1003+
}
1004+
1005+
// showUpdateNotification displays an update notification after command completes.
1006+
func showUpdateNotification(latestVersion string) {
1007+
// Don't show in JSON/YAML output modes
1008+
if flagOutput == "json" || flagOutput == "yaml" {
1009+
return
1010+
}
1011+
1012+
// Don't show in quiet mode
1013+
if flagQuiet {
1014+
return
1015+
}
1016+
1017+
c := ui.NewColorConfig()
1018+
c.Enabled = c.Enabled && !flagNoColor
1019+
1020+
fmt.Println()
1021+
fmt.Println(c.Warning("─────────────────────────────────────────────────────────────"))
1022+
fmt.Printf(c.Warning(" Update available: %s → %s\n"), Version, latestVersion)
1023+
fmt.Println(c.Info(" Run: push-validator update"))
1024+
fmt.Println(c.Warning("─────────────────────────────────────────────────────────────"))
1025+
}

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module github.com/pushchain/push-validator-cli
22

3-
go 1.23.0
4-
5-
toolchain go1.23.11
3+
go 1.24.0
64

75
require (
86
github.com/cespare/xxhash/v2 v2.3.0
@@ -43,6 +41,7 @@ require (
4341
github.com/tklauser/numcpus v0.6.1 // indirect
4442
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
4543
github.com/yusufpapurcu/wmi v1.2.4 // indirect
44+
golang.org/x/mod v0.32.0 // indirect
4645
golang.org/x/net v0.17.0 // indirect
4746
golang.org/x/sync v0.11.0 // indirect
4847
golang.org/x/sys v0.30.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
8484
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
8585
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
8686
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
87+
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
88+
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
8789
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
8890
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
8991
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=

internal/dashboard/dashboard.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/pushchain/push-validator-cli/internal/metrics"
1818
"github.com/pushchain/push-validator-cli/internal/node"
1919
"github.com/pushchain/push-validator-cli/internal/process"
20+
"github.com/pushchain/push-validator-cli/internal/update"
2021
"github.com/pushchain/push-validator-cli/internal/validator"
2122
)
2223

@@ -570,7 +571,7 @@ func (m *Dashboard) fetchCmd() tea.Cmd {
570571

571572
// fetchData does the actual blocking I/O (called from fetchCmd)
572573
func (m *Dashboard) fetchData(ctx context.Context) (DashboardData, error) {
573-
data := DashboardData{LastUpdate: time.Now()}
574+
data := DashboardData{LastUpdate: time.Now(), CLIVersion: m.opts.CLIVersion}
574575

575576
// Use persistent collector for continuous CPU monitoring
576577
data.Metrics = m.collector.Collect(ctx, m.opts.Config.RPCLocal, m.opts.Config.GenesisDomain)
@@ -667,6 +668,12 @@ func (m *Dashboard) fetchData(ctx context.Context) (DashboardData, error) {
667668
}
668669
}
669670

671+
// Check for CLI update (uses cache, no network call)
672+
if cache, err := update.LoadCache(m.opts.Config.HomeDir); err == nil && cache.UpdateAvailable {
673+
data.UpdateInfo.Available = true
674+
data.UpdateInfo.LatestVersion = cache.LatestVersion
675+
}
676+
670677
return data, nil
671678
}
672679

internal/dashboard/header.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,37 @@ func (c *Header) View(w, h int) string {
6969
// Apply bold + cyan highlighting to title
7070
titleStyled := FormatTitle(c.Title(), inner)
7171

72+
// Add version after title (dimmed)
73+
if c.data.CLIVersion != "" {
74+
versionStyle := lipgloss.NewStyle().
75+
Foreground(lipgloss.Color("241")) // Dim gray
76+
titleStyled = titleStyled + " " + versionStyle.Render("v"+strings.TrimPrefix(c.data.CLIVersion, "v"))
77+
}
78+
79+
// Build the title line with optional update notification
80+
var titleLine string
81+
if c.data.UpdateInfo.Available && c.data.UpdateInfo.LatestVersion != "" {
82+
// Style for update notification
83+
updateStyle := lipgloss.NewStyle().
84+
Foreground(lipgloss.Color("226")). // Yellow/gold
85+
Bold(true)
86+
updateText := updateStyle.Render(fmt.Sprintf("⬆ Update v%s available", c.data.UpdateInfo.LatestVersion))
87+
88+
// Calculate spacing to push update text to the right
89+
titleLen := lipgloss.Width(titleStyled)
90+
updateLen := lipgloss.Width(updateText)
91+
spacing := inner - titleLen - updateLen
92+
if spacing < 2 {
93+
spacing = 2
94+
}
95+
96+
titleLine = titleStyled + strings.Repeat(" ", spacing) + updateText
97+
} else {
98+
titleLine = titleStyled
99+
}
100+
72101
var lines []string
73-
lines = append(lines, titleStyled)
102+
lines = append(lines, titleLine)
74103

75104
if c.data.Err != nil {
76105
errLine := fmt.Sprintf("⚠ %s", c.data.Err.Error())

0 commit comments

Comments
 (0)