mirror of
https://github.com/prometheus-community/windows_exporter.git
synced 2026-10-17 10:09:05 +02:00
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jan-Otto Kröpke <mail@jkroepke.de>
1753 lines
69 KiB
YAML
1753 lines
69 KiB
YAML
name: CI
|
|
|
|
# Windows tests are gated on Go, workflow and Go test script changes for pull requests and branch pushes.
|
|
on:
|
|
push:
|
|
branches:
|
|
- master
|
|
- next
|
|
- main
|
|
- "0.*"
|
|
- "1.*"
|
|
tags:
|
|
- "v*"
|
|
pull_request:
|
|
workflow_dispatch:
|
|
release:
|
|
types:
|
|
- published
|
|
|
|
permissions: {}
|
|
|
|
jobs:
|
|
detect-changes:
|
|
name: Detect Windows test changes
|
|
runs-on: ubuntu-26.04
|
|
permissions:
|
|
contents: read
|
|
outputs:
|
|
changed: ${{ steps.detect.outputs.changed }}
|
|
steps:
|
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
persist-credentials: 'false'
|
|
fetch-depth: '0'
|
|
|
|
- name: Detect changes
|
|
id: detect
|
|
shell: bash
|
|
env:
|
|
EVENT_NAME: ${{ github.event_name }}
|
|
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
run: |
|
|
# Manual, tag, release, and new-branch runs have no previous revision to compare.
|
|
if [[ "$EVENT_NAME" != "pull_request" && "$EVENT_NAME" != "push" ]] ||
|
|
[[ "$GITHUB_REF" == refs/tags/* || -z "$BASE_SHA" || "$BASE_SHA" =~ ^0+$ ]]; then
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$EVENT_NAME" == "pull_request" ]]; then
|
|
BASE_SHA=$(git merge-base "$BASE_SHA" "$HEAD_SHA")
|
|
elif ! git cat-file -e "$BASE_SHA^{commit}"; then
|
|
# A force push may remove the previous commit from the fetched history.
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
changed_files=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- '*.go' '.github/workflows/ci.yml' 'tools/build-go-tests.ps1' 'tools/run-go-tests.ps1')
|
|
if [[ -n "$changed_files" ]]; then
|
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
build:
|
|
name: Build Windows binaries
|
|
runs-on: ubuntu-26.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.2'
|
|
|
|
- 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.3
|
|
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.3
|
|
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-26.04
|
|
if: ${{ !cancelled() && needs.detect-changes.result == 'success' && needs.package.result == 'success' && (needs.windows-test.result == 'success' || needs.windows-test.result == 'skipped') }}
|
|
# Images are pushed on branch and tag events, so gate them on the same
|
|
# validation as the release binaries in the publish job.
|
|
needs:
|
|
- detect-changes
|
|
- 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-26.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 }})
|
|
needs: detect-changes
|
|
if: ${{ needs.detect-changes.outputs.changed == 'true' }}
|
|
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,storage_spaces,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 }}
|
|
|
|
# Its PATH changes apply only after a wait step includes it.
|
|
- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
|
id: setup-go
|
|
background: true
|
|
with:
|
|
# renovate: golang=go
|
|
go-version: '1.27.2'
|
|
|
|
# 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 step. Steps that register services,
|
|
# such as SQL Server setup, New-Cluster, and containerd, start only after
|
|
# "Wait for Windows features": changing service configuration during the
|
|
# install aborts its registry transaction, and the rollback then requires
|
|
# a restart.
|
|
- name: Install Windows features
|
|
id: install-windows-features
|
|
# Setting up Go and building the tests register no services, so they overlap the install.
|
|
background: true
|
|
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: Wait for Go
|
|
wait: [setup-go]
|
|
|
|
# Linking the race-enabled test binaries takes minutes; do it while the
|
|
# fixtures below are provisioned instead of in the Go tests step.
|
|
- name: Build Go test binaries
|
|
id: build-go-tests
|
|
background: true
|
|
shell: pwsh
|
|
run: |
|
|
./tools/build-go-tests.ps1 -OutputDirectory (Join-Path $env:RUNNER_TEMP "go-tests")
|
|
go build -trimpath -o windows_exporter.exe ./cmd/windows_exporter
|
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
|
|
- name: Wait for Windows features
|
|
wait: [install-windows-features]
|
|
|
|
- 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 tests require the cluster and its resource fixtures.
|
|
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
|
|
|
|
# A private network lets an IP Address resource come online without
|
|
# adding addresses to the runner's primary adapter.
|
|
$networkSwitch = "CIClusterSwitch"
|
|
$networkAddress = "198.51.100.0"
|
|
Start-Service vmms
|
|
if ($null -eq (Get-VMSwitch -Name $networkSwitch -ErrorAction SilentlyContinue)) {
|
|
New-VMSwitch -Name $networkSwitch -SwitchType Internal | Out-Null
|
|
}
|
|
|
|
$adapter = Get-NetAdapter -Name "vEthernet ($networkSwitch)"
|
|
if ($null -eq (Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -IPAddress "198.51.100.1" -ErrorAction SilentlyContinue)) {
|
|
New-NetIPAddress -InterfaceIndex $adapter.ifIndex -IPAddress "198.51.100.1" -PrefixLength 24 | Out-Null
|
|
}
|
|
|
|
# A Cluster Name needs a static address on a routed client network and a
|
|
# DNS registration; the DHCP-only runner network provides neither. Keep
|
|
# the cluster without an administrative access point and add workload
|
|
# resources instead.
|
|
$cluster = Get-Cluster -ErrorAction SilentlyContinue
|
|
if ($null -eq $cluster) {
|
|
New-Cluster `
|
|
-Name "CICluster" `
|
|
-Node $env:COMPUTERNAME `
|
|
-NoStorage `
|
|
-AdministrativeAccessPoint None `
|
|
-Force | Out-Null
|
|
}
|
|
|
|
$deadline = (Get-Date).AddMinutes(2)
|
|
do {
|
|
$network = Get-ClusterNetwork | Where-Object Address -EQ $networkAddress
|
|
if ($null -ne $network) {
|
|
break
|
|
}
|
|
|
|
if ((Get-Date) -ge $deadline) {
|
|
Get-ClusterNetwork | Format-Table Name, State, Role, Address
|
|
throw "Cluster network $networkAddress did not appear"
|
|
}
|
|
|
|
Start-Sleep -Seconds 2
|
|
} while ($true)
|
|
|
|
# Networks without a default gateway default to cluster-only traffic,
|
|
# which cannot host IP Address resources. 3 = ClusterAndClient.
|
|
$network.Role = 3
|
|
|
|
# Resource fixtures for the resource and resourcegroup sub-collectors:
|
|
# CIResources: an online Generic Service that depends on an online IP
|
|
# Address, plus an IP Address that stays offline.
|
|
# CIFailedResources: a Generic Service whose binary is missing.
|
|
if ($null -eq (Get-ClusterGroup -Name "CIResources" -ErrorAction SilentlyContinue)) {
|
|
Add-ClusterGroup -Name "CIResources" | Out-Null
|
|
|
|
Add-ClusterResource -Name "CI IP Address" -Group "CIResources" -ResourceType "IP Address" |
|
|
Set-ClusterParameter -Multiple @{
|
|
Address = "198.51.100.10"
|
|
SubnetMask = "255.255.255.0"
|
|
Network = $network.Name
|
|
EnableDhcp = 0
|
|
}
|
|
|
|
Add-ClusterResource -Name "CI Offline IP Address" -Group "CIResources" -ResourceType "IP Address" |
|
|
Set-ClusterParameter -Multiple @{
|
|
Address = "198.51.100.11"
|
|
SubnetMask = "255.255.255.0"
|
|
Network = $network.Name
|
|
EnableDhcp = 0
|
|
}
|
|
|
|
# Windows Event Collector is unused on the runner and stays running.
|
|
Add-ClusterResource -Name "CI Generic Service" -Group "CIResources" -ResourceType "Generic Service" |
|
|
Set-ClusterParameter -Name ServiceName -Value "Wecsvc"
|
|
Add-ClusterResourceDependency -Resource "CI Generic Service" -Provider "CI IP Address" | Out-Null
|
|
}
|
|
|
|
if ($null -eq (Get-ClusterGroup -Name "CIFailedResources" -ErrorAction SilentlyContinue)) {
|
|
New-Service `
|
|
-Name "WindowsExporterCIFailing" `
|
|
-BinaryPathName "$env:SystemRoot\System32\windows-exporter-ci-missing.exe" `
|
|
-StartupType Manual | Out-Null
|
|
|
|
Add-ClusterGroup -Name "CIFailedResources" | Out-Null
|
|
$failing = Add-ClusterResource -Name "CI Failed Service" -Group "CIFailedResources" -ResourceType "Generic Service"
|
|
$failing | Set-ClusterParameter -Name ServiceName -Value "WindowsExporterCIFailing"
|
|
# 0 = ClusterResourceDontRestart: keep the resource failed.
|
|
$failing.RestartAction = 0
|
|
}
|
|
|
|
Start-ClusterResource -Name "CI Generic Service" -Wait 60 | Out-Null
|
|
|
|
try {
|
|
Start-ClusterResource -Name "CI Failed Service" -Wait 60 | Out-Null
|
|
}
|
|
catch {
|
|
Write-Host "CI Failed Service failed to start as expected: $_"
|
|
}
|
|
|
|
$expected = @{
|
|
"CI IP Address" = "Online"
|
|
"CI Generic Service" = "Online"
|
|
"CI Offline IP Address" = "Offline"
|
|
"CI Failed Service" = "Failed"
|
|
}
|
|
|
|
$deadline = (Get-Date).AddMinutes(1)
|
|
do {
|
|
$resources = @(Get-ClusterResource)
|
|
$mismatched = @($expected.GetEnumerator() | Where-Object {
|
|
$name = $_.Key
|
|
"$(($resources | Where-Object Name -EQ $name).State)" -ne $_.Value
|
|
})
|
|
|
|
if ($mismatched.Count -eq 0) {
|
|
break
|
|
}
|
|
|
|
if ((Get-Date) -ge $deadline) {
|
|
$resources | Format-Table Name, State, OwnerGroup, ResourceType
|
|
throw "Cluster resources did not reach their expected states: $($mismatched.Key -join ', ')"
|
|
}
|
|
|
|
Start-Sleep -Seconds 2
|
|
} while ($true)
|
|
|
|
Get-Cluster | Format-List Name, AdministrativeAccessPoint
|
|
Get-ClusterNode | Format-Table Name, State, NodeWeight, DynamicWeight
|
|
Get-ClusterNetwork | Format-Table Name, State, Role, Address
|
|
Get-ClusterGroup | Format-Table Name, State, OwnerNode, GroupType
|
|
Get-ClusterResource | Format-Table Name, State, OwnerGroup, ResourceType
|
|
|
|
- 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"
|
|
|
|
# Registry connections can be reset while downloading image layers.
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
|
$crictl = Join-Path $env:ProgramFiles "containerd\crictl.exe"
|
|
$maxAttempts = 3
|
|
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
|
& $crictl `
|
|
--runtime-endpoint $env:CRI_ENDPOINT `
|
|
pull mcr.microsoft.com/oss/kubernetes/windows-host-process-containers-base-image:v1.0.0
|
|
|
|
if ($LASTEXITCODE -eq 0) {
|
|
break
|
|
}
|
|
if ($attempt -eq $maxAttempts) {
|
|
throw "Failed to pull HostProcess base image after $maxAttempts attempts"
|
|
}
|
|
|
|
$delay = 5 * $attempt
|
|
Write-Warning "Failed to pull HostProcess base image (attempt $attempt/$maxAttempts); retrying in $delay seconds"
|
|
Start-Sleep -Seconds $delay
|
|
}
|
|
|
|
- name: Pull Nano Server image
|
|
id: pull-nanoserver
|
|
shell: pwsh
|
|
background: true
|
|
run: |
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
# Registry connections can be reset while downloading image layers.
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
|
$crictl = Join-Path $env:ProgramFiles "containerd\crictl.exe"
|
|
$maxAttempts = 3
|
|
for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
|
|
& $crictl `
|
|
--runtime-endpoint $env:CRI_ENDPOINT `
|
|
pull $env:NANOSERVER_IMAGE
|
|
|
|
if ($LASTEXITCODE -eq 0) {
|
|
break
|
|
}
|
|
if ($attempt -eq $maxAttempts) {
|
|
throw "Failed to pull Nano Server image $env:NANOSERVER_IMAGE after $maxAttempts attempts"
|
|
}
|
|
|
|
$delay = 5 * $attempt
|
|
Write-Warning "Failed to pull Nano Server image $env:NANOSERVER_IMAGE (attempt $attempt/$maxAttempts); retrying in $delay seconds"
|
|
Start-Sleep -Seconds $delay
|
|
}
|
|
|
|
- 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"
|
|
}
|
|
|
|
|
|
- name: Wait for background setup
|
|
wait:
|
|
- build-go-tests
|
|
- 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
|
|
shell: pwsh
|
|
run: ./tools/run-go-tests.ps1 -BinaryDirectory (Join-Path $env:RUNNER_TEMP "go-tests") -Timeout 10m
|
|
|
|
- name: Smoke test exporter
|
|
shell: pwsh
|
|
run: ./tools/end-to-end-test.ps1
|
|
|
|
- name: Collect Windows setup logs
|
|
if: ${{ always() }}
|
|
shell: pwsh
|
|
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
|
|
|
|
- name: Upload test logs
|
|
if: ${{ always() }}
|
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
with:
|
|
name: windows-test-results-${{ matrix.os }}
|
|
path: |
|
|
${{ 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.2'
|
|
|
|
- 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-26.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:
|
|
- detect-changes
|
|
- build
|
|
- package
|
|
- docker
|
|
- windows-test
|
|
- lint
|
|
- super-linter
|
|
runs-on: ubuntu-26.04
|
|
steps:
|
|
- name: Verify checks passed
|
|
run: |
|
|
echo "Some checks have failed!"
|
|
exit 1
|