# run-enrollment.ps1 # Installs GCCH enrollment provisioning package. That's it. # # Install-ProvisioningPackage triggers an immediate reboot -- nothing after # that call executes. The sync_intune task and all other post-enrollment # setup are registered by Run-ShopfloorSetup.ps1 BEFORE calling this script. $ErrorActionPreference = 'Continue' $logFile = "C:\Logs\enrollment.log" New-Item -ItemType Directory -Path "C:\Logs" -Force -ErrorAction SilentlyContinue | Out-Null function Log { param([string]$Message) $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $line = "$ts $Message" Write-Host $line Add-Content -Path $logFile -Value $line } Log "=== GE Aerospace GCCH Enrollment ===" # --- Run once ------------------------------------------------------------ # This script has been observed running twice on one build (579C144, # 2026-08-06: 13:31:55 and 13:36:45). The second provtool call returned # 0x800700B7 ERROR_ALREADY_EXISTS and achieved nothing except re-applying a # pending computer rename over the one the package had just set. The rename is # gone now, but a second full pass is still pointless work on an 8 GB package. $appliedMarker = 'C:\Enrollment\.ppkg-applied' if (Test-Path $appliedMarker) { Log "Provisioning package already applied on $(Get-Content $appliedMarker -First 1) - skipping." return } # --- Find the .ppkg --- $ppkgFile = Get-ChildItem "C:\Enrollment\*.ppkg" -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $ppkgFile) { Log "No .ppkg found in C:\Enrollment\ - skipping enrollment." return } Log "Package: $($ppkgFile.Name)" # --- Computer name: the PPKG owns it, do NOT rename here --- # The package declares F%SERIAL%, so a bay is # meant to come up as F. This script used to run # Rename-Computer -NewName "E$serial" # which is a second pending rename racing the package's. Observed on 579C144 # 2026-08-06: # # 13:31:55 run-enrollment Rename-Computer -> E579C144 (pending) # 13:32:17 ppkg Reboot/DeviceName -> F%SERIAL% (pending) # "Reboot successfully scheduled. Device name set successfully." # 13:36:45 run-enrollment runs AGAIN -> E579C144 (overwrites F) # ~13:37 reboot, E579C144 wins # # Last writer wins at reboot, so the hardcoded E prefix silently beat the # package's F. Nothing in this repo ever produced an F name; the convention only # works if we leave naming to the package. # --- Install provisioning package --- # IMPORTANT: The PPKG must be installed BEFORE OOBEComplete is set. Bulk # enrollment PPKGs are designed to run during OOBE; on Windows 11 22H2+ they # can hang indefinitely if OOBE is already marked complete. # # We invoke provtool.exe directly instead of Install-ProvisioningPackage. # The PowerShell cmdlet enforces a hardcoded 180-second timeout on the # underlying provtool call, which a 7-8 GB GCCH PPKG often exceeds on # slower disks. When the cmdlet times out it throws, and the Add- # ProvisioningPackage fallback has been observed to invoke provtool with # an empty packagePathsToAdd (session registered but never started), # leaving the PC un-enrolled. provtool.exe directly has no caller-side # timeout; Start-Process -Wait waits on the actual child process. # # The PPKG triggers an IMMEDIATE reboot once fully applied. Nothing below # that point executes on the current boot. BPRT app installs (Chrome, # Office, Tanium, etc.) happen on the next boot. The sync_intune # scheduled task (registered by Run-ShopfloorSetup.ps1 before calling us) # fires at the next logon to monitor Intune enrollment. $ppkgLogDir = "C:\Logs\PPKG" New-Item -ItemType Directory -Path $ppkgLogDir -Force -ErrorAction SilentlyContinue | Out-Null $provtool = Join-Path $env:SystemRoot 'System32\provtool.exe' # Arg order matches what the Install-ProvisioningPackage cmdlet invokes # internally (observed in ProvEventLog.txt): positional path, then /quiet, # then /source. No /log: or /ppkg: prefix - those are not valid provtool # flags and caused 0x80004005 E_FAIL in the first test. # # /source PSCmdlet matches what Install-ProvisioningPackage invokes # internally and is the correct post-OOBE context. BPRT was tried first # and verified to stop after the first RestartRequired command (DotNet35): # only 3 of 21 commands ran (PPKG Version Check, Lock Screen, DotNet35), # leaving Office/Chrome/Tanium/Activate-Windows etc never executed # because BPRT expects the OOBE runtime to own the reboot-and-resume # loop, and there is no OOBE runtime here. PSCmdlet registers a # RunOnce-style resume handler so the remaining commands continue after # the reboot Run-ShopfloorSetup issues. Timeout concerns that previously # motivated BPRT don't apply here because we invoke provtool.exe # directly, not via the 180s-capped Install-ProvisioningPackage cmdlet. $provArgs = @("`"$($ppkgFile.FullName)`"", "/quiet", "/source", "PSCmdlet") # Enable the Provisioning-Diagnostics-Provider Admin channel so events # from the BPRT run land somewhere we can export afterward. This is # idempotent - running each time is safe. wevtutil.exe set-log 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' /enabled:true 2>$null | Out-Null Log "Installing provisioning package via provtool.exe (no PowerShell timeout)..." Log "Command: $provtool $($provArgs -join ' ')" Log "PPKG diagnostic logs -> $ppkgLogDir (provtool writes them automatically)" try { $p = Start-Process -FilePath $provtool -ArgumentList $provArgs -Wait -PassThru -NoNewWindow -ErrorAction Stop Log "provtool.exe exit code: $($p.ExitCode)" if ($p.ExitCode -eq 0) { Set-Content -Path $appliedMarker -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ErrorAction SilentlyContinue Log "Wrote applied-marker $appliedMarker" } else { $hex = '0x{0:X8}' -f $p.ExitCode Log "WARNING: provtool.exe returned non-zero exit code ($hex). Check $ppkgLogDir for diagnostic bundle." # 0x800700B7 ERROR_ALREADY_EXISTS means the package is already applied. # That is a success for our purposes, and it is what a second run of # this script produced on 579C144 (2026-08-06) - so mark it applied and # let the next run skip instead of repeating the work. if ($p.ExitCode -eq -2147024713) { Set-Content -Path $appliedMarker -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ErrorAction SilentlyContinue Log "Package was already applied (ERROR_ALREADY_EXISTS) - marker written." } } } catch { Log "ERROR: Failed to launch provtool.exe: $_" } # --- Harvest Windows' own provisioning diagnostics into $ppkgLogDir --- # provtool.exe /quiet does not drop a zip bundle on success, so the real # detail lives under C:\ProgramData\Microsoft\Provisioning\, in the # registry under HKLM\Software\Microsoft\Provisioning\Sessions\*, and in # the Provisioning-Diagnostics-Provider event log. Copy/export all three # into our log dir so they ride back with the shopfloor logs bundle. Log "Harvesting Windows provisioning diagnostics to $ppkgLogDir..." try { $provData = 'C:\ProgramData\Microsoft\Provisioning' if (Test-Path $provData) { Copy-Item -Path (Join-Path $provData '*') -Destination $ppkgLogDir ` -Recurse -Force -ErrorAction SilentlyContinue Log " copied $provData -> $ppkgLogDir" } else { Log " $provData not present (provtool may not have touched it)" } } catch { Log " WARN: ProgramData copy threw: $_" } try { $sessions = Get-ChildItem 'HKLM:\Software\Microsoft\Provisioning\Sessions' -ErrorAction SilentlyContinue if ($sessions) { $snap = $sessions | ForEach-Object { $props = Get-ItemProperty $_.PSPath -ErrorAction SilentlyContinue if ($props) { [pscustomobject]@{ Session = $_.PSChildName BeginTime = $props.BeginTime LastRunTime = $props.LastRunTime RebootCount = $props.RebootCount State = $props.State StateValue = $props.StateValue } } } $snap | ConvertTo-Json -Depth 3 | Out-File -FilePath (Join-Path $ppkgLogDir 'provisioning-sessions.json') -Encoding UTF8 Log " wrote provisioning-sessions.json ($($snap.Count) session(s))" foreach ($s in $snap) { Log " session $($s.Session): State=$($s.State) RebootCount=$($s.RebootCount)" } } else { Log " no sessions under HKLM:\Software\Microsoft\Provisioning\Sessions" } } catch { Log " WARN: session snapshot threw: $_" } try { $evtx = Join-Path $ppkgLogDir 'Provisioning-Diagnostics-Admin.evtx' if (Test-Path $evtx) { Remove-Item $evtx -Force -ErrorAction SilentlyContinue } $null = & wevtutil.exe epl 'Microsoft-Windows-Provisioning-Diagnostics-Provider/Admin' $evtx 2>&1 if (Test-Path $evtx) { Log " exported $evtx" } } catch { Log " WARN: wevtutil export threw: $_" } # --- Set OOBE complete (only reached if PPKG didn't trigger immediate reboot) --- Log "Setting OOBE as complete..." reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v OOBEComplete /t REG_DWORD /d 1 /f | Out-Null reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Setup\OOBE" /v SetupDisplayedEula /t REG_DWORD /d 1 /f | Out-Null # If we get here, the PPKG didn't reboot immediately. Unlikely but handle it. Log "PPKG did not trigger immediate reboot. Returning to caller."