Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
53 changes: 26 additions & 27 deletions cli/azd/pkg/update/msi_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,35 +150,34 @@ func isStandardMSIInstall() error {
return nil
}

// versionFlag returns the install script parameter value for the given channel.
func versionFlag(channel Channel) string {
// buildInstallScriptArgs constructs the PowerShell arguments to run install-azd.ps1.
//
// For the stable channel the script is piped directly through Invoke-Expression.
// No named parameters are needed because the default MSI install location is correct.
//
// For other channels (e.g. daily) the script must be downloaded to a temp directory
// first, because Invoke-Expression from a pipe does not support passing named
// parameters such as -Version or -InstallFolder to the script.
//
// installDir is the target installation directory (e.g. %LOCALAPPDATA%\Programs\Azure Dev CLI).
Comment thread
hemarina marked this conversation as resolved.
Outdated
//
// Returns the arguments to pass to the "powershell" command.
Comment thread
hemarina marked this conversation as resolved.
func buildInstallScriptArgs(channel Channel) []string {
switch channel {
case ChannelDaily:
return "daily"
case ChannelStable:
return "stable"
script := fmt.Sprintf(
"$tmpDir = Join-Path $env:TEMP 'azd-install.ps1'; "+
"Invoke-RestMethod '%s' -OutFile $tmpDir; "+
"& $tmpDir -Version 'daily' -InstallFolder '%s'; "+
"Remove-Item $tmpDir -Force -ErrorAction SilentlyContinue",
Comment thread
hemarina marked this conversation as resolved.
Outdated
installScriptURL, expectedPerUserInstallDir(),
)
return []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script}
default:
Comment thread
hemarina marked this conversation as resolved.
return "stable"
script := fmt.Sprintf(
"Invoke-RestMethod '%s' | Invoke-Expression",
installScriptURL,
)
return []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script}
Comment thread
hemarina marked this conversation as resolved.
Outdated
}
}

// buildInstallScriptArgs constructs the PowerShell arguments to download and run
// install-azd.ps1 with the appropriate -Version flag.
// The -SkipVerify flag is passed because Authenticode verification via
// Get-AuthenticodeSignature failed.
// The MSI is already downloaded over HTTPS from a Microsoft-controlled domain,
// so the transport-level integrity is sufficient.
// Returns the arguments to pass to the "powershell" command.
func buildInstallScriptArgs(channel Channel) []string {
version := versionFlag(channel)
// Download the script to a temp file, then invoke it with the appropriate -Version flag.
// Using -ExecutionPolicy Bypass ensures the script runs even if the system policy is restrictive.
script := fmt.Sprintf(
`$script = Join-Path $env:TEMP 'install-azd.ps1'; `+
`Invoke-RestMethod '%s' -OutFile $script; `+
`& $script -Version '%s' -SkipVerify; `+
`Remove-Item $script -Force -ErrorAction SilentlyContinue`,
installScriptURL, version,
)
return []string{"-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script}
}
65 changes: 38 additions & 27 deletions cli/azd/pkg/update/msi_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,31 +42,16 @@ func TestExpectedPerUserInstallDir(t *testing.T) {
}
}

func TestVersionFlag(t *testing.T) {
tests := []struct {
name string
channel Channel
want string
}{
{"stable channel", ChannelStable, "stable"},
{"daily channel", ChannelDaily, "daily"},
{"unknown defaults to stable", Channel("nightly"), "stable"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := versionFlag(tt.channel)
require.Equal(t, tt.want, got)
})
}
}

func TestBuildInstallScriptArgs(t *testing.T) {
t.Setenv("LOCALAPPDATA", `C:\Users\testuser\AppData\Local`)
expectedDir := expectedPerUserInstallDir()

tests := []struct {
name string
channel Channel
// We check that certain substrings appear in the constructed args
wantContains []string
wantContains []string
wantNotContains []string
}{
{
name: "stable",
Expand All @@ -76,8 +61,12 @@ func TestBuildInstallScriptArgs(t *testing.T) {
"-ExecutionPolicy", "Bypass",
"-Command",
installScriptURL,
"-Version 'stable'",
"-SkipVerify",
"Invoke-Expression",
},
wantNotContains: []string{
"-Version",
"-InstallFolder",
Comment thread
hemarina marked this conversation as resolved.
"Remove-Item",
},
},
{
Expand All @@ -89,7 +78,9 @@ func TestBuildInstallScriptArgs(t *testing.T) {
"-Command",
installScriptURL,
"-Version 'daily'",
"-SkipVerify",
"-InstallFolder",
expectedDir,
"Remove-Item",
},
},
}
Expand All @@ -105,26 +96,46 @@ func TestBuildInstallScriptArgs(t *testing.T) {
for _, s := range tt.wantContains {
require.Contains(t, joined, s, "expected args to contain %q", s)
}
for _, s := range tt.wantNotContains {
require.NotContains(t, joined, s, "expected args NOT to contain %q", s)
}
})
}
}

func TestBuildInstallScriptArgs_Structure(t *testing.T) {
t.Setenv("LOCALAPPDATA", `C:\Users\testuser\AppData\Local`)
expectedDir := expectedPerUserInstallDir()

args := buildInstallScriptArgs(ChannelStable)

// The args should be: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", <script>]
// Stable: ["-NoProfile", "-ExecutionPolicy", "AllSigned", "-Command", <script>]
require.Equal(t, 5, len(args), "expected exactly 5 args")
require.Equal(t, "-NoProfile", args[0])
require.Equal(t, "-ExecutionPolicy", args[1])
require.Equal(t, "Bypass", args[2])
Comment thread
hemarina marked this conversation as resolved.
Outdated
require.Equal(t, "-Command", args[3])

// The script (args[4]) should be a single string containing the full PowerShell pipeline
// Stable pipes directly — no temp file download
script := args[4]
require.Contains(t, script, "Invoke-RestMethod")
require.Contains(t, script, installScriptURL)
require.Contains(t, script, "-SkipVerify")
require.Contains(t, script, "Remove-Item")
require.Contains(t, script, "Invoke-Expression")
require.NotContains(t, script, "-InstallFolder")
require.NotContains(t, script, "Remove-Item")
require.NotContains(t, script, "-Version")

// Daily downloads to temp file with -Version 'daily'
argsDaily := buildInstallScriptArgs(ChannelDaily)
require.Equal(t, 5, len(argsDaily))
require.Equal(t, "Bypass", argsDaily[2])
scriptDaily := argsDaily[4]
require.Contains(t, scriptDaily, "Invoke-RestMethod")
require.Contains(t, scriptDaily, installScriptURL)
require.Contains(t, scriptDaily, "-Version 'daily'")
require.Contains(t, scriptDaily, "-InstallFolder")
require.Contains(t, scriptDaily, expectedDir)
require.Contains(t, scriptDaily, "Remove-Item")
}

func TestIsStandardMSIInstall_StandardPath(t *testing.T) {
Expand Down
Loading