@@ -5,41 +5,83 @@ import (
55 "errors"
66 "fmt"
77 "io"
8- "io/ioutil"
98 "net/http"
109 "os"
1110 "os/exec"
11+ "path/filepath"
1212 "runtime"
1313 "strings"
14+ "sync"
1415 "time"
1516
1617 "github.com/fatih/color"
1718 "github.com/rs/zerolog/log"
1819)
1920
20- func CheckForUpdate () {
21- CheckForUpdateWithWriter (os .Stderr )
21+ var githubHTTPClient = & http.Client {Timeout : 8 * time .Second }
22+
23+ var updateCheckWg sync.WaitGroup
24+
25+ const updateCheckCacheTTL = 24 * time .Hour
26+ const urgentUpdateCheckCacheTTL = 5 * time .Minute
27+
28+ type UpdateCheckCache struct {
29+ LastCheckTime time.Time `json:"lastCheckTime"`
30+ LatestVersion string `json:"latestVersion"`
31+ LatestVersionPublishedAt time.Time `json:"latestVersionPublishedAt"`
32+ CurrentVersionPublishedAt time.Time `json:"currentVersionPublishedAt"`
33+ IsUrgent bool `json:"isUrgent"`
34+ CurrentVersionAtCheck string `json:"currentVersionAtCheck"`
2235}
2336
2437func CheckForUpdateWithWriter (w io.Writer ) {
2538 if checkEnv := os .Getenv ("INFISICAL_DISABLE_UPDATE_CHECK" ); checkEnv != "" {
2639 return
2740 }
28- latestVersion , _ , isUrgent , err := getLatestTag ("Infisical" , "cli" )
29- if err != nil {
30- log .Debug ().Err (err )
31- // do nothing and continue
32- return
41+
42+ cache := readUpdateCheckCache ()
43+
44+ displayCachedUpdateNotice (w , cache )
45+
46+ if ! isCacheFresh (cache ) {
47+ updateCheckWg .Add (1 )
48+ go func () {
49+ defer updateCheckWg .Done ()
50+ performUpdateCheckInBackground ()
51+ }()
3352 }
53+ }
3454
35- if latestVersion == CLI_VERSION {
36- return
55+ // WaitForUpdateCheck blocks until the background update check goroutine completes.
56+ // Call this before program exit to ensure the cache gets written.
57+ func WaitForUpdateCheck () {
58+ updateCheckWg .Wait ()
59+ }
60+
61+ // isCacheFresh returns true if the cache is fresh enough to skip a network check.
62+ func isCacheFresh (cache * UpdateCheckCache ) bool {
63+ if cache == nil || cache .LatestVersion == "" || cache .CurrentVersionAtCheck != CLI_VERSION {
64+ return false
3765 }
66+ ttl := updateCheckCacheTTL
67+ if cache .IsUrgent {
68+ ttl = urgentUpdateCheckCacheTTL
69+ }
70+ return time .Since (cache .LastCheckTime ) < ttl
71+ }
3872
39- // Only prompt if the user's current version is at least 48 hours old, unless urgent.
40- // This avoids nagging users who recently updated.
41- currentVersionPublishedAt , err := getReleasePublishedAt ("Infisical" , "cli" , CLI_VERSION )
42- if err == nil && ! isUrgent && time .Since (currentVersionPublishedAt ).Hours () < 48 {
73+ // displayCachedUpdateNotice prints an update notification from cached data.
74+ func displayCachedUpdateNotice (w io.Writer , cache * UpdateCheckCache ) {
75+ if cache == nil || cache .LatestVersion == "" || cache .LatestVersion == CLI_VERSION {
76+ return
77+ }
78+ // Don't show stale notifications after the user has upgraded.
79+ if cache .CurrentVersionAtCheck != CLI_VERSION {
80+ return
81+ }
82+ // Unless urgent, skip notification if the current version is less than 48h old.
83+ if ! cache .IsUrgent && ! cache .CurrentVersionPublishedAt .IsZero () &&
84+ time .Since (cache .CurrentVersionPublishedAt ).Hours () < 48 {
4385 return
4486 }
4587
@@ -51,19 +93,122 @@ func CheckForUpdateWithWriter(w io.Writer) {
5193 yellow ("A new release of infisical is available:" ),
5294 blue (CLI_VERSION ),
5395 black ("->" ),
54- blue (latestVersion ),
96+ blue (cache . LatestVersion ),
5597 )
5698
5799 fmt .Fprintln (w , msg )
58100
59101 updateInstructions := GetUpdateInstructions ()
60-
61102 if updateInstructions != "" {
62- msg = fmt .Sprintf ("\n %s\n " , GetUpdateInstructions () )
103+ msg = fmt .Sprintf ("\n %s\n " , updateInstructions )
63104 fmt .Fprintln (w , msg )
64105 }
65106}
66107
108+ // performUpdateCheckInBackground fetches update info from GitHub and writes to cache.
109+ // It is designed to be called as a fire-and-forget goroutine.
110+ func performUpdateCheckInBackground () {
111+ latestVersion , latestPublishedAt , isUrgent , err := getLatestTag ("Infisical" , "cli" )
112+ if err != nil {
113+ log .Debug ().Err (err ).Msg ("background update check: failed to get latest tag" )
114+ return
115+ }
116+
117+ cache := & UpdateCheckCache {
118+ LastCheckTime : time .Now (),
119+ LatestVersion : latestVersion ,
120+ LatestVersionPublishedAt : latestPublishedAt ,
121+ IsUrgent : isUrgent ,
122+ CurrentVersionAtCheck : CLI_VERSION ,
123+ }
124+
125+ // If versions differ, fetch the publish date for the current version (for 48h grace).
126+ if latestVersion != CLI_VERSION {
127+ currentPublishedAt , err := getReleasePublishedAt ("Infisical" , "cli" , CLI_VERSION )
128+ if err != nil {
129+ log .Debug ().Err (err ).Msg ("background update check: failed to get current version publish date" )
130+ // Non-fatal — we just won't have the 48h grace period data.
131+ } else {
132+ cache .CurrentVersionPublishedAt = currentPublishedAt
133+ }
134+ }
135+
136+ if err := writeUpdateCheckCache (cache ); err != nil {
137+ log .Debug ().Err (err ).Msg ("background update check: failed to write cache" )
138+ }
139+ }
140+
141+ // getUpdateCheckCachePath returns the path to ~/.infisical/update-check.json.
142+ func getUpdateCheckCachePath () (string , error ) {
143+ homeDir , err := GetHomeDir ()
144+ if err != nil {
145+ return "" , err
146+ }
147+ return filepath .Join (homeDir , CONFIG_FOLDER_NAME , UPDATE_CHECK_CACHE_FILE_NAME ), nil
148+ }
149+
150+ // readUpdateCheckCache reads and unmarshals the cache file. Returns nil on any error (cache miss).
151+ func readUpdateCheckCache () * UpdateCheckCache {
152+ path , err := getUpdateCheckCachePath ()
153+ if err != nil {
154+ return nil
155+ }
156+
157+ data , err := os .ReadFile (path )
158+ if err != nil {
159+ return nil
160+ }
161+
162+ var cache UpdateCheckCache
163+ if err := json .Unmarshal (data , & cache ); err != nil {
164+ return nil
165+ }
166+
167+ return & cache
168+ }
169+
170+ // writeUpdateCheckCache atomically writes the cache file using a temp file + rename.
171+ func writeUpdateCheckCache (cache * UpdateCheckCache ) error {
172+ path , err := getUpdateCheckCachePath ()
173+ if err != nil {
174+ return err
175+ }
176+
177+ dir := filepath .Dir (path )
178+ if err := os .MkdirAll (dir , 0700 ); err != nil {
179+ return fmt .Errorf ("failed to create cache directory: %w" , err )
180+ }
181+
182+ data , err := json .Marshal (cache )
183+ if err != nil {
184+ return fmt .Errorf ("failed to marshal cache: %w" , err )
185+ }
186+
187+ tmpFile , err := os .CreateTemp (dir , "update-check-*.json.tmp" )
188+ if err != nil {
189+ return fmt .Errorf ("failed to create temp file: %w" , err )
190+ }
191+ tmpPath := tmpFile .Name ()
192+
193+ if _ , err := tmpFile .Write (data ); err != nil {
194+ tmpFile .Close ()
195+ os .Remove (tmpPath )
196+ return fmt .Errorf ("failed to write temp file: %w" , err )
197+ }
198+
199+ if err := tmpFile .Close (); err != nil {
200+ os .Remove (tmpPath )
201+ return fmt .Errorf ("failed to close temp file: %w" , err )
202+ }
203+
204+ if err := os .Rename (tmpPath , path ); err != nil {
205+ os .Remove (tmpPath )
206+ return fmt .Errorf ("failed to rename temp file: %w" , err )
207+ }
208+
209+ return nil
210+ }
211+
67212func DisplayAptInstallationChangeBanner (isSilent bool ) {
68213 DisplayAptInstallationChangeBannerWithWriter (isSilent , os .Stderr )
69214}
@@ -89,7 +234,7 @@ func DisplayAptInstallationChangeBannerWithWriter(isSilent bool, w io.Writer) {
89234
90235func getLatestTag (repoOwner string , repoName string ) (string , time.Time , bool , error ) {
91236 url := fmt .Sprintf ("https://api.github.com/repos/%s/%s/releases/latest" , repoOwner , repoName )
92- resp , err := http .Get (url )
237+ resp , err := githubHTTPClient .Get (url )
93238 if err != nil {
94239 return "" , time.Time {}, false , err
95240 }
@@ -132,7 +277,7 @@ func getLatestTag(repoOwner string, repoName string) (string, time.Time, bool, e
132277func getReleasePublishedAt (repoOwner string , repoName string , version string ) (time.Time , error ) {
133278 tag := "v" + version
134279 url := fmt .Sprintf ("https://api.github.com/repos/%s/%s/releases/tags/%s" , repoOwner , repoName , tag )
135- resp , err := http .Get (url )
280+ resp , err := githubHTTPClient .Get (url )
136281 if err != nil {
137282 return time.Time {}, err
138283 }
@@ -218,7 +363,7 @@ func IsRunningInDocker() bool {
218363 return true
219364 }
220365
221- cgroup , err := ioutil .ReadFile ("/proc/self/cgroup" )
366+ cgroup , err := os .ReadFile ("/proc/self/cgroup" )
222367 if err != nil {
223368 return false
224369 }
0 commit comments