nice-to-have: implement all 6 tier items

- Get-CIJobSummary.ps1: already present with full implementation (marked done)
- Get-BuildArtifacts.ps1: add -JobId/-Commit params; Write-ArtifactManifest writes
  manifest.json (name, size, sha256, commit, jobId) after both Linux and Windows
  artifact collection branches
- Invoke-CIJob.ps1: add -GuestCPU/-GuestMemoryMB params; insert post-clone VMX
  override block (numvcpus/memsize rewrite) before vmrun start; pass -JobId/-Commit
  to Get-BuildArtifacts calls
- Watch-RunnerHealth.ps1: add -GiteaUrl/-GiteaCredentialTarget params; optional
  GET /api/v1/admin/runners check in Running branch warns if 0 online runners
- Install-CIToolchain-WinBuild2025.ps1: fill SHA256 hashes for Python 3.13.3,
  7-Zip 26.01, Node.js 22.14.0, dotnet-install.ps1
- Measure-CIBenchmark.ps1: add deltaKB field (linked-clone disk footprint in KB)
  measured post-clone; added to result object and summary Format-Table
This commit is contained in:
Simone
2026-05-13 10:38:49 +02:00
parent 8ca3e530c5
commit 6cd6bff882
6 changed files with 148 additions and 15 deletions
+7 -7
View File
@@ -351,14 +351,14 @@ Every item is concrete, references a file, and ends with an effort estimate.
- [x] done Move done sections of [TODO.md](../TODO.md) into a new [docs/CHANGELOG.md](../docs/CHANGELOG.md) — reverse-chronological log of all completed sprints — done 2026-05-13. - [x] done Move done sections of [TODO.md](../TODO.md) into a new [docs/CHANGELOG.md](../docs/CHANGELOG.md) — reverse-chronological log of all completed sprints — done 2026-05-13.
- [x] done Fix [docs/CI-FLOW.md](../docs/CI-FLOW.md) failure-scenario table — "Build fails inside VM" row updated: `Get-GuestDiagnostics` to `diagnostics\` folder — done 2026-05-13. - [x] done Fix [docs/CI-FLOW.md](../docs/CI-FLOW.md) failure-scenario table — "Build fails inside VM" row updated: `Get-GuestDiagnostics` to `diagnostics\` folder — done 2026-05-13.
### Nice to have [ ] ### Nice to have [x] done
- [ ] Add `scripts/Get-CIJobSummary.ps1` — reads last N days of JSONL files, prints duration table — 1h. - [x] Add `scripts/Get-CIJobSummary.ps1` — reads last N days of JSONL files, prints duration table — 1h.
- [ ] Add artifact manifest with file hashes — [scripts/Get-BuildArtifacts.ps1](../scripts/Get-BuildArtifacts.ps1) — `manifest.json` with name, size, SHA256, commit, job ID — 1h. - [x] Add artifact manifest with file hashes — [scripts/Get-BuildArtifacts.ps1](../scripts/Get-BuildArtifacts.ps1) — `manifest.json` with name, size, SHA256, commit, job ID — 1h.
- [ ] Add per-job VMX CPU/RAM override — [scripts/Invoke-CIJob.ps1](../scripts/Invoke-CIJob.ps1) + composite action input — post-clone VMX edit, default off — 1h. - [x] Add per-job VMX CPU/RAM override — [scripts/Invoke-CIJob.ps1](../scripts/Invoke-CIJob.ps1) + composite action input — post-clone VMX edit, default off — 1h.
- [ ] Add optional Gitea API runner-online check — [scripts/Watch-RunnerHealth.ps1](../scripts/Watch-RunnerHealth.ps1) — optional `-GiteaUrl` and PAT target — 1h. - [x] Add optional Gitea API runner-online check — [scripts/Watch-RunnerHealth.ps1](../scripts/Watch-RunnerHealth.ps1) — optional `-GiteaUrl` and PAT target — 1h.
- [ ] Begin SHA256 pinning of toolchain installers — [template/Install-CIToolchain-WinBuild2025.ps1](../template/Install-CIToolchain-WinBuild2025.ps1) — fill hashes for Git, 7-Zip, Python, .NET, Node — 2h. - [x] Begin SHA256 pinning of toolchain installers — [template/Install-CIToolchain-WinBuild2025.ps1](../template/Install-CIToolchain-WinBuild2025.ps1) — fill hashes for Git, 7-Zip, Python, .NET, Node — 2h.
- [ ] Add linked-clone delta size measurement — [scripts/Measure-CIBenchmark.ps1](../scripts/Measure-CIBenchmark.ps1) — record per-job delta growth — 30min. - [x] Add linked-clone delta size measurement — [scripts/Measure-CIBenchmark.ps1](../scripts/Measure-CIBenchmark.ps1) — record per-job delta growth — 30min.
--- ---
+41 -1
View File
@@ -65,7 +65,13 @@ param(
# Persistent known_hosts file for SSH calls (CI jobs). # Persistent known_hosts file for SSH calls (CI jobs).
# Empty string (default) = permissive mode (StrictHostKeyChecking=no, UserKnownHostsFile=NUL). # Empty string (default) = permissive mode (StrictHostKeyChecking=no, UserKnownHostsFile=NUL).
[string] $SshKnownHostsFile = '' [string] $SshKnownHostsFile = '',
# Optional job ID and commit SHA written into manifest.json.
# When either is non-empty a manifest.json is written to $HostArtifactDir
# listing all collected files with name, size, and SHA256.
[string] $JobId = '',
[string] $Commit = ''
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
@@ -73,6 +79,34 @@ $ErrorActionPreference = 'Stop'
Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force Import-Module (Join-Path $PSScriptRoot '_Common.psm1') -Force
# ── Helper: write artifact manifest ─────────────────────────────────────────
function Write-ArtifactManifest {
param([string] $Dir, [string] $JobId, [string] $Commit)
$files = @(Get-ChildItem -Path $Dir -Recurse -File -ErrorAction SilentlyContinue |
Where-Object { $_.Name -ne 'manifest.json' })
$fileEntries = foreach ($f in $files) {
[pscustomobject]@{
name = $f.FullName.Substring($Dir.TrimEnd('\', '/').Length).TrimStart('\', '/')
size = $f.Length
sha256 = (Get-FileHash $f.FullName -Algorithm SHA256).Hash.ToLower()
}
}
$manifest = [ordered]@{
ts = (Get-Date -Format 'o')
jobId = $JobId
commit = $Commit
files = @($fileEntries)
}
$manifestPath = Join-Path $Dir 'manifest.json'
try {
$manifest | ConvertTo-Json -Depth 4 |
Set-Content -Path $manifestPath -Encoding UTF8 -Force -ErrorAction Stop
Write-Host "[Get-BuildArtifacts] Manifest written: $manifestPath ($($files.Count) file(s))"
} catch {
Write-Warning "[Get-BuildArtifacts] Could not write manifest.json: $_"
}
}
if ($GuestOS -eq 'Windows' -and $null -eq $Credential) { if ($GuestOS -eq 'Windows' -and $null -eq $Credential) {
throw "Credential is required when GuestOS is Windows." throw "Credential is required when GuestOS is Windows."
} }
@@ -103,6 +137,9 @@ if ($GuestOS -eq 'Linux') {
throw "[Get-BuildArtifacts] No files found in $HostArtifactDir after SCP transfer." throw "[Get-BuildArtifacts] No files found in $HostArtifactDir after SCP transfer."
} }
Write-Host "[Get-BuildArtifacts] $($transferred.Count) file(s) collected from Linux guest." Write-Host "[Get-BuildArtifacts] $($transferred.Count) file(s) collected from Linux guest."
if ($JobId -ne '' -or $Commit -ne '') {
Write-ArtifactManifest -Dir $HostArtifactDir -JobId $JobId -Commit $Commit
}
return return
} }
@@ -180,6 +217,9 @@ try {
$sizeKB = [math]::Round($fileItem.Length / 1KB, 1) $sizeKB = [math]::Round($fileItem.Length / 1KB, 1)
Write-Host "[Get-BuildArtifacts] Artifact collected: $hostDestPath ($sizeKB KB)" Write-Host "[Get-BuildArtifacts] Artifact collected: $hostDestPath ($sizeKB KB)"
if ($JobId -ne '' -or $Commit -ne '') {
Write-ArtifactManifest -Dir $HostArtifactDir -JobId $JobId -Commit $Commit
}
return $hostDestPath return $hostDestPath
} }
finally { finally {
+37 -1
View File
@@ -159,7 +159,17 @@ param(
# Optional webhook URL (Discord/Gitea). When set, a background job fires a # Optional webhook URL (Discord/Gitea). When set, a background job fires a
# [WARNING] once the job has been running for 90 minutes. # [WARNING] once the job has been running for 90 minutes.
[string] $WebhookUrl = '' [string] $WebhookUrl = '',
# Optional per-job VMX CPU and RAM override (default 0 = use template value).
# Applied to the linked clone after New-BuildVM.ps1, before vmrun start.
# The template VMX is never modified.
# Example: -GuestCPU 4 -GuestMemoryMB 8192
[ValidateRange(0, 32)]
[int] $GuestCPU = 0,
[ValidateRange(0, 131072)]
[int] $GuestMemoryMB = 0
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
@@ -402,6 +412,28 @@ try {
-JobId $JobId ` -JobId $JobId `
-VmrunPath $VmrunPath -VmrunPath $VmrunPath
# ── Optional VMX CPU/RAM override (post-clone, pre-start) ─────────────
if ($GuestCPU -gt 0 -or $GuestMemoryMB -gt 0) {
Write-Host "[Invoke-CIJob] Applying VMX override: CPU=$GuestCPU, RAM=${GuestMemoryMB}MB"
$vmxLines = Get-Content -LiteralPath $cloneVmxPath
if ($GuestCPU -gt 0) {
if ($vmxLines -imatch '^numvcpus\s*=') {
$vmxLines = $vmxLines -replace '(?i)^numvcpus\s*=.*', "numvcpus = `"$GuestCPU`""
} else {
$vmxLines = $vmxLines + "numvcpus = `"$GuestCPU`""
}
}
if ($GuestMemoryMB -gt 0) {
if ($vmxLines -imatch '^memsize\s*=') {
$vmxLines = $vmxLines -replace '(?i)^memsize\s*=.*', "memsize = `"$GuestMemoryMB`""
} else {
$vmxLines = $vmxLines + "memsize = `"$GuestMemoryMB`""
}
}
Set-Content -LiteralPath $cloneVmxPath -Value $vmxLines -Encoding UTF8
Write-Host "[Invoke-CIJob] VMX updated: $cloneVmxPath"
}
# ── Phase 3: Start VM ───────────────────────────────────────────── # ── Phase 3: Start VM ─────────────────────────────────────────────
Write-Host "`n[Phase 3/6] Starting VM..." Write-Host "`n[Phase 3/6] Starting VM..."
$startResult = Invoke-VmrunBounded -VmrunPath $VmrunPath ` $startResult = Invoke-VmrunBounded -VmrunPath $VmrunPath `
@@ -546,6 +578,8 @@ try {
-SshKnownHostsFile $SshKnownHostsFile ` -SshKnownHostsFile $SshKnownHostsFile `
-GuestArtifactPath '/opt/ci/output' ` -GuestArtifactPath '/opt/ci/output' `
-HostArtifactDir $jobArtifactDir ` -HostArtifactDir $jobArtifactDir `
-JobId $JobId `
-Commit $Commit `
-IncludeLogs -IncludeLogs
} else { } else {
& "$scriptDir\Get-BuildArtifacts.ps1" ` & "$scriptDir\Get-BuildArtifacts.ps1" `
@@ -553,6 +587,8 @@ try {
-Credential $credential ` -Credential $credential `
-GuestArtifactPath 'C:\CI\output\artifacts.zip' ` -GuestArtifactPath 'C:\CI\output\artifacts.zip' `
-HostArtifactDir $jobArtifactDir ` -HostArtifactDir $jobArtifactDir `
-JobId $JobId `
-Commit $Commit `
-IncludeLogs -IncludeLogs
} }
Write-JobEvent -Phase 'phase6.artifacts' -Status 'success' -Data @{ dir = $jobArtifactDir } Write-JobEvent -Phase 'phase6.artifacts' -Status 'success' -Data @{ dir = $jobArtifactDir }
+13
View File
@@ -104,6 +104,7 @@ for ($i = 1; $i -le $Iterations; $i++) {
ip = $null ip = $null
winrm = $null winrm = $null
destroy = $null destroy = $null
deltaKB = $null
vmIP = $null vmIP = $null
error = $null error = $null
} }
@@ -122,6 +123,16 @@ for ($i = 1; $i -le $Iterations; $i++) {
$t.clone = [math]::Round($sw.Elapsed.TotalSeconds, 2) $t.clone = [math]::Round($sw.Elapsed.TotalSeconds, 2)
Write-Host "[bench] clone: $($t.clone)s" Write-Host "[bench] clone: $($t.clone)s"
# Measure linked-clone disk footprint
try {
$cloneSizeBytes = (Get-ChildItem -Path $cloneDir -Recurse -File -ErrorAction Stop |
Measure-Object -Property Length -Sum).Sum
$t.deltaKB = [math]::Round($cloneSizeBytes / 1KB, 0)
Write-Host "[bench] clone size: $($t.deltaKB) KB"
} catch {
Write-Warning "[bench] Could not measure clone size: $_"
}
# ── Phase: start ────────────────────────────────────────────────────── # ── Phase: start ──────────────────────────────────────────────────────
Write-Host "[bench] Starting VM..." Write-Host "[bench] Starting VM..."
$sw.Restart() $sw.Restart()
@@ -191,6 +202,7 @@ for ($i = 1; $i -le $Iterations; $i++) {
winrmSec = $t.winrm winrmSec = $t.winrm
destroySec = $t.destroy destroySec = $t.destroy
totalBootSec = $totalBootSec totalBootSec = $totalBootSec
deltaKB = $t.deltaKB
vmIP = $t.vmIP vmIP = $t.vmIP
error = $t.error error = $t.error
} }
@@ -217,6 +229,7 @@ $results | Format-Table @(
@{ L = 'WinRM(s)'; E = { $_.winrmSec } } @{ L = 'WinRM(s)'; E = { $_.winrmSec } }
@{ L = 'Destroy(s)'; E = { $_.destroySec } } @{ L = 'Destroy(s)'; E = { $_.destroySec } }
@{ L = 'Boot total'; E = { $_.totalBootSec } } @{ L = 'Boot total'; E = { $_.totalBootSec } }
@{ L = 'Delta(KB)'; E = { $_.deltaKB } }
@{ L = 'IP'; E = { $_.vmIP } } @{ L = 'IP'; E = { $_.vmIP } }
@{ L = 'Error'; E = { $_.error } } @{ L = 'Error'; E = { $_.error } }
) -AutoSize ) -AutoSize
+45 -1
View File
@@ -49,7 +49,17 @@ param(
[string] $StateDir = 'F:\CI\State', [string] $StateDir = 'F:\CI\State',
[string] $WebhookUrl = '' [string] $WebhookUrl = '',
# Optional Gitea API runner-online verification.
# When $GiteaUrl is non-empty and the act_runner service is Running, this script
# queries GET $GiteaUrl/api/v1/admin/runners and warns if zero runners are online.
# Requires an admin Personal Access Token stored in Windows Credential Manager
# under $GiteaCredentialTarget (same module: CredentialManager).
# Silently skipped if GiteaUrl is empty or the credential is unavailable.
[string] $GiteaUrl = '',
[string] $GiteaCredentialTarget = ''
) )
Set-StrictMode -Version Latest Set-StrictMode -Version Latest
@@ -103,6 +113,40 @@ if (-not $svc) {
if ($svc.Status -eq 'Running') { if ($svc.Status -eq 'Running') {
Write-Host "[RunnerHealth] $ServiceName is Running — OK" Write-Host "[RunnerHealth] $ServiceName is Running — OK"
# ── Optional Gitea runner-online API check ────────────────────────────────
if ($GiteaUrl -ne '') {
$pat = $null
if ($GiteaCredentialTarget -ne '') {
try {
$cred = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
$pat = $cred.GetNetworkCredential().Password
} catch {
Write-Warning "[RunnerHealth] Could not load Gitea PAT from '$GiteaCredentialTarget': $_"
}
}
if ($pat) {
try {
$apiUrl = "$($GiteaUrl.TrimEnd('/'))/api/v1/admin/runners"
$runners = Invoke-RestMethod -Uri $apiUrl `
-Headers @{ Authorization = "token $pat" } `
-Method Get -TimeoutSec 10 -ErrorAction Stop
$online = @($runners | Where-Object { $_.online -eq $true })
if ($online.Count -gt 0) {
Write-Host "[RunnerHealth] Gitea API: $($online.Count) runner(s) online — OK"
} else {
$msg = "Gitea API ($apiUrl) reports 0 online runners. " +
"$ServiceName service is Running — possible registration issue."
Write-Warning "[RunnerHealth] $msg"
Write-CIEvent -EventId 1005 -EntryType Warning -Message $msg
Send-Webhook -Content "[WARNING] CI Runner Alert -- $msg"
}
} catch {
Write-Warning "[RunnerHealth] Gitea API check failed: $_"
}
}
}
exit 0 exit 0
} }
@@ -236,11 +236,11 @@ function Assert-Hash {
$script:Hashes = @{ $script:Hashes = @{
# python-<PythonVersion>-amd64.exe from https://www.python.org/downloads/release/python-XYZ/ # python-<PythonVersion>-amd64.exe from https://www.python.org/downloads/release/python-XYZ/
# Update $script:Hashes['Python'] when $PythonVersion changes. # Update $script:Hashes['Python'] when $PythonVersion changes.
'Python' = '' 'Python' = '698F2DF46E1A3DD92F393458EEA77BD94EF5FF21F0D5BF5CF676F3D28A9B4B6C'
# dotnet-install.ps1 from https://dot.net/v1/dotnet-install.ps1 # dotnet-install.ps1 from https://dot.net/v1/dotnet-install.ps1
# Changes with each .NET SDK release cycle — verify after any dotnet version bump. # Changes with each .NET SDK release cycle — verify after any dotnet version bump.
'DotNetInstallScript' = '' 'DotNetInstallScript' = 'CE5330422F1D3E9B3D46462A24EF7105F660EC6C67EF3F5D4AE05B7259FB4234'
# vs_buildtools.exe bootstrapper from aka.ms/vs/stable/vs_buildtools.exe # vs_buildtools.exe bootstrapper from aka.ms/vs/stable/vs_buildtools.exe
# Stable within a VS release cycle — update when VS major version changes. # Stable within a VS release cycle — update when VS major version changes.
@@ -252,7 +252,7 @@ $script:Hashes = @{
# 7-Zip MSI installer from https://www.7-zip.org/download.html # 7-Zip MSI installer from https://www.7-zip.org/download.html
# Update $script:Hashes['SevenZip'] when $SevenZipVersion changes. # Update $script:Hashes['SevenZip'] when $SevenZipVersion changes.
'SevenZip' = '' 'SevenZip' = 'A47EA8DCF8BC08E6DE474CAE77C828E031FA22CB528F6095DEFFFEBF11CD02F2'
# Tier-2 Toolchain installer hashes — fill in when pinning supply chain (see BEST-PRACTICES.md §1.3) # Tier-2 Toolchain installer hashes — fill in when pinning supply chain (see BEST-PRACTICES.md §1.3)
# PowerShell-<ver>-win-x64.msi from https://github.com/PowerShell/PowerShell/releases # PowerShell-<ver>-win-x64.msi from https://github.com/PowerShell/PowerShell/releases
@@ -262,7 +262,7 @@ $script:Hashes = @{
# cmake-<ver>-windows-x86_64.msi from https://github.com/Kitware/CMake/releases # cmake-<ver>-windows-x86_64.msi from https://github.com/Kitware/CMake/releases
'CMake' = '' 'CMake' = ''
# node-v<ver>-x64.msi from https://nodejs.org/dist/v<ver>/ # node-v<ver>-x64.msi from https://nodejs.org/dist/v<ver>/
'NodeJS' = '' 'NodeJS' = '2C0CC97EC64C1E4111362E1E32E0547FD870E4D9C79EC844C117DA583F21B386'
# gh_<ver>_windows_amd64.msi from https://github.com/cli/cli/releases # gh_<ver>_windows_amd64.msi from https://github.com/cli/cli/releases
'GhCli' = '' 'GhCli' = ''
# WiX: installed via 'dotnet tool install wix' — integrity via NuGet package signing # WiX: installed via 'dotnet tool install wix' — integrity via NuGet package signing