feat(sprint5): quality pass, smoke test workflow, Get-CIJobSummary

Task 5.1: Delete runner/Install-Runner.ps1 (superseded by Setup-Host.ps1).
Task 5.2: Replace Unicode/Discord emoji in Watch-DiskSpace.ps1, Watch-RunnerHealth.ps1,
          Test-E2E-Section3.3.ps1 with plain-text prefixes ([WARNING], [ALERT], [WARN]).
Task 5.3: docs/TEST-PLAN-v1.3-to-HEAD.md: ForEach-Object -Parallel replaced with Start-Job;
          -RepositoryUrl replaced with -RepoUrl (3 occurrences); PS7 syntax removed.
Task 5.4: scripts/Get-CIJobSummary.ps1 (new): scans JSONL logs in F:\CI\Logs, prints summary
          table per job, -JobId detail view, -Failed filter, -Last N.
Task 3.5: gitea/workflows/self-test.yml (new): workflow_dispatch, smoke-windows + smoke-linux.
          scripts/Test-Smoke.ps1 (new): invokes Invoke-CIJob.ps1 with marker-file build,
          asserts artifact dir and JSONL success event.
Task 5.6: docs/BEST-PRACTICES.md Section 10 added: SHA256 pinning policy, priority table,
          PS 5.1 implementation pattern, update guidance.
This commit is contained in:
Simone
2026-05-12 21:13:15 +02:00
parent 9de86ac289
commit c04a2f25aa
9 changed files with 447 additions and 222 deletions
+35
View File
@@ -357,3 +357,38 @@ When a new .NET SDK or VS Build Tools version is released:
6. Take new snapshot: `BaseClean_$(Get-Date -Format yyyyMMdd)`
7. Update `SnapshotName` in `runner/config.yaml`
8. Delete the old snapshot after confirming new one works for 1 week
---
## 10. SHA256 Pinning for Tier-1 Toolchain Installers
**Homelab policy**: partial-coverage pinning is acceptable. Pin only the installers
that are re-downloaded as part of template provisioning (not installers already
cached in the ISO or in `F:\CI\ISO\`).
Priority targets (descending risk):
| Installer | Script | Where to pin |
|-----------|--------|--------------|
| Git for Windows `.exe` | `template/Install-CIToolchain-WinBuild2025.ps1` | `-sha256` param or `Get-FileHash` check after download |
| 7-Zip `.msi` / `.exe` | `template/Install-CIToolchain-WinBuild2025.ps1` | same |
| Python `.exe` | `template/Install-CIToolchain-WinBuild2025.ps1` | same |
| .NET SDK install script | `template/Install-CIToolchain-WinBuild2025.ps1` | HTTPS only; hash less critical |
| Ubuntu cloud VMDK | `template/Deploy-LinuxBuild2404.ps1` | already implemented via `-VmdkSha256` parameter |
**Implementation pattern** (PS 5.1):
```powershell
# After downloading $installerPath:
if ($ExpectedSha256 -ne '') {
$actual = (Get-FileHash -Path $installerPath -Algorithm SHA256).Hash
if ($actual -ne $ExpectedSha256.ToUpper()) {
throw "SHA256 mismatch for $installerPath.`n Expected: $ExpectedSha256`n Actual: $actual"
}
Write-Host "[Install] SHA256 OK: $installerPath"
}
```
Pin values must be updated each time a new installer version is adopted.
Store the expected hash in the script's parameter default or in a companion
`.sha256` sidecar file next to the cached installer in `F:\CI\ISO\`.
+13 -9
View File
@@ -52,13 +52,17 @@ Test-Path F:\CI\State\ip-leases\
# Expected: True
# 2. Start 4 parallel jobs on nsis-plugin-nsinnounp (or similar)
# Simulate via:
1..4 | ForEach-Object -Parallel {
& 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
-RepositoryUrl 'http://10.10.20.11:3100/Simone/nsis-plugin-nsinnounp' `
-JobId "test-parallel-$_" `
-Branch main
# Simulate via (PS 5.1 — Start-Job, not ForEach-Object -Parallel):
$jobs = 1..4 | ForEach-Object {
$i = $_
Start-Job -ScriptBlock {
& 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
-RepoUrl 'http://10.10.20.11:3100/Simone/nsis-plugin-nsinnounp' `
-JobId "test-parallel-$using:i" `
-Branch main
}
}
$jobs | Wait-Job | Receive-Job
# 3. Monitor IP leases during build
Get-ChildItem F:\CI\State\ip-leases\ | ForEach-Object {
@@ -228,7 +232,7 @@ to text transcript, enabling machine parsing and future Loki/Grafana integration
# 1. Run a complete build and capture job ID
$jobId = 'test-jsonl-logging'
& 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
-RepositoryUrl 'http://10.10.20.11:3100/Simone/nsis-plugin-nsinnounp' `
-RepoUrl 'http://10.10.20.11:3100/Simone/nsis-plugin-nsinnounp' `
-JobId $jobId `
-Branch main
@@ -832,7 +836,7 @@ After validating new features, run one full end-to-end build to ensure nothing b
```powershell
# Full e2e validation
& 'N:\Code\Workspace\Local-CI-CD-System\scripts\Invoke-CIJob.ps1' `
-RepositoryUrl 'http://10.10.20.11:3100/Simone/nsis-plugin-nsinnounp' `
-RepoUrl 'http://10.10.20.11:3100/Simone/nsis-plugin-nsinnounp' `
-JobId 'e2e-test-post-v1.3' `
-Branch main `
-Verbose
@@ -840,4 +844,4 @@ After validating new features, run one full end-to-end build to ensure nothing b
# Expected: Build completes successfully, artifact created, JSONL log present, no errors
```
+46
View File
@@ -0,0 +1,46 @@
# CI Self-Test (smoke) — Local CI/CD System
#
# On-demand smoke test that validates the full pipeline end-to-end:
# VM clone -> start -> IP detect -> trivial build -> artifact collect -> destroy
#
# Trigger: workflow_dispatch only (never runs on push/tag).
# Both jobs are independent — a failure in one does not cancel the other.
#
# Prerequisites (must be in place before running):
# - Runner labels: windows-build:host and linux-build:host active
# - Templates at GITEA_CI_TEMPLATE_PATH / GITEA_CI_LINUX_TEMPLATE_PATH
# - Snapshots BaseClean (Windows) and BaseClean-Linux (Linux) exist
# - ci-report-ip.service enabled in LinuxBuild2404 template
name: CI Self-Test (smoke)
on:
workflow_dispatch:
jobs:
smoke-windows:
runs-on: windows-build
steps:
- uses: Simone/local-ci-system/.gitea/actions/local-ci-build@main
with:
# Trivial no-op build: write a marker file to the CI output directory.
# The Windows build VM maps C:\CI\output as the artifact source.
build-command: 'New-Item -ItemType Directory -Path C:\CI\output -Force | Out-Null; Set-Content C:\CI\output\smoke.txt "smoke-ok"'
artifact-source: 'C:\CI\output'
guest-os: 'Windows'
job-id-suffix: 'smoke-win'
artifact-name: 'smoke-windows-${{ github.run_id }}'
smoke-linux:
runs-on: linux-build
steps:
- uses: Simone/local-ci-system/.gitea/actions/local-ci-build@main
with:
# Trivial no-op build: write a marker file to the CI output directory.
# The Linux build VM uses /opt/ci/output as the artifact source.
build-command: 'mkdir -p /opt/ci/output && echo smoke-ok > /opt/ci/output/smoke.txt'
artifact-source: '/opt/ci/output'
guest-os: 'Linux'
job-id-suffix: 'smoke-linux'
artifact-name: 'smoke-linux-${{ github.run_id }}'
-207
View File
@@ -1,207 +0,0 @@
#Requires -RunAsAdministrator
#Requires -Version 5.1
<#
.SYNOPSIS
Downloads, registers, and installs act_runner as a Windows service.
.NOTES
DEPRECATED — use Setup-Host.ps1 instead.
Setup-Host.ps1 handles the full host bootstrap including act_runner service
installation in a single idempotent script. Install-Runner.ps1 is kept for
reference but may drift from Setup-Host.ps1 over time.
.DESCRIPTION
1. Downloads the act_runner binary from the specified URL (or Gitea releases).
2. Registers the runner with the Gitea instance using a registration token.
3. Installs it as a Windows service that auto-starts with the system.
Prerequisites:
- Run as Administrator
- NSSM (Non-Sucking Service Manager) must be installed and in PATH,
OR use the built-in sc.exe method (creates a wrapper via PowerShell).
- The Gitea instance must be reachable from this host.
.PARAMETER GiteaURL
Base URL of your Gitea instance. Example: http://192.168.1.50:3000
.PARAMETER RegistrationToken
Runner registration token from Gitea:
Admin Panel → Settings → Actions → Runners → "Create Runner Token"
.PARAMETER InstallDir
Directory where act_runner will be installed.
Default: F:\CI\act_runner
.PARAMETER RunnerName
Display name for this runner in the Gitea UI.
Default: local-windows-runner
.PARAMETER ActRunnerVersion
Version tag to download. Default: latest (resolves via GitHub API).
Example: v0.2.11
.PARAMETER ServiceName
Windows service name. Default: act_runner
.EXAMPLE
.\Install-Runner.ps1 `
-GiteaURL "http://192.168.1.50:3000" `
-RegistrationToken "abc123xyz"
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string] $GiteaURL,
[Parameter(Mandatory)]
[string] $RegistrationToken,
[string] $InstallDir = 'F:\CI\act_runner',
[string] $RunnerName = 'local-windows-runner',
[string] $ActRunnerVersion = 'latest',
[string] $ServiceName = 'act_runner'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ── Resolve download URL ──────────────────────────────────────────────────────
if ($ActRunnerVersion -eq 'latest') {
Write-Host "[Install-Runner] Resolving latest act_runner version from Gitea releases..."
try {
$releaseInfo = Invoke-RestMethod -Uri 'https://dl.gitea.com/act_runner/version.json' -UseBasicParsing
$ActRunnerVersion = $releaseInfo.latest
}
catch {
Write-Warning "Could not resolve latest version automatically. Set -ActRunnerVersion explicitly."
Write-Warning "Find releases at: https://gitea.com/gitea/act_runner/releases"
throw
}
}
$downloadUrl = "https://dl.gitea.com/act_runner/${ActRunnerVersion}/act_runner-${ActRunnerVersion}-windows-amd64.exe"
$binaryPath = Join-Path $InstallDir 'act_runner.exe'
$configPath = Join-Path $InstallDir 'config.yaml'
Write-Host "[Install-Runner] Version : $ActRunnerVersion"
Write-Host "[Install-Runner] Install dir: $InstallDir"
Write-Host "[Install-Runner] Service : $ServiceName"
# ── Create install directory ──────────────────────────────────────────────────
if (-not (Test-Path $InstallDir)) {
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
Write-Host "[Install-Runner] Created directory: $InstallDir"
}
# ── Download act_runner binary ────────────────────────────────────────────────
if (Test-Path $binaryPath) {
Write-Host "[Install-Runner] act_runner.exe already exists — skipping download."
Write-Host " Delete $binaryPath to force re-download."
}
else {
Write-Host "[Install-Runner] Downloading from: $downloadUrl"
Invoke-WebRequest -Uri $downloadUrl -OutFile $binaryPath -UseBasicParsing
Write-Host "[Install-Runner] Downloaded: $binaryPath"
}
# ── Copy config.yaml if not already present ───────────────────────────────────
$repoConfigPath = Join-Path $PSScriptRoot 'config.yaml'
if (-not (Test-Path $configPath)) {
if (Test-Path $repoConfigPath) {
Copy-Item $repoConfigPath $configPath
Write-Host "[Install-Runner] Copied config.yaml from repo to $configPath"
Write-Host " Review and update paths in $configPath before proceeding."
}
else {
Write-Warning "[Install-Runner] config.yaml not found at $repoConfigPath"
Write-Warning " Generating default config and registering without custom config."
# Generate default config
Push-Location $InstallDir
& $binaryPath generate-config | Out-File $configPath -Encoding UTF8
Pop-Location
}
}
else {
Write-Host "[Install-Runner] Using existing config.yaml: $configPath"
}
# ── Register runner with Gitea ────────────────────────────────────────────────
$stateFile = Join-Path $InstallDir '.runner'
if (Test-Path $stateFile) {
Write-Host "[Install-Runner] Runner already registered (.runner state file exists)."
Write-Host " Delete $stateFile to re-register."
}
else {
Write-Host "[Install-Runner] Registering runner with Gitea at $GiteaURL..."
Push-Location $InstallDir
try {
& $binaryPath register `
--no-interactive `
--instance $GiteaURL `
--token $RegistrationToken `
--name $RunnerName `
--labels 'windows-build:host,dotnet:host,msbuild:host' `
--config $configPath
if ($LASTEXITCODE -ne 0) {
throw "act_runner register exited with code $LASTEXITCODE"
}
}
finally {
Pop-Location
}
Write-Host "[Install-Runner] Runner registered successfully."
}
# ── Install as Windows service ────────────────────────────────────────────────
$existingService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existingService) {
Write-Host "[Install-Runner] Service '$ServiceName' already exists."
Write-Host " Stopping and updating..."
Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
}
# Build service command (act_runner daemon with explicit config)
$serviceCmd = "`"$binaryPath`" daemon --config `"$configPath`""
if ($existingService) {
# Update existing service binary path
sc.exe config $ServiceName binpath= $serviceCmd | Out-Null
}
else {
# Create new service
New-Service `
-Name $ServiceName `
-BinaryPathName $serviceCmd `
-DisplayName 'Gitea act_runner (CI/CD)' `
-Description 'Gitea Actions runner for ephemeral VMware build VMs' `
-StartupType Automatic | Out-Null
Write-Host "[Install-Runner] Service created: $ServiceName"
}
# Configure service recovery: restart on failure
sc.exe failure $ServiceName reset= 86400 actions= restart/5000/restart/10000/restart/30000 | Out-Null
# Start the service
Start-Service -Name $ServiceName
Write-Host "[Install-Runner] Service '$ServiceName' started."
# ── Verify ────────────────────────────────────────────────────────────────────
Start-Sleep -Seconds 3
$svc = Get-Service -Name $ServiceName
if ($svc.Status -eq 'Running') {
Write-Host "[Install-Runner] act_runner is running as a Windows service."
Write-Host "[Install-Runner] Check Gitea Admin → Settings → Actions → Runners to confirm it appears Online."
}
else {
Write-Warning "[Install-Runner] Service status: $($svc.Status). Check Windows Event Log for errors."
}
Write-Host ""
Write-Host "Installation complete."
Write-Host " Binary : $binaryPath"
Write-Host " Config : $configPath"
Write-Host " Service : $ServiceName (startup: Automatic)"
+181
View File
@@ -0,0 +1,181 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Displays a summary table of CI job results parsed from JSONL log files.
.DESCRIPTION
Scans F:\CI\Logs\ (or a custom path) for invoke-ci.jsonl files and
prints a compact table with job ID, status, total elapsed time, and the
last error message (if any).
Each row corresponds to one completed (or failed) CI job.
Use -JobId to inspect a single job in detail.
.PARAMETER LogDir
Base directory containing per-job log subdirectories.
Default: F:\CI\Logs
.PARAMETER Last
Show only the N most recent jobs (sorted by job-start timestamp).
Default: 20
.PARAMETER JobId
When specified, shows a detailed phase-by-phase breakdown for that job.
.PARAMETER Failed
When specified, filters to failed jobs only.
.EXAMPLE
# Show last 20 jobs
.\Get-CIJobSummary.ps1
.EXAMPLE
# Show last 5 failed jobs
.\Get-CIJobSummary.ps1 -Last 5 -Failed
.EXAMPLE
# Detailed breakdown for a specific job
.\Get-CIJobSummary.ps1 -JobId 'run-12345'
#>
[CmdletBinding()]
param(
[string] $LogDir = 'F:\CI\Logs',
[ValidateRange(1, 1000)]
[int] $Last = 20,
[string] $JobId = '',
[switch] $Failed
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# ── Helpers ────────────────────────────────────────────────────────────────────
function Format-SecToHMS {
param([int] $Seconds)
if ($Seconds -lt 0) { return '?' }
$h = [math]::Floor($Seconds / 3600)
$m = [math]::Floor(($Seconds % 3600) / 60)
$s = $Seconds % 60
if ($h -gt 0) { return '{0}h{1:00}m{2:00}s' -f $h, $m, $s }
return '{0}m{1:00}s' -f $m, $s
}
function Read-JobEvents {
param([string] $JsonlPath)
$events = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($line in (Get-Content $JsonlPath -Encoding UTF8 -ErrorAction SilentlyContinue)) {
$line = $line.Trim()
if (-not $line) { continue }
try {
$events.Add(($line | ConvertFrom-Json))
} catch {
Write-Debug "[Get-CIJobSummary] Skipping malformed JSONL line in $JsonlPath"
}
}
return $events
}
# ── Validate log dir ────────────────────────────────────────────────────────
if (-not (Test-Path $LogDir -PathType Container)) {
Write-Warning "Log directory not found: $LogDir"
exit 1
}
# ── Single-job detail mode ──────────────────────────────────────────────────
if ($JobId -ne '') {
$jsonlPath = Join-Path $LogDir (Join-Path $JobId 'invoke-ci.jsonl')
if (-not (Test-Path $jsonlPath)) {
Write-Warning "No JSONL log found for job '$JobId' at: $jsonlPath"
exit 1
}
$events = Read-JobEvents -JsonlPath $jsonlPath
if ($events.Count -eq 0) {
Write-Warning "JSONL file is empty or unreadable: $jsonlPath"
exit 1
}
Write-Host "`nJob: $JobId"
Write-Host ('=' * 60)
Write-Host ('{0,-35} {1,-10} {2}' -f 'Phase', 'Status', 'Timestamp')
Write-Host ('{0,-35} {1,-10} {2}' -f ('-'*35), ('-'*10), ('-'*24))
foreach ($ev in $events) {
$ts = if ($ev.ts) { $ev.ts } else { '' }
Write-Host ('{0,-35} {1,-10} {2}' -f $ev.phase, $ev.status, $ts)
}
# Print error if present
$failEvent = $events | Where-Object { $_.status -eq 'failure' } | Select-Object -Last 1
if ($failEvent -and $failEvent.data -and $failEvent.data.error) {
Write-Host "`nError: $($failEvent.data.error)"
}
exit 0
}
# ── Scan all job JSONL files ─────────────────────────────────────────────────
$jsonlFiles = Get-ChildItem -Path $LogDir -Recurse -Filter 'invoke-ci.jsonl' -ErrorAction SilentlyContinue |
Sort-Object { $_.LastWriteTime } -Descending
if ($jsonlFiles.Count -eq 0) {
Write-Host "No invoke-ci.jsonl files found under $LogDir"
exit 0
}
$rows = [System.Collections.Generic.List[PSCustomObject]]::new()
foreach ($file in $jsonlFiles) {
$events = Read-JobEvents -JsonlPath $file.FullName
if ($events.Count -eq 0) { continue }
$jobEvent = $events | Where-Object { $_.phase -eq 'job' -and $_.status -eq 'start' } | Select-Object -First 1
$successEvent= $events | Where-Object { $_.phase -eq 'job' -and $_.status -eq 'success' } | Select-Object -First 1
$failEvent = $events | Where-Object { $_.phase -eq 'job' -and $_.status -eq 'failure' } | Select-Object -First 1
$jId = if ($events[0].jobId) { $events[0].jobId } else { (Split-Path (Split-Path $file.FullName -Parent) -Leaf) }
$started = if ($jobEvent -and $jobEvent.ts) { $jobEvent.ts } else { '' }
if ($successEvent) {
$status = 'success'
$elSec = if ($successEvent.data -and $null -ne $successEvent.data.elapsedSec) { [int]$successEvent.data.elapsedSec } else { -1 }
$errMsg = ''
} elseif ($failEvent) {
$status = 'FAILED'
$elSec = if ($failEvent.data -and $null -ne $failEvent.data.elapsedSec) { [int]$failEvent.data.elapsedSec } else { -1 }
$errMsg = if ($failEvent.data -and $failEvent.data.error) { "$($failEvent.data.error)" } else { '' }
# Truncate error for table display
if ($errMsg.Length -gt 60) { $errMsg = $errMsg.Substring(0, 57) + '...' }
} else {
$status = 'in-progress'
$elSec = -1
$errMsg = ''
}
$rows.Add([PSCustomObject]@{
JobId = $jId
Status = $status
Elapsed = Format-SecToHMS -Seconds $elSec
Started = $started
Error = $errMsg
})
}
# ── Apply filters + limit ────────────────────────────────────────────────────
if ($Failed) {
$rows = @($rows | Where-Object { $_.Status -eq 'FAILED' })
}
$rows = @($rows | Select-Object -First $Last)
if ($rows.Count -eq 0) {
Write-Host "No matching jobs found."
exit 0
}
# ── Print table ──────────────────────────────────────────────────────────────
Write-Host "`nCI Job Summary (last $($rows.Count) jobs):"
Write-Host ('{0,-40} {1,-12} {2,-10} {3,-26} {4}' -f 'JobId', 'Status', 'Elapsed', 'Started', 'Error')
Write-Host ('{0,-40} {1,-12} {2,-10} {3,-26} {4}' -f ('-'*40), ('-'*12), ('-'*10), ('-'*26), ('-'*30))
foreach ($r in $rows) {
Write-Host ('{0,-40} {1,-12} {2,-10} {3,-26} {4}' -f $r.JobId, $r.Status, $r.Elapsed, $r.Started, $r.Error)
}
Write-Host ''
+1 -1
View File
@@ -198,7 +198,7 @@ if ($baselineResult -and $guestCloneResult) {
Write-Host "✓ §3.3 guest-clone is FASTER by $(Format-Elapsed ([TimeSpan]::FromSeconds($diff)))" -ForegroundColor Green
}
else {
Write-Host " Host-side is faster by $(Format-Elapsed ([TimeSpan]::FromSeconds(-$diff)))" -ForegroundColor Yellow
Write-Host "[WARN] Host-side is faster by $(Format-Elapsed ([TimeSpan]::FromSeconds(-$diff)))" -ForegroundColor Yellow
}
}
else {
+166
View File
@@ -0,0 +1,166 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Smoke test for the CI pipeline. Validates the full lifecycle end-to-end
using a trivial no-op build.
.DESCRIPTION
Invokes Invoke-CIJob.ps1 with a minimal build command (write a marker file)
and checks that the pipeline completes successfully:
- Invoke-CIJob.ps1 exits 0
- Artifact directory is created: F:\CI\Artifacts\<JobId>\
- invoke-ci.jsonl contains a job-success event
This script can be run directly on the CI host without act_runner.
It is also invoked by gitea/workflows/self-test.yml via the composite action.
.PARAMETER GuestOS
Guest OS template to use. Windows (default) or Linux.
.PARAMETER TemplatePath
Override the VMX template path. Defaults to the standard template for
the chosen GuestOS.
.PARAMETER SnapshotName
Snapshot to clone from. Default: BaseClean (Windows) or BaseClean-Linux (Linux).
.PARAMETER JobId
Job ID for this test run. Default: smoke-<timestamp>.
.PARAMETER VmrunPath
Path to vmrun.exe. Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe
.EXAMPLE
# Test Windows template
.\Test-Smoke.ps1
.EXAMPLE
# Test Linux template
.\Test-Smoke.ps1 -GuestOS Linux
#>
[CmdletBinding()]
param(
[ValidateSet('Windows', 'Linux')]
[string] $GuestOS = 'Windows',
[string] $TemplatePath,
[string] $SnapshotName,
[string] $JobId = "smoke-$(Get-Date -Format 'yyyyMMdd-HHmmss')",
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
[string] $ArtifactBaseDir = 'F:\CI\Artifacts',
[string] $LogDir = 'F:\CI\Logs'
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$scriptDir = $PSScriptRoot
# ── Resolve defaults ──────────────────────────────────────────────────────────
if (-not $TemplatePath) {
if ($GuestOS -eq 'Linux') {
$TemplatePath = if ($env:GITEA_CI_LINUX_TEMPLATE_PATH) {
$env:GITEA_CI_LINUX_TEMPLATE_PATH
} else {
'F:\CI\Templates\LinuxBuild2404\LinuxBuild2404.vmx'
}
} else {
$TemplatePath = if ($env:GITEA_CI_TEMPLATE_PATH) {
$env:GITEA_CI_TEMPLATE_PATH
} else {
'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx'
}
}
}
if (-not $SnapshotName) {
$SnapshotName = if ($GuestOS -eq 'Linux') { 'BaseClean-Linux' } else { 'BaseClean' }
}
# ── Build command ─────────────────────────────────────────────────────────────
# Writes a marker file to the standard output directory for the chosen OS.
if ($GuestOS -eq 'Linux') {
$buildCommand = 'mkdir -p /opt/ci/output && echo smoke-ok > /opt/ci/output/smoke.txt'
$artifactSource = '/opt/ci/output'
} else {
$buildCommand = 'New-Item -ItemType Directory -Path C:\CI\output -Force | Out-Null; Set-Content C:\CI\output\smoke.txt "smoke-ok"'
$artifactSource = 'C:\CI\output'
}
Write-Host "============================================================"
Write-Host "[Test-Smoke] GuestOS : $GuestOS"
Write-Host "[Test-Smoke] Template : $TemplatePath"
Write-Host "[Test-Smoke] Snapshot : $SnapshotName"
Write-Host "[Test-Smoke] JobId : $JobId"
Write-Host "============================================================"
# ── Run the pipeline ──────────────────────────────────────────────────────────
$invokeParams = @{
JobId = $JobId
RepoUrl = 'https://example.invalid/smoke-test.git' # never cloned; UseGitClone omitted
Branch = 'main'
TemplatePath = $TemplatePath
SnapshotName = $SnapshotName
BuildCommand = $buildCommand
GuestArtifactSource = $artifactSource
ArtifactBaseDir = $ArtifactBaseDir
LogDir = $LogDir
GuestOS = $GuestOS
VmrunPath = $VmrunPath
}
$invokeScript = Join-Path $scriptDir 'Invoke-CIJob.ps1'
& $invokeScript @invokeParams
$pipelineExit = $LASTEXITCODE
# ── Assert artifacts ──────────────────────────────────────────────────────────
$artifactDir = Join-Path $ArtifactBaseDir $JobId
$jsonlPath = Join-Path $LogDir $JobId 'invoke-ci.jsonl'
Write-Host "`n[Test-Smoke] Verifying results..."
$pass = $true
if ($pipelineExit -ne 0) {
Write-Host "[Test-Smoke] FAIL: Invoke-CIJob.ps1 exited $pipelineExit" -ForegroundColor Red
$pass = $false
} else {
Write-Host "[Test-Smoke] OK: pipeline exit 0" -ForegroundColor Green
}
if (Test-Path $artifactDir) {
Write-Host "[Test-Smoke] OK: artifact dir exists: $artifactDir" -ForegroundColor Green
} else {
Write-Host "[Test-Smoke] FAIL: artifact dir not found: $artifactDir" -ForegroundColor Red
$pass = $false
}
if (Test-Path $jsonlPath) {
$jobSuccess = Get-Content $jsonlPath -Raw -ErrorAction SilentlyContinue |
Where-Object { $_ } |
ForEach-Object { $_ } |
Where-Object { $_ -match '"phase"\s*:\s*"job"' -and $_ -match '"status"\s*:\s*"success"' }
if ($jobSuccess) {
Write-Host "[Test-Smoke] OK: JSONL contains job/success event" -ForegroundColor Green
} else {
Write-Host "[Test-Smoke] FAIL: JSONL missing job/success event in: $jsonlPath" -ForegroundColor Red
$pass = $false
}
} else {
Write-Host "[Test-Smoke] FAIL: JSONL log not found: $jsonlPath" -ForegroundColor Red
$pass = $false
}
Write-Host ''
if ($pass) {
Write-Host "[Test-Smoke] PASSED — $GuestOS smoke test completed successfully." -ForegroundColor Green
exit 0
} else {
Write-Host "[Test-Smoke] FAILED — one or more checks did not pass." -ForegroundColor Red
exit 1
}
+1 -1
View File
@@ -83,7 +83,7 @@ try {
# Optional webhook (Discord / Gitea)
if ($WebhookUrl) {
$body = @{ content = ":warning: **CI Disk Alert** — $msg" } | ConvertTo-Json -Compress
$body = @{ content = "[WARNING] CI Disk Alert -- $msg" } | ConvertTo-Json -Compress
try {
$null = Invoke-RestMethod -Uri $WebhookUrl -Method Post `
-Body $body -ContentType 'application/json' -TimeoutSec 10
+4 -4
View File
@@ -127,7 +127,7 @@ if ($recentCount -ge $MaxRestarts) {
"Check Event Viewer → Application (source: $logSource) and F:\CI\act_runner\logs\."
Write-Warning "[RunnerHealth] $limitMsg"
Write-CIEvent -EventId 1004 -EntryType Error -Message $limitMsg
Send-Webhook -Content ":sos: **CI Runner Alert** — $limitMsg"
Send-Webhook -Content "[ALERT] CI Runner Alert -- $limitMsg"
exit 1
}
@@ -147,8 +147,8 @@ try {
$evtType = if ($newStatus -eq 'Running') { 'Information' } else { 'Warning' }
Write-CIEvent -EventId 1003 -EntryType $evtType -Message $restartMsg
$icon = if ($newStatus -eq 'Running') { ':warning:' } else { ':sos:' }
Send-Webhook -Content "$icon **CI Runner Alert** — $restartMsg"
$prefix = if ($newStatus -eq 'Running') { '[WARNING]' } else { '[ALERT]' }
Send-Webhook -Content "$prefix CI Runner Alert -- $restartMsg"
exit $(if ($newStatus -eq 'Running') { 0 } else { 1 })
}
@@ -156,6 +156,6 @@ catch {
$errMsg = "$ServiceName restart failed: $_"
Write-Warning "[RunnerHealth] $errMsg"
Write-CIEvent -EventId 1004 -EntryType Error -Message $errMsg
Send-Webhook -Content ":sos: **CI Runner Alert** — $errMsg"
Send-Webhook -Content "[ALERT] CI Runner Alert -- $errMsg"
exit 1
}