Repository: hashicorp/packer (Go)

Issue:
PowerShell provisioner silently ignores some types of errors
Packer v1.0.0 on Windows 10 Enterprise 15063.296.

Building a Windows Server 2016 Standard image on Hyper-V.

The following provisioner contains invalid PowerShell commands. I would expect the build to fail. However, these errors are silently ignored.

```
    {
      "type": "powershell",
      "inline": "this is not a valid powershell command"
    }
```

Debug log snippet:

```
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 [DEBUG] connecting to remote shell using WinRM
2017/05/23 09:47:38 ui: ==> hyperv-iso: Connected to WinRM!
==> hyperv-iso: Connected to WinRM!
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Running the provision hook
==> hyperv-iso: Provisioning with Powershell...
2017/05/23 09:47:38 ui: ==> hyperv-iso: Provisioning with Powershell...
==> hyperv-iso: Provisioning with shell script: C:\Users\saares\AppData\Local\Temp\packer-powershell-provisioner887540923
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Found command: this is not a valid powershell command
2017/05/23 09:47:38 ui: ==> hyperv-iso: Provisioning with shell script: C:\Users\saares\AppData\Local\Temp\packer-powershell-provisioner887540923
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Opening C:\Users\saares\AppData\Local\Temp\packer-powershell-provisioner887540923 for reading
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Building command line for: if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};$env:PACKER_BUILDER_TYPE="hyperv-iso"; $env:PACKER_BUILD_NAME="hyperv-iso"; &'c:/Windows/Temp/script.ps1';exit $LastExitCode
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 [INFO] 39 bytes written for 'uploadData'
2017/05/23 09:47:38 [INFO] 39 bytes written for 'uploadData'
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Uploading file to 'c:/Windows/Temp/script.ps1'
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Copying file to $env:TEMP\winrmcp-848ef57a-37f2-474c-60ab-df80537f7458.tmp
2017/05/23 09:47:38 packer.exe: 2017/05/23 09:47:38 Moving file from $env:TEMP\winrmcp-848ef57a-37f2-474c-60ab-df80537f7458.tmp to c:\Windows\Temp\script.ps1
2017/05/23 09:47:41 packer.exe: #< CLIXML
2017/05/23 09:47:41 packer.exe: <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj></Objs>2017/05/23 09:47:41 Removing temporary file $env:TEMP\winrmcp-848ef57a-37f2-474c-60ab-df80537f7458.tmp
2017/05/23 09:47:43 packer.exe: 2017/05/23 09:47:43 [INFO] starting remote command: powershell -executionpolicy bypass -encodedCommand aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAEUAUgBfAFQAWQBQAEUAPQAiAGgAeQBwAGUAcgB2AC0AaQBzAG8AIgA7ACAAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAF8ATgBBAE0ARQA9ACIAaAB5AHAAZQByAHYALQBpAHMAbwAiADsAIAAmACcAYwA6AC8AVwBpAG4AZABvAHcAcwAvAFQAZQBtAHAALwBzAGMAcgBpAHAAdAAuAHAAcwAxACcAOwBlAHgAaQB0ACAAJABMAGEAcwB0AEUAeABpAHQAQwBvAGQAZQA=
2017/05/23 09:47:46 ui:     hyperv-iso: #< CLIXML
    hyperv-iso: #< CLIXML
2017/05/23 09:47:48 packer.exe: 2017/05/23 09:47:48 [INFO] command 'powershell -executionpolicy bypass -encodedCommand aQBmACAAKABUAGUAcwB0AC0AUABhAHQAaAAgAHYAYQByAGkAYQBiAGwAZQA6AGcAbABvAGIAYQBsADoAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAKQB7ACQAUAByAG8AZwByAGUAcwBzAFAAcgBlAGYAZQByAGUAbgBjAGUAPQAnAFMAaQBsAGUAbgB0AGwAeQBDAG8AbgB0AGkAbgB1AGUAJwB9ADsAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAEUAUgBfAFQAWQBQAEUAPQAiAGgAeQBwAGUAcgB2AC0AaQBzAG8AIgA7ACAAJABlAG4AdgA6AFAAQQBDAEsARQBSAF8AQgBVAEkATABEAF8ATgBBAE0ARQA9ACIAaAB5AHAAZQByAHYALQBpAHMAbwAiADsAIAAmACcAYwA6AC8AVwBpAG4AZABvAHcAcwAvAFQAZQBtAHAALwBzAGMAcgBpAHAAdAAuAHAAcwAxACcAOwBlAHgAaQB0ACAAJABMAGEAcwB0AEUAeABpAHQAQwBvAGQAZQA=' exited with code: 0
2017/05/23 09:47:48 packer.exe: 2017/05/23 09:47:48 [INFO] RPC endpoint: Communicator ended with: 0
2017/05/23 09:47:48 [INFO] 1082 bytes written for 'stderr'
    hyperv-iso: <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><S S="Error">this : The term 'this' is not recognized as the name of a cmdlet, function, script file, or operable program. Check _x000D__x000A_</S><S S="Error">the spelling of the name, or if a path was included, verify that the path is correct and try again._x000D__x000A_</S><S S="Error">At C:\Windows\Temp\script.ps1:1 char:1_x000D__x000A_</S><S S="Error">+ this is not a valid powershell command_x000D__x000A_</S><S S="Error">+ ~~~~_x000D__x000A_</S><S S="Error">    + CategoryInfo          : ObjectNotFound: (this:String) [], CommandNotFoundException_x000D__x000A_</S><S S="Error">    + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S></Objs>
2017/05/23 09:47:48 [INFO] 0 bytes written for 'stdout'
2017/05/23 09:47:48 [INFO] RPC client: Communicator ended with: 0
2017/05/23 09:47:48 [INFO] RPC endpoint: Communicator ended with: 0
2017/05/23 09:47:48 packer.exe: 2017/05/23 09:47:48 [INFO] 1082 bytes written for 'stderr'
2017/05/23 09:47:48 packer.exe: 2017/05/23 09:47:48 [INFO] 0 bytes written for 'stdout'
2017/05/23 09:47:48 packer.exe: 2017/05/23 09:47:48 [INFO] RPC client: Communicator ended with: 0
2017/05/23 09:47:48 ui:     hyperv-iso: <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><S S="Error">this : The term 'this' is not recognized as the name of a cmdlet, function, script file, or operable program. Check _x000D__x000A_</S><S S="Error">the spelling of the name, or if a path was included, verify that the path is correct and try again._x000D__x000A_</S><S S="Error">At C:\Windows\Temp\script.ps1:1 char:1_x000D__x000A_</S><S S="Error">+ this is not a valid powershell command_x000D__x000A_</S><S S="Error">+ ~~~~_x000D__x000A_</S><S S="Error">    + CategoryInfo          : ObjectNotFound: (this:String) [], CommandNotFoundException_x000D__x000A_</S><S S="Error">    + FullyQualifiedErrorId : CommandNotFoundException_x000D__x000A_</S><S S="Error"> _x000D__x000A_</S></Objs>
```

You can see some errors printed there but Packer just continues as if all was fine.


Discussion:
This seems to be due to the fact that while an error is written, `$LastExitCode` is not set. The powershell provisioner uses `$LastExitCode` as its own exit code, so there we are - Packer thinks all went well.

A workaround is to prefix the command in the template with `$ErrorActionPreference="Stop"`, which causes the whole PowerShell execution to stop on first error (including the parent script, which would otherwise exit with `$LastExitCode`).

From the usage of `$LastExitCode` in the default content for the autogenerated parent script, I infer that the intent is to fail the provisioner if something in the script fails. Therefore it seems like it might be valid to also include `$ErrorActionPreference="Stop"` in the default parent script.
Thanks for the explanation, has helped me. Seeing this when I try to use the PowerShell Inline provisioner with the Azure-arm builder thats building a Windows 2012r2 .vhd image. 
I've been pulling my hair out with this. Thank you for the solution!
It looks like the powershell execute command has been subject to entropy. I see the docs still call it out as `powershell "& { {{.Vars}}{{.Path}}; exit $LastExitCode}"`, which I think is what it used to be.

I'll see if I can work out the correct command to run. Currently it's `if (Test-Path variable:global:ProgressPreference){$ProgressPreference='SilentlyContinue'};{{.Vars}}&'{{.Path}}';exit $LastExitCode`


The fun part of it is that `$LastExitCode` is only set based on external programs called from powershell, not based on any errors.

I looked through the code base for [Octopus Deploy](https://octopus.com) (which does a _lot_ of powershell script running) and we do:

```
var command = "powershell -NonInteractive -NoProfile -NoLogo -ExecutionPolicy Unrestricted -Command \"$ErrorActionPreference = 'Stop'; . {{. '" + scriptFile + "' " + args + "; if ((test-path variable:global:lastexitcode)) {{ exit $LastExitCode }}}}\"";
```

Hope that helps.

that looks useful, thanks!
I have been working out the kinks for how to remotely run powershell without encoding issues and returning expected results. I think it is almost ready to be pulled into Packer.

https://github.com/taliesins/terraform/blob/HyperVProvider/builtin/providers/hyperv/powershell/template.go
@taliesins think this'll be ready for this milestone?  (Scheduled for Sept 27)
Pushing this to the next milestone, since I haven't heard from @taliesins and I assume that means he's got other stuff going on.
I think we should probably conclude that @taliesins is too busy with other things to get this across the finish line; unscheduling for now.
@taliesins Can we get this issue resolved? Weren't to you close to a fix already?
I still encounter this problem with packer 1.4.0. A script with a syntax error doesn't abort the build.
I also just hit this, with another twist that the error itself was the powershell provisioner seeming not copying the script?

> ==> openstack: Using winrm communicator to connect: 10.141.130.30
> ==> openstack: Waiting for WinRM to become available...
> ==> openstack: #< CLIXML
>     openstack: WinRM connected.
> ==> openstack: <Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"><Obj S="progress" RefId="0"><TN RefId="0"><T>System.Management.Automation.PSCustomObject</T><T>System.Object</T></TN><MS><I64 N="SourceId">1</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj><Obj S="progress" RefId="1"><TNRef RefId="0" /><MS><I64 N="SourceId">2</I64><PR N="Record"><AV>Preparing modules for first use.</AV><AI>0</AI><Nil /><PI>-1</PI><PC>-1</PC><T>Completed</T><SR>-1</SR><SD> </SD></PR></MS></Obj></Objs>
> ==> openstack: Connected to WinRM!
> ==> openstack: Provisioning with Powershell...
> ==> openstack: Provisioning with powershell script: ../common/install-python.ps1
> ==> openstack: & : The term 'c:/Windows/Temp/script-5da72be0-bc01-7db9-53da-45767cb4574a.ps1' is not recognized as the name of a
> ==> openstack: cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify
> ==> openstack: that the path is correct and try again.
> ==> openstack: At line:1 char:216
> ==> openstack: + ... 43fe7.ps1; &'c:/Windows/Temp/script-5da72be0-bc01-7db9-53da-45767cb45 ...
> ==> openstack: +                 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
> ==> openstack:     + CategoryInfo          : ObjectNotFound: (c:/Windows/Temp...5767cb4574a.ps1:String) [], CommandNotFoundException
> ==> openstack:     + FullyQualifiedErrorId : CommandNotFoundException
> ==> openstack:
> ==> openstack: Provisioning with Powershell...
> ==> openstack: Provisioning with powershell script: ../conan/scripts/install-conan.ps1

So not only did the error not stop the build, it also seems to be that the file which was missing is the script packer supposedly just uploaded (maybe that failed too, and that error was also silently ignored)?
On a subsequent run (no changes to the template or source image) it worked fine, so it was apparently some transient hiccup, and there's nothing more in the log (the next provisioner script actually failed because it uses python, which hadn't been installed because the script got skipped by this error, and that failure did abort the build).
This fix for this issue has been reverted, as there were issues with the updated provisioner when executing over Windows SSH. I am reopening for further investigation. 
What is the status of this issue? Is it still being worked on?
The -NonInteractive Parameter should be added by default when starting a powershell shell.
ATM packer waits endless for user input when there is a confirmation prompt
This issue has been synced to JIRA for planning.

JIRA ID: [HPR-762](https://hashicorp.atlassian.net/browse/HPR-762)
Following for any future updates
There seem to be a long-standing bug in Packer
- https://github.com/hashicorp/packer/issues/11198#issuecomment-2734862274



Patch:
diff --git a/provisioner/powershell/provisioner.go b/provisioner/powershell/provisioner.go
index aae71f9fb3e..0f27192751e 100644
--- a/provisioner/powershell/provisioner.go
+++ b/provisioner/powershell/provisioner.go
@@ -8,7 +8,6 @@
 package powershell
 
 import (
-	"bufio"
 	"context"
 	"errors"
 	"fmt"
@@ -38,6 +37,33 @@ var psEscape = strings.NewReplacer(
 	"'", "`'",
 )
 
+// wraps the content in try catch block and exits with a status.
+const wrapPowershellString string = `
+	if (Test-Path variable:global:ProgressPreference) {
+	  set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'
+	}
+	{{if .DebugMode}}
+	Set-PsDebug -Trace {{.DebugMode}}
+	{{- end}}
+	$exitCode = 0
+	try {
+	{{.Vars}}
+	{{.Payload}}
+	$exitCode = 0
+	} catch {
+	Write-Error "An error occurred: $_"
+	$exitCode = 1
+	}
+	
+	if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) {
+		$exitCode = $LASTEXITCODE
+	}
+	
+	Write-Host $result
+	exit $exitCode
+
+`
+
 type Config struct {
 	shell.Provisioner `mapstructure:",squash"`
 
@@ -105,23 +131,15 @@ type Provisioner struct {
 }
 
 func (p *Provisioner) defaultExecuteCommand() string {
-	baseCmd := `& { if (Test-Path variable:global:ProgressPreference)` +
-		`{set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};`
-
-	if p.config.DebugMode != 0 {
-		baseCmd += fmt.Sprintf(`Set-PsDebug -Trace %d;`, p.config.DebugMode)
-	}
-
-	baseCmd += `. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }`
 
 	if p.config.ExecutionPolicy == ExecutionPolicyNone {
-		return baseCmd
+		return `-file {{.Path}}`
 	}
 
 	if p.config.UsePwsh {
-		return fmt.Sprintf(`pwsh -executionpolicy %s -command "%s"`, p.config.ExecutionPolicy, baseCmd)
+		return fmt.Sprintf(`pwsh -executionpolicy %s -file {{.Path}}`, p.config.ExecutionPolicy)
 	} else {
-		return fmt.Sprintf(`powershell -executionpolicy %s "%s"`, p.config.ExecutionPolicy, baseCmd)
+		return fmt.Sprintf(`powershell -executionpolicy %s -file {{.Path}}`, p.config.ExecutionPolicy)
 	}
 
 }
@@ -247,24 +265,41 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
 	return nil
 }
 
-// Takes the inline scripts, concatenates them into a temporary file and
+// Takes the inline scripts, adds a wrapper around the inline scripts, concatenates them into a temporary file and
 // returns a string containing the location of said file.
 func extractScript(p *Provisioner) (string, error) {
 	temp, err := tmp.File("powershell-provisioner")
 	if err != nil {
 		return "", err
 	}
+
 	defer temp.Close()
-	writer := bufio.NewWriter(temp)
+
+	var commandBuilder strings.Builder
+
+	// we concatenate all the inline commands
 	for _, command := range p.config.Inline {
 		log.Printf("Found command: %s", command)
-		if _, err := writer.WriteString(command + "\n"); err != nil {
-			return "", fmt.Errorf("Error preparing powershell script: %s", err)
+		if _, err := commandBuilder.WriteString(command); err != nil {
+			return "", fmt.Errorf("failed to wrap script contents: %w", err)
 		}
 	}
 
-	if err := writer.Flush(); err != nil {
-		return "", fmt.Errorf("Error preparing powershell script: %s", err)
+	// injecting all the variables in the string
+	ctxData := p.generatedData
+	ctxData["Vars"] = p.createFlattenedEnvVars(p.config.ElevatedUser != "")
+	ctxData["Payload"] = commandBuilder.String()
+	ctxData["DebugMode"] = p.config.DebugMode
+	p.config.ctx.Data = ctxData
+
+	data, err := interpolate.Render(wrapPowershellString, &p.config.ctx)
+	if err != nil {
+		return "", fmt.Errorf("Error building powershell wrapper: %w", err)
+	}
+
+	log.Printf("Writing PowerShell script to file: %s", temp.Name())
+	if _, err := temp.WriteString(data); err != nil {
+		return "", fmt.Errorf("Error writing PowerShell script: %w", err)
 	}
 
 	return temp.Name(), nil


Test patch:
diff --git a/provisioner/powershell/provisioner_acc_test.go b/provisioner/powershell/provisioner_acc_test.go
index 8073d011334..90c16ddefd8 100644
--- a/provisioner/powershell/provisioner_acc_test.go
+++ b/provisioner/powershell/provisioner_acc_test.go
@@ -111,3 +111,26 @@ func TestAccPowershellProvisioner_Script(t *testing.T) {
 
 	provisioneracc.TestProvisionersAgainstBuilders(testCase, t)
 }
+
+func TestAccPowershellProvisioner_ExitCodes(t *testing.T) {
+	templateString, err := LoadProvisionerFragment("powershell-exit_codes-provisioner.txt")
+	if err != nil {
+		t.Fatalf("Couldn't load test fixture; %s", err.Error())
+	}
+	testCase := &provisioneracc.ProvisionerTestCase{
+		IsCompatible: powershellIsCompatible,
+		Name:         "powershell-provisioner-script",
+		Template:     templateString,
+		Type:         TestProvisionerType,
+		Check: func(buildcommand *exec.Cmd, logfile string) error {
+			if buildcommand.ProcessState != nil {
+				if buildcommand.ProcessState.ExitCode() != 0 {
+					return fmt.Errorf("Bad exit code. Logfile: %s", logfile)
+				}
+			}
+			return nil
+		},
+	}
+	provisioneracc.TestProvisionersAgainstBuilders(testCase, t)
+
+}
diff --git a/provisioner/powershell/provisioner_test.go b/provisioner/powershell/provisioner_test.go
index 024b0585fa2..b858700b9e5 100644
--- a/provisioner/powershell/provisioner_test.go
+++ b/provisioner/powershell/provisioner_test.go
@@ -23,6 +23,7 @@ func TestProvisionerPrepare_extractScript(t *testing.T) {
 	config := testConfig()
 	p := new(Provisioner)
 	_ = p.Prepare(config)
+	p.generatedData = generatedData()
 	file, err := extractScript(p)
 	defer os.Remove(file)
 	if err != nil {
@@ -35,13 +36,15 @@ func TestProvisionerPrepare_extractScript(t *testing.T) {
 
 	// File contents should contain 2 lines concatenated by newlines: foo\nbar
 	readFile, err := os.ReadFile(file)
-	expectedContents := "foo\nbar\n"
+	expectedContents := "if (Test-Path variable:global:ProgressPreference) {\n         set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'\n         }\n         \n         $exitCode = 0\n         try {\n         $env:PACKER_BUILDER_TYPE=\"\"; $env:PACKER_BUILD_NAME=\"\"; \n         foobar\n         $exitCode = 0\n         } catch {\n         Write-Error \"An error occurred: $_\"\n         $exitCode = 1\n         }\n         \n         if ($LASTEXITCODE -ne $null -and $LASTEXITCODE -ne 0) {\n         $exitCode = $LASTEXITCODE\n         }\n         \n         Write-Host $result\n         exit $exitCode"
+	normalizedExpectedContent := normalizeWhiteSpace(expectedContents)
 	if err != nil {
 		t.Fatalf("Should not be error: %s", err)
 	}
 	s := string(readFile[:])
-	if s != expectedContents {
-		t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", expectedContents, s)
+	normalizedString := normalizeWhiteSpace(s)
+	if normalizedString != normalizedExpectedContent {
+		t.Fatalf("Expected generated inlineScript to equal '%s', got '%s'", normalizedExpectedContent, normalizedString)
 	}
 }
 
@@ -74,12 +77,12 @@ func TestProvisionerPrepare_Defaults(t *testing.T) {
 		t.Error("expected elevated_password to be empty")
 	}
 
-	if p.config.ExecuteCommand != `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"` {
-		t.Fatalf(`Default command should be 'powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"', but got '%s'`, p.config.ExecuteCommand)
+	if p.config.ExecuteCommand != `powershell -executionpolicy bypass -file {{.Path}}` {
+		t.Fatalf(`Default command should be 'powershell -executionpolicy bypass -file {{.Path}}', but got '%s'`, p.config.ExecuteCommand)
 	}
 
-	if p.config.ElevatedExecuteCommand != `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"` {
-		t.Fatalf(`Default command should be 'powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"', but got '%s'`, p.config.ElevatedExecuteCommand)
+	if p.config.ElevatedExecuteCommand != `powershell -executionpolicy bypass -file {{.Path}}` {
+		t.Fatalf(`Default command should be 'powershell -executionpolicy bypass -file {{.Path}}', but got '%s'`, p.config.ElevatedExecuteCommand)
 	}
 
 	if p.config.ElevatedEnvVarFormat != `$env:%s="%s"; ` {
@@ -120,7 +123,7 @@ func TestProvisionerPrepare_DebugMode(t *testing.T) {
 		t.Fatalf("err: %s", err)
 	}
 
-	command := `powershell -executionpolicy bypass "& { if (Test-Path variable:global:ProgressPreference){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};Set-PsDebug -Trace 1;. {{.Vars}}; &'{{.Path}}'; exit $LastExitCode }"`
+	command := `powershell -executionpolicy bypass -file {{.Path}}`
 	if p.config.ExecuteCommand != command {
 		t.Fatalf(fmt.Sprintf(`Expected command should be '%s' but got '%s'`, command, p.config.ExecuteCommand))
 	}
@@ -483,7 +486,8 @@ func TestProvisionerProvision_Inline(t *testing.T) {
 	}
 
 	cmd := comm.StartCmd.Command
-	re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1'; exit \$LastExitCode }"`)
+	re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/inlineScript.ps1`)
+
 	matched := re.MatchString(cmd)
 	if !matched {
 		t.Fatalf("Got unexpected command: %s", cmd)
@@ -503,7 +507,7 @@ func TestProvisionerProvision_Inline(t *testing.T) {
 	}
 
 	cmd = comm.StartCmd.Command
-	re = regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/inlineScript.ps1'; exit \$LastExitCode }"`)
+	re = regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/inlineScript.ps1`)
 	matched = re.MatchString(cmd)
 	if !matched {
 		t.Fatalf("Got unexpected command: %s", cmd)
@@ -533,7 +537,7 @@ func TestProvisionerProvision_Scripts(t *testing.T) {
 	}
 
 	cmd := comm.StartCmd.Command
-	re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`)
+	re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`)
 	matched := re.MatchString(cmd)
 	if !matched {
 		t.Fatalf("Got unexpected command: %s", cmd)
@@ -570,7 +574,7 @@ func TestProvisionerProvision_ScriptsWithEnvVars(t *testing.T) {
 	}
 
 	cmd := comm.StartCmd.Command
-	re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`)
+	re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`)
 	matched := re.MatchString(cmd)
 	if !matched {
 		t.Fatalf("Got unexpected command: %s", cmd)
@@ -595,11 +599,11 @@ func TestProvisionerProvision_SkipClean(t *testing.T) {
 	}{
 		{
 			SkipClean:                true,
-			LastExecutedCommandRegex: `powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`,
+			LastExecutedCommandRegex: `powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`,
 		},
 		{
 			SkipClean:                false,
-			LastExecutedCommandRegex: `powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/packer-cleanup-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1'; exit \$LastExitCode }"`,
+			LastExecutedCommandRegex: `powershell -executionpolicy bypass -file c:/Windows/Temp/packer-cleanup-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1`,
 		},
 	}
 
@@ -917,7 +921,7 @@ func TestProvision_createCommandText(t *testing.T) {
 	p.generatedData = make(map[string]interface{})
 	cmd, _ := p.createCommandText()
 
-	re := regexp.MustCompile(`powershell -executionpolicy bypass "& { if \(Test-Path variable:global:ProgressPreference\){set-variable -name variable:global:ProgressPreference -value 'SilentlyContinue'};\. c:/Windows/Temp/packer-ps-env-vars-[[:alnum:]]{8}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{4}-[[:alnum:]]{12}\.ps1; &'c:/Windows/Temp/script.ps1'; exit \$LastExitCode }"`)
+	re := regexp.MustCompile(`powershell -executionpolicy bypass -file c:/Windows/Temp/script.ps1`)
 	matched := re.MatchString(cmd)
 	if !matched {
 		t.Fatalf("Got unexpected command: %s", cmd)
@@ -934,6 +938,28 @@ func TestProvision_createCommandText(t *testing.T) {
 	}
 }
 
+func TestProvision_createCommandTextNoneExecutionPolicy(t *testing.T) {
+	config := testConfig()
+	config["remote_path"] = "c:/Windows/Temp/script.ps1"
+	p := new(Provisioner)
+
+	comm := new(packersdk.MockCommunicator)
+	p.communicator = comm
+	config["execution_policy"] = ExecutionPolicyNone
+	_ = p.Prepare(config)
+
+	// Non-elevated
+	p.generatedData = make(map[string]interface{})
+
+	cmd, _ := p.createCommandText()
+	re := regexp.MustCompile(`-file c:/Windows/Temp/script.ps1`)
+	matched := re.MatchString(cmd)
+	if !matched {
+		t.Fatalf("Got unexpected command: %s", cmd)
+	}
+
+}
+
 func TestProvision_uploadEnvVars(t *testing.T) {
 	p := new(Provisioner)
 	comm := new(packersdk.MockCommunicator)
@@ -976,3 +1002,18 @@ func generatedData() map[string]interface{} {
 		"PackerHTTPPort": commonsteps.HttpPortNotImplemented,
 	}
 }
+
+func normalizeWhiteSpace(s string) string {
+	// Replace multiple spaces/tabs with a single space
+	re := regexp.MustCompile(`[\t ]+`)
+	s = re.ReplaceAllString(s, " ")
+
+	// Trim leading/trailing spaces and newlines
+	s = strings.TrimSpace(s)
+
+	// Normalize line breaks (remove excessive empty lines)
+	s = strings.ReplaceAll(s, "\r\n", "\n") // Convert Windows line endings to Unix
+	s = strings.ReplaceAll(s, "\r", "\n")   // Convert old Mac line endings to Unix
+
+	return s
+}
diff --git a/provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt b/provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt
new file mode 100644
index 00000000000..f0445951d57
--- /dev/null
+++ b/provisioner/powershell/test-fixtures/powershell-exit_codes-provisioner.txt
@@ -0,0 +1,76 @@
+{
+  "type": "powershell",
+  "inline": ["invalid-cmdlet"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": ["#Requires -Version 10.0"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": ["exit 1"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": ["}}"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": ["$LASTEXITCODE=1"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": ["throw 'XXX'"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "script": "../../provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1",
+  "valid_exit_codes": ["0"]
+},
+{
+  "type": "powershell",
+  "elevated_user": "Administrator",
+  "elevated_password": "{{.WinRMPassword}}",
+  "inline": "Get-ItemProperty -Path HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion",
+  "valid_exit_codes": ["0"]
+},
+{
+  "type": "powershell",
+  "inline": "ping invalidhost",
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": "sc.exe start command",
+  "valid_exit_codes": ["1060"]
+},
+{
+  "type": "powershell",
+  "inline": "echo 'Hi testing echo'; invalid command!; echo 'Another valid command';",
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": ["$ErrorActionPreference='Stop'", "Get-Item 'C:\\nonexistent.txt'"],
+  "valid_exit_codes": ["1"]
+},
+{
+  "type": "powershell",
+  "inline": [    
+                "try {",
+                " invalid command",
+                "} catch {",
+                "    exit 1",
+                "}"
+            ],
+  "valid_exit_codes": ["1"]
+}
+
+
+
diff --git a/provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt b/provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt
new file mode 100644
index 00000000000..ea3aedf706b
--- /dev/null
+++ b/provisioner/powershell/test-fixtures/scripts/bootstrap_win.txt
@@ -0,0 +1,40 @@
+<powershell>
+# Set administrator password
+net user Administrator SuperS3cr3t!!!!
+wmic useraccount where "name='Administrator'" set PasswordExpires=FALSE
+
+# First, make sure WinRM can't be connected to
+netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes action=block
+
+# Delete any existing WinRM listeners
+winrm delete winrm/config/listener?Address=*+Transport=HTTP  2>$Null
+winrm delete winrm/config/listener?Address=*+Transport=HTTPS 2>$Null
+
+# Disable group policies which block basic authentication and unencrypted login
+
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowBasic -Value 1
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Client -Name AllowUnencryptedTraffic -Value 1
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowBasic -Value 1
+Set-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WinRM\Service -Name AllowUnencryptedTraffic -Value 1
+
+
+# Create a new WinRM listener and configure
+winrm create winrm/config/listener?Address=*+Transport=HTTP
+winrm set winrm/config/winrs '@{MaxMemoryPerShellMB="0"}'
+winrm set winrm/config '@{MaxTimeoutms="7200000"}'
+winrm set winrm/config/service '@{AllowUnencrypted="true"}'
+winrm set winrm/config/service '@{MaxConcurrentOperationsPerUser="12000"}'
+winrm set winrm/config/service/auth '@{Basic="true"}'
+winrm set winrm/config/client/auth '@{Basic="true"}'
+
+# Configure UAC to allow privilege elevation in remote shells
+$Key = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
+$Setting = 'LocalAccountTokenFilterPolicy'
+Set-ItemProperty -Path $Key -Name $Setting -Value 1 -Force
+
+# Configure and restart the WinRM Service; Enable the required firewall exception
+Stop-Service -Name WinRM
+Set-Service -Name WinRM -StartupType Automatic
+netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new action=allow localip=any remoteip=any
+Start-Service -Name WinRM
+</powershell>
\ No newline at end of file
diff --git a/provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1 b/provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1
new file mode 100644
index 00000000000..f548cea7a90
--- /dev/null
+++ b/provisioner/powershell/test-fixtures/scripts/set_version_latest.ps1
@@ -0,0 +1,13 @@
+# Test fixture is a modified version of the example found at
+# https://www.powershellmagazine.com/2012/10/23/pstip-set-strictmode-why-should-you-care/
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = "Stop"
+
+$myNumbersCollection = 1..5
+if($myNumbersCollection -contains 3) {
+    "collection contains 3"
+}
+else {
+    "collection doesn't contain 3"
+}
\ No newline at end of file