Skip to content

Improve Update Instructions#134

Open
pangeran-bottor wants to merge 6 commits intoInfisical:mainfrom
pangeran-bottor:main
Open

Improve Update Instructions#134
pangeran-bottor wants to merge 6 commits intoInfisical:mainfrom
pangeran-bottor:main

Conversation

@pangeran-bottor
Copy link
Copy Markdown

Description 📣

Fixes incorrect update instructions shown to users based on their install method. Previously, the update hint was hardcoded per OS — for example, Windows users who installed via npm would always see scoop update infisical regardless of how they actually installed the CLI.

This PR refactors GetUpdateInstructions() to detect the install method at runtime by inspecting the executable path via os.Executable() + filepath.EvalSymlinks(). Symlink resolution is explicitly handled to cover macOS and Linux where package managers like npm create symlinks in $PATH pointing to the real binary inside node_modules.

Before:

Windows → always: scoop update infisical
macOS   → always: brew update && brew upgrade infisical

After:

Windows + npm    → npm update -g @infisical/cli
Windows + scoop  → scoop update infisical
Windows + winget → winget upgrade Infisical.Infisical
macOS + npm      → npm update -g @infisical/cli
macOS + brew     → brew update && brew upgrade infisical
Linux + npm      → npm update -g @infisical/cli
Linux + apt/yum/apk/yay → (unchanged)

Type ✨

  • Bug fix
  • New feature
  • Improvement
  • Breaking change
  • Documentation

Tests 🛠️

Added unit tests covering all install method/OS combinations, and multi-OS integration tests that build a test helper binary, place it at real install paths (including via symlink to simulate npm's behavior), and assert the correct instruction is returned.

Run unit tests:

go test ./packages/util/ -run TestGetUpdateInstructions -v

Run integration tests locally (macOS):

go build -o update-hint ./test/update-hint

# brew (default)
mkdir -p /tmp/homebrew/bin
cp update-hint /tmp/homebrew/bin/update-hint
/tmp/homebrew/bin/update-hint
# → To update, run: brew update && brew upgrade infisical

# npm via symlink (as npm installs it)
mkdir -p /tmp/node_modules/@infisical/cli/bin /tmp/bin
cp update-hint /tmp/node_modules/@infisical/cli/bin/update-hint
ln -sf /tmp/node_modules/@infisical/cli/bin/update-hint /tmp/bin/update-hint
/tmp/bin/update-hint
# → To update, run: npm update -g @infisical/cli

CI runs automatically on ubuntu-latest, macos-latest, and windows-latest via .github/workflows/test-update-instructions.yml, covering npm, brew, scoop, winget, and apt detection on their respective platforms.

Sample CI ran successfully: https://github.com/pangeran-bottor/cli/actions/runs/22317820293


@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 23, 2026

Greptile Summary

Refactors update instruction logic to detect the actual install method (npm, brew, scoop, winget, apt, etc.) by inspecting the executable path at runtime, fixing the issue where users would see incorrect update commands. Adds comprehensive unit and integration tests across all platforms.

Key changes:

  • Modified GetUpdateInstructions() to use os.Executable() and filepath.EvalSymlinks() for path-based detection
  • Added npm detection via node_modules path matching across all platforms
  • Added winget detection for Windows installations
  • New CI workflow with multi-OS integration tests covering real install path scenarios
  • Unit tests with 100% coverage of all OS/install method combinations

Issues found:

  • Invalid Go version 1.24.13 specified in all workflow jobs (should be 1.21 or similar valid version)

Confidence Score: 4/5

  • Safe to merge after fixing the invalid Go version in CI configuration
  • The refactoring is well-designed with comprehensive testing, but the invalid Go version (1.24.13) in all CI jobs needs correction before merge. The core logic is sound - using symlink resolution and path matching for install method detection is appropriate. No security issues or breaking changes identified.
  • .github/workflows/test-update-instructions.yml requires correction of the Go version

Important Files Changed

Filename Overview
.github/workflows/test-update-instructions.yml New CI workflow added to test update instruction detection across Windows, macOS, and Linux with comprehensive integration tests
packages/util/check-for-update.go Refactored update instruction logic to detect install method from executable path, with symlink resolution for npm installs

Last reviewed commit: fd824d3

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

Comment thread .github/workflows/test-update-instructions.yml
Comment thread .github/workflows/test-update-instructions.yml
Comment thread .github/workflows/test-update-instructions.yml
Comment thread .github/workflows/test-update-instructions.yml
@mxbi
Copy link
Copy Markdown

mxbi commented Apr 8, 2026

This would be great to have, it's a bit confusing for users to see instructions that they can't run every time they use infisical.

@x032205
Copy link
Copy Markdown
Member

x032205 commented Apr 16, 2026

@claude review this once

Comment on lines 165 to +176
}

func GetUpdateInstructions() string {
os := runtime.GOOS
switch os {
execPath, err := os.Executable()
if err != nil {
execPath = ""
}
if resolved, err := filepath.EvalSymlinks(execPath); err == nil {
execPath = resolved
}
return getUpdateInstructions(runtime.GOOS, execPath)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 In CheckForUpdateWithWriter, the result of GetUpdateInstructions() is stored in updateInstructions and used for the empty-string guard, but then GetUpdateInstructions() is called a second time inside the fmt.Sprintf, discarding the stored value. This is a pre-existing issue, but this PR makes it more expensive by refactoring GetUpdateInstructions() to now call os.Executable() and filepath.EvalSymlinks() on every invocation — meaning two unnecessary syscalls happen on the second call. Fix: use the updateInstructions variable in the Sprintf call instead of calling GetUpdateInstructions() again.

Extended reasoning...

The double-call bug

In CheckForUpdateWithWriter (packages/util/check-for-update.go), GetUpdateInstructions() is called at line 60 and the result is stored in the updateInstructions variable. That variable is then used correctly on line 62 for the empty-string guard (if updateInstructions != ""). However, on line 63, the code calls GetUpdateInstructions() a second time inside the fmt.Sprintf instead of using the already-stored variable:

updateInstructions := GetUpdateInstructions()  // line 60 – result stored

if updateInstructions \!= "" {                  // line 62 – stored var used correctly
    msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions())  // line 63 – called AGAIN\!
    fmt.Fprintln(w, msg)
}

The stored updateInstructions variable is effectively used only as a boolean check and never appears in the actual output.

Why existing code does not prevent it

This is a straightforward oversight: the developer wrote the guard correctly using the stored variable but forgot to reuse it in the format string one line later. No compiler warning or linter would catch this because the variable is used (in the if-condition), just not where it matters most.

Impact made worse by this PR

Before this PR, GetUpdateInstructions() was a trivial GOOS switch — essentially a compile-time constant lookup with no I/O. The redundant second call had negligible cost.

After this PR, GetUpdateInstructions() delegates to the new public wrapper that calls os.Executable() (a syscall) and filepath.EvalSymlinks() (which may involve multiple stat/readlink syscalls for each path component). These are cheap in absolute terms, but calling them twice per update check is purely wasteful — the result is deterministic during a single process invocation, so there is no reason to repeat the work.

Concrete proof

  1. User runs a version of infisical older than the latest release.
  2. CheckForUpdateWithWriter is called.
  3. GetUpdateInstructions() is invoked at line 60: os.Executable() + filepath.EvalSymlinks() are called; result, e.g., "To update, run: brew update && brew upgrade infisical", is stored in updateInstructions.
  4. The if-guard on line 62 passes (non-empty string).
  5. GetUpdateInstructions() is invoked again at line 63: os.Executable() + filepath.EvalSymlinks() are called a second time; the same string is returned and embedded in msg.
  6. The variable updateInstructions from step 3 is never used in the output.

How to fix

Change line 63 from:

msg = fmt.Sprintf("\n%s\n", GetUpdateInstructions())

to:

msg = fmt.Sprintf("\n%s\n", updateInstructions)

This is a one-word change with no behavioral difference, eliminating the redundant syscalls.

Comment on lines 183 to 187
case "darwin":
if isNpm {
return "To update, run: npm update -g @infisical/cli"
}
return "To update, run: brew update && brew upgrade infisical"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 The darwin case in getUpdateInstructions() only checks for npm via a node_modules path substring, then unconditionally returns brew instructions for all other macOS installs — so users who installed via MacPorts, Nix, direct binary download, or any non-brew/non-npm method will see incorrect brew update instructions even if Homebrew is not installed. This is a pre-existing limitation the PR explicitly chose to maintain, but unlike Windows (which checks for scoop/winget path substrings), macOS has no homebrew path check before recommending brew.

Extended reasoning...

What the bug is: The getUpdateInstructions() darwin case (lines 183-187 of packages/util/check-for-update.go) performs exactly one check — whether the resolved executable path contains node_modules — and if not, unconditionally returns brew update instructions. There is no verification that the binary actually lives under a Homebrew prefix (e.g. /opt/homebrew, /usr/local/Cellar).

The specific code path:
The darwin switch arm checks isNpm first, and if false, immediately returns 'brew update && brew upgrade infisical' with no further path inspection. Any macOS user whose executable path does not contain node_modules falls straight through to the brew instruction, regardless of actual install method.

Why existing code does not prevent it: The PR intentionally chose not to add a homebrew path check for macOS, as explicitly documented in the PR description (macOS + brew -> brew update && brew upgrade infisical is the stated 'After' behavior). The previous code also defaulted to brew for all macOS installs; this PR improves only the npm detection case. This makes it a pre-existing design decision that the PR deliberately preserves.

Addressing the refutation: The refutation correctly notes that this is intentional behavior documented in the PR, that brew IS the dominant macOS install method, and that the PR does not make the situation worse for non-npm users — it only improves things. These points are valid. However, the inconsistency is still worth noting as a nit: Windows gets path-based detection for scoop (contains 'scoop') and winget (contains 'winget'), but macOS gets no analogous homebrew path check. Users who installed via MacPorts, Nix, pkgx, or direct binary download will see instructions they cannot run.

Step-by-step proof: A user installs infisical via nix-env -iA nixpkgs.infisical. The binary resolves to something like /nix/store/abc123-infisical/bin/infisical. The variable p is set to this lowercased path. It does not contain node_modules, so isNpm is false. The darwin branch falls through and returns 'To update, run: brew update && brew upgrade infisical'. The user may not have Homebrew installed at all, so this instruction is useless to them.

How to fix it: Mirror the Windows approach by checking for a Homebrew path prefix before defaulting. For example, check if the path contains '/homebrew/' or '/cellar/' before returning brew instructions, and return an empty string or generic message for unrecognized paths. This would be consistent with the pattern already established for Windows and avoids misleading non-brew macOS users.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants