Files
windows_exporter/.github/workflows/ci.yml
T
2026-10-07 23:51:31 +02:00

1564 lines
61 KiB
YAML

name: CI
# Trigger on pull requests and pushes to master branch where Go-related files
# have been changed.
on:
push:
branches:
- master
- next
- main
- "0.*"
- "1.*"
tags:
- "v*"
pull_request:
workflow_dispatch:
release:
types:
- published
permissions: {}
jobs:
build:
name: Build Windows binaries
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: 'false'
fetch-depth: '0'
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: Generate Windows resources
working-directory: cmd/windows_exporter
run: go run github.com/tc-hib/go-winres@v0.3.3 make --product-version=git-tag --file-version=git-tag --arch=amd64,arm64
- name: Build snapshot
if: ${{ !startsWith(github.ref, 'refs/tags/') }}
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
# renovate: github=goreleaser/goreleaser
version: v2.18.2
args: build --snapshot --clean
- name: Build release
if: ${{ startsWith(github.ref, 'refs/tags/') }}
uses: goreleaser/goreleaser-action@f06c13b6b1a9625abc9e6e439d9c05a8f2190e94 # v7.2.3
with:
# renovate: github=goreleaser/goreleaser
version: v2.18.2
args: build --clean
- name: Stage build artifacts
run: |
mkdir -p output
version="$(git describe --tags --always)"
version="${version#v}"
version="${version//+/_}"
printf '%s\n' "$version" | tee output/VERSION
cp dist/windows_exporter-amd64.exe output/
cp dist/windows_exporter-arm64.exe output/
- name: Upload unsigned binaries
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: windows_exporter_unsigned
path: output/
package:
name: Package Windows artifacts
runs-on: windows-2025
needs:
- build
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: 'false'
- name: Download unsigned binaries
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: windows_exporter_unsigned
path: output
- name: Add version to binary names
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content output\VERSION
foreach($Arch in "amd64", "arm64") {
Move-Item output\windows_exporter-$Arch.exe output\windows_exporter-$Version-$Arch.exe
}
Get-ChildItem -Path output
- name: Sign build artifacts
if: ${{ (github.event_name != 'pull_request' && github.repository == 'prometheus-community/windows_exporter') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'prometheus-community/windows_exporter') }}
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content output\VERSION
$b64 = $env:CODE_SIGN_KEY
$filename = 'windows_exporter_CodeSign.pfx'
$bytes = [Convert]::FromBase64String($b64)
[IO.File]::WriteAllBytes($filename, $bytes)
$basePath = "C:\Program Files (x86)\Windows Kits\10\bin"
$latestSigntool = Get-ChildItem -Path $basePath -Directory |
Where-Object { $_.Name -match "^\d+\.\d+\.\d+\.\d+$" } |
Sort-Object { [Version]$_.Name } -Descending |
Select-Object -First 1 |
ForEach-Object { Join-Path $_.FullName "x64\signtool.exe" }
if (Test-Path $latestSigntool) {
Write-Output $latestSigntool
} else {
throw "signtool.exe not found"
}
foreach($Arch in "amd64", "arm64") {
& $latestSigntool sign /v /tr "http://timestamp.digicert.com" /d "Prometheus exporter for Windows machines" /td SHA256 /fd SHA256 /a /f "windows_exporter_CodeSign.pfx" /p $env:CODE_SIGN_PASSWORD "output\windows_exporter-$Version-$Arch.exe"
}
Remove-Item windows_exporter_CodeSign.pfx
env:
CODE_SIGN_KEY: ${{ secrets.CODE_SIGN_KEY }}
CODE_SIGN_PASSWORD: ${{ secrets.CODE_SIGN_PASSWORD }}
- name: Install WiX
run: dotnet tool install --global wix --version 5.0.2
- name: Install WiX extensions
run: |
wix extension add -g WixToolset.Util.wixext/5.0.2
wix extension add -g WixToolset.Ui.wixext/5.0.2
wix extension add -g WixToolset.Firewall.wixext/5.0.2
- name: Build installer artifacts
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content output\VERSION
foreach($Arch in "amd64", "arm64") {
Write-Host "Building windows_exporter $Version msi for $Arch"
.\installer\build.ps1 -PathToExecutable .\output\windows_exporter-$Version-$Arch.exe -Version $Version -Arch "$Arch"
}
Move-Item installer\*.msi output\
Get-ChildItem -Path output\
- name: Sign installer artifacts
if: ${{ (github.event_name != 'pull_request' && github.repository == 'prometheus-community/windows_exporter') || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == 'prometheus-community/windows_exporter') }}
run: |
$ErrorActionPreference = "Stop"
$Version = Get-Content output\VERSION
$b64 = $env:CODE_SIGN_KEY
$filename = 'windows_exporter_CodeSign.pfx'
$bytes = [Convert]::FromBase64String($b64)
[IO.File]::WriteAllBytes($filename, $bytes)
$basePath = "C:\Program Files (x86)\Windows Kits\10\bin"
$latestSigntool = Get-ChildItem -Path $basePath -Directory |
Where-Object { $_.Name -match "^\d+\.\d+\.\d+\.\d+$" } |
Sort-Object { [Version]$_.Name } -Descending |
Select-Object -First 1 |
ForEach-Object { Join-Path $_.FullName "x64\signtool.exe" }
if (Test-Path $latestSigntool) {
Write-Output $latestSigntool
} else {
throw "signtool.exe not found"
}
foreach($Arch in "amd64", "arm64") {
& $latestSigntool sign /v /tr "http://timestamp.digicert.com" /d "Prometheus exporter for Windows machines" /td SHA256 /fd SHA256 /a /f "windows_exporter_CodeSign.pfx" /p $env:CODE_SIGN_PASSWORD "output\windows_exporter-$Version-$Arch.msi"
}
Remove-Item windows_exporter_CodeSign.pfx
env:
CODE_SIGN_KEY: ${{ secrets.CODE_SIGN_KEY }}
CODE_SIGN_PASSWORD: ${{ secrets.CODE_SIGN_PASSWORD }}
- name: Generate checksums
run: |
$ErrorActionPreference = "Stop"
$checksums = Get-ChildItem -Path output -File |
Where-Object { $_.Name -match '^windows_exporter-.*\.(exe|msi)$' } |
Sort-Object Name |
ForEach-Object {
$hash = (Get-FileHash -Algorithm SHA256 $_.FullName).Hash.ToLowerInvariant()
"$hash $($_.Name)"
}
$checksums | Set-Content output\sha256sums.txt -Encoding ascii
Get-Content output\sha256sums.txt
- name: Upload artifacts
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: windows_exporter_binaries
path: |
output\windows_exporter-*.exe
output\windows_exporter-*.msi
output\sha256sums.txt
docker:
name: Build docker images
runs-on: ubuntu-24.04
# Images are pushed on branch and tag events, so gate them on the same
# validation as the release binaries in the publish job.
needs:
- package
- windows-test
permissions:
contents: read
packages: write
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: 'false'
fetch-depth: '0'
- name: Download Artifacts
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: windows_exporter_binaries
- name: Login to Docker Hub
if: ${{ github.repository == 'prometheus-community/windows_exporter' && github.event_name != 'pull_request' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
username: ${{ secrets.DOCKER_HUB_LOGIN }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
- name: Login to quay.io
if: ${{ github.repository == 'prometheus-community/windows_exporter' && github.event_name != 'pull_request' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: quay.io
username: ${{ secrets.QUAY_IO_LOGIN }}
password: ${{ secrets.QUAY_IO_PASSWORD }}
- name: Login to GitHub container registry
if: ${{ github.repository == 'prometheus-community/windows_exporter' && github.event_name != 'pull_request' }}
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta
id: meta
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: |
ghcr.io/prometheus-community/windows-exporter
docker.io/prometheuscommunity/windows-exporter
quay.io/prometheuscommunity/windows-exporter
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
type=ref,event=pr
labels: |
org.opencontainers.image.title=windows_exporter
org.opencontainers.image.description=A Prometheus exporter for Windows machines.
org.opencontainers.image.vendor=The Prometheus Community
org.opencontainers.image.licenses=MIT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
- name: Build and push
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
push: ${{ github.repository == 'prometheus-community/windows_exporter' && github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: windows/amd64
annotations: ${{ steps.meta.outputs.labels }}
publish:
if: github.repository == 'prometheus-community/windows_exporter' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-24.04
needs:
- package
- windows-test
permissions:
contents: write
steps:
- name: Download Artifacts
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: windows_exporter_binaries
- name: Publish Artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload "$GITHUB_REF_NAME" \
windows_exporter-*.exe \
windows_exporter-*.msi \
sha256sums.txt
windows-test:
name: Windows tests (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 25
strategy:
fail-fast: false
matrix:
include:
# Server 2025 runners are much slower to provision the same fixtures.
- os: windows-2022
nanoserver: ltsc2022
smtp: true
permissions:
contents: read
env:
CNI_VERSION: "1.9.1"
CONTAINERD_VERSION: "2.4.1"
CRI_ENDPOINT: "npipe:////./pipe/containerd-containerd"
CRITOOLS_VERSION: "1.37.0"
KUBERNETES_VERSION: "v1.37.1"
NANOSERVER_IMAGE: mcr.microsoft.com/windows/nanoserver:${{ matrix.nanoserver }}
SMTP_ENABLED: ${{ matrix.smtp }}
WINDOWS_EXPORTER_TEST_RDP_USER: rdp-ci
WINDOWS_TEST_OS: ${{ matrix.os }}
# These collectors must work on the fixtures below; missing features are failures.
WINDOWS_EXPORTER_TEST_COLLECTORS: "cache,container,cpu,cpu_info,dhcp,diskdrive,dmi,dns,fsrmquota,hyperv,iis,license,logical_disk,memory,mscluster,msmq,mssql,net,netframework,nps,os,pagefile,physical_disk,printer,process,scheduled_task,service,smb,smbclient,system,tcp,terminal_services,time,udp,update${{ matrix.smtp && ',smtp' || '' }}"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: 'false'
# Skip the SQL Server download and extraction when the setup files are cached.
- uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ runner.temp }}/sql-server/setup
key: sql-server-2025-express-fa7e1fabc9a2-${{ runner.os }}
# Steps that do not register services run in the background while Windows features install.
- name: Download SQL Server media
id: download-mssql
background: true
shell: powershell
run: ./tools/setup-sql-server.ps1 -DownloadOnly
- name: Install FreeRDP client
id: install-freerdp
background: true
shell: powershell
run: |
foreach ($attempt in 1..3) {
choco install freerdp.portable --version=3.28.0 --yes --no-progress
if ($LASTEXITCODE -eq 0) { break }
if ($attempt -eq 3) { throw "Failed to install FreeRDP" }
Start-Sleep -Seconds 10
}
Get-Command wfreerdp.exe -ErrorAction Stop
- name: Setup BitLocker volumes
id: setup-bitlocker
background: true
shell: powershell
run: |
$ErrorActionPreference = "Stop"
. ./tools/vhd-lock.ps1
# Hosted images ship the BitLocker feature; data volumes need no reboot.
function New-BitLockerVolume([string]$Name) {
$vhd = Join-Path $env:RUNNER_TEMP "$Name.vhdx"
New-VHD -Path $vhd -Dynamic -SizeBytes 512MB | Out-Null
$volume = Invoke-WithVhdLock {
Format-NewDisk (Mount-VHD $vhd -PassThru | Get-Disk) $Name
}
$mountPoint = "$($volume.DriveLetter):"
$password = ConvertTo-SecureString ("CI-" + [Guid]::NewGuid().ToString('N')) -AsPlainText -Force
Enable-BitLocker -MountPoint $mountPoint -PasswordProtector -Password $password -UsedSpaceOnly | Out-Null
$deadline = (Get-Date).AddMinutes(2)
while ((Get-BitLockerVolume -MountPoint $mountPoint).VolumeStatus -ne "FullyEncrypted") {
if ((Get-Date) -ge $deadline) { throw "BitLocker volume $Name did not finish encryption" }
Start-Sleep -Seconds 2
}
return $mountPoint
}
$unlocked = New-BitLockerVolume CIBitLocker
$locked = New-BitLockerVolume CIBitLockerLocked
Lock-BitLocker -MountPoint $locked -ForceDismount | Out-Null
Get-BitLockerVolume -MountPoint $unlocked, $locked |
Format-Table MountPoint, VolumeStatus, ProtectionStatus, LockStatus
@{ Unlocked = $unlocked; Locked = $locked } | ConvertTo-Json |
Set-Content (Join-Path $env:RUNNER_TEMP "bitlocker-fixture.json")
- name: Setup storage pool
id: setup-storage-pool
background: true
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$disk1 = Join-Path $env:RUNNER_TEMP "storage-pool-1.vhdx"
$disk2 = Join-Path $env:RUNNER_TEMP "storage-pool-2.vhdx"
$diskpartScript = Join-Path $env:RUNNER_TEMP "storage-pool-diskpart.txt"
@"
create vdisk file="$disk1" maximum=10240 type=expandable
attach vdisk
create vdisk file="$disk2" maximum=10240 type=expandable
attach vdisk
"@ | Set-Content -Path $diskpartScript -Encoding ascii
. ./tools/vhd-lock.ps1
Invoke-WithVhdLock {
diskpart /s $diskpartScript
if ($LASTEXITCODE -ne 0) { throw "diskpart failed to attach the storage pool disks" }
Update-HostStorageCache
}
# Background steps attach their own VHDX files concurrently.
$diskNumbers = @($disk1, $disk2 | ForEach-Object { (Get-DiskImage -ImagePath $_).Number })
$newDisks = @(Get-PhysicalDisk | Where-Object { [int]$_.DeviceId -in $diskNumbers })
$newDisks | Format-Table FriendlyName, DeviceId, MediaType, BusType, Size, CanPool, CannotPoolReason
$poolableDisks = @($newDisks | Where-Object CanPool)
if ($poolableDisks.Count -ne 2) {
throw "Expected two poolable VHDX disks, found $($poolableDisks.Count)"
}
New-StoragePool `
-FriendlyName "GitHubActions" `
-StorageSubsystemFriendlyName "Windows Storage*" `
-PhysicalDisks $poolableDisks
$pool = Get-StoragePool -FriendlyName "GitHubActions" -ErrorAction Stop
$pool | Format-List FriendlyName, HealthStatus, OperationalStatus, Size, AllocatedSize
if ($pool.IsPrimordial) {
throw "Expected a concrete storage pool"
}
New-VirtualDisk -StoragePoolFriendlyName "GitHubActions" `
-FriendlyName "CIVirtualDisk" -Size 1GB -ResiliencySettingName Simple
- name: Start Hyper-V VM
id: start-hyperv-vm
background: true
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Import-Module Hyper-V
Start-Service vmms
New-VMSwitch -Name "CISwitch" -SwitchType Internal
New-VM `
-Name "GitHubActions" `
-Generation 1 `
-MemoryStartupBytes 256MB `
-NewVHDPath (Join-Path $env:RUNNER_TEMP "hyperv-ci.vhdx") `
-NewVHDSizeBytes 1GB `
-SwitchName "CISwitch"
Add-VMNetworkAdapter -VMName "GitHubActions" -Name "CILegacy" `
-IsLegacy $true -SwitchName "CISwitch"
Start-VM -Name "GitHubActions"
$vm = Get-VM -Name "GitHubActions"
$vm | Format-List Name, State, Status, Generation, Uptime
if ($vm.State -ne "Running") {
throw "Expected Hyper-V VM to be running, got $($vm.State)"
}
- name: Download container prerequisites
id: download-container-prereqs
background: true
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$cacheDir = Join-Path $env:RUNNER_TEMP "container-prereqs"
New-Item -ItemType Directory -Force -Path $cacheDir | Out-Null
$downloads = @{
"Install-Containerd.ps1" = "https://raw.githubusercontent.com/kubernetes-sigs/sig-windows-tools/877cfb9ae91749ab9d73314337504ac962d6f90e/hostprocess/Install-Containerd.ps1"
"containerd.tar.gz" = "https://github.com/containerd/containerd/releases/download/v$($env:CONTAINERD_VERSION)/containerd-$($env:CONTAINERD_VERSION)-windows-amd64.tar.gz"
"crictl.tar.gz" = "https://github.com/kubernetes-sigs/cri-tools/releases/download/v$($env:CRITOOLS_VERSION)/crictl-v$($env:CRITOOLS_VERSION)-windows-amd64.tar.gz"
"cni-plugins.tgz" = "https://github.com/containernetworking/plugins/releases/download/v$($env:CNI_VERSION)/cni-plugins-windows-amd64-v$($env:CNI_VERSION).tgz"
"kubelet.exe" = "https://dl.k8s.io/release/$($env:KUBERNETES_VERSION)/bin/windows/amd64/kubelet.exe"
"kubelet.exe.sha256" = "https://dl.k8s.io/release/$($env:KUBERNETES_VERSION)/bin/windows/amd64/kubelet.exe.sha256"
"hns.psm1" = "https://raw.githubusercontent.com/microsoft/SDN/32df9732ef6dfab07dba205440e30bdfbd8e579a/Kubernetes/windows/hns.psm1"
}
foreach ($download in $downloads.GetEnumerator()) {
Invoke-WebRequest `
-Uri $download.Value `
-OutFile (Join-Path $cacheDir $download.Key) `
-MaximumRetryCount 5 `
-RetryIntervalSec 5
}
$installer = Join-Path $cacheDir "Install-Containerd.ps1"
$installerContent = Get-Content -Raw $installer
$installerContent = $installerContent.Replace(
' curl.exe --silent --fail -Lo $destination $source',
' if (Test-Path $destination) { Write-Host "Using pre-downloaded $destination"; return }' + "`r`n" + ' curl.exe --silent --fail -Lo $destination $source'
)
Set-Content -Path $installer -Value $installerContent -Encoding utf8
# Downloads stall once the DNS Server role is installed, so finish them first.
- name: Wait for downloads
wait: [download-mssql, install-freerdp, download-container-prereqs]
# Install every Windows feature in one foreground step. Steps that register
# services, such as SQL Server setup, New-Cluster, and containerd, start only
# afterwards: changing service configuration during the install aborts its
# registry transaction, and the rollback then requires a restart.
- name: Install Windows features
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$features = @(
"Containers"
"DHCP"
"DNS"
"Failover-Clustering"
"FS-FileServer"
"FS-Resource-Manager"
"MSMQ-Server"
"NPAS"
"Print-Server"
"RSAT-Clustering-PowerShell"
"RSAT-DHCP"
"RSAT-DNS-Server"
"RSAT-Print-Services"
"Web-Server"
)
# SMTP was removed from Windows Server 2025.
if ($env:SMTP_ENABLED -eq 'true') {
$features += "SMTP-Server"
}
$result = Install-WindowsFeature -Name $features
$result | Format-List Success, RestartNeeded, ExitCode, FeatureResult
if (-not $result.Success -or $result.RestartNeeded -ne "No") {
Get-WindowsPackage -Online | Where-Object PackageState -eq InstallPending |
Format-Table PackageName, PackageState
throw "Required Windows features could not be installed without a restart"
}
Get-WindowsFeature -Name $features |
Format-Table DisplayName, Name, InstallState
- name: Setup SQL Server Express
id: setup-mssql
background: true
shell: powershell
run: ./tools/setup-sql-server.ps1
- name: Setup MSCluster
id: setup-mscluster
# The Go test requires the cluster and virtual disk.
background: true
shell: powershell
run: |
$ErrorActionPreference = "Stop"
$deadline = (Get-Date).AddMinutes(3)
do {
$module = Get-Module -ListAvailable FailoverClusters |
Select-Object -First 1
if ($null -ne $module) {
break
}
if ((Get-Date) -ge $deadline) {
throw "FailoverClusters module did not become available"
}
Start-Sleep -Seconds 2
} while ($true)
Import-Module FailoverClusters
$cluster = Get-Cluster -ErrorAction SilentlyContinue
if ($null -eq $cluster) {
New-Cluster `
-Name "CICluster" `
-Node $env:COMPUTERNAME `
-NoStorage `
-AdministrativeAccessPoint None `
-Force | Out-Null
}
Get-Cluster | Format-List Name, AdministrativeAccessPoint
Get-ClusterNode | Format-Table Name, State, NodeWeight, DynamicWeight
Get-ClusterNetwork | Format-Table Name, State, Role, Address
- name: Prepare container runtime
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$cacheDir = Join-Path $env:RUNNER_TEMP "container-prereqs"
$containerdInstaller = Join-Path $cacheDir "Install-Containerd.ps1"
$containerdPath = Join-Path $env:ProgramFiles "containerd"
New-Item -ItemType Directory -Force -Path $containerdPath | Out-Null
Copy-Item (Join-Path $cacheDir "containerd.tar.gz") `
(Join-Path $containerdPath "containerd.tar.gz")
Copy-Item (Join-Path $cacheDir "crictl.tar.gz") `
(Join-Path $containerdPath "crictl.tar.gz")
& $containerdInstaller `
-ContainerDVersion $env:CONTAINERD_VERSION `
-crictlVersion $env:CRITOOLS_VERSION
$containerd = Join-Path $containerdPath "containerd.exe"
$crictl = Join-Path $containerdPath "crictl.exe"
if (-not (Test-Path $containerd)) {
throw "containerd was not installed"
}
$cniBinPath = "C:\opt\cni\bin"
$cniConfigPath = "C:\etc\cni\net.d"
New-Item -ItemType Directory -Force -Path $cniBinPath,$cniConfigPath | Out-Null
$cniArchive = Join-Path $cacheDir "cni-plugins.tgz"
tar.exe -xzf $cniArchive -C $cniBinPath
@'
{
"cniVersion": "0.3.1",
"name": "ci",
"type": "win-bridge",
"apiVersion": 2,
"ipam": {
"type": "host-local",
"subnet": "192.168.255.0/24",
"rangeStart": "192.168.255.10",
"rangeEnd": "192.168.255.250",
"gateway": "192.168.255.2",
"dataDir": "C:/etc/cni/net.d/ipam"
}
}
'@ | Set-Content `
-Path (Join-Path $cniConfigPath "10-ci.conf") `
-Encoding ascii
$hnsModule = Join-Path $env:RUNNER_TEMP "hns.psm1"
Copy-Item (Join-Path $cacheDir "hns.psm1") $hnsModule
$kubeDir = "C:\k"
$manifestDir = Join-Path $kubeDir "manifests"
New-Item -ItemType Directory -Force -Path $kubeDir,$manifestDir | Out-Null
$kubelet = Join-Path $kubeDir "kubelet.exe"
Copy-Item (Join-Path $cacheDir "kubelet.exe") $kubelet
Copy-Item (Join-Path $cacheDir "kubelet.exe.sha256") "$kubelet.sha256"
$expectedHash = (Get-Content "$kubelet.sha256").Trim().Split()[0].ToLowerInvariant()
$actualHash = (Get-FileHash -Algorithm SHA256 $kubelet).Hash.ToLowerInvariant()
if ($actualHash -ne $expectedHash) {
throw "kubelet SHA256 mismatch"
}
@'
apiVersion: kubelet.config.k8s.io/v1beta1
kind: KubeletConfiguration
authentication:
webhook:
enabled: false
authorization:
mode: AlwaysAllow
enableServer: false
address: 127.0.0.1
readOnlyPort: 0
resolvConf: ""
staticPodPath: C:/k/manifests
podLogsDir: C:/k/logs
containerRuntimeEndpoint: npipe:////./pipe/containerd-containerd
'@ | Set-Content `
-Path (Join-Path $kubeDir "kubelet.yaml") `
-Encoding ascii
@'
apiVersion: v1
kind: Pod
metadata:
name: hostprocess
spec:
hostNetwork: true
securityContext:
windowsOptions:
hostProcess: true
runAsUserName: 'NT AUTHORITY\SYSTEM'
containers:
- name: hostprocess
image: mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0
imagePullPolicy: IfNotPresent
command:
- 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'
- -NoLogo
- -NonInteractive
- -Command
- 'while ($true) { Start-Sleep -Seconds 3600 }'
'@ | Set-Content `
-Path (Join-Path $manifestDir "hostprocess.yaml") `
-Encoding ascii
@'
apiVersion: v1
kind: Pod
metadata:
name: process-isolated
spec:
containers:
- name: nanoserver
image: __NANOSERVER_IMAGE__
imagePullPolicy: IfNotPresent
command:
- ping.exe
- -t
- 127.0.0.1
'@.Replace('__NANOSERVER_IMAGE__', $env:NANOSERVER_IMAGE) | Set-Content `
-Path (Join-Path $manifestDir "process-isolated.yaml") `
-Encoding ascii
Restart-Service containerd
(Get-Service containerd).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
& $crictl --runtime-endpoint $env:CRI_ENDPOINT info
if ($LASTEXITCODE -ne 0) {
throw "containerd CRI endpoint is not ready"
}
- name: Pull HostProcess image
id: pull-hostprocess
shell: pwsh
background: true
run: |
$ErrorActionPreference = "Stop"
$crictl = Join-Path $env:ProgramFiles "containerd\crictl.exe"
& $crictl `
--runtime-endpoint $env:CRI_ENDPOINT `
pull mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0
if ($LASTEXITCODE -ne 0) {
throw "Failed to pull HostProcess base image"
}
- name: Pull Nano Server image
id: pull-nanoserver
shell: pwsh
background: true
run: |
$ErrorActionPreference = "Stop"
$crictl = Join-Path $env:ProgramFiles "containerd\crictl.exe"
& $crictl `
--runtime-endpoint $env:CRI_ENDPOINT `
pull $env:NANOSERVER_IMAGE
if ($LASTEXITCODE -ne 0) {
throw "Failed to pull Nano Server image $env:NANOSERVER_IMAGE"
}
- name: Setup container network
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$hnsModule = Join-Path $env:RUNNER_TEMP "hns.psm1"
Import-Module $hnsModule -Force
$hnsNetwork = Get-HNSNetwork | Where-Object Name -EQ "ci"
if ($null -eq $hnsNetwork) {
$hnsNetwork = New-HNSNetwork `
-Type L2Bridge `
-AddressPrefix "192.168.255.0/24" `
-Gateway "192.168.255.2" `
-Name "ci" `
-Verbose
}
$hnsNetwork | Format-List Name, Type, Id, Subnets
- name: Start static pods
# Collector tests require both container types and report any missing fixture.
continue-on-error: true
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$crictl = Join-Path $env:ProgramFiles "containerd\crictl.exe"
$hnsModule = Join-Path $env:RUNNER_TEMP "hns.psm1"
Import-Module $hnsModule -Force
$deadline = (Get-Date).AddMinutes(2)
do {
$networkReady = $false
try {
$networkReady = $null -ne (Get-HnsNetwork | Where-Object Name -EQ "ci")
}
catch {
$networkReady = $false
}
$images = & $crictl --runtime-endpoint $env:CRI_ENDPOINT images -o json | ConvertFrom-Json
$imageNames = @($images.images.repoTags)
$hostProcessReady = $imageNames -contains "mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0"
$nanoServerReady = $imageNames -contains $env:NANOSERVER_IMAGE
if ($networkReady -and $hostProcessReady -and $nanoServerReady) {
break
}
if ((Get-Date) -ge $deadline) {
throw "Timed out waiting for HNS network and container images"
}
Start-Sleep -Seconds 2
} while ($true)
$kubelet = "C:\k\kubelet.exe"
$kubeletStdout = Join-Path $env:RUNNER_TEMP "kubelet.stdout.log"
$kubeletStderr = Join-Path $env:RUNNER_TEMP "kubelet.stderr.log"
New-Item -ItemType Directory -Force -Path "C:\k\logs", "C:\k\kubelet" | Out-Null
$kubeletProcess = Start-Process `
-FilePath $kubelet `
-ArgumentList "--config=C:\k\kubelet.yaml","--root-dir=C:\k\kubelet","--v=4" `
-RedirectStandardOutput $kubeletStdout `
-RedirectStandardError $kubeletStderr `
-PassThru
$deadline = (Get-Date).AddMinutes(2)
$running = $false
do {
if ($kubeletProcess.HasExited) {
Get-Content $kubeletStdout -ErrorAction SilentlyContinue
Get-Content $kubeletStderr -ErrorAction SilentlyContinue
throw "kubelet exited with code $($kubeletProcess.ExitCode)"
}
try {
$json = & $crictl --runtime-endpoint $env:CRI_ENDPOINT ps -o json
if ($LASTEXITCODE -ne 0) { throw "Failed to query container runtime" }
$containers = ($json -join "`n") | ConvertFrom-Json
$hostProcess = $containers.containers | Where-Object { $_.metadata.name -eq "hostprocess" }
$processIsolated = $containers.containers | Where-Object { $_.metadata.name -eq "nanoserver" }
if ($hostProcess.state -eq "CONTAINER_RUNNING" -and $processIsolated.state -eq "CONTAINER_RUNNING") {
$running = $true
break
}
}
catch {
Write-Host "Waiting for static pod containers..."
}
Start-Sleep -Seconds 5
} while ((Get-Date) -lt $deadline)
& $crictl --runtime-endpoint $env:CRI_ENDPOINT pods
& $crictl --runtime-endpoint $env:CRI_ENDPOINT ps -a
if (-not $running) {
Get-Content $kubeletStdout -ErrorAction SilentlyContinue
Get-Content $kubeletStderr -ErrorAction SilentlyContinue
throw "Static pods did not reach Running state"
}
- parallel:
- name: Setup local RDP session
shell: powershell
run: |
$ErrorActionPreference = "Stop"
$userName = "rdp-ci"
$password = "WindowsExporter-CI-2026!"
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force
if ($null -eq (Get-LocalUser -Name $userName -ErrorAction SilentlyContinue)) {
New-LocalUser `
-Name $userName `
-Password $securePassword `
-PasswordNeverExpires `
-AccountNeverExpires | Out-Null
}
Add-LocalGroupMember `
-Group "Remote Desktop Users" `
-Member $userName `
-ErrorAction SilentlyContinue
Set-ItemProperty `
-Path "HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server" `
-Name fDenyTSConnections `
-Value 0
Start-Service TermService
(Get-Service TermService).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
$wfreerdp = (Get-Command wfreerdp.exe -ErrorAction Stop).Source
$stdout = Join-Path $env:RUNNER_TEMP "rdp-client.stdout.log"
$stderr = Join-Path $env:RUNNER_TEMP "rdp-client.stderr.log"
# Ending a step can kill its process tree. Task Scheduler starts the
# client in the runner user's console session instead, so it keeps the
# RDP session active after this step. Runners are disposable, so
# nothing stops it.
$clientArguments = @(
"/v:127.0.0.1"
"/u:$env:COMPUTERNAME\$userName"
"/p:$password"
"/cert:ignore"
"/size:800x600"
"/log-level:INFO"
) -join " "
$action = New-ScheduledTaskAction -Execute "cmd.exe" `
-Argument "/c $wfreerdp $clientArguments > $stdout 2> $stderr"
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2)
Register-ScheduledTask -TaskName "CIFreeRDP" -Action $action -Principal $principal `
-Settings $settings -Force | Out-Null
Start-ScheduledTask -TaskName "CIFreeRDP"
$deadline = (Get-Date).AddSeconds(15)
while ((Get-ScheduledTask -TaskName "CIFreeRDP").State -ne "Running") {
if ((Get-Date) -ge $deadline) { throw "Scheduled task CIFreeRDP did not start" }
Start-Sleep -Milliseconds 500
}
Write-Host "Started wfreerdp.exe through scheduled task CIFreeRDP"
$deadline = (Get-Date).AddSeconds(120)
$rdpSession = $null
do {
$sessions = qwinsta.exe
$sessions | ForEach-Object { Write-Host $_ }
$rdpSession = $sessions |
Where-Object { $_ -match "^\s*>?\s*rdp-tcp#\d+\s+$userName\s+\d+\s+Active\b" } |
Select-Object -First 1
if ($null -ne $rdpSession) {
break
}
if ((Get-ScheduledTask -TaskName "CIFreeRDP").State -ne "Running") {
Get-Content $stdout,$stderr -ErrorAction SilentlyContinue
throw "wfreerdp.exe exited before the RDP user became active"
}
Start-Sleep -Seconds 2
} while ((Get-Date) -lt $deadline)
if ($null -eq $rdpSession) {
Get-Content $stdout,$stderr -ErrorAction SilentlyContinue
throw "No authenticated active RDP session was created by FreeRDP"
}
Write-Host "Active RDP session: $rdpSession"
$sessionName = [regex]::Match($rdpSession, 'rdp-tcp#\d+', 'IgnoreCase').Value.Replace('#', ' ')
$remoteFxNetwork = Get-Counter -ListSet "RemoteFX Network" -ErrorAction Stop
$remoteFxGraphics = Get-Counter -ListSet "RemoteFX Graphics" -ErrorAction Stop
$networkInstances = @($remoteFxNetwork.PathsWithInstances |
Where-Object { $_ -match "RDP-Tcp[# ]\d+" })
$graphicsInstances = @($remoteFxGraphics.PathsWithInstances |
Where-Object { $_ -match "RDP-Tcp[# ]\d+" })
Write-Host "RemoteFX Network instances:"
$networkInstances | ForEach-Object { Write-Host $_ }
Write-Host "RemoteFX Graphics instances:"
$graphicsInstances | ForEach-Object { Write-Host $_ }
if ($networkInstances.Count -eq 0) {
Write-Warning "No RemoteFX Network RDP-Tcp instance was created"
}
if ($graphicsInstances.Count -eq 0) {
Write-Warning "No RemoteFX Graphics RDP-Tcp instance was created"
}
@{
SessionName = $sessionName
RemoteFX = ($networkInstances.Count -gt 0 -and $graphicsInstances.Count -gt 0)
} | ConvertTo-Json | Set-Content (Join-Path $env:RUNNER_TEMP "rdp-ready.json")
- name: Start SMTP server
if: ${{ matrix.smtp }}
shell: powershell
run: |
$ErrorActionPreference = "Stop"
Start-Service SMTPSVC
(Get-Service SMTPSVC).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
- name: Setup IIS
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Start-Service W3SVC
(Get-Service W3SVC).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
"windows_exporter integration test" |
Set-Content -Path "C:\inetpub\wwwroot\index.html" -Encoding ascii
$response = Invoke-WebRequest -Uri "http://127.0.0.1/" -UseBasicParsing
if ($response.StatusCode -ne 200) {
throw "Expected IIS to return HTTP 200, got $($response.StatusCode)"
}
Get-Service W3SVC,WAS | Format-Table Name, Status, StartType
- name: Setup MSMQ
shell: powershell
run: |
$ErrorActionPreference = "Stop"
Start-Service MSMQ
(Get-Service MSMQ).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
Import-Module MSMQ
$queue = Get-MsmqQueue -Name "CIQueue" -QueueType Private -ErrorAction SilentlyContinue
if ($null -eq $queue) {
$queue = New-MsmqQueue -Name "CIQueue" -QueueType Private
}
$queue | Format-List QueueName, QueueType, MessageCount
- name: Setup NPS
shell: powershell
run: |
$ErrorActionPreference = "Stop"
Start-Service IAS
(Get-Service IAS).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
Get-Service IAS |
Format-Table Name, Status, StartType
Get-Counter -ListSet "NPS Authentication Server" -ErrorAction Stop |
Select-Object -ExpandProperty Paths
Get-Counter -ListSet "NPS Accounting Server" -ErrorAction Stop |
Select-Object -ExpandProperty Paths
- name: Enable Windows Update
timeout-minutes: 8
shell: powershell
run: |
$ErrorActionPreference = "Stop"
Set-Service wuauserv -StartupType Manual
Start-Service wuauserv
(Get-Service wuauserv).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
$session = New-Object -ComObject Microsoft.Update.Session
$session.ClientApplicationID = "windows_exporter-ci"
$searcher = $session.CreateUpdateSearcher()
$searcher.Online = $false
$historyCount = $searcher.GetTotalHistoryCount()
Write-Host "Windows Update history count: $historyCount"
# Some runner images have no usable cached catalog. Search online
# once to populate it; the Go collector still searches offline.
# Searching only retrieves metadata, without installing updates.
try {
$result = $searcher.Search("IsInstalled=0 and IsHidden=0")
$pendingCount = $result.Updates.Count
}
catch {
Write-Warning "Offline catalog search failed: $($_.Exception.ToString())"
# The online search usually finishes in under 30 seconds but
# occasionally stalls for minutes. The COM call cannot be
# cancelled, so run it in a job and restart the service on stall.
$onlineSearch = {
$ErrorActionPreference = "Stop"
$session = New-Object -ComObject Microsoft.Update.Session
$session.ClientApplicationID = "windows_exporter-ci"
$searcher = $session.CreateUpdateSearcher()
$searcher.Online = $true
$searcher.Search("IsInstalled=0 and IsHidden=0").Updates.Count
}
$pendingCount = $null
for ($attempt = 1; $attempt -le 3; $attempt++) {
$job = Start-Job -ScriptBlock $onlineSearch
if (Wait-Job $job -Timeout 90) {
try {
$pendingCount = Receive-Job $job
break
}
catch {
Write-Warning "Online catalog search attempt $attempt failed: $($_.Exception.Message)"
}
}
else {
Write-Warning "Online catalog search attempt $attempt timed out"
Stop-Job $job
Restart-Service wuauserv -Force
(Get-Service wuauserv).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
}
Remove-Job $job -Force
}
if ($null -eq $pendingCount) {
throw "Online catalog search failed after $($attempt - 1) attempts"
}
}
Write-Host "Windows Update catalog contains $pendingCount pending updates"
Get-Service wuauserv |
Format-Table Name, Status, StartType
- name: Setup printer
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Start-Service Spooler
(Get-Service Spooler).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
$driver = Get-PrinterDriver -ErrorAction SilentlyContinue |
Where-Object Name -EQ "Generic / Text Only" |
Select-Object -First 1
if ($null -eq $driver) {
try {
Add-PrinterDriver -Name "Generic / Text Only" -ErrorAction Stop
}
catch {
Write-Host "Generic / Text Only driver is not staged; using an installed driver"
}
$driver = Get-PrinterDriver -ErrorAction SilentlyContinue |
Where-Object Name -EQ "Generic / Text Only" |
Select-Object -First 1
}
if ($null -eq $driver) {
$driver = Get-PrinterDriver -ErrorAction Stop | Select-Object -First 1
}
if ($null -eq $driver) {
throw "No printer driver is available"
}
if ($null -eq (Get-PrinterPort -Name "CIPrinterPort:" -ErrorAction SilentlyContinue)) {
Add-PrinterPort -Name "CIPrinterPort:"
}
if ($null -eq (Get-Printer -Name "CIPrinter" -ErrorAction SilentlyContinue)) {
Add-Printer `
-Name "CIPrinter" `
-DriverName $driver.Name `
-PortName "CIPrinterPort:"
}
Get-Printer -Name "CIPrinter" |
Format-List Name, DriverName, PortName, PrinterStatus
- name: Setup FSRM quota
# The Go test requires the quota, so still run other collector tests on setup failure.
continue-on-error: true
shell: powershell
run: |
$ErrorActionPreference = "Stop"
Start-Service SrmSvc
(Get-Service SrmSvc).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
Import-Module FileServerResourceManager
$deadline = (Get-Date).AddSeconds(30)
do {
try {
Get-CimClass `
-Namespace "root/Microsoft/Windows/FSRM" `
-ClassName "MSFT_FSRMQuota" `
-ErrorAction Stop | Out-Null
break
}
catch {
if ((Get-Date) -ge $deadline) {
throw
}
Start-Sleep -Seconds 2
}
} while ($true)
# Initialize the quota provider through its native COM API.
$quotaPath = "C:\fsrm-ci"
New-Item -ItemType Directory -Force -Path $quotaPath | Out-Null
$quota = Get-FsrmQuota -Path $quotaPath -ErrorAction SilentlyContinue
if ($null -eq $quota) {
$manager = New-Object -ComObject Fsrm.FsrmQuotaManager
$quota = $manager.CreateQuota($quotaPath)
$quota.QuotaLimit = 100MB
$quota.Description = "windows_exporter integration test"
$quota.Commit()
}
"windows_exporter integration test" |
Set-Content -Path (Join-Path $quotaPath "test.txt") -Encoding ascii
$manager = New-Object -ComObject Fsrm.FsrmQuotaManager
$manager.GetQuota($quotaPath) | Format-List Path, QuotaLimit, QuotaUsed, Description
Restart-Service SrmSvc
(Get-Service SrmSvc).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
$deadline = (Get-Date).AddSeconds(60)
do {
$quotas = @(Get-CimInstance `
-Namespace "root/Microsoft/Windows/FSRM" `
-ClassName "MSFT_FSRMQuota" -ErrorAction Stop)
$quotas | Format-List Path, Size, Usage, Description
$cimQuota = $quotas | Where-Object { $_.Path.TrimEnd('\') -eq $quotaPath }
if ($null -ne $cimQuota) { break }
if ((Get-Date) -ge $deadline) {
fltmc.exe filters
fltmc.exe instances
Get-WinEvent -FilterHashtable @{LogName='Application'; StartTime=(Get-Date).AddMinutes(-10)} `
-ErrorAction SilentlyContinue | Where-Object ProviderName -Match 'SRM|FSRM' |
Select-Object TimeCreated, ProviderName, Message
throw "MSFT_FSRMQuota instance for $quotaPath was not found"
}
Start-Sleep -Seconds 2
} while ($true)
$cimQuota | Format-List Path, Size, Usage, PeakUsage, SoftLimit, Disabled, Description
- name: Setup SMB
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$sharePath = Join-Path $env:RUNNER_TEMP "smb"
New-Item -ItemType Directory -Force -Path $sharePath | Out-Null
if ($null -eq (Get-SmbShare -Name "CIShare" -ErrorAction SilentlyContinue)) {
New-SmbShare `
-Name "CIShare" `
-Path $sharePath `
-FullAccess "Everyone"
}
$unc = "\\$env:COMPUTERNAME\CIShare"
"windows_exporter integration test" | Set-Content "$unc\test.txt" -Encoding ascii
$content = Get-Content "$unc\test.txt"
if ($content -ne "windows_exporter integration test") {
throw "Unexpected content read back over SMB"
}
Get-SmbShare -Name "CIShare"
Get-SmbConnection | Format-Table ServerName, ShareName, Dialect, NumOpens
# Keep an SMB file open and the .NET runtime loaded during the Go tests.
# Task Scheduler keeps the workload alive after this step.
$workload = Join-Path $env:RUNNER_TEMP "netframework-workload.ps1"
@'
$data = New-Object byte[] 1MB
$path = "\\$env:COMPUTERNAME\CIShare\test.txt"
$stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::ReadWrite)
try {
while ($true) {
$stream.Position = 0
$stream.WriteByte(65)
$stream.Flush()
[System.GC]::KeepAlive($data)
Start-Sleep -Seconds 1
}
} finally {
$stream.Dispose()
}
'@ | Set-Content $workload
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -File $workload"
$principal = New-ScheduledTaskPrincipal -UserId "$env:USERDOMAIN\$env:USERNAME" `
-LogonType Interactive -RunLevel Highest
$settings = New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Hours 2)
Register-ScheduledTask -TaskName "CIWorkload" -Action $action -Principal $principal `
-Settings $settings -Force | Out-Null
Start-ScheduledTask -TaskName "CIWorkload"
- name: Setup DNS and DHCP
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
Start-Service DNS
Start-Service DHCPServer
(Get-Service DNS).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
(Get-Service DHCPServer).WaitForStatus("Running", [TimeSpan]::FromSeconds(30))
Get-Service DNS,DHCPServer |
Format-Table Name, Status, StartType
Add-DnsServerPrimaryZone `
-Name "ci.contoso.com" `
-ZoneFile "ci.contoso.com.dns"
Add-DnsServerResourceRecordA `
-Name "test" `
-ZoneName "ci.contoso.com" `
-IPv4Address "192.0.2.10"
$zone = Get-DnsServerZone -Name "ci.contoso.com" -ErrorAction Stop
$zone | Format-List ZoneName, ZoneType, IsDsIntegrated
Get-DnsServerResourceRecord `
-ZoneName "ci.contoso.com" `
-Name "test" `
-RRType A
Add-DhcpServerv4Scope `
-Name "GitHubActions" `
-StartRange "192.0.2.100" `
-EndRange "192.0.2.200" `
-SubnetMask "255.255.255.0" `
-State Active
$scope = Get-DhcpServerv4Scope `
-ScopeId "192.0.2.0" `
-ErrorAction Stop
$scope | Format-List ScopeId, Name, State, StartRange, EndRange, SubnetMask
if ($zone.ZoneType -ne "Primary") {
throw "Expected a primary DNS zone"
}
if ($scope.State -ne "Active") {
throw "Expected an active DHCP scope"
}
# The foreground waits for SQL Server here anyway, so Go setup costs nothing.
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: Wait for background setup
wait:
- pull-hostprocess
- pull-nanoserver
- setup-bitlocker
- setup-mscluster
- setup-mssql
- setup-storage-pool
- start-hyperv-vm
- name: Export background fixtures
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$readyFile = Join-Path $env:RUNNER_TEMP "rdp-ready.json"
if (-not (Test-Path $readyFile)) {
Get-Content (Join-Path $env:RUNNER_TEMP "rdp-client.*.log") -ErrorAction SilentlyContinue
throw "RDP fixture did not become ready"
}
$fixture = Get-Content $readyFile -Raw | ConvertFrom-Json
"WINDOWS_EXPORTER_TEST_RDP_SESSION=$($fixture.SessionName)" | Add-Content $env:GITHUB_ENV
if ($fixture.RemoteFX) {
"WINDOWS_EXPORTER_TEST_COLLECTORS=$env:WINDOWS_EXPORTER_TEST_COLLECTORS,remote_fx" |
Add-Content $env:GITHUB_ENV
}
$bitlocker = Get-Content (Join-Path $env:RUNNER_TEMP "bitlocker-fixture.json") -Raw | ConvertFrom-Json
"WINDOWS_EXPORTER_TEST_BITLOCKER_VOLUME=$($bitlocker.Unlocked)" | Add-Content $env:GITHUB_ENV
"WINDOWS_EXPORTER_TEST_BITLOCKER_LOCKED_VOLUME=$($bitlocker.Locked)" | Add-Content $env:GITHUB_ENV
- name: Resync WMI performance counters
shell: pwsh
run: |
winmgmt.exe /resyncperf
if ($LASTEXITCODE -ne 0) { throw "Failed to synchronize WMI performance counters" }
- name: Go tests
id: go-tests
shell: pwsh
run: |
go test -json -count=1 -race -timeout=10m ./... 2>&1 | Tee-Object test-results.jsonl |
ForEach-Object {
$line = $_.ToString()
try {
$event = $line | ConvertFrom-Json -ErrorAction Stop
if ($event.Output) { [Console]::Write($event.Output) }
} catch {
Write-Host $line
}
}
$testExitCode = $LASTEXITCODE
"exit_code=$testExitCode" | Add-Content $env:GITHUB_OUTPUT
exit $testExitCode
- name: Summarize Windows feature tests
if: ${{ always() }}
shell: pwsh
env:
TEST_EXIT_CODE: ${{ steps.go-tests.outputs.exit_code || '1' }}
run: |
Copy-Item "$env:windir/Logs/DISM/dism.log" (Join-Path $env:RUNNER_TEMP "dism.log") -ErrorAction SilentlyContinue
# CBS.log names the files that make role installation request a restart.
Copy-Item "$env:windir/Logs/CBS/CBS.log" (Join-Path $env:RUNNER_TEMP "cbs.log") -ErrorAction SilentlyContinue
./tools/test-summary.ps1 -TestExitCode ([int]$env:TEST_EXIT_CODE)
- name: Build and smoke test exporter
shell: pwsh
run: |
go build -trimpath -o windows_exporter.exe ./cmd/windows_exporter
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
./tools/end-to-end-test.ps1
- name: Upload test logs
if: ${{ always() }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: windows-test-results-${{ matrix.os }}
path: |
test-output.txt
test-results.jsonl
feature-summary.md
${{ runner.temp }}/windows_exporter.*.log
${{ runner.temp }}/kubelet.*.log
${{ runner.temp }}/sql-server-summary.txt
${{ runner.temp }}/rdp-client.*.log
${{ runner.temp }}/dism.log
${{ runner.temp }}/cbs.log
if-no-files-found: ignore
lint:
runs-on: windows-2025
permissions:
contents: read
steps:
# `gofmt` linter run by golangci-lint fails on CRLF line endings (the default for Windows)
- name: Set git to use LF
run: |
git config --global core.autocrlf false
git config --global core.eol lf
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: 'false'
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
# renovate: golang=go
go-version: '1.27.1'
- name: golangci-lint
uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9.3.0
with:
# renovate: github=golangci/golangci-lint
version: v2.14.0
args: "--max-same-issues=0"
super-linter:
name: Super Linter
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Checkout Code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
fetch-tags: 'false'
- name: Lint Code Base
uses: super-linter/super-linter/slim@2da136927bd4a73596db63044b504547c62cb854 # v9.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MULTI_STATUS: false
LINTER_RULES_PATH: .github/linters
GITHUB_ACTIONS_ZIZMOR_CONFIG_FILE: ../../../github/workspace/.github/linters/zizmor.yml
ENABLE_GITHUB_ACTIONS_STEP_SUMMARY: true
ENABLE_GITHUB_PULL_REQUEST_SUMMARY_COMMENT: false
SAVE_SUPER_LINTER_SUMMARY: true
VALIDATE_ALL_CODEBASE: false
VALIDATE_BASH: true
VALIDATE_BASH_EXEC: true
VALIDATE_EDITORCONFIG: true
VALIDATE_ENV: true
# VALIDATE_GITHUB_ACTIONS: true
VALIDATE_GITHUB_ACTIONS_ZIZMOR: true
VALIDATE_GITLEAKS: true
VALIDATE_GO_RELEASER: true
VALIDATE_HTML: true
VALIDATE_JSON: true
# VALIDATE_MARKDOWN: true
VALIDATE_NATURAL_LANGUAGE: true
VALIDATE_PYTHON: true
VALIDATE_RENOVATE: true
VALIDATE_SHELL_SHFMT: true
VALIDATE_SPELL_CODESPELL: true
# VALIDATE_XML: true
# VALIDATE_YAML: true
# Require this job in the merge rules. It is skipped, which counts as passing,
# unless a needed job failed or was cancelled.
fail:
if: ${{ cancelled() || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'failure') }}
needs:
- build
- package
- docker
- windows-test
- lint
- super-linter
runs-on: ubuntu-24.04
steps:
- name: Verify checks passed
run: |
echo "Some checks have failed!"
exit 1