Files
windows_exporter/.github/workflows/ci.yml
T
2026-10-06 17:47:21 +02:00

1335 lines
49 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
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
needs:
- package
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@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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@37fe631027851001ddb9b187196cc803df7f5f0e # v4.3.0
- name: Build and push
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.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
- test
permissions:
contents: write
steps:
- name: Download Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
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
test:
runs-on: windows-2025
timeout-minutes: 25
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"
# These collectors must work on the fixtures below; missing features are failures.
WINDOWS_EXPORTER_TEST_COLLECTORS: "cache,container,cpu,cpu_info,dhcp,diskdrive,dns,fsrmquota,hyperv,iis,license,logical_disk,memory,mscluster,msmq,net,netframework,nps,os,pagefile,physical_disk,printer,process,scheduled_task,service,smb,smbclient,system,tcp,terminal_services,time,udp,update"
steps:
- 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'
- parallel:
- name: Install Containers feature
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$features = @(
"Containers"
"Failover-Clustering"
"RSAT-Clustering-PowerShell"
)
$result = Install-WindowsFeature -Name $features
$result | Format-List Success, RestartNeeded, ExitCode, FeatureResult
if (-not $result.Success -or $result.RestartNeeded -ne "No") {
throw "Containers feature could not be installed without a restart"
}
Get-WindowsFeature -Name $features |
Format-Table DisplayName, Name, InstallState
- name: Setup storage pool
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$existingDiskIds = @(Get-PhysicalDisk | ForEach-Object ObjectId)
$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
diskpart /s $diskpartScript
Update-HostStorageCache
$newDisks = @(Get-PhysicalDisk | Where-Object { $_.ObjectId -notin $existingDiskIds })
$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: Download container prerequisites
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)
}
$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
- name: Start Hyper-V VM
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: Setup local RDP session
id: setup-rdp
continue-on-error: true
background: true
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))
choco install freerdp.portable `
--version=3.28.0 `
--yes `
--no-progress
if ($LASTEXITCODE -ne 0) {
throw "Failed to install FreeRDP"
}
$wfreerdp = (Get-Command wfreerdp.exe -ErrorAction Stop).Source
$freeRdp = Start-Process `
-FilePath $wfreerdp `
-ArgumentList @(
"/v:127.0.0.1"
"/u:$env:COMPUTERNAME\$userName"
"/p:$password"
"/cert:ignore"
"/size:800x600"
) `
-PassThru
Write-Host "Started wfreerdp.exe with PID $($freeRdp.Id)"
$deadline = (Get-Date).AddSeconds(60)
$rdpSession = $null
do {
$sessions = qwinsta.exe
$sessions | ForEach-Object { Write-Host $_ }
$rdpSession = $sessions |
Where-Object { $_ -match "RDP-Tcp#" } |
Select-Object -First 1
if ($null -ne $rdpSession) {
break
}
if ($freeRdp.HasExited) {
throw "wfreerdp.exe exited before an RDP session was created"
}
Start-Sleep -Seconds 2
} while ((Get-Date) -lt $deadline)
if ($null -eq $rdpSession) {
throw "No RDP-Tcp session was created by FreeRDP"
}
Write-Host "RDP session created: $rdpSession"
$remoteFxNetwork = Get-Counter -ListSet "RemoteFX Network" -ErrorAction Stop
$remoteFxGraphics = Get-Counter -ListSet "RemoteFX Graphics" -ErrorAction Stop
$networkInstances = @($remoteFxNetwork.PathsWithInstances |
Where-Object { $_ -match "RDP-Tcp#" })
$graphicsInstances = @($remoteFxGraphics.PathsWithInstances |
Where-Object { $_ -match "RDP-Tcp#" })
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"
}
- 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: Install Windows roles
id: install-windows-roles
background: true
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$features = @(
"DHCP"
"DNS"
"FS-FileServer"
"FS-Resource-Manager"
"MSMQ-Server"
"NPAS"
"Print-Server"
"RSAT-DHCP"
"RSAT-DNS-Server"
"RSAT-Print-Services"
"Web-Server"
)
$result = Install-WindowsFeature -Name $features
$result | Format-List Success, RestartNeeded, ExitCode, FeatureResult
if (-not $result.Success -or $result.RestartNeeded -ne "No") {
throw "Required Windows roles could not be installed without a restart"
}
Get-WindowsFeature -Name $features |
Format-Table DisplayName, Name, InstallState
- 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: mcr.microsoft.com/windows/nanoserver:ltsc2025
imagePullPolicy: IfNotPresent
command:
- ping.exe
- -t
- 127.0.0.1
'@ | 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 mcr.microsoft.com/windows/nanoserver:ltsc2025
if ($LASTEXITCODE -ne 0) {
throw "Failed to pull Windows Server 2025 Nano Server 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 "mcr.microsoft.com/windows/nanoserver:ltsc2025"
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"
}
- name: Wait for Windows roles
wait: [install-windows-roles]
- parallel:
- 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
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"
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
- 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"
}
- name: Wait for background setup
wait: [pull-hostprocess, pull-nanoserver, setup-mscluster, setup-rdp]
- name: Start .NET Framework and SMB workloads
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$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
$process = Start-Process powershell.exe -PassThru `
-ArgumentList "-NoProfile", "-File", $workload
$process.Id | Set-Content (Join-Path $env:RUNNER_TEMP "netframework.pid")
winmgmt.exe /resyncperf
if ($LASTEXITCODE -ne 0) { throw "Failed to synchronize WMI performance counters" }
- name: Go tests
shell: pwsh
run: |
go test -v -count=1 -race -timeout=10m ./... 2>&1 | Tee-Object test-output.txt
$testExitCode = $LASTEXITCODE
Select-String -Path test-output.txt -Pattern '^--- SKIP:' |
ForEach-Object Line | Add-Content $env:GITHUB_STEP_SUMMARY
exit $testExitCode
- 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@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: windows-test-results
path: |
test-output.txt
${{ runner.temp }}/windows_exporter.*.log
${{ runner.temp }}/kubelet.*.log
if-no-files-found: ignore
- name: Stop test workloads
if: ${{ always() }}
shell: pwsh
run: |
$pidFile = Join-Path $env:RUNNER_TEMP "netframework.pid"
if (Test-Path $pidFile) {
Stop-Process -Id (Get-Content $pidFile) -ErrorAction SilentlyContinue
}
Get-Process kubelet -ErrorAction SilentlyContinue | Stop-Process -ErrorAction SilentlyContinue
Get-VM -Name GitHubActions -ErrorAction SilentlyContinue | Stop-VM -TurnOff -Force -ErrorAction SilentlyContinue
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.13.2
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