diff options
| author | 5an7y-Microsoft <[email protected]> | 2026-07-09 14:59:40 -0700 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-07-09 14:59:40 -0700 |
| commit | 15640356a5a6eeafefff403f6d287821213635cd (patch) | |
| tree | 49e5fcd168d50912decc42dda76767b3566887b1 /.github | |
| parent | 7c8087c61eb674e321458f935b6676652c39924d (diff) | |
| parent | 2ee527bfeb0aeb6be11f0a8b6dce4011b358ce89 (diff) | |
Merge pull request #1402 from microsoft/main
FI: Main -> Develop
Diffstat (limited to '.github')
| -rw-r--r-- | .github/crashdetect/CrashDetectCreateUsb.cmd | 189 | ||||
| -rw-r--r-- | .github/crashdetect/CrashDetectOsReinstall.cmd | 258 | ||||
| -rw-r--r-- | .github/crashdetect/CrashDetectSetupGuide.md | 256 | ||||
| -rw-r--r-- | .github/crashdetect/Unattend.xml | 64 | ||||
| -rw-r--r-- | .github/pdu/Pdu.ps1 | 250 | ||||
| -rw-r--r-- | .github/pdu/PduReadme.md | 65 | ||||
| -rw-r--r-- | .github/scripts/Build-ChangedSamples.ps1 | 2 | ||||
| -rw-r--r-- | .github/scripts/Join-CsvReports.ps1 | 220 | ||||
| -rw-r--r-- | .github/workflows/ci-pr.yml | 33 | ||||
| -rw-r--r-- | .github/workflows/ci.yml | 35 |
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 — Build Overview</h1> +<div class="meta">Generated: $generated | 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 × _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 — 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: |
