Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.9.1
github.com/go-mysql-org/go-mysql v1.13.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.6.8
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68=
github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w=
Expand Down
1 change: 1 addition & 0 deletions packages/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ func RootCmdStdoutWriter() io.Writer {
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the RootCmd.
func Execute() {
defer util.WaitForUpdateCheck()
err := RootCmd.Execute()
if err != nil {
os.Exit(1)
Expand Down
183 changes: 163 additions & 20 deletions packages/util/check-for-update.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,81 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"sync"
"time"

"github.com/fatih/color"
"github.com/rs/zerolog/log"
)

func CheckForUpdate() {
CheckForUpdateWithWriter(os.Stderr)
var githubHTTPClient = &http.Client{Timeout: 8 * time.Second}

var updateCheckWg sync.WaitGroup

const updateCheckCacheTTL = 24 * time.Hour

type UpdateCheckCache struct {
LastCheckTime time.Time `json:"lastCheckTime"`
LatestVersion string `json:"latestVersion"`
LatestVersionPublishedAt time.Time `json:"latestVersionPublishedAt"`
CurrentVersionPublishedAt time.Time `json:"currentVersionPublishedAt"`
IsUrgent bool `json:"isUrgent"`
Comment thread
PrestigePvP marked this conversation as resolved.
CurrentVersionAtCheck string `json:"currentVersionAtCheck"`
}

func CheckForUpdateWithWriter(w io.Writer) {
if checkEnv := os.Getenv("INFISICAL_DISABLE_UPDATE_CHECK"); checkEnv != "" {
return
}
latestVersion, _, isUrgent, err := getLatestTag("Infisical", "cli")
if err != nil {
log.Debug().Err(err)
// do nothing and continue
return

cache := readUpdateCheckCache()

displayCachedUpdateNotice(w, cache)

if !isCacheFresh(cache) {
updateCheckWg.Add(1)
go func() {
defer updateCheckWg.Done()
performUpdateCheckInBackground()
}()
}
}

if latestVersion == CLI_VERSION {
return
// WaitForUpdateCheck blocks until the background update check goroutine completes.
// Call this before program exit to ensure the cache gets written.
func WaitForUpdateCheck() {
updateCheckWg.Wait()
}

// isCacheFresh returns true if the cache is fresh enough to skip a network check.
func isCacheFresh(cache *UpdateCheckCache) bool {
if cache == nil || cache.LatestVersion == "" || cache.CurrentVersionAtCheck != CLI_VERSION {
return false
}
if cache.IsUrgent {
return false
}
return time.Since(cache.LastCheckTime) < updateCheckCacheTTL
}
Comment thread
PrestigePvP marked this conversation as resolved.

// Only prompt if the user's current version is at least 48 hours old, unless urgent.
// This avoids nagging users who recently updated.
currentVersionPublishedAt, err := getReleasePublishedAt("Infisical", "cli", CLI_VERSION)
if err == nil && !isUrgent && time.Since(currentVersionPublishedAt).Hours() < 48 {
// displayCachedUpdateNotice prints an update notification from cached data.
func displayCachedUpdateNotice(w io.Writer, cache *UpdateCheckCache) {
if cache == nil || cache.LatestVersion == "" || cache.LatestVersion == CLI_VERSION {
return
}
// Don't show stale notifications after the user has upgraded.
if cache.CurrentVersionAtCheck != CLI_VERSION {
return
}
// Unless urgent, skip notification if the current version is less than 48h old.
if !cache.IsUrgent && !cache.CurrentVersionPublishedAt.IsZero() &&
time.Since(cache.CurrentVersionPublishedAt).Hours() < 48 {
return
}

Expand All @@ -51,19 +91,122 @@ func CheckForUpdateWithWriter(w io.Writer) {
yellow("A new release of infisical is available:"),
blue(CLI_VERSION),
black("->"),
blue(latestVersion),
blue(cache.LatestVersion),
)

fmt.Fprintln(w, msg)

updateInstructions := GetUpdateInstructions()

if updateInstructions != "" {
msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions())
msg = fmt.Sprintf("\n%s\n", updateInstructions)
fmt.Fprintln(w, msg)
}
}

// performUpdateCheckInBackground fetches update info from GitHub and writes to cache.
// It is designed to be called as a fire-and-forget goroutine.
func performUpdateCheckInBackground() {
latestVersion, latestPublishedAt, isUrgent, err := getLatestTag("Infisical", "cli")
if err != nil {
log.Debug().Err(err).Msg("background update check: failed to get latest tag")
return
}

cache := &UpdateCheckCache{
LastCheckTime: time.Now(),
LatestVersion: latestVersion,
LatestVersionPublishedAt: latestPublishedAt,
IsUrgent: isUrgent,
CurrentVersionAtCheck: CLI_VERSION,
}

// If versions differ, fetch the publish date for the current version (for 48h grace).
if latestVersion != CLI_VERSION {
currentPublishedAt, err := getReleasePublishedAt("Infisical", "cli", CLI_VERSION)
if err != nil {
log.Debug().Err(err).Msg("background update check: failed to get current version publish date")
// Non-fatal — we just won't have the 48h grace period data.
} else {
cache.CurrentVersionPublishedAt = currentPublishedAt
}
}

if err := writeUpdateCheckCache(cache); err != nil {
log.Debug().Err(err).Msg("background update check: failed to write cache")
}
}

// getUpdateCheckCachePath returns the path to ~/.infisical/update-check.json.
func getUpdateCheckCachePath() (string, error) {
homeDir, err := GetHomeDir()
if err != nil {
return "", err
}
return filepath.Join(homeDir, CONFIG_FOLDER_NAME, UPDATE_CHECK_CACHE_FILE_NAME), nil
}

// readUpdateCheckCache reads and unmarshals the cache file. Returns nil on any error (cache miss).
func readUpdateCheckCache() *UpdateCheckCache {
path, err := getUpdateCheckCachePath()
if err != nil {
return nil
}

data, err := os.ReadFile(path)
if err != nil {
return nil
}

var cache UpdateCheckCache
if err := json.Unmarshal(data, &cache); err != nil {
return nil
}

return &cache
}

// writeUpdateCheckCache atomically writes the cache file using a temp file + rename.
func writeUpdateCheckCache(cache *UpdateCheckCache) error {
path, err := getUpdateCheckCachePath()
if err != nil {
return err
}

dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0700); err != nil {
return fmt.Errorf("failed to create cache directory: %w", err)
}

data, err := json.Marshal(cache)
if err != nil {
return fmt.Errorf("failed to marshal cache: %w", err)
}

tmpFile, err := os.CreateTemp(dir, "update-check-*.json.tmp")
if err != nil {
return fmt.Errorf("failed to create temp file: %w", err)
}
tmpPath := tmpFile.Name()

if _, err := tmpFile.Write(data); err != nil {
tmpFile.Close()
os.Remove(tmpPath)
return fmt.Errorf("failed to write temp file: %w", err)
}

if err := tmpFile.Close(); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to close temp file: %w", err)
}

if err := os.Rename(tmpPath, path); err != nil {
os.Remove(tmpPath)
return fmt.Errorf("failed to rename temp file: %w", err)
}

return nil
}

func DisplayAptInstallationChangeBanner(isSilent bool) {
DisplayAptInstallationChangeBannerWithWriter(isSilent, os.Stderr)
}
Expand All @@ -89,7 +232,7 @@ func DisplayAptInstallationChangeBannerWithWriter(isSilent bool, w io.Writer) {

func getLatestTag(repoOwner string, repoName string) (string, time.Time, bool, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", repoOwner, repoName)
resp, err := http.Get(url)
resp, err := githubHTTPClient.Get(url)
if err != nil {
return "", time.Time{}, false, err
}
Expand Down Expand Up @@ -132,7 +275,7 @@ func getLatestTag(repoOwner string, repoName string) (string, time.Time, bool, e
func getReleasePublishedAt(repoOwner string, repoName string, version string) (time.Time, error) {
tag := "v" + version
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/tags/%s", repoOwner, repoName, tag)
resp, err := http.Get(url)
resp, err := githubHTTPClient.Get(url)
if err != nil {
return time.Time{}, err
}
Expand Down Expand Up @@ -218,7 +361,7 @@ func IsRunningInDocker() bool {
return true
}

cgroup, err := ioutil.ReadFile("/proc/self/cgroup")
cgroup, err := os.ReadFile("/proc/self/cgroup")
if err != nil {
return false
}
Expand Down
Loading
Loading