Skip to content
Open
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
13 changes: 13 additions & 0 deletions .github/workflows/release_build_infisical_cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ name: Build and release CLI

on:
workflow_dispatch:
inputs:
is_urgent:
description: "Mark this release as urgent (bypasses 48h grace period for update notifications)"
type: boolean
default: false
Comment on lines +5 to +9
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is how we use the dispatch here. This workflow is actually not made to be used with dispatch because we need the tag; we use it in the workflow to create the image names and all that.

If we dispatch with is_urgent here, we need to select a new tag, but we can't create a new tag when we run a workflow manually (only when creating a release), which means that we would need to create the tag first, but if we create the tag first, this workflow would run without the is_urgent and that's our lock here.

For this case, I think a new workflow just to mark a release as urgent would make more sense. We can follow the same release process we have now, and after it, we can run another workflow against the tag, like in the image below, to mark it as urgent (which would basically update the description of the release)

Image

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, yeah, I've never used that in github before so I thought it might just work but I see the issue.


push:
# run only against tags
Expand Down Expand Up @@ -123,6 +128,14 @@ jobs:
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- name: Mark release as urgent
if: ${{ inputs.is_urgent == true }}
env:
GH_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
run: |
CURRENT_BODY=$(gh release view "${{ github.ref_name }}" --json body -q .body)
gh release edit "${{ github.ref_name }}" --notes "${CURRENT_BODY}
<!-- #urgent -->"
- uses: actions/setup-python@v4
with:
python-version: "3.12"
Expand Down
2 changes: 1 addition & 1 deletion e2e/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ require (
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/glog v1.2.5 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.4 // indirect
Expand Down
4 changes: 2 additions & 2 deletions e2e/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -379,8 +379,8 @@ github.com/gogo/protobuf v1.0.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
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.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
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-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/glog v1.2.5 h1:DrW6hGnjIhtvhOIiAKT6Psh/Kd/ldepEa81DKeiRJ5I=
Expand Down
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
185 changes: 165 additions & 20 deletions packages/util/check-for-update.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,41 +5,83 @@ 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
const urgentUpdateCheckCacheTTL = 5 * time.Minute

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"`
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
}
ttl := updateCheckCacheTTL
if cache.IsUrgent {
ttl = urgentUpdateCheckCacheTTL
}
return time.Since(cache.LastCheckTime) < ttl
}

// 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 +93,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 +234,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 +277,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 +363,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