chore(scripts): archive PowerShell scripts superseded by the Python CLI
Move 16 .ps1 files to scripts/archived/ — they have no live caller: - 10 pure shims (New-BuildVM, Remove-BuildVM, Cleanup-OrphanedBuildVMs, Wait-VMReady, Invoke-RemoteBuild, Get-BuildArtifacts, Watch-DiskSpace, Watch-RunnerHealth, Get-CIJobSummary, Invoke-CIJob) that only forwarded to `ci_orchestrator`; the live Gitea composite action now calls `python -m ci_orchestrator job` directly. - 6 Phase-C host tools reimplemented natively in Python (Start-BurnInTest [+ -Linux], Test-CapacityBurnIn → bench; Test-Smoke → smoke run; Validate-HostState → validate host; Set-CIGuestCredential → creds set). Add scripts/archived/README.md mapping each file to its `ci` replacement, and exempt scripts/archived/ from the PSScriptAnalyzer lint job. Kept (still used / not yet ported): Register-CIScheduledTasks, Backup-CITemplate, Invoke-RetentionPolicy, Set-TemplateSharedFolders, Test-CIGuestWinRM, the e2e build harnesses, and all template/ guest provisioning scripts.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A2-closeout.md.
|
||||
# PowerShell common params (-ErrorAction etc.) must not be forwarded
|
||||
# to the Python CLI. Value-taking ones also consume the next token.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator vm cleanup @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,69 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI (`artifacts collect`).
|
||||
# Original PS implementation moved to git history; see plans/A3-closeout.md.
|
||||
#
|
||||
# The bound `-Credential` is intentionally discarded — Python uses keyring
|
||||
# via `--credential-target` (defaulting to `BuildVMGuest`).
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $IPAddress,
|
||||
|
||||
[Parameter()]
|
||||
[System.Management.Automation.PSCredential] $Credential,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $GuestArtifactPath,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $HostArtifactDir,
|
||||
|
||||
[switch] $IncludeLogs,
|
||||
|
||||
[ValidateSet('Windows', 'Linux')]
|
||||
[string] $GuestOS = 'Windows',
|
||||
|
||||
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
|
||||
[string] $SshUser = 'ci_build',
|
||||
[string] $SshKnownHostsFile = '',
|
||||
|
||||
[string] $JobId = '',
|
||||
[string] $Commit = '',
|
||||
|
||||
[string] $CredentialTarget = ''
|
||||
)
|
||||
|
||||
$pyArgs = @(
|
||||
'--ip-address', $IPAddress,
|
||||
'--guest-os', $GuestOS.ToLower(),
|
||||
'--guest-artifact-path', $GuestArtifactPath,
|
||||
'--host-artifact-dir', $HostArtifactDir,
|
||||
'--ssh-user', $SshUser,
|
||||
'--ssh-key-path', $SshKeyPath,
|
||||
'--job-id', $JobId,
|
||||
'--commit', $Commit
|
||||
)
|
||||
|
||||
if ($SshKnownHostsFile) { $pyArgs += @('--ssh-known-hosts-file', $SshKnownHostsFile) }
|
||||
if ($CredentialTarget) { $pyArgs += @('--credential-target', $CredentialTarget) }
|
||||
if ($IncludeLogs.IsPresent) { $pyArgs += '--include-logs' }
|
||||
|
||||
if ($Credential) {
|
||||
Write-Host '[Get-BuildArtifacts] -Credential ignored; Python CLI uses keyring (set -CredentialTarget to override target name).'
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator artifacts collect @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,222 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A2-closeout.md.
|
||||
# PowerShell common params (-ErrorAction etc.) must not be forwarded
|
||||
# to the Python CLI. Value-taking ones also consume the next token.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator report job @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
|
||||
|
||||
.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 ''
|
||||
@@ -0,0 +1,44 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A4-closeout.md.
|
||||
# PowerShell common params (-ErrorAction etc.) must not be forwarded
|
||||
# to the Python CLI. Value-taking ones also consume the next token.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator job @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,98 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI (`build run`).
|
||||
# Original PS implementation moved to git history; see plans/A3-closeout.md.
|
||||
#
|
||||
# Typed parameters (`[PSCredential]`, `[hashtable]`, `[switch]`) cannot cross
|
||||
# a process boundary as-is; this shim translates them. Credentials are pulled
|
||||
# inside Python via keyring (`--credential-target`), so the bound `-Credential`
|
||||
# is intentionally discarded — callers should set `-CredentialTarget` instead
|
||||
# (or rely on the default `BuildVMGuest` resolved by the Python CLI).
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $IPAddress,
|
||||
|
||||
[Parameter()]
|
||||
[System.Management.Automation.PSCredential] $Credential,
|
||||
|
||||
[string] $HostSourceDir = '',
|
||||
[string] $CloneUrl = '',
|
||||
[string] $CloneBranch = 'main',
|
||||
[string] $CloneCommit = '',
|
||||
[switch] $CloneSubmodules,
|
||||
[string] $GiteaCredentialTarget = 'GiteaPAT',
|
||||
[string] $Configuration = 'Release',
|
||||
[string] $BuildCommand = '',
|
||||
[string] $GuestArtifactSource = 'dist',
|
||||
[string] $GuestWorkDir = 'C:\CI\build',
|
||||
[string] $GuestArtifactZip = 'C:\CI\output\artifacts.zip',
|
||||
[switch] $UseSharedCache,
|
||||
[switch] $SkipArtifact,
|
||||
|
||||
[ValidateSet('Windows', 'Linux')]
|
||||
[string] $GuestOS = 'Windows',
|
||||
|
||||
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
|
||||
[string] $SshUser = 'ci_build',
|
||||
[string] $SshKnownHostsFile = '',
|
||||
[string] $GuestLinuxWorkDir = '/opt/ci/build',
|
||||
[string] $GuestLinuxOutputDir = '/opt/ci/output',
|
||||
|
||||
[hashtable] $ExtraGuestEnv = @{},
|
||||
|
||||
# Optional override for the Credential Manager target (overrides any
|
||||
# fallback resolved by Python). Discards $Credential.
|
||||
[string] $CredentialTarget = ''
|
||||
)
|
||||
|
||||
$pyArgs = @(
|
||||
'--ip-address', $IPAddress,
|
||||
'--guest-os', $GuestOS.ToLower(),
|
||||
'--ssh-user', $SshUser,
|
||||
'--ssh-key-path', $SshKeyPath,
|
||||
'--clone-branch', $CloneBranch,
|
||||
'--gitea-credential-target', $GiteaCredentialTarget,
|
||||
'--configuration', $Configuration,
|
||||
'--guest-work-dir', $GuestWorkDir,
|
||||
'--guest-artifact-zip', $GuestArtifactZip,
|
||||
'--guest-linux-work-dir', $GuestLinuxWorkDir,
|
||||
'--guest-linux-output-dir', $GuestLinuxOutputDir,
|
||||
'--guest-artifact-source', $GuestArtifactSource
|
||||
)
|
||||
|
||||
if ($HostSourceDir) { $pyArgs += @('--host-source-dir', $HostSourceDir) }
|
||||
if ($CloneUrl) { $pyArgs += @('--clone-url', $CloneUrl) }
|
||||
if ($CloneCommit) { $pyArgs += @('--clone-commit', $CloneCommit) }
|
||||
if ($BuildCommand) { $pyArgs += @('--build-command', $BuildCommand) }
|
||||
if ($SshKnownHostsFile) { $pyArgs += @('--ssh-known-hosts-file', $SshKnownHostsFile) }
|
||||
if ($CredentialTarget) { $pyArgs += @('--credential-target', $CredentialTarget) }
|
||||
|
||||
if ($CloneSubmodules.IsPresent) { $pyArgs += '--clone-submodules' }
|
||||
if ($UseSharedCache.IsPresent) { $pyArgs += '--use-shared-cache' }
|
||||
if ($SkipArtifact.IsPresent) { $pyArgs += '--skip-artifact' }
|
||||
|
||||
if ($ExtraGuestEnv -and $ExtraGuestEnv.Count -gt 0) {
|
||||
foreach ($key in $ExtraGuestEnv.Keys) {
|
||||
$pyArgs += @('--extra-env', "$key=$($ExtraGuestEnv[$key])")
|
||||
}
|
||||
}
|
||||
|
||||
if ($Credential) {
|
||||
Write-Host '[Invoke-RemoteBuild] -Credential ignored; Python CLI uses keyring (set -CredentialTarget to override target name).'
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator build run @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,43 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI (`vm new`).
|
||||
# Original PS implementation moved to git history; see plans/A3-closeout.md.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateScript({ Test-Path $_ -PathType Leaf })]
|
||||
[string] $TemplatePath,
|
||||
|
||||
[string] $SnapshotName = 'BaseClean',
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $CloneBaseDir,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[string] $JobId,
|
||||
|
||||
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe'
|
||||
)
|
||||
|
||||
$pyArgs = @(
|
||||
'--template-path', $TemplatePath,
|
||||
'--snapshot-name', $SnapshotName,
|
||||
'--clone-base-dir', $CloneBaseDir,
|
||||
'--job-id', $JobId,
|
||||
'--vmrun-path', $VmrunPath
|
||||
)
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator vm new @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,39 @@
|
||||
# Archived PowerShell scripts
|
||||
|
||||
These `.ps1` files are **frozen** and no longer part of the live pipeline. They
|
||||
are kept for history and reference only. Do not call them and do not extend
|
||||
them — the production entry point is the Python orchestrator (`ci ...` /
|
||||
`python -m ci_orchestrator`).
|
||||
|
||||
They are exempt from PSScriptAnalyzer lint (see `.gitea/workflows/lint.yml`).
|
||||
|
||||
## Why archived
|
||||
|
||||
**Pure shims** — these only forwarded their arguments to the Python CLI. The
|
||||
live Gitea composite action (`.gitea/actions/local-ci-build/action.yml`) now
|
||||
invokes `python -m ci_orchestrator job` directly, so the shims have no caller.
|
||||
|
||||
| Archived shim | Python replacement |
|
||||
|---------------|--------------------|
|
||||
| `Invoke-CIJob.ps1` | `ci job` |
|
||||
| `New-BuildVM.ps1` | `ci vm new` |
|
||||
| `Remove-BuildVM.ps1` | `ci vm remove` |
|
||||
| `Cleanup-OrphanedBuildVMs.ps1` | `ci vm cleanup` |
|
||||
| `Wait-VMReady.ps1` | `ci wait-ready` |
|
||||
| `Invoke-RemoteBuild.ps1` | `ci build run` |
|
||||
| `Get-BuildArtifacts.ps1` | `ci artifacts collect` |
|
||||
| `Watch-DiskSpace.ps1` | `ci monitor disk` |
|
||||
| `Watch-RunnerHealth.ps1` | `ci monitor runner` |
|
||||
| `Get-CIJobSummary.ps1` | `ci report job` |
|
||||
|
||||
**Phase C host tooling** — real PowerShell logic that ran on the Linux host via
|
||||
`pwsh`, now reimplemented natively in Python so the Linux host needs no pwsh.
|
||||
|
||||
| Archived script | Python replacement |
|
||||
|-----------------|--------------------|
|
||||
| `Start-BurnInTest.ps1` | `ci bench run` |
|
||||
| `Start-BurnInTest-Linux.ps1` | `ci bench run` |
|
||||
| `Test-CapacityBurnIn.ps1` | `ci bench run` / `ci bench measure` |
|
||||
| `Test-Smoke.ps1` | `ci smoke run` |
|
||||
| `Validate-HostState.ps1` | `ci validate host` |
|
||||
| `Set-CIGuestCredential.ps1` | `ci creds set` |
|
||||
@@ -0,0 +1,47 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A2-closeout.md.
|
||||
#
|
||||
# Callers (e.g. Measure-CIBenchmark.ps1) may pass PowerShell common
|
||||
# parameters like -ErrorAction SilentlyContinue. Those must NOT be
|
||||
# forwarded as --error-action etc. (the Python CLI rejects them).
|
||||
# Value-taking common params consume the following token too.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator vm remove @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,218 @@
|
||||
#Requires -Version 5.1
|
||||
#Requires -RunAsAdministrator
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Stores a CI guest-VM credential into the LocalSystem keyring vault.
|
||||
|
||||
.DESCRIPTION
|
||||
act_runner runs as the LocalSystem service account. Windows Credential
|
||||
Manager / keyring vaults are per-user, so a credential added with
|
||||
`cmdkey` or the Credential Manager UI under an interactive user is
|
||||
invisible to the runner. The orchestrator then fails with:
|
||||
|
||||
Credential '<target>' not found in keyring.
|
||||
|
||||
This script writes the credential into the SYSTEM vault using the
|
||||
production venv's `keyring` itself (the exact backend
|
||||
`ci_orchestrator` reads with `keyring.get_credential(target, None)`),
|
||||
via a transient SYSTEM scheduled task. Using keyring for both write
|
||||
and read avoids cmdkey/keyring format and persistence mismatches.
|
||||
|
||||
Known-issue references:
|
||||
plans/A1-closeout.md (KeyError: 'BuildVMGuest')
|
||||
plans/implementation-plan-A-B.md (keyring under SYSTEM)
|
||||
|
||||
The plaintext password unavoidably transits a temporary .py file.
|
||||
The file is created with an Administrators/SYSTEM-only ACL, overwritten
|
||||
with random bytes, then deleted. The password is never placed in a
|
||||
scheduled-task argument or the command line.
|
||||
|
||||
.PARAMETER Target
|
||||
Credential target name. Must match the runner env
|
||||
GITEA_CI_GUEST_CRED_TARGET. Default: BuildVMGuest
|
||||
|
||||
.PARAMETER UserName
|
||||
Guest login username in the form the transport expects (e.g.
|
||||
'WINBUILD\ci_build', '.\ci_build', or 'ci_build').
|
||||
|
||||
.PARAMETER Password
|
||||
Guest password as a SecureString. If omitted, prompted securely.
|
||||
|
||||
.PARAMETER VenvPython
|
||||
Path to the production venv interpreter act_runner uses. Default:
|
||||
CI_VENV_PYTHON env var, else F:\CI\python\venv\Scripts\python.exe
|
||||
|
||||
.PARAMETER SkipVerify
|
||||
Skip the post-write read-back verification (also run as SYSTEM).
|
||||
|
||||
.EXAMPLE
|
||||
.\Set-CIGuestCredential.ps1 -UserName 'WINBUILD\ci_build'
|
||||
|
||||
.EXAMPLE
|
||||
# Non-interactive (e.g. from another provisioning script)
|
||||
$pw = Read-Host 'pw' -AsSecureString
|
||||
.\Set-CIGuestCredential.ps1 -Target BuildVMGuest -UserName '.\ci_build' -Password $pw
|
||||
|
||||
.EXAMPLE
|
||||
.\Set-CIGuestCredential.ps1 -UserName '.\ci_build' -WhatIf
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess)]
|
||||
param(
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $Target = 'BuildVMGuest',
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
[string] $UserName,
|
||||
|
||||
[System.Security.SecureString] $Password,
|
||||
|
||||
[string] $VenvPython = $(if ($env:CI_VENV_PYTHON) { $env:CI_VENV_PYTHON }
|
||||
elseif ($null -ne $IsWindows -and $IsWindows -eq $false) { '/opt/ci/venv/bin/python' }
|
||||
else { 'F:\CI\python\venv\Scripts\python.exe' }),
|
||||
|
||||
[switch] $SkipVerify
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $VenvPython)) {
|
||||
throw "venv interpreter not found: '$VenvPython'. Set -VenvPython or CI_VENV_PYTHON."
|
||||
}
|
||||
|
||||
if (-not $Password) {
|
||||
$Password = Read-Host "Password for '$UserName'" -AsSecureString
|
||||
}
|
||||
|
||||
# --- Run a script block as SYSTEM via a transient scheduled task ----------
|
||||
function Invoke-AsSystem {
|
||||
param(
|
||||
[Parameter(Mandatory)] [string] $TaskName,
|
||||
[Parameter(Mandatory)] [string] $Execute,
|
||||
[Parameter(Mandatory)] [string] $Argument,
|
||||
[int] $TimeoutSec = 30
|
||||
)
|
||||
$action = New-ScheduledTaskAction -Execute $Execute -Argument $Argument
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' `
|
||||
-LogonType ServiceAccount -RunLevel Highest
|
||||
Register-ScheduledTask -TaskName $TaskName -Action $action `
|
||||
-Principal $principal -Force | Out-Null
|
||||
try {
|
||||
Start-ScheduledTask -TaskName $TaskName
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
do {
|
||||
Start-Sleep -Milliseconds 500
|
||||
$info = Get-ScheduledTaskInfo -TaskName $TaskName
|
||||
$state = (Get-ScheduledTask -TaskName $TaskName).State
|
||||
} while ($state -eq 'Running' -and (Get-Date) -lt $deadline)
|
||||
return $info.LastTaskResult
|
||||
}
|
||||
finally {
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false `
|
||||
-ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# --- Harden + shred helper for the transient secret file ------------------
|
||||
function Remove-Securely {
|
||||
# Best-effort cleanup helper; ShouldProcess prompting is undesirable
|
||||
# for an internal shred-and-delete of our own temp file.
|
||||
[Diagnostics.CodeAnalysis.SuppressMessageAttribute(
|
||||
'PSUseShouldProcessForStateChangingFunctions', '')]
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory)] [string] $Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return }
|
||||
try {
|
||||
$len = (Get-Item -LiteralPath $Path).Length
|
||||
if ($len -gt 0) {
|
||||
$rnd = New-Object byte[] $len
|
||||
[Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($rnd)
|
||||
[IO.File]::WriteAllBytes($Path, $rnd)
|
||||
}
|
||||
} catch {
|
||||
Write-Verbose "secure overwrite failed (continuing to delete): $_"
|
||||
}
|
||||
Remove-Item -LiteralPath $Path -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
$tmpDir = $env:TEMP
|
||||
$setPy = Join-Path $tmpDir ("setcred_{0}.py" -f ([guid]::NewGuid().ToString('N')))
|
||||
$verPy = Join-Path $tmpDir ("vercred_{0}.py" -f ([guid]::NewGuid().ToString('N')))
|
||||
$verOut = Join-Path $tmpDir ("vercred_{0}.txt" -f ([guid]::NewGuid().ToString('N')))
|
||||
|
||||
if (-not $PSCmdlet.ShouldProcess("$Target ($UserName)",
|
||||
"store credential in SYSTEM keyring vault")) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
# Decrypt SecureString only at the moment of writing the temp script.
|
||||
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($Password)
|
||||
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto($bstr)
|
||||
[Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
|
||||
|
||||
# Python literals: pass via triple-quoted raw-ish strings. Reject quotes
|
||||
# that would break out of the literal rather than risk silent corruption.
|
||||
if ($UserName -match "'''" -or $plain -match "'''") {
|
||||
throw "UserName/Password contains a triple-quote sequence; unsupported."
|
||||
}
|
||||
$setBody = @"
|
||||
import keyring
|
||||
keyring.set_password(r'''$Target''', r'''$UserName''', r'''$plain''')
|
||||
print('OK')
|
||||
"@
|
||||
Set-Content -LiteralPath $setPy -Value $setBody -Encoding UTF8
|
||||
Remove-Variable plain -ErrorAction SilentlyContinue
|
||||
|
||||
# Lock the secret file down to SYSTEM + Administrators before it runs.
|
||||
$acl = New-Object Security.AccessControl.FileSecurity
|
||||
$acl.SetAccessRuleProtection($true, $false)
|
||||
foreach ($id in 'SYSTEM', 'Administrators') {
|
||||
$acl.AddAccessRule((New-Object Security.AccessControl.FileSystemAccessRule(
|
||||
$id, 'FullControl', 'Allow')))
|
||||
}
|
||||
Set-Acl -LiteralPath $setPy -AclObject $acl
|
||||
|
||||
$rc = Invoke-AsSystem -TaskName 'CI-SetGuestCred' `
|
||||
-Execute $VenvPython -Argument "`"$setPy`""
|
||||
if ($rc -ne 0) {
|
||||
throw "keyring.set_password task exited with code $rc."
|
||||
}
|
||||
Write-Host "Stored '$Target' (user '$UserName') in the SYSTEM vault." `
|
||||
-ForegroundColor Green
|
||||
}
|
||||
finally {
|
||||
Remove-Securely -Path $setPy
|
||||
}
|
||||
|
||||
if ($SkipVerify) { return }
|
||||
|
||||
try {
|
||||
$verBody = @"
|
||||
import keyring
|
||||
c = keyring.get_credential(r'''$Target''', None)
|
||||
open(r'''$verOut''', 'w', encoding='utf-8').write(
|
||||
'NONE' if c is None else 'OK:' + c.username)
|
||||
"@
|
||||
Set-Content -LiteralPath $verPy -Value $verBody -Encoding UTF8
|
||||
Invoke-AsSystem -TaskName 'CI-VerifyGuestCred' `
|
||||
-Execute $VenvPython -Argument "`"$verPy`"" | Out-Null
|
||||
|
||||
$result = if (Test-Path -LiteralPath $verOut) {
|
||||
(Get-Content -LiteralPath $verOut -Raw).Trim()
|
||||
} else { 'NONE' }
|
||||
|
||||
if ($result -like 'OK:*') {
|
||||
Write-Host "Verified: SYSTEM keyring returns user '$($result.Substring(3))'." `
|
||||
-ForegroundColor Green
|
||||
}
|
||||
else {
|
||||
throw "Verification failed: SYSTEM keyring.get_credential('$Target', None) " +
|
||||
"returned None. Credential did not persist."
|
||||
}
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $verPy -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -LiteralPath $verOut -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Wrapper di lancio per Test-CapacityBurnIn.ps1 con il template Linux (LinuxBuild2404).
|
||||
|
||||
.DESCRIPTION
|
||||
Esegue il burn-in a 4 job / 3 round contro il repo burnin-dummy su Gitea,
|
||||
usando il template Ubuntu 24.04. Speculare a Start-BurnInTest.ps1 (Windows).
|
||||
|
||||
Prerequisiti:
|
||||
- Repo burnin-dummy su Gitea con build.sh nella root del branch main
|
||||
- Credenziali guest nel Credential Manager target 'BuildVMGuest'
|
||||
(usate per il PAT Gitea; SSH usa chiave F:\CI\keys\ci_linux)
|
||||
- Gitea PAT nel Credential Manager target 'GiteaPAT'
|
||||
New-StoredCredential -Target 'GiteaPAT' -UserName 'ci' -Password '<pat>' -Persist LocalMachine
|
||||
- Template LinuxBuild2404 con snapshot BaseClean-Linux in stato powered-off
|
||||
- VMware Tools attivo nel guest (per auto-detect IP)
|
||||
- build.sh eseguibile (chmod +x) nel repo — oppure viene richiamato via bash
|
||||
|
||||
Esempi di utilizzo:
|
||||
# Run completo (3 round, 4 job paralleli)
|
||||
.\Start-BurnInTest-Linux.ps1
|
||||
|
||||
# Smoke veloce (1 round, 2 job)
|
||||
.\Start-BurnInTest-Linux.ps1 -Rounds 1 -Parallelism 2
|
||||
|
||||
# Build piu' lungo per osservare concorrenza prolungata
|
||||
.\Start-BurnInTest-Linux.ps1 -Rounds 1 -BuildCommand 'sleep 120'
|
||||
|
||||
.PARAMETER Rounds
|
||||
Numero di round sequenziali. Default: 3.
|
||||
|
||||
.PARAMETER Parallelism
|
||||
Job concorrenti per round. Default: 4.
|
||||
|
||||
.PARAMETER BuildCommand
|
||||
Comando da eseguire nel guest come passo build (eseguito via SSH).
|
||||
Default: 'bash build.sh'
|
||||
|
||||
.PARAMETER RoundTimeoutMinutes
|
||||
Minuti massimi di attesa per ogni round prima di interrompere i job rimasti.
|
||||
Default: 60.
|
||||
#>
|
||||
param(
|
||||
[int] $Rounds = 3,
|
||||
[int] $Parallelism = 4,
|
||||
[string] $BuildCommand = 'bash build.sh',
|
||||
[int] $RoundTimeoutMinutes = 60
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$burnInParams = @{
|
||||
RepoUrl = 'http://10.10.20.11:3100/Simone/burnin-dummy.git'
|
||||
Branch = 'main'
|
||||
TemplatePath = '/var/lib/ci/templates/LinuxBuild2404/LinuxBuild2404.vmx'
|
||||
SnapshotName = 'BaseClean-Linux'
|
||||
CloneBaseDir = '/var/lib/ci/build-vms/'
|
||||
ArtifactBaseDir = '/var/lib/ci/artifacts/'
|
||||
GuestCredentialTarget = 'BuildVMGuest'
|
||||
GiteaCredentialTarget = 'GiteaPAT'
|
||||
VmrunPath = '/usr/bin/vmrun'
|
||||
GuestOS = 'Linux'
|
||||
BuildCommand = $BuildCommand
|
||||
Parallelism = $Parallelism
|
||||
Rounds = $Rounds
|
||||
RoundTimeoutMinutes = $RoundTimeoutMinutes
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot 'Test-CapacityBurnIn.ps1') @burnInParams
|
||||
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,77 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Wrapper di lancio per Test-CapacityBurnIn.ps1 con i parametri del lab locale.
|
||||
|
||||
.DESCRIPTION
|
||||
Esegue il burn-in a 4 job / 3 round contro il repo burnin-dummy su Gitea.
|
||||
Tutti i parametri fissi del lab sono qui hardcoded; i parametri variabili
|
||||
(Rounds, Parallelism, BuildCommand) possono essere sovrascritti da CLI.
|
||||
|
||||
Prerequisiti:
|
||||
- Repo burnin-dummy creato su Gitea (anche vuoto con branch main)
|
||||
- Credenziali guest nel Credential Manager target 'BuildVMGuest'
|
||||
- Gitea PAT nel Credential Manager target 'GiteaPAT'
|
||||
New-StoredCredential -Target 'GiteaPAT' -UserName 'ci' -Password '<pat>' -Persist LocalMachine
|
||||
- Template WinBuild2025 con snapshot BaseClean in stato powered-off
|
||||
- VMware Tools attivo nel guest (per auto-detect IP)
|
||||
|
||||
Esempi di utilizzo:
|
||||
# Run completo (3 round, 4 job paralleli)
|
||||
.\Start-BurnInTest.ps1
|
||||
|
||||
# Smoke veloce (1 round, 2 job)
|
||||
.\Start-BurnInTest.ps1 -Rounds 1 -Parallelism 2
|
||||
|
||||
# Verifica parallelismo reale (build da 2 minuti, 1 round)
|
||||
.\Start-BurnInTest.ps1 -Rounds 1 -BuildCommand 'Start-Sleep 120'
|
||||
|
||||
.PARAMETER Rounds
|
||||
Numero di round sequenziali. Default: 3.
|
||||
|
||||
.PARAMETER Parallelism
|
||||
Job concorrenti per round. Default: 4.
|
||||
|
||||
.PARAMETER BuildCommand
|
||||
Comando da eseguire nel guest come passo build (eseguito via cmd /c nel guest).
|
||||
Default: 'ping 127.0.0.1 -n 31 > nul' (attesa ~30s, nessun output reale).
|
||||
Quando il build command non produce la directory dist, Invoke-RemoteBuild crea
|
||||
automaticamente un artifact placeholder (build-ok.txt in zip) per permettere
|
||||
all'intero pipeline di completarsi.
|
||||
Usare 'ping 127.0.0.1 -n 601 > nul' per mantenere 4 VM attive ~600s
|
||||
e osservare la concorrenza prolungata.
|
||||
|
||||
.PARAMETER RoundTimeoutMinutes
|
||||
Minuti massimi di attesa per ogni round prima di interrompere i job rimasti.
|
||||
Default: 60.
|
||||
#>
|
||||
param(
|
||||
[int] $Rounds = 3,
|
||||
[int] $Parallelism = 4,
|
||||
[string] $BuildCommand = 'powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File build.ps1',
|
||||
[int] $RoundTimeoutMinutes = 60
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$burnInParams = @{
|
||||
RepoUrl = 'http://10.10.20.11:3100/Simone/burnin-dummy.git'
|
||||
Branch = 'main'
|
||||
TemplatePath = '/var/lib/ci/templates/WinBuild2025/WinBuild2025.vmx'
|
||||
SnapshotName = 'BaseClean'
|
||||
CloneBaseDir = '/var/lib/ci/build-vms/'
|
||||
ArtifactBaseDir = '/var/lib/ci/artifacts/'
|
||||
GuestCredentialTarget = 'BuildVMGuest'
|
||||
GiteaCredentialTarget = 'GiteaPAT'
|
||||
VmrunPath = '/usr/bin/vmrun'
|
||||
GuestOS = 'Auto'
|
||||
BuildCommand = $BuildCommand
|
||||
Parallelism = $Parallelism
|
||||
Rounds = $Rounds
|
||||
RoundTimeoutMinutes = $RoundTimeoutMinutes
|
||||
}
|
||||
|
||||
& (Join-Path $PSScriptRoot 'Test-CapacityBurnIn.ps1') @burnInParams
|
||||
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,368 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Concurrency burn-in harness: runs N simultaneous Invoke-CIJob.ps1 jobs
|
||||
for R rounds and asserts correct cleanup after each round.
|
||||
|
||||
.DESCRIPTION
|
||||
Launches $Parallelism child PowerShell processes (via Start-Job) all
|
||||
running Invoke-CIJob.ps1 concurrently. After each round verifies that:
|
||||
- All job exit codes were 0
|
||||
- No clone directories for burn-in jobs remain in CloneBaseDir
|
||||
- No stale vm-start.lock file remains (older than 5 min)
|
||||
|
||||
Exits 0 only when ALL rounds and assertions pass.
|
||||
|
||||
.PARAMETER RepoUrl
|
||||
Git URL to pass to Invoke-CIJob.ps1 as -RepoUrl.
|
||||
|
||||
.PARAMETER Branch
|
||||
Branch to build. Default: main
|
||||
|
||||
.PARAMETER IPList
|
||||
Optional array of IP addresses, one per concurrent job slot.
|
||||
When provided, each job receives the corresponding -VMIPAddress (useful when
|
||||
VMs have static IPs or per-slot DHCP reservations).
|
||||
When omitted, -VMIPAddress is not passed to Invoke-CIJob.ps1, which then
|
||||
auto-detects the IP via Get-GuestIPAddress (requires VMware Tools / ci-report-ip.service).
|
||||
When provided, must have at least $Parallelism entries.
|
||||
|
||||
.PARAMETER TemplatePath
|
||||
VMX template path. Falls back to GITEA_CI_TEMPLATE_PATH env var.
|
||||
|
||||
.PARAMETER SnapshotName
|
||||
Snapshot to clone from. Falls back to GITEA_CI_SNAPSHOT_NAME env var.
|
||||
Default: BaseClean
|
||||
|
||||
.PARAMETER CloneBaseDir
|
||||
Clone VM base directory. Falls back to GITEA_CI_CLONE_BASE_DIR env var.
|
||||
Default: F:\CI\BuildVMs
|
||||
|
||||
.PARAMETER ArtifactBaseDir
|
||||
Artifact output directory. Default: F:\CI\Artifacts
|
||||
|
||||
.PARAMETER GuestCredentialTarget
|
||||
Windows Credential Manager target for guest credentials.
|
||||
Default: BuildVMGuest
|
||||
|
||||
.PARAMETER VmrunPath
|
||||
Path to vmrun.exe.
|
||||
Default: C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe
|
||||
|
||||
.PARAMETER GuestOS
|
||||
Guest OS transport. Default: Auto (detected from VMX).
|
||||
|
||||
.PARAMETER BuildCommand
|
||||
Custom build command forwarded to Invoke-CIJob.ps1.
|
||||
Default: empty (uses dotnet build).
|
||||
|
||||
.PARAMETER Parallelism
|
||||
Number of concurrent jobs per round. Default: 4.
|
||||
|
||||
.PARAMETER Rounds
|
||||
Number of sequential rounds. Default: 3.
|
||||
|
||||
.PARAMETER RoundTimeoutMinutes
|
||||
Maximum wait time per round before force-stopping remaining jobs.
|
||||
Default: 60.
|
||||
|
||||
.PARAMETER ScriptDir
|
||||
Directory containing sibling CI scripts. Default: directory of this script.
|
||||
|
||||
.EXAMPLE
|
||||
.\Test-CapacityBurnIn.ps1 `
|
||||
-RepoUrl 'http://gitea.local/ci/hello-world.git' `
|
||||
-IPList '192.168.79.101','192.168.79.102','192.168.79.103','192.168.79.104' `
|
||||
-Rounds 3 -Parallelism 4
|
||||
|
||||
.EXAMPLE
|
||||
# Quick smoke round (1 round, 2 jobs) — auto-detect IPs via VMware Tools
|
||||
.\Test-CapacityBurnIn.ps1 `
|
||||
-RepoUrl 'http://gitea.local/ci/hello-world.git' `
|
||||
-TemplatePath 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' `
|
||||
-Parallelism 2 -Rounds 1
|
||||
|
||||
.EXAMPLE
|
||||
# With explicit static IPs (DHCP reservations configured)
|
||||
.\Test-CapacityBurnIn.ps1 `
|
||||
-RepoUrl 'http://gitea.local/ci/hello-world.git' `
|
||||
-IPList '192.168.79.101','192.168.79.102' `
|
||||
-TemplatePath 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' `
|
||||
-Parallelism 2 -Rounds 1
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[string] $RepoUrl,
|
||||
|
||||
[string[]] $IPList = @(),
|
||||
|
||||
[string] $Branch = 'main',
|
||||
|
||||
[string] $TemplatePath = $env:GITEA_CI_TEMPLATE_PATH,
|
||||
|
||||
[string] $SnapshotName = $(
|
||||
if ($env:GITEA_CI_SNAPSHOT_NAME) { $env:GITEA_CI_SNAPSHOT_NAME } else { 'BaseClean' }
|
||||
),
|
||||
|
||||
[string] $CloneBaseDir = $(
|
||||
if ($env:GITEA_CI_CLONE_BASE_DIR) { $env:GITEA_CI_CLONE_BASE_DIR } else { 'F:\CI\BuildVMs' }
|
||||
),
|
||||
|
||||
[string] $ArtifactBaseDir = 'F:\CI\Artifacts',
|
||||
|
||||
[string] $GuestCredentialTarget = 'BuildVMGuest',
|
||||
|
||||
# Credential Manager target for the Gitea PAT (used by Invoke-CIJob -UseGitClone).
|
||||
# Store with: New-StoredCredential -Target 'GiteaPAT' -UserName '<user>' -Password '<pat>' -Persist LocalMachine
|
||||
[string] $GiteaCredentialTarget = 'GiteaPAT',
|
||||
|
||||
[string] $VmrunPath = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe',
|
||||
|
||||
[ValidateSet('Windows', 'Linux', 'Auto')]
|
||||
[string] $GuestOS = 'Auto',
|
||||
|
||||
[string] $BuildCommand = '',
|
||||
|
||||
[ValidateRange(1, 10)]
|
||||
[int] $Parallelism = 4,
|
||||
|
||||
[ValidateRange(1, 10)]
|
||||
[int] $Rounds = 3,
|
||||
|
||||
[ValidateRange(5, 240)]
|
||||
[int] $RoundTimeoutMinutes = 60,
|
||||
|
||||
[string] $ScriptDir = $PSScriptRoot
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ── Validate inputs ───────────────────────────────────────────────────────────
|
||||
if ($IPList.Count -gt 0 -and $IPList.Count -lt $Parallelism) {
|
||||
throw "IPList must contain at least $Parallelism entries (got $($IPList.Count))."
|
||||
}
|
||||
if (-not $TemplatePath) {
|
||||
throw "TemplatePath is required. Pass -TemplatePath or set GITEA_CI_TEMPLATE_PATH."
|
||||
}
|
||||
if (-not (Test-Path $TemplatePath -PathType Leaf)) {
|
||||
throw "TemplatePath not found: $TemplatePath"
|
||||
}
|
||||
|
||||
# Resolve script dir — $PSScriptRoot may be empty when dot-sourced interactively
|
||||
if (-not $ScriptDir) {
|
||||
$ScriptDir = Split-Path $MyInvocation.MyCommand.Path -Parent
|
||||
}
|
||||
$invokeCIJobPath = Join-Path $ScriptDir 'Invoke-CIJob.ps1'
|
||||
if (-not (Test-Path $invokeCIJobPath -PathType Leaf)) {
|
||||
throw "Invoke-CIJob.ps1 not found at: $invokeCIJobPath"
|
||||
}
|
||||
|
||||
# ── State trackers ────────────────────────────────────────────────────────────
|
||||
$roundResults = [System.Collections.Generic.List[PSCustomObject]]::new()
|
||||
$totalPass = 0
|
||||
$totalFail = 0
|
||||
$roundsFailed = 0
|
||||
|
||||
Write-Host '============================================================'
|
||||
Write-Host '[Test-CapacityBurnIn] Burn-in configuration:'
|
||||
Write-Host " Parallelism : $Parallelism"
|
||||
Write-Host " Rounds : $Rounds"
|
||||
Write-Host " Round timeout : ${RoundTimeoutMinutes}m"
|
||||
Write-Host " Template : $TemplatePath"
|
||||
Write-Host " Snapshot : $SnapshotName"
|
||||
Write-Host " CloneBaseDir : $CloneBaseDir"
|
||||
$ipDisplay = if ($IPList.Count -ge $Parallelism) { $IPList[0..($Parallelism - 1)] -join ', ' } else { '(auto-detect via VMware Tools)' }
|
||||
Write-Host " IPs : $ipDisplay"
|
||||
Write-Host '============================================================'
|
||||
|
||||
for ($r = 1; $r -le $Rounds; $r++) {
|
||||
|
||||
Write-Host "`n[Round $r/$Rounds] Launching $Parallelism concurrent jobs..."
|
||||
$roundStart = Get-Date
|
||||
|
||||
# ── Launch jobs ───────────────────────────────────────────────────────
|
||||
$jobEntries = [System.Collections.Generic.List[hashtable]]::new()
|
||||
|
||||
for ($j = 1; $j -le $Parallelism; $j++) {
|
||||
$jId = "burnin-r${r}-j${j}-$(Get-Date -Format 'HHmmss')"
|
||||
$ipAddr = if ($IPList.Count -ge $j) { $IPList[$j - 1] } else { '' }
|
||||
|
||||
# Build a flat string array for powershell.exe -File
|
||||
$argList = [System.Collections.Generic.List[string]]::new()
|
||||
$argList.Add('-NonInteractive')
|
||||
$argList.Add('-ExecutionPolicy'); $argList.Add('Bypass')
|
||||
$argList.Add('-File'); $argList.Add($invokeCIJobPath)
|
||||
$argList.Add('-JobId'); $argList.Add($jId)
|
||||
$argList.Add('-RepoUrl'); $argList.Add($RepoUrl)
|
||||
$argList.Add('-Branch'); $argList.Add($Branch)
|
||||
$argList.Add('-TemplatePath'); $argList.Add($TemplatePath)
|
||||
$argList.Add('-SnapshotName'); $argList.Add($SnapshotName)
|
||||
$argList.Add('-CloneBaseDir'); $argList.Add($CloneBaseDir)
|
||||
$argList.Add('-ArtifactBaseDir'); $argList.Add($ArtifactBaseDir)
|
||||
if ($ipAddr -ne '') {
|
||||
$argList.Add('-VMIPAddress'); $argList.Add($ipAddr)
|
||||
}
|
||||
$argList.Add('-GuestCredentialTarget'); $argList.Add($GuestCredentialTarget)
|
||||
$argList.Add('-GiteaCredentialTarget'); $argList.Add($GiteaCredentialTarget)
|
||||
$argList.Add('-VmrunPath'); $argList.Add($VmrunPath)
|
||||
$argList.Add('-GuestOS'); $argList.Add($GuestOS)
|
||||
if ($BuildCommand -ne '') {
|
||||
$argList.Add('-BuildCommand'); $argList.Add($BuildCommand)
|
||||
}
|
||||
|
||||
$argArray = $argList.ToArray()
|
||||
|
||||
# Start-Job runs a new PS session; call the platform PS executable as child
|
||||
# process so exit $exitCode from Invoke-CIJob.ps1 is captured in $LASTEXITCODE.
|
||||
# $IsWindows is evaluated inside the job worker (correct OS context).
|
||||
# NOTE: do NOT name the parameter $Args — @Args is a PS reserved splatting
|
||||
# alias for the automatic $Args variable (unbound arguments), which would
|
||||
# be empty here since the parameter binding consumed all positional args.
|
||||
$psJob = Start-Job -ScriptBlock {
|
||||
param([string[]]$ChildArgs)
|
||||
$psExe = if ($IsWindows) { 'powershell.exe' } else { 'pwsh' }
|
||||
$out = & $psExe @ChildArgs 2>&1
|
||||
[PSCustomObject]@{
|
||||
ExitCode = $LASTEXITCODE
|
||||
Output = ($out -join "`n")
|
||||
}
|
||||
} -ArgumentList (, $argArray)
|
||||
|
||||
$jobEntries.Add(@{
|
||||
PSJob = $psJob
|
||||
JobId = $jId
|
||||
IP = $ipAddr
|
||||
Slot = $j
|
||||
})
|
||||
|
||||
$ipDisplay2 = if ($ipAddr -ne '') { $ipAddr } else { '(auto)' }
|
||||
Write-Host " Slot $j — JobId=$jId IP=$ipDisplay2 PSJob=$($psJob.Id)"
|
||||
}
|
||||
|
||||
# ── Wait for round to complete ────────────────────────────────────────
|
||||
Write-Host " Waiting (timeout: ${RoundTimeoutMinutes}m)..."
|
||||
$psJobArray = @($jobEntries | ForEach-Object { $_.PSJob })
|
||||
$null = Wait-Job -Job $psJobArray -Timeout ($RoundTimeoutMinutes * 60)
|
||||
|
||||
# Force-stop anything still running (timeout exceeded)
|
||||
foreach ($entry in $jobEntries) {
|
||||
if ($entry.PSJob.State -eq 'Running') {
|
||||
Write-Warning " [Round $r] Slot $($entry.Slot) ($($entry.JobId)) still running after timeout — stopping."
|
||||
Stop-Job $entry.PSJob
|
||||
}
|
||||
}
|
||||
|
||||
$roundElapsed = (Get-Date) - $roundStart
|
||||
$roundPass = 0
|
||||
$roundFail = 0
|
||||
|
||||
# ── Print per-job results ─────────────────────────────────────────────
|
||||
Write-Host ''
|
||||
Write-Host (' ' + ('{0,-44} {1,-16} {2,-8} {3}' -f 'JobId', 'IP', 'Status', 'ExitCode'))
|
||||
Write-Host (' ' + ('{0,-44} {1,-16} {2,-8} {3}' -f ('-' * 44), ('-' * 16), ('-' * 8), ('-' * 8)))
|
||||
|
||||
foreach ($entry in $jobEntries) {
|
||||
$savedState = $entry.PSJob.State
|
||||
$result = Receive-Job $entry.PSJob -ErrorAction SilentlyContinue
|
||||
Remove-Job $entry.PSJob -Force
|
||||
|
||||
if ($savedState -eq 'Stopped') {
|
||||
$status = 'TIMEOUT'; $ec = -1
|
||||
} elseif ($null -eq $result) {
|
||||
$status = 'NO-RESULT'; $ec = -1
|
||||
} else {
|
||||
$ec = $result.ExitCode
|
||||
$status = if ($ec -eq 0) { 'PASS' } else { 'FAIL' }
|
||||
}
|
||||
|
||||
Write-Host (' ' + ('{0,-44} {1,-16} {2,-8} {3}' -f $entry.JobId, $entry.IP, $status, $ec))
|
||||
|
||||
# On failure, print the last 20 lines of output to aid diagnosis
|
||||
if ($status -ne 'PASS' -and $null -ne $result -and $result.Output -ne '') {
|
||||
$lines = $result.Output -split "`n"
|
||||
$tail = if ($lines.Count -gt 20) { $lines[-20..-1] } else { $lines }
|
||||
Write-Host ' --- last output ---'
|
||||
foreach ($line in $tail) { Write-Host " $line" }
|
||||
Write-Host ' ---'
|
||||
}
|
||||
|
||||
if ($status -eq 'PASS') { $roundPass++ } else { $roundFail++ }
|
||||
}
|
||||
|
||||
# ── Assert: no orphaned clone dirs ────────────────────────────────────
|
||||
$orphaned = [System.Collections.Generic.List[string]]::new()
|
||||
if (Test-Path $CloneBaseDir) {
|
||||
foreach ($entry in $jobEntries) {
|
||||
$dirs = Get-ChildItem -Path $CloneBaseDir -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -like "Clone_$($entry.JobId)_*" }
|
||||
if ($dirs) {
|
||||
foreach ($d in $dirs) { $orphaned.Add($d.FullName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
if ($orphaned.Count -eq 0) {
|
||||
Write-Host " Clone cleanup : OK (0 orphaned)"
|
||||
} else {
|
||||
Write-Host " Clone cleanup : FAIL ($($orphaned.Count) orphaned directories)"
|
||||
foreach ($p in $orphaned) { Write-Host " $p" }
|
||||
$roundFail++
|
||||
}
|
||||
|
||||
# ── Assert: no stale lock file ────────────────────────────────────────
|
||||
$lockPath = Join-Path $CloneBaseDir 'vm-start.lock'
|
||||
$staleLock = $false
|
||||
if (Test-Path $lockPath) {
|
||||
$ageMin = ((Get-Date) - (Get-Item $lockPath).LastWriteTime).TotalMinutes
|
||||
if ($ageMin -gt 5) {
|
||||
$staleLock = $true
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $staleLock) {
|
||||
Write-Host " Lock cleanup : OK"
|
||||
} else {
|
||||
Write-Host " Lock cleanup : FAIL (stale vm-start.lock present)"
|
||||
$roundFail++
|
||||
}
|
||||
|
||||
$roundStatus = if ($roundFail -eq 0) { 'PASS' } else { 'FAIL' }
|
||||
Write-Host (" [Round $r] Elapsed: {0:mm\:ss} -- $roundStatus ($roundPass PASS, $roundFail FAIL)" -f $roundElapsed)
|
||||
|
||||
$totalPass += $roundPass
|
||||
$totalFail += $roundFail
|
||||
if ($roundFail -gt 0) { $roundsFailed++ }
|
||||
|
||||
$roundResults.Add([PSCustomObject]@{
|
||||
Round = $r
|
||||
Pass = $roundPass
|
||||
Fail = $roundFail
|
||||
Status = $roundStatus
|
||||
ElapsedSec = [int]$roundElapsed.TotalSeconds
|
||||
})
|
||||
}
|
||||
|
||||
# ── Overall summary ───────────────────────────────────────────────────────────
|
||||
Write-Host "`n============================================================"
|
||||
Write-Host '[Test-CapacityBurnIn] SUMMARY'
|
||||
Write-Host (' {0,-8} {1,-6} {2,-6} {3,-10} {4}' -f 'Round', 'Pass', 'Fail', 'Elapsed', 'Status')
|
||||
Write-Host (' {0,-8} {1,-6} {2,-6} {3,-10} {4}' -f ('-'*8), ('-'*6), ('-'*6), ('-'*10), ('-'*8))
|
||||
foreach ($rr in $roundResults) {
|
||||
Write-Host (' {0,-8} {1,-6} {2,-6} {3,-10} {4}' -f $rr.Round, $rr.Pass, $rr.Fail, "$($rr.ElapsedSec)s", $rr.Status)
|
||||
}
|
||||
Write-Host ''
|
||||
Write-Host " Total jobs : $($Rounds * $Parallelism)"
|
||||
Write-Host " Passed : $totalPass"
|
||||
Write-Host " Failed : $totalFail"
|
||||
Write-Host " Rounds OK : $($Rounds - $roundsFailed) / $Rounds"
|
||||
$overallStatus = if ($totalFail -eq 0) { 'PASS' } else { 'FAIL' }
|
||||
Write-Host "`n OVERALL: $overallStatus"
|
||||
Write-Host '============================================================'
|
||||
|
||||
if ($totalFail -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
exit 0
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
#Requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Validates host-side security prerequisites for the CI system.
|
||||
|
||||
.DESCRIPTION
|
||||
Checks that:
|
||||
1. The SSH private key for Linux guest access exists and has restrictive ACLs
|
||||
(only the current user and SYSTEM/Administrators may read it).
|
||||
2. The Windows Credential Manager target used by CI guest sessions is accessible.
|
||||
3. The CI key directories exist with expected permissions.
|
||||
|
||||
Run after `Setup-Host.ps1` and any time credentials or keys are rotated.
|
||||
Exits with code 0 on all-pass, non-zero if any check fails.
|
||||
|
||||
.PARAMETER SshKeyPath
|
||||
Path to the SSH private key used for Linux guest access.
|
||||
Default: F:\CI\keys\ci_linux
|
||||
|
||||
.PARAMETER GuestCredTarget
|
||||
Windows Credential Manager target name for Windows guest credentials.
|
||||
Default: BuildVMGuest
|
||||
|
||||
.PARAMETER CIRootDir
|
||||
Root CI directory to verify existence.
|
||||
Default: F:\CI
|
||||
|
||||
.EXAMPLE
|
||||
.\Validate-HostState.ps1
|
||||
|
||||
.EXAMPLE
|
||||
.\Validate-HostState.ps1 -SshKeyPath 'F:\CI\keys\ci_linux' -Verbose
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
|
||||
[string] $GuestCredTarget = 'BuildVMGuest',
|
||||
[string] $CIRootDir = 'F:\CI'
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
$script:passCount = 0
|
||||
$script:failCount = 0
|
||||
|
||||
function Write-Check {
|
||||
param([string]$Label, [scriptblock]$Test)
|
||||
try {
|
||||
$result = & $Test
|
||||
if ($result) {
|
||||
Write-Host " [OK] $Label"
|
||||
$script:passCount++
|
||||
} else {
|
||||
Write-Host " [FAIL] $Label"
|
||||
$script:failCount++
|
||||
}
|
||||
} catch {
|
||||
Write-Host " [FAIL] $Label — $_"
|
||||
$script:failCount++
|
||||
}
|
||||
}
|
||||
|
||||
# ── Check 1: SSH key file exists ──────────────────────────────────────────────
|
||||
|
||||
Write-Host "`n=== SSH Key ($SshKeyPath) ==="
|
||||
|
||||
Write-Check "SSH private key file exists" {
|
||||
Test-Path $SshKeyPath -PathType Leaf
|
||||
}
|
||||
|
||||
Write-Check "SSH public key file exists" {
|
||||
Test-Path "${SshKeyPath}.pub" -PathType Leaf
|
||||
}
|
||||
|
||||
# Check ACLs: only SYSTEM, Administrators, and the current user should have access
|
||||
Write-Check "SSH key ACL — no unexpected identities" {
|
||||
if (-not (Test-Path $SshKeyPath -PathType Leaf)) { return $false }
|
||||
|
||||
$acl = Get-Acl -Path $SshKeyPath
|
||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
|
||||
# Allowed identities (case-insensitive prefix matching)
|
||||
$allowedPatterns = @(
|
||||
'NT AUTHORITY\SYSTEM',
|
||||
'BUILTIN\Administrators',
|
||||
$currentUser
|
||||
)
|
||||
|
||||
$unexpected = @()
|
||||
foreach ($ace in $acl.Access) {
|
||||
$identity = $ace.IdentityReference.Value
|
||||
$matched = $false
|
||||
foreach ($pattern in $allowedPatterns) {
|
||||
if ($identity -ieq $pattern) { $matched = $true; break }
|
||||
}
|
||||
if (-not $matched) {
|
||||
$unexpected += $identity
|
||||
}
|
||||
}
|
||||
|
||||
if ($unexpected.Count -gt 0) {
|
||||
Write-Verbose "[ACL] Unexpected identities on SSH key: $($unexpected -join ', ')"
|
||||
return $false
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
Write-Check "SSH key not world-readable (Everyone/Users absent)" {
|
||||
if (-not (Test-Path $SshKeyPath -PathType Leaf)) { return $false }
|
||||
|
||||
$acl = Get-Acl -Path $SshKeyPath
|
||||
$broad = $acl.Access | Where-Object {
|
||||
$id = $_.IdentityReference.Value
|
||||
$id -ilike '*Everyone*' -or $id -ilike 'BUILTIN\Users'
|
||||
}
|
||||
return ($null -eq $broad -or @($broad).Count -eq 0)
|
||||
}
|
||||
|
||||
# ── Check 2: Credential Manager target ────────────────────────────────────────
|
||||
|
||||
Write-Host "`n=== Credential Manager (target: $GuestCredTarget) ==="
|
||||
|
||||
Write-Check "CredentialManager module available" {
|
||||
$null -ne (Get-Module -Name CredentialManager -ListAvailable -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
Write-Check "Credential target '$GuestCredTarget' accessible" {
|
||||
# Use cmdkey.exe as a fallback — avoids requiring CredentialManager module
|
||||
$cmdkeyOut = & cmdkey.exe /list:$GuestCredTarget 2>&1 | Out-String
|
||||
if ($cmdkeyOut -imatch [regex]::Escape($GuestCredTarget)) {
|
||||
return $true
|
||||
}
|
||||
|
||||
# Try via CredentialManager module if available
|
||||
if ($null -ne (Get-Module -Name CredentialManager -ListAvailable -ErrorAction SilentlyContinue)) {
|
||||
Import-Module CredentialManager -ErrorAction SilentlyContinue
|
||||
$cred = Get-StoredCredential -Target $GuestCredTarget -ErrorAction SilentlyContinue
|
||||
return ($null -ne $cred)
|
||||
}
|
||||
|
||||
return $false
|
||||
}
|
||||
|
||||
# ── Check 3: CI directory structure ───────────────────────────────────────────
|
||||
|
||||
Write-Host "`n=== CI Directory Structure ($CIRootDir) ==="
|
||||
|
||||
$requiredDirs = @(
|
||||
'BuildVMs',
|
||||
'Artifacts',
|
||||
'Logs',
|
||||
'State',
|
||||
'keys',
|
||||
'Cache\NuGet',
|
||||
'Cache\pip',
|
||||
'Templates'
|
||||
)
|
||||
|
||||
foreach ($rel in $requiredDirs) {
|
||||
$full = Join-Path $CIRootDir $rel
|
||||
Write-Check "Directory exists: $rel" { Test-Path $full -PathType Container }
|
||||
}
|
||||
|
||||
# ── Check 4: vmrun.exe reachable ──────────────────────────────────────────────
|
||||
|
||||
Write-Host "`n=== VMware ==="
|
||||
|
||||
Write-Check "vmrun.exe found in PATH or default location" {
|
||||
$vmrunDefault = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe'
|
||||
$inPath = $null -ne (Get-Command 'vmrun.exe' -ErrorAction SilentlyContinue)
|
||||
$atDefault = Test-Path $vmrunDefault -PathType Leaf
|
||||
$inPath -or $atDefault
|
||||
}
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
Write-Host "`n=== Summary ==="
|
||||
Write-Host " Passed : $($script:passCount)"
|
||||
Write-Host " Failed : $($script:failCount)"
|
||||
|
||||
if ($script:failCount -gt 0) {
|
||||
Write-Host "`n[Validate-HostState] $($script:failCount) check(s) failed. Review output above." -ForegroundColor Red
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host "`n[Validate-HostState] All checks passed." -ForegroundColor Green
|
||||
exit 0
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A2-closeout.md.
|
||||
# PowerShell common params (-ErrorAction etc.) must not be forwarded
|
||||
# to the Python CLI. Value-taking ones also consume the next token.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator wait-ready @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,44 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A2-closeout.md.
|
||||
# PowerShell common params (-ErrorAction etc.) must not be forwarded
|
||||
# to the Python CLI. Value-taking ones also consume the next token.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator monitor disk @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
@@ -0,0 +1,44 @@
|
||||
#Requires -Version 5.1
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# Shim: delegates to the Python ci_orchestrator CLI.
|
||||
# Original PS implementation moved to git history; see plans/A2-closeout.md.
|
||||
# PowerShell common params (-ErrorAction etc.) must not be forwarded
|
||||
# to the Python CLI. Value-taking ones also consume the next token.
|
||||
$commonWithValue = @{
|
||||
erroraction = $true; warningaction = $true; informationaction = $true
|
||||
progressaction = $true; errorvariable = $true; warningvariable = $true
|
||||
informationvariable = $true; outvariable = $true; outbuffer = $true
|
||||
pipelinevariable = $true
|
||||
}
|
||||
$commonSwitch = @{ verbose = $true; debug = $true; whatif = $true; confirm = $true }
|
||||
|
||||
$pyArgs = @()
|
||||
for ($i = 0; $i -lt $args.Count; $i++) {
|
||||
$a = $args[$i]
|
||||
if ($a -is [string] -and $a.Length -gt 1 -and $a.StartsWith('-') -and -not $a.StartsWith('--')) {
|
||||
$name = $a.Substring(1)
|
||||
$lname = $name.ToLower()
|
||||
if ($commonSwitch.ContainsKey($lname)) { continue }
|
||||
if ($commonWithValue.ContainsKey($lname)) { $i++; continue }
|
||||
$kebab = [Regex]::Replace($name, '(?<=[a-z0-9])([A-Z])', '-$1')
|
||||
$kebab = [Regex]::Replace($kebab, '([A-Z]+)([A-Z][a-z])', '$1-$2')
|
||||
$pyArgs += '--' + $kebab.ToLower()
|
||||
}
|
||||
else {
|
||||
$pyArgs += $a
|
||||
}
|
||||
}
|
||||
|
||||
$venvPython = $env:CI_VENV_PYTHON
|
||||
if (-not $venvPython) {
|
||||
# $IsWindows exists only on PS 6+; absent on PS 5.1 (always Windows)
|
||||
if ($null -ne $IsWindows -and $IsWindows -eq $false) {
|
||||
$venvPython = '/opt/ci/venv/bin/python'
|
||||
} else {
|
||||
$venvPython = 'F:\CI\python\venv\Scripts\python.exe'
|
||||
}
|
||||
}
|
||||
& $venvPython -m ci_orchestrator monitor runner @pyArgs
|
||||
exit $LASTEXITCODE
|
||||
Reference in New Issue
Block a user