summaryrefslogtreecommitdiff
path: root/.github
diff options
context:
space:
mode:
author5an7y-Microsoft <[email protected]>2026-07-09 14:59:40 -0700
committerGitHub <[email protected]>2026-07-09 14:59:40 -0700
commit15640356a5a6eeafefff403f6d287821213635cd (patch)
tree49e5fcd168d50912decc42dda76767b3566887b1 /.github
parent7c8087c61eb674e321458f935b6676652c39924d (diff)
parent2ee527bfeb0aeb6be11f0a8b6dce4011b358ce89 (diff)
Merge pull request #1402 from microsoft/main
FI: Main -> Develop
Diffstat (limited to '.github')
-rw-r--r--.github/crashdetect/CrashDetectCreateUsb.cmd189
-rw-r--r--.github/crashdetect/CrashDetectOsReinstall.cmd258
-rw-r--r--.github/crashdetect/CrashDetectSetupGuide.md256
-rw-r--r--.github/crashdetect/Unattend.xml64
-rw-r--r--.github/pdu/Pdu.ps1250
-rw-r--r--.github/pdu/PduReadme.md65
-rw-r--r--.github/scripts/Build-ChangedSamples.ps12
-rw-r--r--.github/scripts/Join-CsvReports.ps1220
-rw-r--r--.github/workflows/ci-pr.yml33
-rw-r--r--.github/workflows/ci.yml35
10 files changed, 1338 insertions, 34 deletions
diff --git a/.github/crashdetect/CrashDetectCreateUsb.cmd b/.github/crashdetect/CrashDetectCreateUsb.cmd
new file mode 100644
index 00000000..571f0393
--- /dev/null
+++ b/.github/crashdetect/CrashDetectCreateUsb.cmd
@@ -0,0 +1,189 @@
+@echo off
+setlocal
+
+set WINPE_DRIVE=A
+set WINUSB_DRIVE=B
+set EXITCODE=0
+
+rem Pre-check
+if not exist "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools\amd64\DISM\dism.exe" (
+ echo ERROR: Missing DISM tool!
+ goto error
+)
+if not exist "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\en-us\winpe.wim" (
+ echo ERROR: Missing winpe.wim image!
+ goto error
+)
+if not exist "C:\WinPE_USB\Scripts\CrashDetectOsReinstall.cmd" (
+ echo ERROR: Missing script C:\WinPE_USB\Scripts\CrashDetectOsReinstall.cmd!
+ goto error
+)
+if not exist "C:\WinPE_USB\Scripts\Unattend.xml" (
+ echo ERROR: Missing XML C:\WinPE_USB\Scripts\Unattend.xml!
+ goto error
+)
+if not exist "C:\WinPE_USB\Images\install.wim" (
+ echo ERROR: Missing WIM C:\WinPE_USB\Images\install.wim!
+ goto error
+)
+
+rem Environment setup.
+echo Setting up environment for ADK tools...
+cd "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Deployment Tools>"
+call DandISetEnv.bat
+echo Done setting up environment.
+
+rem Ask for the USB disk number displayed in Diskpart.
+echo Displaying detected disk drives..
+echo Creating DiskPart script...
+(
+echo list disk
+) > C:\WinPE_USB\diskpart.txt
+diskpart /s C:\WinPE_USB\diskpart.txt
+del C:\WinPE_USB\diskpart.txt
+echo(
+echo(
+echo Note that Disk 0 is often the OS drive you do NOT want to format!
+echo(
+echo Ensure drive letters %WINPE_DRIVE%: and %WINUSB_DRIVE%: are not currently assigned to other drives!
+echo It's ok if it's assigned to the target USB.
+echo(
+set /p USBDISK=Enter the Disk Number of the USB drive you want to make WinPE bootable:
+echo(
+echo Disk %USBDISK% selected.
+
+rem Create a USB drive with WinPE and data partitions.
+echo(
+echo WARNING!!!: VERIFY DISK %USBDISK% IS THE CORRECT USB DISK TO FORMAT!
+choice /C YN /M "ARE YOU SURE YOU WANT TO FORMAT DISK %USBDISK%?"
+echo(
+if %errorlevel% equ 2 (
+ echo You chose NO, exiting without modifying USB key.
+ exit /b 0
+)
+if %errorlevel% equ 1 (
+ echo You chose YES, proceeding with formatting USB key.
+)
+
+:dism
+rem Update startnet.cmd autorun script in WinPE boot image (winpe.wim).
+cd /d "C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\amd64"
+echo(
+if not exist "C:\WinPE_USB\WinPE_amd64\mount" (
+ echo Creating temp folders C:\WinPE_USB\WinPE_amd64\mount...
+ mkdir "C:\WinPE_USB\WinPE_amd64\mount"
+)
+echo Mounting WinPE image to modify...
+Dism /Mount-Image /ImageFile:"en-us\winpe.wim" /index:1 /MountDir:"C:\WinPE_USB\WinPE_amd64\mount"
+if errorlevel 1 (
+ echo ERROR: DISM failed to mount ImageFile:"en-us\winpe.wim"!
+ goto error
+)
+echo Done mounting winpe.wim image.
+echo Updating startnet.cmd autorun script in WinPE boot image...
+(
+echo wpeinit
+echo.
+echo @echo off
+echo rem Find USB drive.
+echo for %%%%D in (D E F G H I J K L M N O P Q R S T U V W Y Z^) do ^(
+echo if exist %%%%D:Scripts\CrashDetectOsReinstall.cmd ^(
+echo call %%%%D:Scripts\CrashDetectOsReinstall.cmd
+echo goto EOF
+echo ^)
+echo ^)
+) > "C:\WinPE_USB\WinPE_amd64\mount\Windows\System32\startnet.cmd"
+echo Done adding CrashDetectOsReinstall.cmd to startnet.cmd.
+echo Unmounting WinPE image with committed changes...
+Dism /Unmount-Image /MountDir:"C:\WinPE_USB\WinPE_amd64\mount" /commit
+if errorlevel 1 (
+ echo ERROR: DISM failed to unmount ImageFile:"en-us\winpe.wim"!
+ echo USB drive have not been touched yet.
+ goto error
+)
+echo Done unmounting winpe.wim image.
+
+rem Delete temp "WinPE_amd64" folder, else the "copype.cmd" below will fail if the folder is already present.
+rmdir /s /q "C:\WinPE_USB\WinPE_amd64"
+echo Deleted temp folder "C:\WinPE_USB\WinPE_amd64".
+echo(
+echo Wiping out USB and creating 2 partitions...
+echo Creating DiskPart script...
+(
+echo select disk %USBDISK%
+echo clean
+echo create partition primary size=2048
+echo active
+echo format fs=FAT32 quick label="WinPE"
+echo assign letter=%WINPE_DRIVE%
+echo create partition primary
+echo format fs=NTFS quick label="WinUSB"
+echo assign letter=%WINUSB_DRIVE%
+) > C:\WinPE_USB\diskpart.txt
+echo Partitioning USB...
+diskpart /s C:\WinPE_USB\diskpart.txt
+if errorlevel 1 (
+ echo ERROR: DiskPart failed during partitioning and formatting USB!
+ del C:\WinPE_USB\diskpart.txt
+ goto error
+)
+del C:\WinPE_USB\diskpart.txt
+echo Done partitioning and formatting USB.
+echo(
+rem The "copype.cmd" script will create working directory "WinPE_amd64", it will fail if directory already exist.
+echo Copying WinPE working files to "C:\WinPE_USB\WinPE_amd64"...
+call copype.cmd amd64 "C:\WinPE_USB\WinPE_amd64"
+if errorlevel 1 (
+ echo ERROR: Script "copype.cmd" failed to copy working files!
+ goto error
+)
+echo Done copying WinPE files.
+echo(
+rem Install WinPE to the USB and make it bootable.
+echo Creating bootable WinPE USB...
+call Makewinpemedia.cmd /ufd /f "C:\WinPE_USB\WinPE_amd64" "%WINPE_DRIVE%:" /bootex
+if errorlevel 1 (
+ echo ERROR: Script "Makewinpemedia.cmd" failed to make WinPE USB bootable!
+ goto error
+)
+echo Done, WinPE USB drive is now bootable.
+echo(
+rem Copy Unattend.xml, scripts and install.wim over to USB
+echo Copying Unattend.xml and scripts over to USB...
+xcopy "C:\WinPE_USB\Scripts\" "%WINUSB_DRIVE%:\Scripts\" /E /I /R /Y
+if errorlevel 1 (
+ echo ERROR: Failed copying scripts from "C:\WinPE_USB\Scripts\" to USB!
+ goto error
+)
+echo Done copying script files.
+echo(
+echo Copying install.wim over to USB...this could take a while...
+robocopy "C:\WinPE_USB\Images" "%WINUSB_DRIVE%:\Images" install.wim /ETA /J
+if %errorlevel% geq 8 (
+ echo ERROR: Failed copying install.wim from "C:\WinPE_USB\Images\" to USB!
+ goto error
+)
+echo Done copying WIM file.
+echo(
+goto cleanup
+
+:error
+set EXITCODE=1
+goto cleanup
+
+:cleanup
+rem Clean up any temp folders.
+if exist "C:\WinPE_USB\WinPE_amd64" (
+ rmdir /s /q "C:\WinPE_USB\WinPE_amd64"
+ echo Cleaned up temp folder "C:\WinPE_USB\WinPE_amd64".
+)
+
+:done
+if %EXITCODE% equ 0 (
+ echo(
+ echo SUCCESSFULLY CREATED BOOTABLE WINPE USB DRIVE.
+) else (
+ echo(
+ echo FAILED CREATING BOOTABLE WINPE USB DRIVE!
+)
+endlocal & exit /b %EXITCODE% \ No newline at end of file
diff --git a/.github/crashdetect/CrashDetectOsReinstall.cmd b/.github/crashdetect/CrashDetectOsReinstall.cmd
new file mode 100644
index 00000000..faebb443
--- /dev/null
+++ b/.github/crashdetect/CrashDetectOsReinstall.cmd
@@ -0,0 +1,258 @@
+@echo off
+setlocal EnableExtensions EnableDelayedExpansion
+
+echo CrashDetectOsReinstall.cmd...START
+
+echo Searching for the USB drive volume letter...
+set "USB="
+
+for %%D in (D E F G H I J K L M N O P Q R S T U V W Y Z) do (
+ if exist "%%D:\Scripts\CrashDetectOsReinstall.cmd" (
+ set "USB=%%D"
+ echo Found USB at drive volume letter !USB!:
+ )
+)
+
+if not defined USB (
+ echo ERROR: Did not find USB drive volume letter
+ goto error
+)
+
+if not exist "!USB!:\Logs" (
+ echo Creating Logs directory on USB
+ mkdir "!USB!:\Logs"
+)
+set "LOG=!USB!:\Logs\CrashDetectOsReinstall.log"
+echo Created log file "%LOG%" on USB
+echo [%DATE% %TIME%] CrashDetectOsReinstall.cmd...START >> "%LOG%"
+echo [%DATE% %TIME%] Found USB at drive volume letter !USB!: >> "%LOG%"
+
+set "FLAG_RETRIES=!USB!:\Logs\Retries.flg"
+set "FLAG_INSTALLOS=!USB!:\Logs\InstallOs.flg"
+set "FLAG_DEPLOYOSDONE=!USB!:\Logs\DeployOsDone.flg"
+
+
+:installos
+if exist "%FLAG_INSTALLOS%" (
+ if exist "%FLAG_DEPLOYOSDONE%" (
+ echo OS reinstall done.
+ echo [%DATE% %TIME%] OS reinstall done. >> "%LOG%"
+ del "%FLAG_INSTALLOS%" >> "%LOG%" 2>&1
+ del "%FLAG_DEPLOYOSDONE%" >> "%LOG%" 2>&1
+ goto reboot
+ )
+ echo Reinstalling OS...
+ echo [%DATE% %TIME%] Reinstalling OS... >> "%LOG%"
+ goto deployos
+)
+
+
+:detectcrash
+if exist "C:\Windows\Minidump\" (
+ echo OS crashed on last reboot into the OS!
+ echo [%date% %time%] OS crashed on last reboot into the OS! >> "%LOG%"
+
+ set "MININAME="
+ for %%F in ("C:\Windows\Minidump\*.dmp") do (
+ set "MININAME=%%~nF"
+ echo Minidump filename: !MININAME! >> "%LOG%"
+ )
+
+ echo Backing up Minidump files...
+ copy /y "C:\Windows\Minidump\*.dmp" "!USB!:\Logs" >> "%LOG%" 2>&1
+ if !errorlevel! neq 0 (
+ echo ERROR: Failed to back up Mini dump file
+ echo [%date% %time%] ERROR: Failed to back up Mini dump file >> "%LOG%"
+ )
+ rmdir /s /q "C:\Windows\Minidump" >> "%LOG%" 2>&1
+ if !errorlevel! neq 0 (
+ echo ERROR: Failed to delete Minidump folder
+ echo [%date% %time%] Failed to delete Minidump folder >> "%LOG%"
+ )
+
+ if exist "C:\Windows\MEMORY.DMP" (
+ if defined MININAME (
+ ren "C:\Windows\MEMORY.DMP" "MEMORY-!MININAME!.DMP" >> "%LOG%" 2>&1
+ )
+ if !errorlevel! neq 0 (
+ echo ERROR: Failed to rename MEMORY.DMP file
+ echo [%date% %time%] ERROR: Failed to rename MEMORY.DMP file >> "%LOG%"
+ echo Backing up MEMORY.DMP file...
+ copy /y "C:\Windows\MEMORY.DMP" "!USB!:\Logs" >> "%LOG%" 2>&1
+ ) else (
+ echo Backing up MEMORY-!MININAME!.DMP file...
+ copy /y "C:\Windows\MEMORY-!MININAME!.DMP" "!USB!:\Logs" >> "%LOG%" 2>&1
+ )
+ if !errorlevel! neq 0 (
+ echo ERROR: Failed to back up MEMORY.DMP file
+ echo [%date% %time%] ERROR: Failed to back up MEMORY.DMP file >> "%LOG%"
+ )
+ )
+
+ echo Done trying to back up files to USB
+ echo [%date% %time%] Done trying to back up files to USB >> "%LOG%"
+ goto retry
+) else (
+ rem If OS crashed just now, it will be detected on next reboot.
+ rem Minidump folder used for crash detection is not created yet on automatic first crash reboot until OS is loaded.
+ echo No OS crash on last reboot into the OS.
+ echo [%date% %time%] No OS crash on last reboot into the OS. >> "%LOG%"
+ if exist %FLAG_RETRIES% (
+ del %FLAG_RETRIES% >> "%LOG%" 2>&1
+ )
+ goto reboot
+)
+
+
+:retry
+set "TRIES=0"
+rem Retries are always one less than actual crashes because first crash is not detected.
+set "MAX_RETRIES=2"
+
+if not exist "%FLAG_RETRIES%" (
+ echo Creating "%FLAG_RETRIES%" with default retries set to 0 >> "%LOG%"
+ echo RETRIES=0 > "%FLAG_RETRIES%"
+)
+
+for /f "tokens=1,2 delims==" %%A in (%FLAG_RETRIES%) do (
+ if /i "%%A"=="RETRIES" (
+ set "TRIES=%%B" >> "%LOG%" 2>&1
+ )
+)
+
+set /a TRIES+=0 2>nul >> "%LOG%" 2>&1
+set /a TRIES+=1 >> "%LOG%" 2>&1
+echo RETRIES=!TRIES! > %FLAG_RETRIES%
+
+echo Current OS boot retries: !TRIES!
+echo Current OS boot retries: !TRIES! >> "%LOG%"
+if !TRIES! GEQ %MAX_RETRIES% (
+ echo Max %MAX_RETRIES% retries reached, reinstalling OS...
+ del %FLAG_RETRIES% >> "%LOG%" 2>&1
+ type nul > %FLAG_INSTALLOS%
+ goto installos
+) else (
+ echo Max %MAX_RETRIES% retries allowed, booting into OS...
+ goto reboot
+)
+
+
+:deployos
+rem Image index is the OS edition to install from a multi-edition OS install image.
+rem For "Windows 11 25H2" image, "Windows 11 Pro" is the 6th item on the list of editions available.
+rem Adjust accordingly to install other OS editions.
+rem For single edition OS images, set index to 1.
+set "IMAGE_INDEX=6"
+set "TARGET_DISK=0"
+set "SYSTEM_DRIVE=S"
+set "SYSTEM_LABEL=System"
+set "TARGET_DRIVE=W"
+set "TARGET_LABEL=TestOS"
+set "INSTALL_WIM=install.wim"
+set "UNATTEND_XML=Unattend.xml"
+
+echo Setting flag InstallOs.flg for CrashDetectOsReinstall.cmd autorun script to detect OS install. >> "%LOG%"
+type nul > "%FLAG_INSTALLOS%"
+
+echo [1/8] Creating DiskPart script...
+echo [%DATE% %TIME%] [1/8] Creating DiskPart script... >> "%LOG%"
+(
+echo select disk "%TARGET_DISK%"
+echo clean
+echo convert gpt
+echo create partition efi size=100
+echo format quick fs=fat32 label="%SYSTEM_LABEL%"
+echo assign letter="%SYSTEM_DRIVE%"
+echo create partition msr size=16
+echo create partition primary
+echo format quick fs=ntfs label="%TARGET_LABEL%"
+echo assign letter="%TARGET_DRIVE%"
+) > X:\diskpart.txt
+
+echo [2/8] Partitioning target disk...
+echo [%DATE% %TIME%] [2/8] Partitioning target disk... >> "%LOG%"
+diskpart /s X:\diskpart.txt >> "%LOG%" 2>&1
+if errorlevel 1 (
+ echo ERROR: DiskPart failed. See %LOG%
+ goto error
+)
+
+echo [3/8] Applying OS WIM image...
+echo [%DATE% %TIME%] [3/8] Applying image... >> "%LOG%"
+dism /Apply-Image /ImageFile:"!USB!:\Images\%INSTALL_WIM%" /Index:"%IMAGE_INDEX%" /ApplyDir:"%TARGET_DRIVE%":\ >> "%LOG%" 2>&1
+if errorlevel 1 (
+ echo ERROR: DISM apply failed. See %LOG%
+ goto error
+)
+
+echo [4/8] Copying "%UNATTEND_XML%"...
+echo [%DATE% %TIME%] [4/8] Copying %UNATTEND_XML%... >> "%LOG%"
+if not exist "%TARGET_DRIVE%:\Windows\Panther" (
+ mkdir "%TARGET_DRIVE%:\Windows\Panther" >> "%LOG%" 2>&1
+)
+copy /y "!USB!:\Scripts\%UNATTEND_XML%" "%TARGET_DRIVE%:\Windows\Panther\%UNATTEND_XML%" >> "%LOG%" 2>&1
+if errorlevel 1 (
+ echo ERROR: Failed to copy %UNATTEND_XML%. See %LOG%
+ echo [%DATE% %TIME%] ERROR: Failed to copy %UNATTEND_XML%. >> %LOG%
+ goto error
+)
+
+echo [5/8] Creating boot files...
+echo [%DATE% %TIME%] [5/8] Creating boot files... >> "%LOG%"
+bcdboot W:\Windows /s S: /f UEFI >> "%LOG%" 2>&1
+if errorlevel 1 (
+ echo ERROR: BCDBOOT failed. See %LOG%
+ goto error
+)
+
+echo [6/8] Setting one-time bootsequence to OS boot manager...
+echo [%DATE% %TIME%] [6/8] Setting one-time bootsequence to OS boot manager... >> "%LOG%"
+bcdedit /set {fwbootmgr} bootsequence {bootmgr}
+echo bcdedit /set {fwbootmgr} bootsequence {bootmgr} >> "%LOG%"
+if errorlevel 1 (
+ echo ERROR: BCDEDIT /set {fwbootmgr} bootsequence {bootmgr} failed. See "%LOG%"
+ goto error
+)
+
+echo [7/8] Deployment complete.
+echo [%DATE% %TIME%] [7/8] Deployment complete. >> "%LOG%"
+echo Deployment complete. >> "%LOG%"
+echo Setting flag DeployOsDone.flg for script to detect OS install complete and boot into OS.
+type nul > "%FLAG_DEPLOYOSDONE%"
+
+echo [8/8] Rebooting into OS in 10sec...
+echo [%DATE% %TIME%] [8/8] Rebooting into OS in 10sec... >> "%LOG%"
+rem Simulate timeout with ping (10 sec)
+ping -n 11 127.0.0.1 >nul
+wpeutil reboot >> "%LOG%" 2>&1
+
+
+:reboot
+echo Rebooting into OS in 10sec...
+echo [%DATE% %TIME%] Rebooting into OS... >> "%LOG%"
+rem Force next boot into Windows boot manager.
+bcdedit /set {fwbootmgr} bootsequence {bootmgr} >> "%LOG%" 2>&1
+if errorlevel 1 (
+ echo ERROR: BCDEDIT /set {fwbootmgr} bootsequence {bootmgr} - failed. See "%LOG%"
+ goto error
+)
+rem Disable WinRE prompt to attempt system recovery, which requires user intervention.
+bcdedit /set {default} recoveryenabled no >> "%LOG%" 2>&1
+if errorlevel 1 (
+ echo ERROR: BCDEDIT /set {default} recoveryenabled no - failed. See "%LOG%"
+)
+rem Simulate timeout with ping (10 sec)
+ping -n 11 127.0.0.1 >nul
+wpeutil reboot >> "%LOG%" 2>&1
+
+
+:error
+echo CrashDetectOsReinstall.cmd...ERROR! See "%LOG%"
+echo [%DATE% %TIME%] CrashDetectOsReinstall.cmd...ERROR! >> "%LOG%"
+exit /b 1 >> "%LOG%" 2>&1
+
+
+:done
+echo CrashDetectOsReinstall.cmd...DONE!
+echo [%DATE% %TIME%] CrashDetectOsReinstall.cmd...DONE! >> "%LOG%"
+exit /b 0 >> "%LOG%" 2>&1
diff --git a/.github/crashdetect/CrashDetectSetupGuide.md b/.github/crashdetect/CrashDetectSetupGuide.md
new file mode 100644
index 00000000..dc974507
--- /dev/null
+++ b/.github/crashdetect/CrashDetectSetupGuide.md
@@ -0,0 +1,256 @@
+# Crash Detect USB Setup Guide: <br>Automating Crash Detection and OS Reinstall of Target Test Systems
+
+---
+
+## A) Overview
+This guide walks through setting up a new bootable WinPE USB drive using:
+
+- A **Host Controller** (to build the USB)
+- A **bootable WinPE USB** (automatic crash detection and OS reinstall)
+- A **target test system**
+
+### High-level flow
+1. Download Windows image (ISO)
+2. Install Windows ADK + WinPE add-on
+3. Create bootable WinPE USB
+4. Copy image + scripts to USB
+5. Boot up target system
+6. OPTIONAL: Install OS on new Target System
+7. OPTIONAL: Add a Custom Script to Windows Setup
+
+---
+
+## B) Requirements
+
+### Host Controller
+- Windows 11 25H2
+- Administrator privileges
+- USB drive (>= 64GB recommended)
+- Internet connection
+
+### Target System
+- Windows 11 25H2
+- BIOS boot priority set to **USB boot** and **Secure Boot disabled**
+- Windows System failure recovery set to **"Automatically restart"**
+- Willing to wipe disk (automatic OS reinstall from crash)
+
+---
+
+## C) Download Required Software and Scripts onto the Host Controller
+
+### Windows OS Image (ISO)
+- [Download Windows 11 (official)](https://www.microsoft.com/en-us/software-download/windows11)
+- Go to section "Download Windows 11 Disk Image (ISO) for x64 devices".
+- Select the option "Windows 11 (multi-edition ISO for x64 devices)".
+- Click "Confirm" button.
+- Section "Select the product language" should appear.
+- Select your language option. (Ex: "English (United States)")
+- Click "Confirm" button.
+- Section "Download - Windows 11 English" should appear.
+- Click "64-bit Download" button.
+
+Mount ISO:
+- In File Explorer, go to the location you downloaded the ISO file `Win11_25H2_English_x64_v2.iso` to.
+- Right-click on the ISO file and select "Mount".
+ - If the "Open File - Security Warning" prompt pops up after a minute then click "Open".
+ - (The prompt may be hidden behind other Windows.)
+- Create new folder and subfolder `C:\WinPE_USB\Images`.
+- Go to the `%MountDriveLetter%:\sources` folder and copy the **`install.wim`** file to **`C:\WinPE_USB\Images`**.
+ - This is the Windows 11 OS image file that the DISM tool will need to deploy the OS.
+ - This file will be copied over to the USB later after bootable WinPE USB creation.
+- Right-click on the %MountDriveLetter% and select "Eject" to unmount the ISO image.
+
+---
+
+### Windows Assessment and Deployment Kit (Deployment Tools) & Windows PE Add-on
+- [Download Windows ADK & WinPE Add-on](https://learn.microsoft.com/en-us/windows-hardware/get-started/adk-install)
+- Go to section "Download the ADK 10.1.26100.2454 (December 2024)".
+- Click on the link "Download the Windows ADK 10.1.26100.2454 (December 2024)" to download the `adksetup.exe` installer.
+- Click on the link "Download the Windows PE add-on for the Windows ADK 10.1.26100.2454 (December 2024)" to download the `adkwinpesetup.exe` installer.
+
+Install ADK:
+- Double-click on the `adksetup.exe` from the location you downloaded the file to launch the installer.
+- "Specify Location" page, click "Next", to install at default location.
+- "Windows Kits Privacy" page, select your privacy option, click "Next".
+- "License Agreement" page, click "Accept".
+- "Select the features you want to install" page, confirm "Deployment Tools" is checked, then click "Install".
+- If "User Account Control" prompt appears, click "Yes" to begin installation process.
+- "Installing features..." page, wait for installation process to complete.
+- "Welcome to the Windows Assessment and Deployment Kit!" page, click "Close".
+
+Install WinPE Add-on: <br>**Important:** Install **ADK first**, then WinPE add-on
+- Double-click on the `adkwinpesetup.exe` from the location you downloaded the file to launch the installer.
+- "Specify Location" page, click "Next", to install at default location.
+- "Windows Kits Privacy" page, select your privacy option, click "Next".
+- "License Agreement" page, click "Accept".
+- "Select the features you want to install" page, confirm "Windows Preinstallation Environment (Windows PE)" is checked, then click "Install".
+- If "User Account Control" prompt appears, click "Yes" to begin installation process.
+- "Installing features..." page, wait for installation process to complete.
+- "Welcome to the Windows Assessment and Deployment Kit Windows Preinstallation Environment Add-ons!" page, click "Close".
+
+---
+
+### Unattend and Script Files from GitHub
+- Download files from [Windows-driver-samples/tree/main/.github/crashdetect](https://github.com/microsoft/Windows-driver-samples/tree/main/.github/crashdetect)
+- Create directory **`C:\WinPE_USB\Scripts\`** and copy the following downloaded files to there.
+ - `CrashDetectCreateUsb.cmd`
+ - `CrashDetectOsReinstall.cmd`
+ - `Unattend.xml`
+
+---
+
+## D) Create Bootable WinPE USB
+- **TIP:** It's a good idea to make a backup copy of the original **"winpe.wim"** image file before editing it in the following steps.
+ - `C:\Program Files (x86)\Windows Kits\10\Assessment and Deployment Kit\Windows Preinstallation Environment\amd64\en-us\winpe.wim`
+
+- Plug a USB drive into the Host Controller.
+- Check to make sure drive letters **A:** and **B:** are not currently used by any other drive. If used by the target USB drive, it's okay.
+- Confirm the folder **`C:\WinPE_USB`** and its subfolders **`Scripts`** and **`Images`** exist.
+- Confirm the **`Scripts`** subfolder contains the following files that were downloaded from previous steps.
+ - `CrashDetectOsReinstall.cmd`
+ - `Unattend.xml`
+- Confirm the **`Images`** subfolder contains the Win11 OS image file.
+ - `install.wim`
+
+### OPTION 1: Use the `CrashDetectCreateUsb.cmd` script to create the USB automatically.
+- Start a `Command Prompt` running as administrator.
+- Run the script by typing the following line into the Command Prompt.
+```cmd
+C:\WinPE_USB\Scripts\CrashDetectCreateUsb.cmd
+```
+- The script will display a list of detected disk drives, usually Disk 0 is the OS disk, DO NOT select that disk.
+- Prompt 1: will ask you to enter the Disk number of your USB drive.
+- Prompt 2: will ask you to confirm one last time before wiping out the USB drive.
+- The last step will copy over the `install.wim` OS image to the USB, which could take a while.
+
+### OPTION 2: Follow the steps below to create the USB manually.
+#### 1. Make sure your PC has the ADK and ADK Windows PE add-on installed.
+ - Start the `Deployment and Imaging Tools Environment` running as administrator.
+
+#### 2. Update the `startnet.cmd` autorun script in the WinPE boot image.
+ - Mount the WinPE boot image (`winpe.wim`) using DISM.
+ - Adds the `CrashDetectOsReinstall.cmd` script to the `startnet.cmd` script.
+```cmd
+(
+echo wpeinit
+echo.
+echo @echo off
+echo REM Find USB drive.
+echo for %%D in (D E F G H I J K L M N O P Q R S T U V W Y Z^) do ^(
+echo if exist %%D:Scripts\CrashDetectOsReinstall.cmd ^(
+echo call %%D:Scripts\CrashDetectOsReinstall.cmd
+echo goto EOF
+echo ^)
+echo ^)
+) > "C:\WinPE_USB\WinPE_amd64\mount\Windows\System32\startnet.cmd"
+```
+ - Unmount the WinPE image using DISM.
+```cmd
+Dism /Unmount-Image /MountDir:"C:\WinPE_USB\WinPE_amd64\mount" /commit
+```
+ - Delete folder `C:\WinPE_USB\WinPE_amd64`, else the `copype.cmd` below will fail if the folder is already present.
+```cmd
+rmdir /s /q "C:\WinPE_USB\WinPE_amd64"
+```
+
+#### 3. Create and format a multiple partition USB drive.
+ - Attach a USB large enough for 2GB WinPE partition + WinUSB partition (Win11 WIM 8GB + Memory dump files 16GB-64GB + Scripts).
+ - Enter the following commands into the command prompt.
+```cmd
+diskpart
+list disk
+select disk X (where X is your USB drive)
+clean
+create partition primary size=2048
+active
+format fs=FAT32 quick label="WinPE"
+assign letter=A
+create partition primary
+format fs=NTFS quick label="WinUSB"
+assign letter=B
+exit
+```
+
+#### 4. Create a bootable Windows PE USB drive.
+ - Copying WinPE boot files to a working directory.
+```cmd
+copype.cmd amd64 "C:\WinPE_USB\WinPE_amd64"
+```
+ - Copy the WinPE files to the WinPE partition on USB.
+```cmd
+MakeWinPEMedia.cmd /UFD /F "C:\WinPE_USB\WinPE_amd64" "A:" /bootex
+```
+
+---
+
+#### 5. Copy scripts and OS install image over to WinUSB partition on USB.
+ - Copy Script files over to USB.
+```cmd
+xcopy "C:\WinPE_USB\Scripts\" "B:\Scripts\" /E /I /R /Y
+```
+ - Copy Windows 11 OS WIM file over to USB, this could take a while...
+```cmd
+robocopy "C:\WinPE_USB\Images" "B:\Images" install.wim /ETA /J
+```
+
+---
+
+## E) Boot Up Target System
+- Insert USB into target PC
+- Power on
+- Enter boot menu (F12 / ESC / DEL depending on vendor)
+- Confirm BIOS/UEFI setting has USB Drive as the first boot priority. (Varies among vendors)
+- Confirm Secure Boot setting is Disabled
+- Save BIOS settings to reboot target system.
+- WinPE will automatically launch the "startnet.cmd" script we edited earlier in the "winpe.wim" image.
+- The script will call "wpeinit", then our "CrashDetectOsReinstall.cmd" script to begin automatic OS crash detection and reimage for WDK driver testing.
+
+---
+
+## F) OPTIONAL: Install OS on new Target System
+### On the bootable USB's second partition `WinPE_USB`
+- Create the folder **`Logs`**.
+- Create an empty file **`InstallOs.flg`** in that folder.
+ - (In File Explorer, ensure file name extensions are visible, else the filename may be accidentally set to `InstallOs.flg.txt`)
+- Plug USB into target system and reboot into USB.
+- The USB will detect the flag and begin reinstalling the OS immediately.
+ - **WARNING**: There will be **NO** prompt to reconfirm OS install, be sure to plug into the correct target system.
+ - Do **NOT** leave this USB plugged into the Host Controller when this flag is set, to avoid accidental OS reinstall.
+
+---
+
+## G) OPTIONAL: Add a Custom Script to Windows Setup
+### Setupcomplete.cmd and ErrorHandler.cmd
+- These are custom scripts that run during or after the Windows Setup process. They can be used to install applications or run other tasks by using cscript/wscript scripts.
+- Follow instructions on this website:
+ - (https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/add-a-custom-script-to-windows-setup)
+
+---
+
+## Troubleshooting
+### ERROR: Script "Makewinpemedia.cmd" failed to make WinPE USB bootable!
+- If your Host Controller is connected to a secured IT network, the actions in this script may have been blocked.
+- Check to see if **"bootsect.exe"** was blocked by Windows Security.
+ - Run "Windows Security"
+ - Select "Virus & threat protection"
+ - Click link "Manage ransomware protection" at the bottom
+ - Click link "Allow an app through Controlled folder access"
+ - Click button "Add an allowed app" button, then select option "Recently blocked apps"
+ - Scroll down and look for the "bootsect.exe" app to add to allow list
+### USB won't boot
+- Check BIOS boot order
+- Disable Secure Boot
+### Disk not visible in WinPE
+- Missing storage drivers
+### Windows doesn't boot
+- Re-run `bcdboot`
+- Verify partition layout
+
+---
+
+## Reference Documentation
+- [WinPE overview](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/winpe-intro)
+- [Create a USB drive with WinPE and data partitions](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/winpe--use-a-single-usb-key-for-winpe-and-a-wim-file---wim#create-a-usb-drive-with-winpe-and-data-partitions)
+- [WinPE: Create bootable media](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/winpe-create-usb-bootable-drive)
+- [Capture and apply Windows (WIM)](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/capture-and-apply-windows-using-a-single-wim)
+- [Answer files (unattend.xml)](https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/update-windows-settings-and-scripts-create-your-own-answer-file-sxs) \ No newline at end of file
diff --git a/.github/crashdetect/Unattend.xml b/.github/crashdetect/Unattend.xml
new file mode 100644
index 00000000..a235fba0
--- /dev/null
+++ b/.github/crashdetect/Unattend.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<unattend xmlns="urn:schemas-microsoft-com:unattend"
+ xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+
+ <settings pass="oobeSystem">
+ <component name="Microsoft-Windows-International-Core"
+ processorArchitecture="amd64"
+ publicKeyToken="31bf3856ad364e35"
+ language="neutral"
+ versionScope="nonSxS">
+ <InputLocale>en-US</InputLocale>
+ <SystemLocale>en-US</SystemLocale>
+ <UILanguage>en-US</UILanguage>
+ <UserLocale>en-US</UserLocale>
+ </component>
+
+ <component name="Microsoft-Windows-Shell-Setup"
+ processorArchitecture="amd64"
+ publicKeyToken="31bf3856ad364e35"
+ language="neutral"
+ versionScope="nonSxS">
+
+ <TimeZone>Pacific Standard Time</TimeZone>
+
+ <!-- Skip all OOBE -->
+ <OOBE>
+ <HideEULAPage>true</HideEULAPage>
+ <HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
+ <HideOnlineAccountScreens>true</HideOnlineAccountScreens>
+ <HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
+ <HideLocalAccountScreen>true</HideLocalAccountScreen>
+ <NetworkLocation>Work</NetworkLocation>
+ <ProtectYourPC>3</ProtectYourPC>
+ </OOBE>
+
+ <UserAccounts>
+ <LocalAccounts>
+ <LocalAccount wcm:action="add">
+ <Name>admin</Name>
+ <Group>Administrators</Group>
+ <Password>
+ <Value>@Password123</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ </LocalAccount>
+ </LocalAccounts>
+ </UserAccounts>
+
+ <!-- Auto logon (lab only) -->
+ <AutoLogon>
+ <Username>admin</Username>
+ <Enabled>true</Enabled>
+ <Password>
+ <Value>@Password123</Value>
+ <PlainText>true</PlainText>
+ </Password>
+ <LogonCount>999</LogonCount>
+ </AutoLogon>
+
+ </component>
+ </settings>
+
+</unattend> \ No newline at end of file
diff --git a/.github/pdu/Pdu.ps1 b/.github/pdu/Pdu.ps1
new file mode 100644
index 00000000..b87fd470
--- /dev/null
+++ b/.github/pdu/Pdu.ps1
@@ -0,0 +1,250 @@
+# ======================================================================
+#
+# Manufacturer: Eaton Tripp Lite Switched Power Distribution Unit (PDU)
+# Website: https://www.eaton.com/us/en-us/skuPage.PDUMH15NET.html
+# Model: PDUMH15NET
+# SNMP/Web Management Accessory Card: WEBCARDLX
+#
+# HW Requirements: Ethernet cable or USBA-to-MicroUSB cable.
+# SW Requirements: Plink.exe from PuTTY terminal client SW (if Ethernet)
+# PuTTY: https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html
+#
+# Script: PowerShell script for turning ON/OFF and power CYCLING the PDU
+# Usage: .\Pdu.ps1 [on | off | cycle] [1-16]
+#
+# ======================================================================
+
+param(
+ [Parameter(Mandatory=$true, Position=0)]
+ [ValidateSet("cycle","on","off")]
+ [string]$Action,
+
+ [Parameter(Mandatory=$true, Position=1)]
+ [ValidateRange(1,16)]
+ [int]$LoadNumber
+)
+
+# =========================
+# Configuration
+# =========================
+$ComNumber = 3
+$ComPort = "COM$ComNumber"
+
+$PduIP = "169.254.0.1"
+$SshPort = 22
+
+$Username = "localadmin"
+$Password = "@Password123"
+
+$Plink = "C:\Program Files\PuTTY\plink.exe"
+
+# =========================
+# Helper Functions
+# =========================
+
+function Test-PduSerialConnection
+{
+ [bool]$Connected = $false
+
+ try
+ {
+ $Port = New-Object System.IO.Ports.SerialPort $ComPort,115200,None,8,one
+ $Port.ReadTimeout = 3000
+ $Port.WriteTimeout = 3000
+ $Port.Open()
+ Start-Sleep 5
+ $Port.DiscardOutBuffer()
+ $Port.DiscardInBuffer()
+ $null = $Port.ReadExisting()
+
+ # Wake up console
+ $Port.WriteLine("")
+ Start-Sleep 2
+
+ $Banner = $Port.ReadExisting()
+ if ($Banner -match "login|localadmin|PowerAlert|Ubuntu") {
+ Write-Host "PDU detected on serial connection at $ComPort"
+ $Connected = $true
+ } else {
+ Write-Host "PDU NOT detected on serial connection at $ComPort" -ForegroundColor Yellow
+ }
+ }
+ catch
+ {
+ Write-Host "Unable to communicate on serial connection at $ComPort" -ForegroundColor Yellow
+ Write-Host $_.Exception.Message
+ }
+ finally
+ {
+ if ($Port -and $Port.IsOpen) {
+ $Port.DiscardOutBuffer()
+ $Port.DiscardInBuffer()
+ if ($Port.BytesToRead -gt 0) {
+ $null = $Port.ReadExisting()
+ }
+ $Port.Close()
+ }
+
+ $Port.Dispose()
+ $Port = $null
+ }
+ return $Connected
+}
+
+function Send-PduSerialCommand
+{
+ param([string]$Action)
+
+ $Port = New-Object System.IO.Ports.SerialPort $ComPort,115200,None,8,one
+
+ try
+ {
+ $Port.ReadTimeout = 3000
+ $Port.WriteTimeout = 3000
+
+ $Port.Open()
+ Start-Sleep 5
+ $Port.DiscardOutBuffer()
+ $Port.DiscardInBuffer()
+ $null = $Port.ReadExisting()
+
+ Write-Host "Using SERIAL connection..." -ForegroundColor Green
+ $Port.WriteLine("")
+ Start-Sleep 2
+
+ Write-Host "Sending Username to PDU..."
+ $Port.WriteLine($Username)
+ Start-Sleep 2
+
+ Write-Host "Sending Password to PDU..."
+ $Port.WriteLine($Password)
+ Start-Sleep 5
+
+ # Sending [ cycle | on | off ] command to PDU.
+ Write-Host "Sending '$Action' command to PDU..."
+ $Port.WriteLine("device; load $LoadNumber; $Action force")
+ Start-Sleep 2
+
+ # "Sending Exit command to PDU..."
+ $Port.WriteLine("exit; exit; exit")
+ Start-Sleep 2
+ }
+ catch
+ {
+ Write-Host "Unable to communicate with $ComPort"
+ Write-Host $_.Exception.Message
+ }
+ finally
+ {
+ if ($Port -and $Port.IsOpen) {
+ $Port.DiscardOutBuffer()
+ $Port.DiscardInBuffer()
+ if ($Port.BytesToRead -gt 0) {
+ $null = $Port.ReadExisting()
+ }
+ $Port.Close()
+ Write-Host "Closed serial port $ComPort"
+ }
+ if ($Port)
+ {
+ $Port.Dispose()
+ }
+ $Port = $null
+ }
+}
+
+function Test-PduNetworkConnection
+{
+ [bool]$Connected = $false
+
+ try
+ {
+ $Client = New-Object System.Net.Sockets.TcpClient
+# $Client.Connect($PduIP,$SshPort)
+
+ $Result = $Client.BeginConnect($PduIP,$SshPort,$null,$null)
+ if (-not $Result.AsyncWaitHandle.WaitOne(3000))
+ {
+ throw "Connection timeout"
+ }
+ $Client.EndConnect($Result)
+
+ $Stream = $Client.GetStream()
+ Start-Sleep 1
+
+ $Buffer = New-Object byte[] 1024
+ $Stream.ReadTimeout = 3000
+ $Bytes = $Stream.Read($Buffer,0,$Buffer.Length)
+
+ $Banner = [System.Text.Encoding]::ASCII.GetString($Buffer,0,$Bytes)
+
+ if ($Banner -match "login|localadmin|PowerAlert|Ubuntu") {
+ Write-Host "PDU detected on network connection at IP: $PduIP, Port: $SshPort"
+ $Connected = $true
+ }
+ else {
+ Write-Host "PDU NOT detected on network connection at IP: $PduIP, Port: $SshPort" -ForegroundColor Yellow
+ }
+
+ $Client.Close()
+ }
+ catch
+ {
+ Write-Host "Unable to establish network connection at IP: $PduIP, Port: $SshPort" -ForegroundColor Yellow
+ Write-Host $_.Exception.Message
+ }
+ return $Connected
+}
+
+function Send-PduNetworkCommand
+{
+ param([string]$Action)
+
+ if (-not (Test-Path $Plink))
+ {
+ throw "plink.exe not found: $Plink"
+ }
+
+ Write-Host "Using NETWORK connection..." -ForegroundColor Green
+
+ $Command = "device; load $LoadNumber; $Action force; exit; exit; exit"
+
+ $Command | & $Plink `
+ -ssh `
+ -batch `
+ -pw $Password `
+ "$Username@$PduIP" `
+}
+
+# =========================
+# Main Logic
+# =========================
+
+Write-Host ""
+Write-Host "Requested Action : $Action"
+Write-Host ""
+
+# First choice = Serial
+if (Test-PduSerialConnection)
+{
+ Send-PduSerialCommand $Action
+ exit 0
+}
+
+Write-Host "Serial connection not available."
+Write-Host "Trying network connection..."
+
+# Second choice = Network
+if (Test-PduNetworkConnection)
+{
+ Send-PduNetworkCommand $Action
+ exit 0
+}
+
+Write-Host ""
+Write-Host "ERROR: No PDU connection available." -ForegroundColor Red
+Write-Host " Serial : $ComPort"
+Write-Host " Ethernet : $PduIP"
+Write-Host ""
+
+exit 1 \ No newline at end of file
diff --git a/.github/pdu/PduReadme.md b/.github/pdu/PduReadme.md
new file mode 100644
index 00000000..524bddd2
--- /dev/null
+++ b/.github/pdu/PduReadme.md
@@ -0,0 +1,65 @@
+# PDU Setup: <br>Eaton Tripp Lite Switched Power Distribution Unit (PDUMH15NET)
+
+## Requirements:
+### HW:
+ - USBA-to-MicroUSB cable, needed for 1st time PDU login and password change, if IP address is unknown.
+ - Ethernet cable, can be used if the IP address is known.
+### SW:
+ - [Tera Term 5.6.1 terminal client:](https://github.com/TeraTermProject/teraterm/releases/tag/v5.6.1)
+ - Terminal client for PDU login and configuration via serial.
+ - [PuTTY 0.84 terminal client:](https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html)
+ - `Plink.exe` needed for PowerShell scripting via network.
+ - **Note**: PuTTY terminal client GUI does not appear to work, so Tera Term client was used instead, but `Plink.exe` is needed for passing passwords via SSH in PowerShell scripts.
+
+## Serial Connection
+### What's my IP address?
+- If the PDU is connected to a network with **DHCP**, then we need to find it's IP address first by using serial connection.
+ - If the PDU is not connected to a network with DHCP, then its default static IP address should be `169.254.0.1`.
+ - If that's the case, then you can login to the PDU via a browser by skipping the the section Network Connection.
+
+Tera Term:
+- Connect the PC to the PDU (CONFIG port) with a USBA-to-MiniUSB cable.
+- On the PC, go to Device Manager and verify under `Ports (COM & LPT)`, the `USB Serial Device (COM3)` device is present.
+- Download and install Tera Term on PC, if not already present.
+ - You may use other Terminal Emulation Programs, but this is the one used in the manufacturer's user manual to connect to the WEBCARDLX network interface card.
+- Launch Tera Term and select the "Serial" connection type and ensure `Port: "COM3: USB Serial Device (COM3)` is selected, then click "OK".
+- When the blinking cursor in the empty prompt stops blinking, hit Enter key to wake up console.
+- The `Ubuntu 18.04.6 LTS poweralert-0006674a215f` (<-- MAC Address) should show up in the prompt asking for login.
+ - Default login is: `localadmin`
+ - Default 1st time password is: `localadmin`
+ - Must change password after 1st logon.
+ - Change password to `@Password123` to match what the `pdu.ps1` PowerShell script uses.
+- Once logged in, type `show network` to display network information such as current IP address.
+ - If on DHCP network, note the IP address assigned to the PDU.
+ - PDU default if no DHCP:
+ - IPv4: `169.254.0.1`
+ - Subnet Mask: `255.255.0.0`
+- Test pinging the IPv4 address of the PDU from the PC.
+ - `ping <IP Address>`
+- Now that you have the IP address, it may be easier to switch over to the browser interface to configure the rest of the PDU.
+
+## Network Connection
+- Connect an Ethernet cable from the PC to the PDU Ethernet port.
+- Since we have the IP of the PDU, it's easier to configure the PDU via a browser interface.
+- Open an Internet browser and enter the IPv4 address of the PDU into the browser.
+ - If it prompts you about any security warnings, just find a way to accept and continue anyway.
+- The `Power Alert LX` web interface should prompt for username and password.
+- If 1st time login...
+ - Default login is: `localadmin`
+ - Default 1st time password is: `localadmin`
+ - Must change password after 1st logon.
+ - Change password to `@Password123` to match what the `pdu.ps1` PowerShell script uses.
+- Select `Load` tab on the left of screen to see all the loads on the 16 power ports.
+- Click on the `State` slider to show the Load options, `Turn Off Load` and `Cycle Load`.
+- To enable SSH for PowerShell scripting later, select `Network` tab, then `Services`, then enable `SSH Enabled`.
+
+## Scripting
+- The `Pdu.ps1` PowerShell script can be used for automating power ON/OFF/CYCLE of the PDU ports.
+- Usage: `.\Pdu.ps1 [on | off | cycle] [1-16]`
+- Update the following variables in the `Pdu.ps1` script if necessary.
+ - `$PduIP = "169.254.0.1"`
+ - `$Password = "@Password123"`
+- The script will check first if there's a serial connection first, then network connection if serial is not present, before issuing the PDU command.
+
+## References
+- Eaton Tripp Lite Switched PDU (PDUMH15NET): [Manuals & User Guides](https://www.eaton.com/us/en-us/skuPage.PDUMH15NET.html#tab-2) \ No newline at end of file
diff --git a/.github/scripts/Build-ChangedSamples.ps1 b/.github/scripts/Build-ChangedSamples.ps1
index 3e91eb88..f03f93f3 100644
--- a/.github/scripts/Build-ChangedSamples.ps1
+++ b/.github/scripts/Build-ChangedSamples.ps1
@@ -22,7 +22,7 @@ foreach ($file in $ChangedFiles) {
$filename = Split-Path $file -Leaf
# Files that can affect how every sample is built should trigger a full build
- if ($filename -eq "Build-Samples.ps1" -or $filename -eq "exclusions.csv" -or $filename -eq "Directory.Build.props" -or $filename -eq "packages.config") {
+ if ($filename -eq "Build-Samples.ps1" -or $filename -eq "Get-NtTargetVersions.ps1" -or $filename -eq "exclusions.csv" -or $filename -eq "Directory.Build.props" -or $filename -eq "packages.config") {
$buildAll = $true
}
if ($dir -like "$root\.github\scripts" -or $dir -like "$root\.github\scripts\*") {
diff --git a/.github/scripts/Join-CsvReports.ps1 b/.github/scripts/Join-CsvReports.ps1
index 32cd9d63..43e0c826 100644
--- a/.github/scripts/Join-CsvReports.ps1
+++ b/.github/scripts/Join-CsvReports.ps1
@@ -1,35 +1,201 @@
-$logsPath = Join-Path (Get-Location).Path "_logs"
+<#
+.SYNOPSIS
+ Joins the per-job Build-Samples CSV reports (one per _NT_TARGET_VERSION x configuration x
+ platform) into a single overview, and writes an easy-to-scan summary to the GitHub Actions
+ run page ($GITHUB_STEP_SUMMARY).
+
+.DESCRIPTION
+ Each build job uploads a "_logs" folder containing a report named
+ _overview.<ntTag>.<configuration>.<platform>.csv
+ with columns: Sample, <Configuration|Platform> (one combination per file). This script:
+ * parses the _NT_TARGET_VERSION tag and combination from every report,
+ * collapses each sample's combinations into one status per version,
+ * writes _overview.all.csv / _overview.all.htm (a colour-coded sample x version matrix), and
+ * appends a Markdown summary (per-version totals + a failures table) to $GITHUB_STEP_SUMMARY
+ so failures are obvious from the run page without opening any logs.
+
+ The older 2-part name (_overview.<configuration>.<platform>.csv, no version) is still
+ understood and bucketed under the "latest" column.
+#>
+
+$logsPath = Join-Path (Get-Location).Path "_logs"
$reportFileName = '_overview.all'
-$idProperty = 'Sample'
-$results = $null
-Get-ChildItem -Path $logsPath -Filter '*.csv' | ForEach-Object {
- $csv = Import-Csv -Path $_
- if ($results -eq $null) {
- $results = $csv
+if (-not (Test-Path $logsPath)) {
+ Write-Warning "No _logs directory found at $logsPath - nothing to report."
+ return
+}
+
+# --- Load every per-job CSV ---------------------------------------------------
+# data[sample][tag][combo] = status
+$data = @{}
+$allSamples = [System.Collections.Generic.SortedSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
+$tagSet = [System.Collections.Generic.HashSet[string]]::new()
+
+Get-ChildItem -Path $logsPath -Filter '_overview.*.csv' |
+ Where-Object { $_.Name -notlike '_overview.all.*' } |
+ ForEach-Object {
+ # _overview.<tag>.<config>.<platform> -> drop _overview; last two are config/platform.
+ $parts = [IO.Path]::GetFileNameWithoutExtension($_.Name).Split('.')
+ $parts = $parts[1..($parts.Count - 1)] # drop the leading "_overview"
+ if ($parts.Count -ge 3) { $tag = ($parts[0..($parts.Count - 3)] -join '.') }
+ else { $tag = 'latest' }
+ [void]$tagSet.Add($tag)
+
+ Import-Csv -Path $_.FullName | ForEach-Object {
+ $sample = $_.Sample
+ if (-not $sample) { return }
+ [void]$allSamples.Add($sample)
+ if (-not $data.ContainsKey($sample)) { $data[$sample] = @{} }
+ if (-not $data[$sample].ContainsKey($tag)) { $data[$sample][$tag] = @{} }
+ foreach ($col in ($_.PSObject.Properties.Name | Where-Object { $_ -ne 'Sample' })) {
+ $data[$sample][$tag][$col] = "$($_.$col)".Trim()
+ }
+ }
}
- else {
- $results = $csv | ForEach-Object {
- $id = $_.$idProperty
- $match = $results | Where-Object { $_.$idProperty -eq $id }
- if ($match) {
- $properties = $_ | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -ne $idProperty } | Select-Object -ExpandProperty Name
- $newObject = New-Object PSObject
- # Add ID property separately to ensure it appears first
- $newObject | Add-Member -MemberType NoteProperty -Name $idProperty -Value $_.$idProperty
- foreach ($property in $properties) {
- $newObject | Add-Member -MemberType NoteProperty -Name $property -Value $_.$property
- }
- foreach ($property in ($match | Get-Member -MemberType NoteProperty | Where-Object { $_.Name -ne $idProperty } | Select-Object -ExpandProperty Name)) {
- if ($properties -notcontains $property) {
- $newObject | Add-Member -MemberType NoteProperty -Name $property -Value $match.$property
- }
- }
- $newObject
+
+if ($tagSet.Count -eq 0) {
+ Write-Warning "No per-job '_overview.*.csv' reports were found in $logsPath."
+ return
+}
+
+# Order versions newest-first (numeric tags descending; non-numeric last)
+$tags = $tagSet | Sort-Object @{ Expression = { if ($_ -match '^\d+$') { [int]$_ } else { 0 } }; Descending = $true }, @{ Expression = { $_ } }
+
+function Get-Status {
+ # Collapse one sample/version's combinations into a single status object.
+ param([hashtable]$Combos)
+ $c = @{ Succeeded = 0; Failed = 0; Sporadic = 0; Unsupported = 0; Excluded = 0 }
+ $details = @()
+ if ($Combos) {
+ foreach ($k in ($Combos.Keys | Sort-Object)) {
+ switch ($Combos[$k]) {
+ 'Succeeded' { $c.Succeeded++ } 'Failed' { $c.Failed++ } 'Sporadic' { $c.Sporadic++ }
+ 'Unsupported' { $c.Unsupported++ } 'Excluded' { $c.Excluded++ }
+ }
+ $details += "$k = $($Combos[$k])"
+ }
+ }
+ $buildable = $c.Succeeded + $c.Failed + $c.Sporadic
+ if (-not $Combos -or $Combos.Count -eq 0) { $label = 'n/a'; $klass = 'na' }
+ elseif ($buildable -eq 0) { $label = '--'; $klass = 'na' }
+ elseif ($c.Failed -eq 0 -and $c.Sporadic -eq 0) { $label = "PASS ($($c.Succeeded)/$buildable)"; $klass = 'pass' }
+ elseif ($c.Failed -eq 0) { $label = "PASS* ($($c.Succeeded + $c.Sporadic)/$buildable)"; $klass = 'flaky' }
+ elseif ($c.Failed -eq $buildable) { $label = "FAIL ($($c.Failed)/$buildable)"; $klass = 'fail' }
+ else { $label = "PARTIAL ($($c.Failed) failed / $buildable)"; $klass = 'partial' }
+ [pscustomobject]@{ Label = $label; Class = $klass; Tooltip = ($details -join ' | '); Counts = $c; Buildable = $buildable }
+}
+
+# --- Build per-version totals + the matrix ------------------------------------
+$totals = @{}; foreach ($t in $tags) { $totals[$t] = [pscustomobject]@{ S = 0; F = 0; O = 0; U = 0; E = 0; pass = 0; flaky = 0; partial = 0; fail = 0; na = 0 } }
+$failuresList = [System.Collections.ArrayList]::new()
+$csvRows = @()
+$bodyRows = New-Object System.Text.StringBuilder
+
+foreach ($sample in $allSamples) {
+ $csvRow = [ordered]@{ Sample = $sample }
+ $cells = ''
+ foreach ($t in $tags) {
+ $combos = $null
+ if ($data[$sample].ContainsKey($t)) { $combos = $data[$sample][$t] }
+ $st = Get-Status -Combos $combos
+ $tt = $totals[$t]
+ $tt.S += $st.Counts.Succeeded; $tt.F += $st.Counts.Failed; $tt.O += $st.Counts.Sporadic
+ $tt.U += $st.Counts.Unsupported; $tt.E += $st.Counts.Excluded
+ switch ($st.Class) { 'pass' { $tt.pass++ } 'flaky' { $tt.flaky++ } 'partial' { $tt.partial++ } 'fail' { $tt.fail++ } 'na' { $tt.na++ } }
+ $csvRow["$t"] = $st.Label
+ $tip = [System.Web.HttpUtility]::HtmlEncode($st.Tooltip)
+ $cells += "<td class='$($st.Class)' title='$tip'>$($st.Label)</td>"
+ if ($combos) {
+ foreach ($k in ($combos.Keys | Sort-Object)) {
+ if ($combos[$k] -eq 'Failed') { [void]$failuresList.Add([pscustomobject]@{ Sample = $sample; Version = $t; Combo = $k }) }
}
}
}
+ $csvRows += [pscustomobject]$csvRow
+ $enc = [System.Web.HttpUtility]::HtmlEncode($sample)
+ [void]$bodyRows.Append("<tr><td class='sample'>$enc</td>$cells</tr>`n")
}
-$results | ConvertTo-Csv | Out-File (Join-Path $logsPath "$reportFileName.csv")
-$results | ConvertTo-Html -Title "Overview" | Out-File (Join-Path $logsPath "$reportFileName.htm")
+Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
+
+# --- CSV ----------------------------------------------------------------------
+$csvRows | Export-Csv -Path (Join-Path $logsPath "$reportFileName.csv") -NoTypeInformation
+
+# --- HTML (colour-coded sample x version matrix) ------------------------------
+$generated = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
+$sumHead = "<tr><th>_NT_TARGET_VERSION</th><th>Pass</th><th>Flaky</th><th>Partial</th><th>Fail</th><th>n/a</th><th>Combos OK</th><th>Sporadic</th><th>Failed</th><th>Excluded</th><th>Pass rate</th></tr>"
+$sumRows = ''
+foreach ($t in $tags) {
+ $x = $totals[$t]; $tot = $x.pass + $x.flaky + $x.partial + $x.fail + $x.na; $elig = $tot - $x.na
+ $rate = if ($elig -gt 0) { '{0:N0}%' -f (100.0 * ($x.pass + $x.flaky) / $elig) } else { 'n/a' }
+ $sumRows += "<tr><td class='sample'>$t</td><td class='pass'>$($x.pass)</td><td class='flaky'>$($x.flaky)</td><td class='partial'>$($x.partial)</td><td class='fail'>$($x.fail)</td><td class='na'>$($x.na)</td><td>$($x.S)</td><td>$($x.O)</td><td>$($x.F)</td><td>$($x.E)</td><td><b>$rate</b></td></tr>`n"
+}
+$matHead = "<tr><th class='sample'>Sample</th>"
+foreach ($t in $tags) { $matHead += "<th>$t</th>" }
+$matHead += "</tr>"
+
+$html = @"
+<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"/>
+<title>WDK Driver Samples - Build Overview</title>
+<style>
+ body{font-family:'Segoe UI',Arial,sans-serif;margin:24px;color:#1b1b1b}
+ h1{font-size:22px;margin-bottom:4px}h2{font-size:17px;margin-top:28px}
+ .meta{color:#555;font-size:13px;margin-bottom:8px}
+ table{border-collapse:collapse;margin-top:8px;font-size:13px}
+ th,td{border:1px solid #cfcfcf;padding:5px 9px;text-align:center}
+ th{background:#f0f3f7;position:sticky;top:0}
+ td.sample,th.sample{text-align:left;font-family:Consolas,monospace;white-space:nowrap}
+ .sub{font-weight:normal;color:#666;font-size:11px}
+ .pass{background:#c8e6c9}.flaky{background:#fff59d}.partial{background:#ffcc80}.fail{background:#ef9a9a}.na{background:#eee;color:#888}
+ .legend span{display:inline-block;padding:3px 9px;margin-right:6px;border:1px solid #cfcfcf;border-radius:3px;font-size:12px}
+</style></head><body>
+<h1>WDK Driver Samples &mdash; Build Overview</h1>
+<div class="meta">Generated: $generated &nbsp;|&nbsp; columns are <b>_NT_TARGET_VERSION</b> (library link version); hover a cell for the per-combination breakdown.</div>
+<div class="legend"><span class="pass">PASS</span><span class="flaky">PASS* (retry)</span><span class="partial">PARTIAL</span><span class="fail">FAIL</span><span class="na">-- n/a</span></div>
+<h2>Summary by _NT_TARGET_VERSION</h2>
+<table>$sumHead
+$sumRows</table>
+<h2>Sample &times; _NT_TARGET_VERSION</h2>
+<table>$matHead
+$($bodyRows.ToString())</table>
+</body></html>
+"@
+$html | Out-File -FilePath (Join-Path $logsPath "$reportFileName.htm") -Encoding UTF8
+
+# --- GitHub Actions run summary (Markdown) ------------------------------------
+if ($env:GITHUB_STEP_SUMMARY) {
+ $totalFailed = ($totals.Values | Measure-Object -Property fail -Sum).Sum + ($totals.Values | Measure-Object -Property partial -Sum).Sum
+ $icon = if ($failuresList.Count -gt 0) { ':x:' } else { ':white_check_mark:' }
+
+ $md = [System.Text.StringBuilder]::new()
+ [void]$md.AppendLine("# $icon WDK Driver Samples &mdash; Build Overview")
+ [void]$md.AppendLine()
+ [void]$md.AppendLine("Columns are **_NT_TARGET_VERSION** (the WDK library version drivers link against). Each version was built for Debug/Release x x64/arm64.")
+ [void]$md.AppendLine()
+ [void]$md.AppendLine("## Summary by _NT_TARGET_VERSION")
+ [void]$md.AppendLine("| _NT_TARGET_VERSION | :white_check_mark: Pass | :warning: Flaky | :large_orange_diamond: Partial | :x: Fail | :heavy_minus_sign: n/a | Pass rate |")
+ [void]$md.AppendLine("|---|---:|---:|---:|---:|---:|---:|")
+ foreach ($t in $tags) {
+ $x = $totals[$t]; $tot = $x.pass + $x.flaky + $x.partial + $x.fail + $x.na; $elig = $tot - $x.na
+ $rate = if ($elig -gt 0) { '{0:N0}%' -f (100.0 * ($x.pass + $x.flaky) / $elig) } else { 'n/a' }
+ [void]$md.AppendLine("| ``$t`` | $($x.pass) | $($x.flaky) | $($x.partial) | $($x.fail) | $($x.na) | **$rate** |")
+ }
+ [void]$md.AppendLine()
+
+ if ($failuresList.Count -gt 0) {
+ [void]$md.AppendLine("## :x: Failures ($($failuresList.Count))")
+ [void]$md.AppendLine("| Sample | _NT_TARGET_VERSION | Config/Platform |")
+ [void]$md.AppendLine("|---|---|---|")
+ foreach ($f in ($failuresList | Sort-Object Sample, Version, Combo)) {
+ [void]$md.AppendLine("| ``$($f.Sample)`` | $($f.Version) | $($f.Combo.Replace('|','/')) |")
+ }
+ [void]$md.AppendLine()
+ [void]$md.AppendLine("> Open the matching **build** job's summary (or the ``logs-*`` artifact) for the exact compiler error.")
+ }
+ else {
+ [void]$md.AppendLine(":tada: **All combinations built successfully.**")
+ }
+
+ $md.ToString() | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
+}
diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml
index 6719270e..8119c94c 100644
--- a/.github/workflows/ci-pr.yml
+++ b/.github/workflows/ci-pr.yml
@@ -8,13 +8,39 @@ on:
- '**.md'
- 'LICENSE'
jobs:
+ # Auto-discover the available _NT_TARGET_VERSION values from the active WDK so the build
+ # matrix never needs a hand-maintained version list. Change -Newest to build more/fewer.
+ discover:
+ name: discover _NT_TARGET_VERSIONs
+ runs-on: windows-2025-vs2026
+ outputs:
+ versions: ${{ steps.nt.outputs.versions }}
+ steps:
+ - name: Check out repository code
+ uses: actions/checkout@v4
+
+ - name: Install Nuget Packages
+ run: nuget restore .\packages.config -PackagesDirectory .\packages\
+
+ - name: Discover the newest _NT_TARGET_VERSION values
+ id: nt
+ shell: pwsh
+ run: |
+ $json = .\Get-NtTargetVersions.ps1 -Newest 4 -AsMatrixJson
+ "versions=$json" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+ Write-Host "Discovered _NT_TARGET_VERSION matrix: $json"
+
build:
- name: Build driver samples
+ name: build ${{ matrix.nt.tag }} ${{ matrix.configuration }} ${{ matrix.platform }}
+ needs: discover
strategy:
fail-fast: false
matrix:
configuration: [Debug, Release]
platform: [x64, arm64]
+ # _NT_TARGET_VERSION values are auto-discovered by the 'discover' job from the active
+ # WDK, so there is no version list to maintain here.
+ nt: ${{ fromJSON(needs.discover.outputs.versions) }}
runs-on: windows-2025-vs2026
steps:
- name: Check out repository code
@@ -38,13 +64,14 @@ jobs:
env:
WDS_Configuration: ${{ matrix.configuration }}
WDS_Platform: ${{ matrix.platform }}
- WDS_ReportFileName: _overview.${{ matrix.configuration }}.${{ matrix.platform }}
+ WDS_NtTargetVersion: ${{ matrix.nt.version }}
+ WDS_ReportFileName: _overview.${{ matrix.nt.tag }}.${{ matrix.configuration }}.${{ matrix.platform }}
- name: Archive build logs and overview build reports
uses: actions/upload-artifact@v4
if: always()
with:
- name: logs-${{ matrix.configuration }}-${{ matrix.platform }}
+ name: logs-${{ matrix.nt.tag }}-${{ matrix.configuration }}-${{ matrix.platform }}
path: _logs
report:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index ca2696d8..4b263a82 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,5 +1,7 @@
name: Build all driver samples
on:
+ # Allows the workflow to be triggered manually from the Actions tab ("Run workflow").
+ workflow_dispatch:
push:
branches:
- main
@@ -11,13 +13,39 @@ on:
# Runs every Saturday at 00:00 PST (08:00 UTC)
- cron: '0 8 * * 6'
jobs:
+ # Auto-discover the available _NT_TARGET_VERSION values from the active WDK so the build
+ # matrix never needs a hand-maintained version list. Change -Newest to build more/fewer.
+ discover:
+ name: discover _NT_TARGET_VERSIONs
+ runs-on: windows-2025-vs2026
+ outputs:
+ versions: ${{ steps.nt.outputs.versions }}
+ steps:
+ - name: Check out repository code
+ uses: actions/checkout@v4
+
+ - name: Install Nuget Packages
+ run: nuget restore .\packages.config -PackagesDirectory .\packages\
+
+ - name: Discover the newest _NT_TARGET_VERSION values
+ id: nt
+ shell: pwsh
+ run: |
+ $json = .\Get-NtTargetVersions.ps1 -Newest 4 -AsMatrixJson
+ "versions=$json" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
+ Write-Host "Discovered _NT_TARGET_VERSION matrix: $json"
+
build:
- name: Build driver samples
+ name: build ${{ matrix.nt.tag }} ${{ matrix.configuration }} ${{ matrix.platform }}
+ needs: discover
strategy:
fail-fast: false
matrix:
configuration: [Debug, Release]
platform: [x64, arm64]
+ # _NT_TARGET_VERSION values are auto-discovered by the 'discover' job from the active
+ # WDK, so there is no version list to maintain here.
+ nt: ${{ fromJSON(needs.discover.outputs.versions) }}
runs-on: windows-2025-vs2026
steps:
- name: Check out repository code
@@ -33,13 +61,14 @@ jobs:
env:
WDS_Configuration: ${{ matrix.configuration }}
WDS_Platform: ${{ matrix.platform }}
- WDS_ReportFileName: _overview.${{ matrix.configuration }}.${{ matrix.platform }}
+ WDS_NtTargetVersion: ${{ matrix.nt.version }}
+ WDS_ReportFileName: _overview.${{ matrix.nt.tag }}.${{ matrix.configuration }}.${{ matrix.platform }}
- name: Archive build logs and overview build reports
uses: actions/upload-artifact@v4
if: always()
with:
- name: logs-${{ matrix.configuration }}-${{ matrix.platform }}
+ name: logs-${{ matrix.nt.tag }}-${{ matrix.configuration }}-${{ matrix.platform }}
path: _logs
report: