Add one-command Windows ACPI E2E #60
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Builds the ODP Validation OS VHDX image on a Windows runner. | |
| # | |
| # SPDX-License-Identifier: MIT | |
| # | |
| # Downloads pre-built driver assets from the latest odp-windows-drivers release, | |
| # unzips them, builds a Validation OS WIM, injects the QEMU drivers, and converts | |
| # the result to VHDX. The list of driver assets to download is maintained in | |
| # postbuild/os/prebuilt/driverlist.txt. | |
| # | |
| # It also downloads the EC test apps from the latest odp-platform-common release, | |
| # extracts them to C:\ectest, and matches the payload's `acpi` source contract. | |
| # | |
| # Pull requests build the image for validation only. Every other trigger publishes | |
| # the stable base used by `make run_os` and `make windows-acpi-e2e`. | |
| # | |
| # Third-party actions are pinned by commit SHA. | |
| # | |
| name: build_os_image | |
| permissions: | |
| contents: read | |
| actions: read | |
| on: | |
| pull_request: &os-image-filters | |
| branches: [main] | |
| paths: | |
| - 'postbuild/os/**' | |
| - '.github/workflows/build-os.yml' | |
| push: *os-image-filters | |
| workflow_dispatch: | |
| inputs: | |
| drivers_repo: | |
| description: 'GitHub repository to download driver assets from (owner/repo)' | |
| required: false | |
| default: 'OpenDevicePartnership/odp-windows-drivers' | |
| type: string | |
| apps_repo: | |
| description: 'GitHub repository to download EC test app assets from (owner/repo)' | |
| required: false | |
| default: 'OpenDevicePartnership/odp-platform-common' | |
| type: string | |
| workflow_call: | |
| inputs: | |
| git_ref: | |
| description: 'Git tag in vYYYY.MM.DD format' | |
| required: true | |
| type: string | |
| drivers_repo: | |
| description: 'GitHub repository to download driver assets from (owner/repo)' | |
| required: false | |
| default: 'OpenDevicePartnership/odp-windows-drivers' | |
| type: string | |
| apps_repo: | |
| description: 'GitHub repository to download EC test app assets from (owner/repo)' | |
| required: false | |
| default: 'OpenDevicePartnership/odp-platform-common' | |
| type: string | |
| concurrency: | |
| group: build_os_image-${{ github.head_ref || github.ref }} | |
| # Superseded PR builds are disposable; a publishing run cancelled mid-upload | |
| # would leave the 'latest' asset deleted and not replaced. | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| build: | |
| runs-on: windows-latest | |
| name: Build QEMU ARM64 OS image | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| defaults: | |
| run: | |
| shell: pwsh | |
| steps: | |
| - name: Clone Repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| ref: ${{ inputs.git_ref || github.ref }} | |
| - name: Resolve driver assets | |
| run: | | |
| $assetNames = Get-Content -Path 'postbuild\os\prebuilt\driverlist.txt' | | |
| ForEach-Object { $_.Trim() } | | |
| Where-Object { $_ -and -not $_.StartsWith('#') } | |
| if (-not $assetNames) { | |
| throw 'No driver names found in postbuild\os\prebuilt\driverlist.txt' | |
| } | |
| if ($assetNames -notcontains 'driver-ectest_kmdf-ARM64-Release') { | |
| throw 'Stable base requires driver-ectest_kmdf-ARM64-Release' | |
| } | |
| $assetNamesFile = Join-Path $env:RUNNER_TEMP 'asset-names.txt' | |
| $assetNames -join "`n" | Out-File -FilePath $assetNamesFile -Encoding utf8 | |
| "DRIVER_ASSET_NAMES_FILE=$assetNamesFile" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | |
| - name: Download driver assets | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| if (-not (Test-Path $env:DRIVER_ASSET_NAMES_FILE)) { | |
| throw "Driver asset list '$env:DRIVER_ASSET_NAMES_FILE' does not exist" | |
| } | |
| $assetNames = Get-Content -Path $env:DRIVER_ASSET_NAMES_FILE | |
| $driversRepo = '${{ inputs.drivers_repo || 'OpenDevicePartnership/odp-windows-drivers' }}' | |
| # Drivers are published as zip assets on the rolling 'latest' release | |
| # (unlike workflow artifacts, gh does not auto-extract them), so each | |
| # asset is downloaded and manually unzipped into drivers/<name>. | |
| foreach ($name in $assetNames) { | |
| $destPath = Join-Path 'drivers' $name | |
| New-Item -ItemType Directory -Path $destPath -Force | Out-Null | |
| $assetName = "$name.zip" | |
| $zipPath = Join-Path $env:RUNNER_TEMP $assetName | |
| if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force } | |
| Write-Host "Downloading asset: $assetName -> $destPath" | |
| gh release download latest --repo $driversRepo --pattern $assetName --dir $env:RUNNER_TEMP --clobber | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to download asset $assetName from the latest release of $driversRepo" } | |
| if (-not (Test-Path $zipPath)) { throw "Downloaded asset '$zipPath' does not exist" } | |
| Expand-Archive -Path $zipPath -DestinationPath $destPath -Force | |
| } | |
| - name: Download EC test apps | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $appsRepo = '${{ inputs.apps_repo || 'OpenDevicePartnership/odp-platform-common' }}' | |
| $assetName = 'ec-test-apps-ARM64.zip' | |
| $zipPath = Join-Path $env:RUNNER_TEMP $assetName | |
| if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force } | |
| Write-Host "Downloading asset: $assetName from $appsRepo" | |
| gh release download latest --repo $appsRepo --pattern $assetName --dir $env:RUNNER_TEMP --clobber | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to download asset $assetName from the latest release of $appsRepo" } | |
| if (-not (Test-Path $zipPath)) { throw "Downloaded asset '$zipPath' does not exist" } | |
| # The archive contains a top-level ectest\ folder; it is extracted into | |
| # the mounted WIM later so it lands at C:\ectest in the running OS. | |
| "EC_TEST_APPS_ZIP=$zipPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | |
| - name: Download Validation OS ISO | |
| run: | | |
| curl.exe --fail --location --retry 5 --retry-all-errors ` | |
| --continue-at - --output ValidationOS.iso ` | |
| https://aka.ms/DownloadValidationOS_26H1_arm64 | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "ValidationOS ISO download failed with exit code $LASTEXITCODE" | |
| } | |
| - name: Build WIM | |
| run: | | |
| $mount = Mount-DiskImage -ImagePath (Resolve-Path ValidationOS.iso).Path -PassThru | |
| $drive = ($mount | Get-Volume).DriveLetter | |
| $layout = Join-Path $env:RUNNER_TEMP 'winvos-package-layout' | |
| try { | |
| if (Test-Path $layout) { Remove-Item -Path $layout -Recurse -Force } | |
| New-Item -ItemType Directory -Path (Join-Path $layout 'cabs') -Force | Out-Null | |
| New-Item -ItemType Directory -Path (Join-Path $layout 'cabs\Extra') -Force | Out-Null | |
| New-Item -ItemType Directory -Path (Join-Path $layout 'GenImage') -Force | Out-Null | |
| New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\neutral') ` | |
| -Target "${drive}:\cabs\Common\neutral" | Out-Null | |
| New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\en-us') ` | |
| -Target "${drive}:\cabs\Common\en-us" | Out-Null | |
| New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\Extra\neutral') ` | |
| -Target "${drive}:\cabs\Extra\neutral" | Out-Null | |
| New-Item -ItemType Junction -Path (Join-Path $layout 'cabs\Extra\en-us') ` | |
| -Target "${drive}:\cabs\Extra\en-us" | Out-Null | |
| New-Item -ItemType Junction -Path (Join-Path $layout 'GenImage\Configs') ` | |
| -Target "${drive}:\GenImage\Configs" | Out-Null | |
| $packagePath = Join-Path $layout 'cabs' | |
| cmd /c "${drive}:\GenImage\GenImage.cmd -PackagesList:postbuild\os\prebuilt\armvirt_config.pkg -PackagePath:$packagePath -ImagePath:${drive}:\ -RegistryImport:postbuild\os\prebuilt\armvirt_policy.reg -OutPath:postbuild\os\build -wim -NoWait" | |
| if ($LASTEXITCODE -ne 0) { throw "GenImage.cmd failed with exit code $LASTEXITCODE" } | |
| $wim = Get-ChildItem -Path 'postbuild\os\build' -Filter *.wim -File | Select-Object -First 1 | |
| if (-not $wim) { throw 'GenImage produced no .wim in postbuild\os\build' } | |
| "WIM_PATH=$($wim.FullName)" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 | |
| } finally { | |
| if (Test-Path $layout) { Remove-Item -Path $layout -Recurse -Force } | |
| Dismount-DiskImage -ImagePath $mount.ImagePath | |
| } | |
| - name: Validate ValidationOS build floor | |
| run: | | |
| $minimumBuild = 28000 | |
| $image = Get-WindowsImage -ImagePath $env:WIM_PATH -Index 1 | |
| $version = [version]$image.Version | |
| if ($version.Build -lt $minimumBuild) { | |
| throw "ValidationOS build $($version.Build) is below required build $minimumBuild" | |
| } | |
| Write-Host "ValidationOS build $($version.Build) satisfies floor $minimumBuild" | |
| - name: Inject QEMU drivers and EC test apps into WIM | |
| run: | | |
| if (-not $env:WIM_PATH) { | |
| throw 'WIM_PATH is not set' | |
| } | |
| if (-not (Test-Path $env:DRIVER_ASSET_NAMES_FILE)) { | |
| throw "Driver asset list '$env:DRIVER_ASSET_NAMES_FILE' does not exist" | |
| } | |
| if (-not $env:EC_TEST_APPS_ZIP) { | |
| throw 'EC_TEST_APPS_ZIP is not set' | |
| } | |
| $wimPath = Resolve-Path $env:WIM_PATH | |
| $mountPath = Join-Path $env:RUNNER_TEMP 'os-image-mount' | |
| if (Test-Path $mountPath) { | |
| Remove-Item -Path $mountPath -Recurse -Force | |
| } | |
| New-Item -ItemType Directory -Path $mountPath -Force | Out-Null | |
| $assetNames = Get-Content -Path $env:DRIVER_ASSET_NAMES_FILE | |
| $driverRoots = $assetNames | ForEach-Object { Resolve-Path (Join-Path 'drivers' $_) } | |
| if (-not $driverRoots) { | |
| throw "No driver paths resolved from $env:DRIVER_ASSET_NAMES_FILE" | |
| } | |
| $infPaths = foreach ($driverRoot in $driverRoots) { | |
| Get-ChildItem -Path $driverRoot -Recurse -Filter *.inf -File | |
| } | |
| if (-not $infPaths) { | |
| throw "No INF files found for any driver listed in $env:DRIVER_ASSET_NAMES_FILE" | |
| } | |
| $infPaths = $infPaths | Sort-Object FullName -Unique | |
| try { | |
| dism.exe /Mount-Image /ImageFile:$wimPath /Index:1 /MountDir:$mountPath | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to mount WIM with exit code $LASTEXITCODE" } | |
| foreach ($infPath in $infPaths) { | |
| Write-Host "Injecting driver $($infPath.FullName)" | |
| dism.exe /Image:$mountPath /Add-Driver /Driver:$($infPath.FullName) | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to add driver $($infPath.FullName) with exit code $LASTEXITCODE" } | |
| } | |
| # ectest must go inside the WIM (not onto the applied partition): | |
| # Validation OS RAM-boots the WIM, so partition-only files are not | |
| # visible at runtime. Drivers already work for this reason. | |
| # The archive contains a top-level ectest\ folder, so extracting it | |
| # at the WIM root produces \ectest (C:\ectest once booted). | |
| Expand-Archive -Path $env:EC_TEST_APPS_ZIP -DestinationPath $mountPath -Force | |
| $ecTestDir = Join-Path $mountPath 'ectest' | |
| $expectedApps = 'ec-test-tui.exe', 'ec-test-cli.exe' | |
| foreach ($app in $expectedApps) { | |
| $appPath = Join-Path $ecTestDir $app | |
| if (-not (Test-Path $appPath)) { | |
| throw "Expected EC test app missing: $appPath" | |
| } | |
| } | |
| Write-Host "Extracted EC test apps into WIM at $ecTestDir" | |
| dism.exe /Unmount-Image /MountDir:$mountPath /Commit | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to commit WIM changes with exit code $LASTEXITCODE" } | |
| } finally { | |
| $mountedImage = Get-WindowsImage -Mounted | Where-Object { $_.Path -eq $mountPath } | |
| if ($mountedImage) { | |
| dism.exe /Unmount-Image /MountDir:$mountPath /Discard | Out-Null | |
| } | |
| if (Test-Path $mountPath) { | |
| Remove-Item -Path $mountPath -Recurse -Force -ErrorAction SilentlyContinue | |
| } | |
| } | |
| - name: Convert WIM to VHDX | |
| run: | | |
| if (-not $env:WIM_PATH) { | |
| throw 'WIM_PATH is not set' | |
| } | |
| $wimPath = Resolve-Path $env:WIM_PATH | |
| $vhdxPath = Join-Path (Get-Location) 'staging\os-image.vhdx' | |
| $vhdxSizeBytes = 2GB | |
| New-Item -ItemType Directory -Force -Path staging | Out-Null | |
| $vhdx = New-VHD -Path $vhdxPath -SizeBytes $vhdxSizeBytes -Dynamic | |
| $disk = Mount-VHD -Path $vhdxPath -Passthru | |
| try { | |
| $disk | Initialize-Disk -PartitionStyle GPT | |
| $efiPart = $disk | New-Partition -Size 100MB -GptType '{c12a7328-f81f-11d2-ba4b-00a0c93ec93b}' -AssignDriveLetter | |
| $efiPart | Format-Volume -FileSystem FAT32 -NewFileSystemLabel 'EFI' -Confirm:$false | Out-Null | |
| $efiLetter = $efiPart.DriveLetter | |
| $osPart = $disk | New-Partition -UseMaximumSize -GptType '{ebd0a0a2-b9e5-4433-87c0-68b6b72699c7}' -AssignDriveLetter | |
| $osPart | Format-Volume -FileSystem NTFS -NewFileSystemLabel 'ValidationOS' -Confirm:$false | Out-Null | |
| $osLetter = $osPart.DriveLetter | |
| Write-Host "EFI partition: ${efiLetter}: OS partition: ${osLetter}:" | |
| Write-Host "Applying WIM to ${osLetter}:\" | |
| dism.exe /Apply-Image /ImageFile:"$wimPath" /Index:1 /ApplyDir:"${osLetter}:\" | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to apply WIM with exit code $LASTEXITCODE" } | |
| Write-Host "Creating boot files on ${efiLetter}:\" | |
| bcdboot "${osLetter}:\Windows" /s "${efiLetter}:" /f UEFI | |
| if ($LASTEXITCODE -ne 0) { throw "bcdboot failed with exit code $LASTEXITCODE" } | |
| Write-Host "Enabling testsigning" | |
| bcdedit /store ${efiLetter}:\EFI\Microsoft\Boot\BCD /set "{default}" testsigning on | |
| if ($LASTEXITCODE -ne 0) { throw "Enabling testsigning failed with exit code $LASTEXITCODE" } | |
| Write-Host "Enabling debug" | |
| bcdedit /store ${efiLetter}:\EFI\Microsoft\Boot\BCD /set "{default}" debug on | |
| if ($LASTEXITCODE -ne 0) { throw "Enabling debug failed with exit code $LASTEXITCODE" } | |
| } finally { | |
| Dismount-VHD -Path $vhdxPath | |
| } | |
| Write-Host "VHDX created: $vhdxPath ($('{0:N1} MB' -f ((Get-Item $vhdxPath).Length / 1MB)))" | |
| - name: Package and publish image | |
| # Pushes only ever fire on main; PRs are the only trigger that must not publish. | |
| if: github.event_name != 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| $vhdxPath = 'staging\os-image.vhdx' | |
| $zipPath = Join-Path $env:RUNNER_TEMP 'os-image.zip' | |
| if (Test-Path $zipPath) { Remove-Item -Path $zipPath -Force } | |
| # Relies on the job's pwsh shell: Windows PowerShell 5.1 cannot write the | |
| # ZIP64 entries a VHDX this size needs. | |
| Compress-Archive -Path $vhdxPath -DestinationPath $zipPath | |
| Write-Host "Packaged: $zipPath ($('{0:N1} MB' -f ((Get-Item $zipPath).Length / 1MB)))" | |
| # Uploading only the asset leaves the hand-created 'latest' prerelease and | |
| # its tag untouched; --clobber replaces the asset from the previous run. | |
| gh release upload latest $zipPath --repo $env:GITHUB_REPOSITORY --clobber | |
| if ($LASTEXITCODE -ne 0) { throw 'Failed to upload os-image.zip to the latest release.' } |