diff --git a/.github/workflows/test-update-instructions.yml b/.github/workflows/test-update-instructions.yml new file mode 100644 index 00000000..6802adb1 --- /dev/null +++ b/.github/workflows/test-update-instructions.yml @@ -0,0 +1,117 @@ +name: Test Update Instructions + +on: + pull_request: + push: + +jobs: + unit-tests: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.24.13" + - name: Run unit tests + run: go test ./packages/util/ -run TestGetUpdateInstructions -v + + integration-windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.24.13" + - name: Build test helper + run: go build -o update-hint.exe ./test/update-hint + + - name: Test scoop path detection + shell: pwsh + run: | + $scoopPath = "$env:USERPROFILE\scoop\apps\infisical\current" + New-Item -ItemType Directory -Force -Path $scoopPath | Out-Null + Copy-Item update-hint.exe "$scoopPath\update-hint.exe" + $output = & "$scoopPath\update-hint.exe" + Write-Host "Output: $output" + if ($output -notmatch "scoop update infisical") { + Write-Error "Expected 'scoop update infisical', got: $output" + exit 1 + } + + - name: Test npm path detection + shell: pwsh + run: | + $npmPath = "$env:APPDATA\npm\node_modules\@infisical\cli\bin" + New-Item -ItemType Directory -Force -Path $npmPath | Out-Null + Copy-Item update-hint.exe "$npmPath\update-hint.exe" + $output = & "$npmPath\update-hint.exe" + Write-Host "Output: $output" + if ($output -notmatch "npm update -g @infisical/cli") { + Write-Error "Expected 'npm update -g @infisical/cli', got: $output" + exit 1 + } + + - name: Test winget path detection + shell: pwsh + run: | + $wingetPath = "$env:LOCALAPPDATA\Microsoft\WinGet\Links" + New-Item -ItemType Directory -Force -Path $wingetPath | Out-Null + Copy-Item update-hint.exe "$wingetPath\update-hint.exe" + $output = & "$wingetPath\update-hint.exe" + Write-Host "Output: $output" + if ($output -notmatch "winget upgrade Infisical.Infisical") { + Write-Error "Expected 'winget upgrade Infisical.Infisical', got: $output" + exit 1 + } + + integration-macos: + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.24.13" + - name: Build test helper + run: go build -o update-hint ./test/update-hint + + - name: Test brew path detection + run: | + mkdir -p /tmp/homebrew/bin + cp update-hint /tmp/homebrew/bin/update-hint + output=$(/tmp/homebrew/bin/update-hint) + echo "Output: $output" + echo "$output" | grep -q "brew update" || (echo "Expected brew instruction, got: $output" && exit 1) + + - name: Test npm path detection (via symlink, as npm installs it) + run: | + 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 + output=$(/tmp/bin/update-hint) + echo "Output: $output" + echo "$output" | grep -q "npm update -g @infisical/cli" || (echo "Expected npm instruction, got: $output" && exit 1) + + integration-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v4 + with: + go-version: "1.24.13" + - name: Build test helper + run: go build -o update-hint ./test/update-hint + + - name: Test apt path detection + run: | + sudo cp update-hint /usr/bin/update-hint + output=$(/usr/bin/update-hint) + echo "Output: $output" + echo "$output" | grep -q "apt-get" || (echo "Expected apt-get instruction, got: $output" && exit 1) + + - name: Test npm path detection + run: | + mkdir -p $HOME/.npm-global/lib/node_modules/@infisical/cli/bin + cp update-hint $HOME/.npm-global/lib/node_modules/@infisical/cli/bin/update-hint + output=$($HOME/.npm-global/lib/node_modules/@infisical/cli/bin/update-hint) + echo "Output: $output" + echo "$output" | grep -q "npm update -g @infisical/cli" || (echo "Expected npm instruction, got: $output" && exit 1) diff --git a/packages/util/check-for-update.go b/packages/util/check-for-update.go index 3b7044e9..989841e8 100644 --- a/packages/util/check-for-update.go +++ b/packages/util/check-for-update.go @@ -9,6 +9,7 @@ import ( "net/http" "os" "os/exec" + "path/filepath" "runtime" "strings" "time" @@ -164,13 +165,41 @@ func getReleasePublishedAt(repoOwner string, repoName string, version string) (t } 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) +} + +func getUpdateInstructions(goos string, execPath string) string { + p := strings.ToLower(execPath) + isNpm := strings.Contains(p, "node_modules") + + switch goos { case "darwin": + if isNpm { + return "To update, run: npm update -g @infisical/cli" + } return "To update, run: brew update && brew upgrade infisical" case "windows": + if isNpm { + return "To update, run: npm update -g @infisical/cli" + } + if strings.Contains(p, "scoop") { + return "To update, run: scoop update infisical" + } + if strings.Contains(p, "winget") { + return "To update, run: winget upgrade Infisical.Infisical" + } return "To update, run: scoop update infisical" case "linux": + if isNpm { + return "To update, run: npm update -g @infisical/cli" + } pkgManager := getLinuxPackageManager() switch pkgManager { case "apt-get": diff --git a/packages/util/check-for-update_test.go b/packages/util/check-for-update_test.go new file mode 100644 index 00000000..802f7004 --- /dev/null +++ b/packages/util/check-for-update_test.go @@ -0,0 +1,93 @@ +package util + +import ( + "testing" +) + +func TestGetUpdateInstructions(t *testing.T) { + tests := []struct { + name string + goos string + execPath string + expected string + }{ + // darwin + { + name: "darwin brew", + goos: "darwin", + execPath: "/opt/homebrew/bin/infisical", + expected: "brew update && brew upgrade infisical", + }, + { + name: "darwin npm", + goos: "darwin", + execPath: "/opt/homebrew/lib/node_modules/@infisical/cli/bin/infisical", + expected: "npm update -g @infisical/cli", + }, + + // windows + { + name: "windows scoop", + goos: "windows", + execPath: `C:\Users\user\scoop\apps\infisical\current\infisical.exe`, + expected: "scoop update infisical", + }, + { + name: "windows npm", + goos: "windows", + execPath: `C:\Users\user\AppData\Roaming\npm\node_modules\@infisical\cli\bin\infisical.exe`, + expected: "npm update -g @infisical/cli", + }, + { + name: "windows winget", + goos: "windows", + execPath: `C:\Users\user\AppData\Local\Microsoft\WinGet\Links\infisical.exe`, + expected: "winget upgrade Infisical.Infisical", + }, + { + name: "windows unknown defaults to scoop", + goos: "windows", + execPath: `C:\infisical\infisical.exe`, + expected: "scoop update infisical", + }, + + // linux + { + name: "linux apt", + goos: "linux", + execPath: "/usr/bin/infisical", + expected: "", // apt detection is runtime — tested in integration + }, + { + name: "linux npm", + goos: "linux", + execPath: "/home/user/.npm-global/lib/node_modules/@infisical/cli/bin/infisical", + expected: "npm update -g @infisical/cli", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getUpdateInstructions(tt.goos, tt.execPath) + if tt.expected == "" { + return // skip runtime-dependent cases + } + if !contains(result, tt.expected) { + t.Errorf("expected %q to contain %q", result, tt.expected) + } + }) + } +} + +func contains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsStr(s, substr)) +} + +func containsStr(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + return false +} diff --git a/test/update-hint/main.go b/test/update-hint/main.go new file mode 100644 index 00000000..eede100a --- /dev/null +++ b/test/update-hint/main.go @@ -0,0 +1,14 @@ +// Helper binary for integration testing of update hint path detection. +// Prints the update instruction for the current executable path and OS, +// without making any network calls. +package main + +import ( + "fmt" + + "github.com/Infisical/infisical-merge/packages/util" +) + +func main() { + fmt.Println(util.GetUpdateInstructions()) +}