Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 234 additions & 10 deletions .github/workflows/ci-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,40 @@ on:
pull_request:
push:
branches: [ main, master ]
tags: [ 'v*' ]
paths-ignore:
- '**.md'
- 'docs/**'
- 'LICENSE*'
workflow_dispatch:
inputs:
sign:
description: Submit the built Release packages to SignPath
required: false
default: false
type: boolean

concurrency:
group: "${{ github.workflow }}-${{ github.ref }}"
cancel-in-progress: true
# A replacement validation build may cancel an older validation build, but a
# submitted signing request must be allowed to complete and download its result.
cancel-in-progress: ${{ !((github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && inputs.sign)) }}

env:
BUILD_CONFIGURATION: Release
VDD_SOLUTION: Virtual Display Driver (HDR)/MttVDD.sln
CONTROL_PANEL_DIR: VirtualDriverControl
# Keep the project's Windows 10-compatible released UMDF baseline. Never
# select the highest directory from the runner: WDKs can contain preview WDF
# headers (for example 2.35) that retail Windows rejects at load time.
UMDF_VERSION: '2.25'
# Signing is deliberate: release tags sign automatically, and a manual run
# must explicitly opt in. Pull requests and ordinary branch pushes never
# receive the SignPath token.
SIGNPATH_SIGNING_RUN: ${{ (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && inputs.sign) }}

permissions:
actions: read
contents: read

jobs:
Expand All @@ -37,6 +56,23 @@ jobs:
with:
submodules: true

- name: Compute release version
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$now = [DateTime]::UtcNow
$buildNumber = [int]$env:GITHUB_RUN_NUMBER
if ($buildNumber -lt 0 -or $buildNumber -gt 65535) {
throw "GITHUB_RUN_NUMBER must fit an INF version component (0-65535): $buildNumber"
}

$releaseBaseVersion = "{0}.{1}.{2}" -f ($now.Year % 100), $now.Month, $now.Day
$releaseVersion = "$releaseBaseVersion.$buildNumber"
Write-Output "Using release version $releaseVersion"
"RELEASE_BASE_VERSION=$releaseBaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"RELEASE_BUILD_NUMBER=$buildNumber" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
"RELEASE_VERSION=$releaseVersion" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append

- name: Setup MSBuild
uses: microsoft/setup-msbuild@30375c66a4eea26614e0d39710365f22f8b0af57 # v3.0.0

Expand Down Expand Up @@ -93,7 +129,7 @@ jobs:
$sln = "${{ env.VDD_SOLUTION }}"
if (-not (Test-Path $sln)) { throw "VDD solution file not found at: $sln" }

# Pick an available UMDF WDF header version on the runner.
# Resolve the project's explicitly supported UMDF WDF headers.
# Kits can be laid out either versioned:
# Include\<sdk>\wdf\umdf\2.xx
# or unversioned:
Expand All @@ -107,17 +143,22 @@ jobs:
throw "UMDF WDF include root not found under $env:WINDOWS_SDK_DIR (tried versioned + unversioned layouts)"
}

$umdfBest =
$umdfDirectory =
$umdfRoots |
ForEach-Object { Get-ChildItem -Path $_ -Directory -ErrorAction SilentlyContinue } |
Where-Object { $_.Name -match '^\d+\.\d+$' } |
Sort-Object -Property Name -Descending |
ForEach-Object { Get-ChildItem -Path $_ -Directory -Filter $env:UMDF_VERSION -ErrorAction SilentlyContinue } |
Select-Object -First 1

if (-not $umdfBest) { throw "No UMDF version directories found under: $($umdfRoots -join ', ')" }
if (-not $umdfDirectory) {
throw "Released UMDF $env:UMDF_VERSION headers were not found under: $($umdfRoots -join ', ')"
}

$umdfMinor = ($umdfBest.Name -split '\.')[1]
Write-Output "Using UMDF version: $($umdfBest.Name) (minor=$umdfMinor) from $($umdfBest.FullName)"
$umdfMinor = ($env:UMDF_VERSION -split '\.')[1]
Write-Output "Using pinned UMDF version: $env:UMDF_VERSION (minor=$umdfMinor) from $($umdfDirectory.FullName)"

$now = [DateTime]::UtcNow
$driverVersion = $env:RELEASE_VERSION
$driverDate = $now.ToString("MM/dd/yyyy", [Globalization.CultureInfo]::InvariantCulture)
Write-Output "Stamping DriverVer=$driverDate,$driverVersion"

msbuild $sln `
/m `
Expand All @@ -127,10 +168,70 @@ jobs:
/p:WindowsSdkDir="$env:WINDOWS_SDK_DIR" `
/p:WindowsTargetPlatformVersion="$env:WINDOWS_TARGET_PLATFORM_VERSION" `
/p:UMDF_VERSION_MINOR="$umdfMinor" `
/p:VddDriverVersion="$driverVersion" `
/p:VddDriverDate="$driverDate" `
/p:EnableInfVerif=false `
/p:RunApiValidator=false `
/verbosity:minimal

- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
cache: npm
cache-dependency-path: VirtualDriverControl/package-lock.json

- name: Build Control Panel (${{ matrix.platform }})
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$controlArch = if ("${{ matrix.platform }}" -eq "ARM64") { "arm64" } else { "x64" }

Push-Location "${{ env.CONTROL_PANEL_DIR }}"
try {
npm ci
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# Fail releases for moderate-or-higher dependency advisories. The
# remaining low advisory is isolated to the local Vite dev server
# and is not included in the packaged application.
npm audit --audit-level=moderate
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

# npm package versions are three-part SemVer. electron-builder
# combines this base with BUILD_NUMBER for the Windows four-part
# FileVersion and ${buildVersion} artifact-name macro.
npm version $env:RELEASE_BASE_VERSION --no-git-tag-version
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

$env:BUILD_NUMBER = $env:RELEASE_BUILD_NUMBER
$env:VDC_RELEASE_VERSION = $env:RELEASE_VERSION

npm run typecheck
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

npm run test:generated
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }

$parseErrors = @()
Get-ChildItem "out-ps-check\*.ps1" | ForEach-Object {
$tokens = $null
$errors = $null
[void][System.Management.Automation.Language.Parser]::ParseFile($_.FullName, [ref]$tokens, [ref]$errors)
$parseErrors += $errors
}
if ($parseErrors.Count -gt 0) {
$parseErrors | Format-List
throw "Generated installer PowerShell failed to parse"
}
Add-Type -Path (Resolve-Path "out-ps-check\core-audio-interop.cs")

npm run "build-portable:$controlArch"
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
} finally {
Pop-Location
}

- name: Collect outputs
shell: pwsh
run: |
Expand All @@ -142,9 +243,132 @@ jobs:
New-Item -ItemType Directory -Path $dest -Force | Out-Null
Copy-Item "$outDir\*" -Destination $dest -Recurse -Force

- name: Upload artifacts
$controlArch = if ("${{ matrix.platform }}" -eq "ARM64") { "arm64" } else { "x64" }
$controlSource = "${{ env.CONTROL_PANEL_DIR }}\dist\Virtual Driver Control-$env:RELEASE_VERSION-$controlArch.exe"
if (-not (Test-Path $controlSource)) {
throw "Control Panel executable not found: $controlSource"
}
Copy-Item $controlSource -Destination (Join-Path $dest "Virtual Driver Control.exe") -Force

# This is the complete installable UMDF package and its matching
# Control Panel. SignPath receives the GitHub Actions ZIP containing
# these exact files, with no stale driver payload mixed in.
$requiredFiles = @("MttVDD.dll", "MttVDD.inf", "MttVDD.cat", "vdd_settings.xml", "Virtual Driver Control.exe")
foreach ($file in $requiredFiles) {
if (-not (Test-Path (Join-Path $dest $file))) {
throw "Required driver package file not found: $file"
}
}

$inf = Join-Path $dest "MttVDD.inf"
if ((Select-String -Path $inf -Pattern "^UmdfLibraryVersion=$([regex]::Escape($env:UMDF_VERSION))\.0\s*$").Count -ne 1) {
throw "Built INF does not target the required released UMDF $env:UMDF_VERSION baseline"
}

- name: Upload unsigned driver package
id: upload_unsigned_driver_package
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}
path: artifacts/VDD/${{ matrix.platform }}/
if-no-files-found: error

- name: Check SignPath configuration
if: env.SIGNPATH_SIGNING_RUN == 'true'
shell: pwsh
env:
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
SIGNPATH_ORG_ID: ${{ vars.SIGNPATH_ORG_ID }}
SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
SIGNPATH_POLICY_SLUG: ${{ vars.SIGNPATH_POLICY_SLUG }}
run: |
$ErrorActionPreference = "Stop"
$required = @("SIGNPATH_API_TOKEN", "SIGNPATH_ORG_ID", "SIGNPATH_PROJECT_SLUG", "SIGNPATH_POLICY_SLUG")
$missing = @($required | Where-Object { [string]::IsNullOrWhiteSpace([Environment]::GetEnvironmentVariable($_)) })
if ($missing.Count -gt 0) {
throw "Missing SignPath configuration: $($missing -join ', ')"
}

# The SignPath project's default artifact configuration must accept the
# GitHub artifact ZIP and preserve MttVDD.dll, MttVDD.inf, MttVDD.cat,
# vdd_settings.xml, and Virtual Driver Control.exe. It should
# Authenticode-sign the DLL, catalog, and Control Panel executable while
# the INF and XML remain package payload.
- name: Submit VDD package to SignPath
id: submit_signing
if: env.SIGNPATH_SIGNING_RUN == 'true'
# v3.0.0 currently rejects otherwise valid GitHub artifacts with
# "User has no access to the requested GitHub resource" (upstream #18).
# v2.3 is the last Node 24-compatible release before that regression.
uses: signpath/github-action-submit-signing-request@c92b958760219087e01f8d67a1669ed57afe2627 # v2.3.0
with:
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
organization-id: ${{ vars.SIGNPATH_ORG_ID }}
project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
signing-policy-slug: ${{ vars.SIGNPATH_POLICY_SLUG }}
artifact-configuration-slug: VDD_driver_package
github-artifact-id: ${{ steps.upload_unsigned_driver_package.outputs.artifact-id }}
github-token: ${{ github.token }}
wait-for-completion: true
wait-for-completion-timeout-in-seconds: 1800
output-artifact-directory: signed-artifacts/VDD/${{ matrix.platform }}/

- name: Verify signed driver package
if: env.SIGNPATH_SIGNING_RUN == 'true'
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$signedRoot = "signed-artifacts\VDD\${{ matrix.platform }}"
if (-not (Test-Path $signedRoot)) { throw "SignPath did not create $signedRoot" }

$driver = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.dll")
$catalog = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.cat")
$inf = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "MttVDD.inf")
$settings = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "vdd_settings.xml")
$controlPanel = @(Get-ChildItem -Path $signedRoot -Recurse -File -Filter "Virtual Driver Control.exe")

foreach ($entry in @(
@{ Name = "MttVDD.dll"; Files = $driver },
@{ Name = "MttVDD.cat"; Files = $catalog },
@{ Name = "MttVDD.inf"; Files = $inf },
@{ Name = "vdd_settings.xml"; Files = $settings },
@{ Name = "Virtual Driver Control.exe"; Files = $controlPanel }
)) {
if ($entry.Files.Count -ne 1) {
throw "Expected exactly one signed package file named $($entry.Name), found $($entry.Files.Count)"
}
}

$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -File -Filter "signtool.exe" |
Where-Object { $_.FullName -match "\\x64\\signtool.exe$" } |
Sort-Object FullName -Descending |
Select-Object -First 1
if (-not $signtool) { throw "signtool.exe was not found in the installed Windows SDK" }

& $signtool.FullName verify /pa /v $driver[0].FullName
if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($driver[0].FullName)" }

& $signtool.FullName verify /pa /v $catalog[0].FullName
if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($catalog[0].FullName)" }

& $signtool.FullName verify /pa /v $controlPanel[0].FullName
if ($LASTEXITCODE -ne 0) { throw "Authenticode verification failed for $($controlPanel[0].FullName)" }

- name: Upload signed driver package
if: env.SIGNPATH_SIGNING_RUN == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}-signed
path: signed-artifacts/VDD/${{ matrix.platform }}/
if-no-files-found: error
retention-days: 90

- name: Add signing summary
if: env.SIGNPATH_SIGNING_RUN == 'true'
shell: pwsh
env:
SIGNING_REQUEST_URL: ${{ steps.submit_signing.outputs.signing-request-web-url }}
run: |
"## SignPath signing (${{ matrix.platform }})" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
'- Signed package: `VDD-${{ matrix.platform }}-${{ env.BUILD_CONFIGURATION }}-signed`' | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
"- Signing request: $env:SIGNING_REQUEST_URL" | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Encoding utf8 -Append
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ For an existing checkout, initialize the submodule before building:
git submodule update --init --recursive
```

The release workflow also builds the Control Panel in [`VirtualDriverControl`](VirtualDriverControl/README.md).
Its portable executable is versioned with the same `YY.M.D.GITHUB_RUN_NUMBER` value as the driver, packaged beside
the matching x64 or ARM64 driver, and submitted with the driver DLL and catalog for code signing.

## ⬇️ Download Latest Version

- [Driver Installer (Windows 10/11)](https://github.com/VirtualDrivers/Virtual-Display-Driver/releases) - Check the [Releases](https://github.com/VirtualDrivers/Virtual-Display-Driver/releases) page for the latest version and release notes.
Expand Down
Loading