Files
pxe-server/playbook/startnet.cmd
cproudlock 04db6d6605 Generic build for other GE sites: no WJ data, one shopfloor type, Windows USB builder
Everything West Jefferson specific moved out to a separate repo
(pxe-site-wj): NTLARS/UDC/heat-treat backups keyed by WJ machine numbers,
CMM and Wax/Trace bay lists, WJ OpenText profiles, site-config.json, the
WJ preinstall list, and the WJ shopfloor PC types (Collections,
NoCollections, Common, Keyence, CMM, Genspect, HeatTreat, Wax/Trace,
Display, PartMarker) with GE-Enforce, machine numbers and the desktop
layout. The part-marker bench tools moved to their own repo. Dead
scripts removed: build-proxmox-iso, deploy-bios, sync_hardware_models,
mirror-from-gold, pull-bios, startnet-template.cmd.

Shopfloor is one generic type, gea-shopfloor-standard, with today's
timing:
- PPKG at first logon (unattend Order 10, unchanged).
- Install-AppList.ps1 runs C:\PreInstall\preinstall.json during setup.
- A new SYSTEM task, GE Shopfloor Post-PPKG, runs postppkg.json once
  after the post-PPKG reboot. It also clears AutoLogonCount after the
  ShopFloor handover (the Users-level monitor can be denied that HKLM
  write) and removes the Intune Sync task when done.
- Both lists ship empty. A site adds entries and installers.
- WinPE choice 3 goes straight to the PPKG menu. Staging, fetch and heal
  carry only the generic payload. The webapp's shopfloor sub-menu editor
  is gone, and the imaging tiles name the new stages.
- The playbook deploys the empty lists only if missing. It now deploys
  the PowerShell 7 MSI the shopfloor unattend installs, which it never
  did before.

Blancco site settings move into the webapp (Updates > Blancco releases):
business location, licensing Wi-Fi SSID and password. They are rendered
into the served preferences.xml and applied to every staged release, and
are seeded from site-vars (site_name, blancco_wifi_ssid,
blancco_wifi_password) only while none exist. The repo XML carries no
site values; the plaintext Wi-Fi password is gone from it.

New site_name and site_timezone settings: report and evidence-pack site
name, server clock and webapp time zone. No more West Jefferson defaults
in webapp/config.py or the dashboard title. Sync-BPRT.ps1 gets the
server address filled in at download instead of WJ's GE address.

USB on Windows:
- scripts/Build-PxeUsb.ps1 runs the same checks as build-usb.sh. It
  writes one FAT32 CIDATA partition (ISO files plus patched grub.cfg plus
  payload) with diskpart and reads every file back by SHA-256.
- Tested with Windows PowerShell 5.1 in the win11 VM, and the layout
  installs and first-boots in the pxetest VM.
- scripts/stage-usb-share.sh stages the build folder on a share.
- site-content/ is optional for both builders.

KB docs/kb/How_To_Deploy_PXE_Server_New_Site.html: new-site deployment,
Windows only, with a site-vars.yml guide.

Checks: webapp tests pass (5 suites); the unattend lint and playbook
syntax checks pass; every .ps1 parses; there are no WJ hosts, IPs, names
or PC types in playbook/, webapp/, scripts/ or autoinstall/.
2026-10-16 20:37:22 -04:00

579 lines
26 KiB
Batchfile

@echo off
echo Please wait while 'WinPE' is being processed. This may take a few seconds.
wpeinit
powercfg /s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c
REM --- Site values -----------------------------------------------------
REM The PXE server address and share credentials appeared 14 and 13 times
REM respectively, so moving this server or rotating the account meant editing
REM every net use in the file and rebuilding boot.wim. Set them once here.
REM These are also written to C:\Enrollment\fetch-source.txt, which
REM Fetch-StagingPayload.ps1 and Verify-And-Heal-Staging.ps1 read at first
REM logon - so the post-boot self-heal follows this value automatically.
set PXESERVER=172.16.9.1
set SHAREUSER=pxe-upload
set SHAREPASS=pxe
REM --- Wait for network (DHCP may take a moment after wpeinit) ---
echo Waiting for network...
:wait_net
ping -n 2 %PXESERVER% >NUL 2>&1
if errorlevel 1 goto wait_net
echo Network ready.
REM --- BIOS update check (runs before imaging menu) ---
REM Mounts [winpeapps_bios] share. CRITICAL: do NOT call check-bios.cmd
REM inside an `if exist (...)` parens block - CMD's variable scoping for
REM CALLed scripts inside parens does not propagate BIOS_STATUS back to
REM this script reliably. Use goto-flow instead so the CALL runs at the
REM top scope and BIOS_STATUS persists.
set BIOS_STATUS=No BIOS check (share unavailable)
net use B: \\%PXESERVER%\winpeapps_bios /user:%SHAREUSER% %SHAREPASS% /persistent:no 2>NUL
if not exist B:\check-bios.cmd goto :bios_check_done
echo.
echo Checking for BIOS updates...
call B:\check-bios.cmd
:bios_check_done
net use B: /delete 2>NUL
:menu
cls
echo.
echo ========================================
echo WinPE Setup Menu
echo ========================================
echo Firmware: %BIOS_STATUS%
echo.
echo Please select an option:
echo.
echo 1. GEA Standard
echo 2. GEA Engineer
echo 3. GEA Shopfloor
echo.
echo ========================================
echo.
set /p choice=Enter your choice (1-3):
REM --- Choice 3 = GEA Shopfloor: one generic type, gea-shopfloor-standard, then
REM the GCCH enrollment (PPKG) menu.
set PPKG=
set PCCONFIG=
if "%choice%"=="3" goto gea_shopfloor_type
REM Standard and engineer enrol through the vendor orchestrator, which asks for
REM tenant + PC purpose with a GUI at first logon. Ask here instead, while the
REM tech who knows what the machine is for is standing at it, and pre-seed the
REM answer so the dialog never appears. See :gea_purpose_submenu.
if "%choice%"=="1" goto gea_purpose_submenu
if "%choice%"=="2" goto gea_purpose_submenu
goto enroll_staged
:gea_purpose_submenu
cls
echo.
echo ========================================
echo PC Purpose - Entra enrollment
echo ========================================
echo.
echo 1. Single-User PC (one assigned person)
echo 2. Shared PC (multiple users)
echo 3. Loaner PC (temporary assignment)
echo.
echo 4. Ask at first logon (show the vendor dialog instead)
echo.
set PCPURPOSE=
set /p purp_choice=Enter your choice (1-4):
if "%purp_choice%"=="1" set PCPURPOSE=PERS
if "%purp_choice%"=="2" set PCPURPOSE=SH
if "%purp_choice%"=="3" set PCPURPOSE=LOAN
if "%purp_choice%"=="4" goto enroll_staged
if "%PCPURPOSE%"=="" goto gea_purpose_submenu
:gea_tenant_submenu
cls
echo.
echo ========================================
echo Entra Tenant
echo ========================================
echo.
echo 1. GCC High (GCCH - US)
echo 2. Rest of World (RoW)
echo.
REM The tenant is not cosmetic: Insert-BPRTToPPKG.ps1 matches the bulk token by
REM Tenant AND Purpose, so this picks which token gets injected.
set PCTENANT=
set /p ten_choice=Enter your choice (1-2):
if "%ten_choice%"=="1" set PCTENANT=GCCH
if "%ten_choice%"=="2" set PCTENANT=RoW
if "%PCTENANT%"=="" goto gea_tenant_submenu
set PCCONFIG=%PCTENANT%_%PCPURPOSE%
echo.
echo PC config: %PCCONFIG%
goto enroll_staged
:gea_shopfloor_type
REM One generic shopfloor type: an (empty) app list during setup, the PPKG, an
REM (empty) post-PPKG app list, then the ShopFloor lockdown handover.
set PCTYPE=gea-shopfloor-standard
goto enroll_menu
:enroll_menu
cls
echo.
echo ========================================
echo GCCH Enrollment Profile
echo ========================================
echo.
echo 1. No Office
echo 2. Standard Office (x86)
echo 3. Standard Office (x64)
echo 4. Pro Plus Office (x86) with Access
echo 5. Pro Plus Office (x64) with Access
echo 6. Skip enrollment
echo.
set /p enroll=Enter your choice (1-6):
REM --- PPKG configuration (constructed at menu time, see docs) ---
REM Vendor ships one source PPKG; we construct the BPRT-tagged filename
REM by filling in Office, Region, Expiry, Version on the target copy.
REM The ACTIVE ppkg is read from \\172.16.9.1\enrollment\ppkg.conf, which the
REM PXE webapp writes on upload - so a new ppkg goes live with no startnet edit.
REM The baked-in values below are only a fallback if ppkg.conf is missing.
net use Y: \\%PXESERVER%\enrollment /user:%SHAREUSER% %SHAREPASS% /persistent:no >nul 2>&1
set SOURCE_PPKG=
set TARGET_PPKG=
set PPKG_VER=
set PPKG_EXP=
if exist "Y:\ppkg.conf" (
for /f "usebackq tokens=1,* delims==" %%A in ("Y:\ppkg.conf") do (
if /i "%%A"=="SOURCE_PPKG" set "SOURCE_PPKG=%%B"
if /i "%%A"=="PPKG_VER" set "PPKG_VER=%%B"
if /i "%%A"=="PPKG_EXP" set "PPKG_EXP=%%B"
if /i "%%A"=="TARGET_PPKG" set "TARGET_PPKG=%%B"
)
)
if "%SOURCE_PPKG%"=="" set SOURCE_PPKG=GCCH_Prod_SFLD_v4.16.ppkg
if "%PPKG_VER%"=="" set PPKG_VER=v4.16
if "%PPKG_EXP%"=="" set PPKG_EXP=20260831
set REGION=US
set OFFICE=
if "%enroll%"=="1" set OFFICE=NoOffice
if "%enroll%"=="2" set OFFICE=StdOffice-x86
if "%enroll%"=="3" set OFFICE=StdOffice-x64
if "%enroll%"=="4" set OFFICE=ProPlusOffice-x86
if "%enroll%"=="5" set OFFICE=ProPlusOffice-x64
if "%enroll%"=="6" set OFFICE=
if "%enroll%"=="" goto enroll_menu
set PPKG=
REM Target filename for the staged package.
REM
REM The SFLD-era scheme encoded an Office variant in the name
REM (GCCH_Prod_SFLD_<Office>_<Region>_Exp_<date>_<ver>.ppkg) because one source
REM package was renamed per Office choice. MCL packages do not work that way -
REM Office is installed by Install-Office.ps1 inside the package - and renaming
REM an MCL package into the SFLD scheme just produces a misleading filename.
REM
REM So: honour TARGET_PPKG from ppkg.conf when present, otherwise keep the
REM source filename unchanged. The SFLD construction remains only as a fallback
REM for a legacy SFLD source package.
if not "%TARGET_PPKG%"=="" (
set PPKG=%TARGET_PPKG%
) else (
REM find, NOT findstr: findstr.exe is not in this WinPE image (find.exe is,
REM and startnet already uses it for the PESetup wait). findstr fails with
REM "not recognized", leaves errorlevel 9009, and "if errorlevel 1" then
REM takes the NON-SFLD branch for an SFLD package - so the staged ppkg keeps
REM its source name and never gets the BPRT-tagged one.
echo %SOURCE_PPKG% | find /I "_SFLD_" >NUL
if errorlevel 1 (
set PPKG=%SOURCE_PPKG%
) else (
if not "%OFFICE%"=="" set PPKG=GCCH_Prod_SFLD_%OFFICE%_%REGION%_Exp_%PPKG_EXP%_%PPKG_VER%.ppkg
)
)
REM --- Map enrollment share early (kept open for copy after imaging) ---
REM Skip the re-map when Y: is already mounted (System error 85 otherwise).
set NEED_ENROLL=0
if not "%PPKG%"=="" set NEED_ENROLL=1
if not "%PCTYPE%"=="" set NEED_ENROLL=1
if "%NEED_ENROLL%"=="0" goto enroll_staged
if exist Y:\ goto enroll_already_mapped
net use Y: \\%PXESERVER%\enrollment /user:%SHAREUSER% %SHAREPASS% /persistent:no
:enroll_already_mapped
if "%PPKG%"=="" goto enroll_staged
if not exist "Y:\ppkgs\%SOURCE_PPKG%" (
echo WARNING: %SOURCE_PPKG% not found on server. Enrollment will be skipped.
set PPKG=
)
:enroll_staged
echo. > X:\Boot.tag
if "%choice%"=="1" goto gea-standard
if "%choice%"=="2" goto gea-engineer
if "%choice%"=="3" goto gea-shopfloor
echo Invalid choice. Please try again.
pause
goto menu
:gea-standard
echo.
echo Starting GEA Standard setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\%PXESERVER%\winpeapps\gea-standard /user:%SHAREUSER% %SHAREPASS% /persistent:no
goto end
:gea-engineer
echo.
echo Starting GEA Engineer setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\%PXESERVER%\winpeapps\gea-engineer /user:%SHAREUSER% %SHAREPASS% /persistent:no
goto end
:gea-shopfloor
echo.
echo Starting GEA Shopfloor setup...
start "FlatApp" %SYSTEMDRIVE%\GESetup\FlatSetupLoader.exe
for /l %%i in (1,1,2000000) do rem
net use Z: \\%PXESERVER%\winpeapps\gea-shopfloor /user:%SHAREUSER% %SHAREPASS% /persistent:no
goto end
:end
echo.
REM Map the enrollment share for the status push and preflight below. The
REM shopfloor/enroll menus map it earlier; the gea-standard/gea-engineer
REM path (purpose -> tenant -> enroll_staged) never did, so neither ran.
if not exist Y:\ net use Y: \\%PXESERVER%\enrollment /user:%SHAREUSER% %SHAREPASS% /persistent:no >NUL 2>&1
REM --- Push initial "WinPE staging" status to PXE webapp ---
REM Externalized to Y:\scripts\winpe-status-push.ps1 so future edits don't
REM require a boot.wim rebuild. Best-effort; the script logs to
REM X:\Windows\Temp\winpe-status-push.log and swallows all errors.
if exist "Y:\scripts\winpe-status-push.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "Y:\scripts\winpe-status-push.ps1"
)
REM --- Pre-imaging checks ---------------------------------------------------
REM Checks the things PESetup fails on, before it fails on them: secure boot
REM (hard failure at GatherData), disk >= 120 GB, a driver pack matching THIS
REM model, and media age. The driver check is the one that earns its keep - a
REM miss is only a WARNING to PESetup, so the bay images with no NIC and the
REM symptom shows up much later as a DNS failure during enrollment.
REM Advisory: it reports and pauses on a blocker, it does not abort. The tech
REM decides. Lives on the share so it can be fixed without a boot.wim rebuild.
if exist "Y:\scripts\preflight.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "Y:\scripts\preflight.ps1" -MediaDrive Z:
if errorlevel 1 (
echo Press any key to image anyway, or power off the bay to stop.
pause >NUL
)
)
echo Waiting for PESetup.exe to start...
:wait_start
ping -n 3 127.0.0.1 >NUL
wmic process where "name='PESetup.exe'" get name 2>NUL | find /I "PESetup" >NUL
if errorlevel 1 goto wait_start
echo PESetup.exe is running. Waiting for imaging to complete...
REM --- Copy enrollment package and shopfloor setup as soon as Windows partition appears ---
REM PCCONFIG is here so standard/engineer builds still reach the volume wait:
REM they select no ppkg and no PCTYPE, but they do need pc-config.txt written.
if "%PPKG%"=="" if "%PCTYPE%"=="" if "%PCCONFIG%"=="" goto wait_finish
REM --- Wait for the volume PESetup applies Windows to -----------------------
REM PESetup hardcodes W: in nine places - every copy destination, both DISM
REM offline sessions, bcdboot and reagentc - and CREATES it during its own disk
REM preparation. See docs/PESETUP-INTERNALS.md. There is nothing to search for.
REM
REM Do NOT try to locate the volume earlier by scanning for a Windows install.
REM Proven on 579C144, 2026-08-06, PESetup 4.0.0.20:
REM 11:30:07 PESetup logs the OLD install still mounted at C:
REM 11:30:18 operator presses Next
REM 11:30:20 PESetup runs DISKPART /S X:\diskpartEFI0.txt - the wipe
REM 11:30:24 a volume scan had returned that C:, startnet assigned it W: and
REM staged into the partition PESetup was mid-way through erasing.
REM Every robocopy died, "The system cannot find the path
REM specified", and NOTHING was staged.
REM 11:30:30 PESetup creates its own W: and starts copying to it
REM The SYSTEM hive is the signal that distinguishes the applied image from the
REM one being wiped: it exists only once the WIM apply has written it. Do not
REM weaken this test to "the volume exists" or "Windows is present".
set OSDRIVE=
echo Waiting for the applied Windows volume at W: ...
set OSWAIT=0
:wait_enroll
if exist W:\Windows\System32\config\system (
set OSDRIVE=W:
goto found_os
)
set /a OSWAIT+=1
REM 270 polls at ~10s = 45 minutes. Was 120 (20 min), which is fine on NVMe but
REM tight on slow media - a WIM apply can run 25+ minutes there, and the failure
REM mode is the scary os_not_found banner plus nothing staged. Waiting longer
REM costs nothing: the loop exits the moment the SYSTEM hive appears.
if %OSWAIT% GEQ 270 goto os_not_found
ping -n 11 127.0.0.1 >NUL
goto wait_enroll
:os_not_found
echo.
echo ******************************************************************
echo ERROR: no applied Windows volume found.
echo NOTHING was staged: pc-type.txt, the enrollment package and
echo shopfloor-setup are all missing. Imaging will finish but the
echo post-install setup will NOT run. Tell the PXE admin.
echo ******************************************************************
echo.
goto cleanup_enroll
:found_os
echo Found Windows at %OSDRIVE%
REM No drive-letter reassignment here. PESetup owns W: and runs its own DISKPART
REM against the same disk; a second diskpart racing it destroys the volume that
REM staging is writing to. The unattend's hardcoded W:\Drivers path is PESetup's
REM own destination (CopyDrivers unzips there), so it lines up by construction.
mkdir %OSDRIVE%\Enrollment 2>NUL
REM Log all copy operations to a file that persists on the target disk.
REM Tech can check C:\Enrollment\winpe-staging.log post-imaging.
REM Each echo in the copy block below also appends to this file via
REM the >> redirect after the screen echo. robocopy /LOG+ appends its
REM own output directly.
set STAGELOG=%OSDRIVE%\Enrollment\winpe-staging.log
echo [%DATE% %TIME%] WinPE staging started >> "%STAGELOG%"
echo PCTYPE=%PCTYPE% PPKG=%PPKG% PCCONFIG=%PCCONFIG% >> "%STAGELOG%"
REM --- Pre-seed the tenant + PC purpose for the enrollment orchestrator -----
REM Start-BulkEnrollOrchestrator.ps1 asks for this with a GUI at first logon
REM (Select-PCConfig.ps1) and uses the answer twice: to pick which MCL package
REM to apply, and to select which bulk token Insert-BPRTToPPKG.ps1 injects -
REM it matches on Tenant AND Purpose. Writing the answer here lets the shim
REM return it non-interactively, so an unattended build never stops on a dialog.
REM Format is exactly what the vendor dialog returns: {Tenant}_{Purpose},
REM e.g. GCCH_PERS, GCCH_SH, RoW_LOAN.
REM Only the CHOICE is pre-seeded. Token injection still happens at first logon
REM because it fetches an encrypted token table from mcl.dwcdn.geaerospace.com,
REM which is unreachable from this isolated imaging LAN.
if not "%PCCONFIG%"=="" (
echo %PCCONFIG%> %OSDRIVE%\Enrollment\pc-config.txt
echo Pre-seeded PC config %PCCONFIG% for the enrollment orchestrator.
echo [%TIME%] Wrote pc-config.txt=%PCCONFIG% >> "%STAGELOG%"
)
REM --- Copy PPKG if selected (renames from SOURCE to BPRT-tagged filename) ---
if "%PPKG%"=="" goto copy_pctype
robocopy "Y:\ppkgs" "%OSDRIVE%\Enrollment" "%SOURCE_PPKG%" /R:1 /W:1 /NFL /NDL /NJH /NJS
REM robocopy exit codes: 0-7 = success, 8+ = failure (inverse of copy's 0/1).
if errorlevel 8 (
echo WARNING: Failed to copy enrollment package.
echo [%TIME%] WARNING: Failed to copy PPKG %SOURCE_PPKG% >> "%STAGELOG%"
goto copy_pctype
)
REM robocopy keeps the source name; rename to the BPRT-tagged target name.
if not "%SOURCE_PPKG%"=="%PPKG%" ren "%OSDRIVE%\Enrollment\%SOURCE_PPKG%" "%PPKG%"
echo [%TIME%] Copied PPKG %SOURCE_PPKG% as %PPKG% >> "%STAGELOG%"
robocopy "Y:\scripts" "%OSDRIVE%\Enrollment" "run-enrollment.ps1" "wait-for-internet.ps1" "migrate-to-wifi.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS
echo [%TIME%] Copied enrollment scripts >> "%STAGELOG%"
REM --- Create enroll.cmd at drive root as manual fallback ---
> %OSDRIVE%\enroll.cmd (
echo @echo off
echo echo Waiting for network...
echo :waitnet
echo ping -n 2 8.8.8.8 ^>NUL 2^>^&1
echo if errorlevel 1 goto waitnet
echo echo Network connected. Running enrollment...
echo powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\run-enrollment.ps1"
)
echo Manual fallback created at %OSDRIVE%\enroll.cmd
:copy_pctype
REM --- Copy shopfloor PC type setup scripts ---
if "%PCTYPE%"=="" goto cleanup_enroll
echo %PCTYPE%> %OSDRIVE%\Enrollment\pc-type.txt
robocopy "Y:\shopfloor-setup" "%OSDRIVE%\Enrollment" "Run-ShopfloorSetup.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS
REM Post-boot bulk re-fetch. WinPE staging below is best-effort (it can fail if
REM the Y: mount went idle-dead during the WIM apply); Fetch-StagingPayload.ps1
REM re-pulls the shopfloor-setup tree + preinstall bundle at first logon on a
REM FRESH mount. The unattend FirstLogonCommands runs it before the PowerShell 7
REM MSI + Run-ShopfloorSetup. Stage the script + its source coords here.
robocopy "Y:\shopfloor-setup" "%OSDRIVE%\Enrollment" "Fetch-StagingPayload.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS
REM Stage the staging self-heal directly too, so the unattend Order 5 heal step
REM runs even if Fetch-StagingPayload itself did not land. Small, reliable copy.
robocopy "Y:\shopfloor-setup" "%OSDRIVE%\Enrollment" "Verify-And-Heal-Staging.ps1" /R:1 /W:1 /NFL /NDL /NJH /NJS
> %OSDRIVE%\Enrollment\fetch-source.txt (
echo \\%PXESERVER%\enrollment
echo %SHAREUSER%
echo %SHAREPASS%
)
REM --- Always copy Shopfloor baseline scripts ---
mkdir %OSDRIVE%\Enrollment\shopfloor-setup 2>NUL
if exist "Y:\shopfloor-setup\Shopfloor" (
mkdir %OSDRIVE%\Enrollment\shopfloor-setup\Shopfloor 2>NUL
robocopy "Y:\shopfloor-setup\Shopfloor" "%OSDRIVE%\Enrollment\shopfloor-setup\Shopfloor" /E /MT:16 /R:1 /W:1 /NFL /NDL /LOG+:"%STAGELOG%"
REM Report what actually happened. These echoes used to run unconditionally
REM and printed "Copied ..." for six copies that moved zero bytes.
REM %ERRORLEVEL% is not printed: inside a parenthesised block it expands at
REM PARSE time, so it shows a stale value from before the robocopy ran.
if errorlevel 8 (
echo WARNING: shopfloor-setup\Shopfloor robocopy FAILED - nothing copied.
echo [%TIME%] FAILED: Shopfloor baseline robocopy >> "%STAGELOG%"
) else (
echo Copied Shopfloor baseline setup files.
echo [%TIME%] Copied Shopfloor baseline >> "%STAGELOG%"
)
)
REM --- Stage the site app lists + installers (pre-install = during setup,
REM postppkg = after the post-PPKG reboot; both empty in the generic build) ---
if exist "Y:\pre-install\preinstall.json" (
mkdir %OSDRIVE%\PreInstall 2>NUL
mkdir %OSDRIVE%\PreInstall\installers 2>NUL
robocopy "Y:\pre-install" "%OSDRIVE%\PreInstall" "preinstall.json" "postppkg.json" /R:1 /W:1 /NFL /NDL /NJH /NJS
if exist "Y:\pre-install\installers" (
robocopy "Y:\pre-install\installers" "%OSDRIVE%\PreInstall\installers" /E /MT:16 /R:1 /W:1 /NFL /NDL /LOG+:"%STAGELOG%"
if errorlevel 8 (
echo WARNING: pre-install\installers robocopy FAILED - nothing staged.
) else (
echo Staged preinstall bundle to %OSDRIVE%\PreInstall.
)
echo [%TIME%] Staged preinstall bundle >> "%STAGELOG%"
) else (
echo WARNING: Y:\pre-install\installers not found - preinstall.json staged without installers.
)
) else (
echo No preinstall bundle on PXE server - skipping.
)
:pctype_done
if defined STAGELOG echo [%TIME%] WinPE staging complete >> "%STAGELOG%"
REM --- Verify what we just staged ------------------------------------------
REM Check the payload BEFORE anything depends on it, while a tech is still
REM standing at the machine. A short copy found here costs 30 seconds; the same
REM copy found at first logon costs 20 minutes, and found never costs a rebuild.
REM Four Display bays sat at "imaging stage 2, green" for weeks because nothing
REM checked. Retry once for the small trees; the ppkg is 8 GB so report it
REM rather than blindly re-pulling it.
if "%PCTYPE%"=="" goto verify_done
set STAGEBAD=
if not exist "%OSDRIVE%\Enrollment\pc-type.txt" set STAGEBAD=%STAGEBAD% pc-type.txt
if not exist "%OSDRIVE%\Enrollment\Run-ShopfloorSetup.ps1" set STAGEBAD=%STAGEBAD% Run-ShopfloorSetup.ps1
if not exist "%OSDRIVE%\Enrollment\shopfloor-setup\Shopfloor\lib\Monitor-IntuneProgress.ps1" set STAGEBAD=%STAGEBAD% shopfloor-setup\Shopfloor
if not exist "%OSDRIVE%\PreInstall\preinstall.json" set STAGEBAD=%STAGEBAD% PreInstall\preinstall.json
if "%STAGEBAD%"=="" goto verify_ppkg
echo.
echo Staging verify: missing%STAGEBAD% - retrying once...
echo [%TIME%] VERIFY: missing%STAGEBAD% - retry >> "%STAGELOG%"
robocopy "Y:\shopfloor-setup" "%OSDRIVE%\Enrollment" "Run-ShopfloorSetup.ps1" /R:2 /W:2 /NFL /NDL /NJH /NJS
robocopy "Y:\shopfloor-setup\Shopfloor" "%OSDRIVE%\Enrollment\shopfloor-setup\Shopfloor" /E /MT:16 /R:2 /W:2 /NFL /NDL /NJH /NJS
robocopy "Y:\pre-install" "%OSDRIVE%\PreInstall" "preinstall.json" "postppkg.json" /R:2 /W:2 /NFL /NDL /NJH /NJS
if not exist "%OSDRIVE%\Enrollment\pc-type.txt" echo %PCTYPE%> %OSDRIVE%\Enrollment\pc-type.txt
set STAGEBAD=
if not exist "%OSDRIVE%\Enrollment\pc-type.txt" set STAGEBAD=%STAGEBAD% pc-type.txt
if not exist "%OSDRIVE%\Enrollment\Run-ShopfloorSetup.ps1" set STAGEBAD=%STAGEBAD% Run-ShopfloorSetup.ps1
if not exist "%OSDRIVE%\Enrollment\shopfloor-setup\Shopfloor\lib\Monitor-IntuneProgress.ps1" set STAGEBAD=%STAGEBAD% shopfloor-setup\Shopfloor
if not exist "%OSDRIVE%\PreInstall\preinstall.json" set STAGEBAD=%STAGEBAD% PreInstall\preinstall.json
if "%STAGEBAD%"=="" (
echo Staging verify: retry fixed it.
echo [%TIME%] VERIFY: retry succeeded >> "%STAGELOG%"
goto verify_ppkg
)
echo.
echo ******************************************************************
echo STAGING INCOMPLETE after retry:%STAGEBAD%
echo This bay will image but the post-install setup will NOT run
echo properly. Fix the share and re-image. Tell the PXE admin.
echo ******************************************************************
echo.
echo [%TIME%] VERIFY FAILED after retry:%STAGEBAD% >> "%STAGELOG%"
:verify_ppkg
REM The enrollment package is the expensive one - 8 GB. Report, do not re-pull.
if "%PPKG%"=="" goto verify_done
if exist "%OSDRIVE%\Enrollment\%PPKG%" (
echo Staging verify: enrollment package present.
echo [%TIME%] VERIFY: %PPKG% present >> "%STAGELOG%"
) else (
echo.
echo WARNING: enrollment package %PPKG% is MISSING from %OSDRIVE%\Enrollment.
echo Enrollment will not run. Re-image this bay.
echo.
echo [%TIME%] VERIFY FAILED: %PPKG% missing >> "%STAGELOG%"
)
:verify_done
REM --- BIOS update sub-stage push (fires AFTER %OSDRIVE% copies complete) ---
REM check-bios.cmd drops X:\bios-fired.flag iff it actually flashed or
REM staged a firmware update. Reading the flag file is more reliable than
REM scanning BIOS_STATUS for substrings (caret escaping inside quoted SET
REM is subtle in WinPE cmd.exe). Push must happen before Y: cleanup since
REM the push script lives on the share.
if exist X:\bios-fired.flag (
if exist "Y:\scripts\winpe-status-push.ps1" (
powershell -NoProfile -ExecutionPolicy Bypass -File "Y:\scripts\winpe-status-push.ps1" -CurrentStage "WinPE: BIOS firmware update"
)
)
:cleanup_enroll
net use Y: /delete 2>NUL
:wait_finish
ping -n 11 127.0.0.1 >NUL
wmic process where "name='PESetup.exe'" get name 2>NUL | find /I "PESetup" >NUL
if not errorlevel 1 goto wait_finish
REM --- Harvest the imaging logs to the share --------------------------------
REM Every diagnosis of a failed build so far has meant walking to the bay and
REM copying files off by hand. PESetup preserves its log and the generated
REM unattend, DISM writes its apply logs, and our staging log sits on the target
REM - all of it is thrown away at reboot unless collected now. Runs after
REM PESetup exits so the logs are final, which means re-mapping Y: (cleanup
REM already dropped it).
REM Best-effort throughout: a bay must never fail to reboot because a log copy
REM did not work.
set SERIAL=
for /f "skip=1 tokens=*" %%S in ('wmic bios get serialnumber 2^>NUL') do if not defined SERIAL set SERIAL=%%S
for /f "tokens=*" %%a in ("%SERIAL%") do set SERIAL=%%a
if "%SERIAL%"=="" set SERIAL=unknown-serial
net use Y: \\%PXESERVER%\enrollment /user:%SHAREUSER% %SHAREPASS% /persistent:no >NUL 2>&1
if not exist "Y:\" goto harvest_done
set LOGDEST=Y:\imaging-logs\%SERIAL%
mkdir "Y:\imaging-logs" 2>NUL
mkdir "%LOGDEST%" 2>NUL
echo Collecting imaging logs to %LOGDEST% ...
REM WinPE side: DISM apply logs, the unattend PESetup generated, BIOS flash log.
robocopy X:\ "%LOGDEST%" "*.log" "Unattend.xml" /R:1 /W:1 /NFL /NDL /NJH /NJS >NUL 2>&1
REM Target side: PESetup's own log lands in Panther, and our staging log.
if defined OSDRIVE (
robocopy "%OSDRIVE%\Windows\Panther" "%LOGDEST%" "PESetup*.log" /R:1 /W:1 /NFL /NDL /NJH /NJS >NUL 2>&1
robocopy "%OSDRIVE%\Enrollment" "%LOGDEST%" "winpe-staging.log" /R:1 /W:1 /NFL /NDL /NJH /NJS >NUL 2>&1
)
REM A breadcrumb of what this build actually was, so the logs are readable
REM without cross-referencing the webapp.
> "%LOGDEST%\build-context.txt" (
echo Serial=%SERIAL%
echo PCTYPE=%PCTYPE%
echo PPKG=%PPKG%
echo OSDRIVE=%OSDRIVE%
echo Media=%MEDIAPATH%
echo Date=%DATE% %TIME%
)
echo Imaging logs collected.
net use Y: /delete /y >NUL 2>&1
:harvest_done
echo.
echo Imaging complete. Rebooting in 15 seconds...
echo Press Ctrl+C to cancel.
ping -n 16 127.0.0.1 >NUL
wpeutil reboot