From 6cd6bff882a4cf40c965e0cc92e2cebb75be9ae8 Mon Sep 17 00:00:00 2001 From: Simone Date: Wed, 13 May 2026 10:38:49 +0200 Subject: [PATCH] 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 --- plans/final-master-plan.md | 14 +++--- scripts/Get-BuildArtifacts.ps1 | 42 ++++++++++++++++- scripts/Invoke-CIJob.ps1 | 38 ++++++++++++++- scripts/Measure-CIBenchmark.ps1 | 13 ++++++ scripts/Watch-RunnerHealth.ps1 | 46 ++++++++++++++++++- template/Install-CIToolchain-WinBuild2025.ps1 | 10 ++-- 6 files changed, 148 insertions(+), 15 deletions(-) diff --git a/plans/final-master-plan.md b/plans/final-master-plan.md index 7d54029..57ca7d5 100644 --- a/plans/final-master-plan.md +++ b/plans/final-master-plan.md @@ -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 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. -- [ ] 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. -- [ ] 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. -- [ ] Add linked-clone delta size measurement — [scripts/Measure-CIBenchmark.ps1](../scripts/Measure-CIBenchmark.ps1) — record per-job delta growth — 30min. +- [x] Add `scripts/Get-CIJobSummary.ps1` — reads last N days of JSONL files, prints duration table — 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. +- [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. +- [x] Add optional Gitea API runner-online check — [scripts/Watch-RunnerHealth.ps1](../scripts/Watch-RunnerHealth.ps1) — optional `-GiteaUrl` and PAT target — 1h. +- [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. +- [x] Add linked-clone delta size measurement — [scripts/Measure-CIBenchmark.ps1](../scripts/Measure-CIBenchmark.ps1) — record per-job delta growth — 30min. --- diff --git a/scripts/Get-BuildArtifacts.ps1 b/scripts/Get-BuildArtifacts.ps1 index 3201380..120d7d6 100644 --- a/scripts/Get-BuildArtifacts.ps1 +++ b/scripts/Get-BuildArtifacts.ps1 @@ -65,7 +65,13 @@ param( # Persistent known_hosts file for SSH calls (CI jobs). # 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 @@ -73,6 +79,34 @@ $ErrorActionPreference = 'Stop' 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) { 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." } 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 } @@ -180,6 +217,9 @@ try { $sizeKB = [math]::Round($fileItem.Length / 1KB, 1) 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 } finally { diff --git a/scripts/Invoke-CIJob.ps1 b/scripts/Invoke-CIJob.ps1 index 0acd93a..a522020 100644 --- a/scripts/Invoke-CIJob.ps1 +++ b/scripts/Invoke-CIJob.ps1 @@ -159,7 +159,17 @@ param( # Optional webhook URL (Discord/Gitea). When set, a background job fires a # [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 @@ -402,6 +412,28 @@ try { -JobId $JobId ` -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 ───────────────────────────────────────────── Write-Host "`n[Phase 3/6] Starting VM..." $startResult = Invoke-VmrunBounded -VmrunPath $VmrunPath ` @@ -546,6 +578,8 @@ try { -SshKnownHostsFile $SshKnownHostsFile ` -GuestArtifactPath '/opt/ci/output' ` -HostArtifactDir $jobArtifactDir ` + -JobId $JobId ` + -Commit $Commit ` -IncludeLogs } else { & "$scriptDir\Get-BuildArtifacts.ps1" ` @@ -553,6 +587,8 @@ try { -Credential $credential ` -GuestArtifactPath 'C:\CI\output\artifacts.zip' ` -HostArtifactDir $jobArtifactDir ` + -JobId $JobId ` + -Commit $Commit ` -IncludeLogs } Write-JobEvent -Phase 'phase6.artifacts' -Status 'success' -Data @{ dir = $jobArtifactDir } diff --git a/scripts/Measure-CIBenchmark.ps1 b/scripts/Measure-CIBenchmark.ps1 index c61a93c..cdb53e4 100644 --- a/scripts/Measure-CIBenchmark.ps1 +++ b/scripts/Measure-CIBenchmark.ps1 @@ -104,6 +104,7 @@ for ($i = 1; $i -le $Iterations; $i++) { ip = $null winrm = $null destroy = $null + deltaKB = $null vmIP = $null error = $null } @@ -122,6 +123,16 @@ for ($i = 1; $i -le $Iterations; $i++) { $t.clone = [math]::Round($sw.Elapsed.TotalSeconds, 2) 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 ────────────────────────────────────────────────────── Write-Host "[bench] Starting VM..." $sw.Restart() @@ -191,6 +202,7 @@ for ($i = 1; $i -le $Iterations; $i++) { winrmSec = $t.winrm destroySec = $t.destroy totalBootSec = $totalBootSec + deltaKB = $t.deltaKB vmIP = $t.vmIP error = $t.error } @@ -217,6 +229,7 @@ $results | Format-Table @( @{ L = 'WinRM(s)'; E = { $_.winrmSec } } @{ L = 'Destroy(s)'; E = { $_.destroySec } } @{ L = 'Boot total'; E = { $_.totalBootSec } } + @{ L = 'Delta(KB)'; E = { $_.deltaKB } } @{ L = 'IP'; E = { $_.vmIP } } @{ L = 'Error'; E = { $_.error } } ) -AutoSize diff --git a/scripts/Watch-RunnerHealth.ps1 b/scripts/Watch-RunnerHealth.ps1 index 61856cf..119754a 100644 --- a/scripts/Watch-RunnerHealth.ps1 +++ b/scripts/Watch-RunnerHealth.ps1 @@ -49,7 +49,17 @@ param( [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 @@ -103,6 +113,40 @@ if (-not $svc) { if ($svc.Status -eq 'Running') { 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 } diff --git a/template/Install-CIToolchain-WinBuild2025.ps1 b/template/Install-CIToolchain-WinBuild2025.ps1 index ac4398e..3fe7661 100644 --- a/template/Install-CIToolchain-WinBuild2025.ps1 +++ b/template/Install-CIToolchain-WinBuild2025.ps1 @@ -236,11 +236,11 @@ function Assert-Hash { $script:Hashes = @{ # python--amd64.exe from https://www.python.org/downloads/release/python-XYZ/ # Update $script:Hashes['Python'] when $PythonVersion changes. - 'Python' = '' + 'Python' = '698F2DF46E1A3DD92F393458EEA77BD94EF5FF21F0D5BF5CF676F3D28A9B4B6C' # dotnet-install.ps1 from https://dot.net/v1/dotnet-install.ps1 # 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 # 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 # 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) # PowerShell--win-x64.msi from https://github.com/PowerShell/PowerShell/releases @@ -262,7 +262,7 @@ $script:Hashes = @{ # cmake--windows-x86_64.msi from https://github.com/Kitware/CMake/releases 'CMake' = '' # node-v-x64.msi from https://nodejs.org/dist/v/ - 'NodeJS' = '' + 'NodeJS' = '2C0CC97EC64C1E4111362E1E32E0547FD870E4D9C79EC844C117DA583F21B386' # gh__windows_amd64.msi from https://github.com/cli/cli/releases 'GhCli' = '' # WiX: installed via 'dotnet tool install wix' — integrity via NuGet package signing @@ -1720,4 +1720,4 @@ Write-Host "Install-CIToolchain-WinBuild2025.ps1 complete. All Assert-Step check # If running this script standalone (manually inside the VM), follow the steps # in docs/WINDOWS-TEMPLATE-SETUP.md (shut down → snapshot BaseClean → power off). Write-Host "" - +