Sprint 13-14: §6.2 composite action + §6.4 build matrix + §6.5 secret injection

§6.2 - New composite action gitea/actions/local-ci-build/action.yml
  Wraps Invoke-CIJob.ps1 for reuse across repos. Inputs: build-command,
  artifact-source, submodules, guest-os, use-git-clone, configuration,
  template-path, snapshot-name, artifact-name, artifact-retention-days.
  Anti-injection: all params via INPUT_* env vars, never interpolated in shell.

§6.4 - Build matrix windows+linux in build-nsis.yml
  gitea/workflows/build-nsis.yml rewritten with strategy.matrix target:
  [windows, linux], fail-fast: false, routes to {target}-build runner.
  Added action inputs: job-id-suffix (matrix disambig for artifact dirs),
  repo-url (SSH URL override for gitea-ci alias).

§6.5 - ExtraGuestEnv secret injection chain
  New param -ExtraGuestEnv [hashtable] in Invoke-CIJob.ps1 and
  Invoke-RemoteBuild.ps1. Windows: SetEnvironmentVariable in WinRM session
  ScriptBlock before build command. Linux: export KEY='val'; prefix before
  cd && buildcmd (POSIX single-quote escaping). Composite action input
  extra-guest-env-json (JSON object -> ConvertFrom-Json -> hashtable),
  never logged.

PSScriptAnalyzer fixes: _Common.psm1, Invoke-RetentionPolicy.ps1,
  Watch-RunnerHealth.ps1 (empty catch, ShouldProcess suppression, etc.)

Docs/test: TEST-PLAN-v1.3-to-HEAD.md updated §3.3/§6.1/§5.4 status.
  TODO.md: §6 fully closed (6.1-6.2 done, 6.3 deferred, 6.4-6.5 done, 6.6 done).
This commit is contained in:
Simone
2026-05-11 22:35:36 +02:00
parent 0f14caad63
commit 7d12dedddd
10 changed files with 431 additions and 93 deletions
+11 -5
View File
@@ -134,7 +134,12 @@ param(
[string] $GuestOS = 'Auto',
[string] $SshKeyPath = 'F:\CI\keys\ci_linux',
[string] $SshUser = 'ci_build'
[string] $SshUser = 'ci_build',
# Extra environment variables injected into the guest VM before the build command (§6.5).
# Keys must be valid environment variable names. Values must be plain strings.
# Example: @{ SIGN_PASS = $env:SIGN_PASS_FROM_SECRET; GPG_KEY_ID = '0xABCD1234' }
[hashtable] $ExtraGuestEnv = @{}
)
Set-StrictMode -Version Latest
@@ -206,7 +211,7 @@ function Write-JobEvent {
[hashtable] $Data = @{}
)
if (-not $script:jsonLog) { return }
$event = [ordered]@{
$logEntry = [ordered]@{
ts = (Get-Date -Format 'o')
jobId = $script:JobId
phase = $Phase
@@ -214,9 +219,9 @@ function Write-JobEvent {
data = $Data
}
try {
$event | ConvertTo-Json -Compress -Depth 5 |
$logEntry | ConvertTo-Json -Compress -Depth 5 |
Add-Content -Path $script:jsonLog -Encoding UTF8 -ErrorAction Stop
} catch { } # never let logging break the build
} catch { Write-Debug "[Write-JobEvent] Logging skipped: $_" } # never let logging fail the build
}
Write-Host "============================================================"
@@ -481,6 +486,7 @@ try {
}
if ($BuildCommand -ne '') { $remoteBuildParams['BuildCommand'] = $BuildCommand }
if ($GuestArtifactSource -ne '') { $remoteBuildParams['GuestArtifactSource'] = $GuestArtifactSource }
if ($ExtraGuestEnv.Count -gt 0) { $remoteBuildParams['ExtraGuestEnv'] = $ExtraGuestEnv }
& "$scriptDir\Invoke-RemoteBuild.ps1" @remoteBuildParams
Write-JobEvent -Phase 'phase5.build' -Status 'success'
@@ -549,7 +555,7 @@ finally {
# ── Stop transcript ───────────────────────────────────────────────────
if ($transcriptStarted) {
try { Stop-Transcript -ErrorAction SilentlyContinue } catch {}
try { Stop-Transcript -ErrorAction SilentlyContinue } catch { Write-Debug "[Invoke-CIJob] Stop-Transcript ignored: $_" }
}
# ── Log retention: purge directories older than $LogRetentionDays days ─
+19 -5
View File
@@ -140,7 +140,11 @@ param(
[string] $GuestLinuxWorkDir = '/opt/ci/build',
# Output directory inside the Linux guest.
[string] $GuestLinuxOutputDir = '/opt/ci/output'
[string] $GuestLinuxOutputDir = '/opt/ci/output',
# Extra environment variables injected into the guest before the build command (§6.5).
# Keys must be valid env var names. Values are plain strings (never logged).
[hashtable] $ExtraGuestEnv = @{}
)
Set-StrictMode -Version Latest
@@ -239,10 +243,16 @@ if ($GuestOS -eq 'Linux') {
}
# Step 3: Run build command (always cd into workdir first)
# Prepend extra env exports (§6.5) — scoped to this SSH command only
$envPrefix = ''
foreach ($envKey in $ExtraGuestEnv.Keys) {
$envVal = $ExtraGuestEnv[$envKey] -replace "'", "'\''" # escape ' for POSIX single-quoted string
$envPrefix += "export $envKey='$envVal'; "
}
if ($BuildCommand -ne '') {
$buildCmd = "cd '$GuestLinuxWorkDir' && $BuildCommand"
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && $BuildCommand"
} else {
$buildCmd = "cd '$GuestLinuxWorkDir' && make"
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && make"
}
Write-Host "[Invoke-RemoteBuild] Running build: $buildCmd"
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath -Command $buildCmd
@@ -401,12 +411,16 @@ try {
# ── Custom build command (e.g. python, msbuild, cmake…) ──────────
Write-Host "[Invoke-RemoteBuild] Running build command: $BuildCommand"
$buildExit = Invoke-Command -Session $session -ScriptBlock {
param($workDir, $cmd, $artifactZip, $artifactSrc, $useCache)
param($workDir, $cmd, $artifactZip, $artifactSrc, $useCache, $extraEnv)
# Unbuffered Python output — lines appear in real-time instead of being held in buffer
$env:PYTHONUNBUFFERED = '1'
if ($useCache) {
$env:PIP_CACHE_DIR = '\\vmware-host\Shared Folders\pip-cache'
}
# Inject extra env vars (§6.5 secret injection)
foreach ($pair in $extraEnv.GetEnumerator()) {
[System.Environment]::SetEnvironmentVariable($pair.Key, $pair.Value, 'Process')
}
Set-Location $workDir
# Stream each output line via Write-Host — WinRM forwards Write-Host in real-time
& cmd /c "$cmd 2>&1" | ForEach-Object { Write-Host $_ }
@@ -430,7 +444,7 @@ try {
}
return $exit
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource, $UseSharedCache.IsPresent
} -ArgumentList $GuestWorkDir, $BuildCommand, $GuestArtifactZip, $GuestArtifactSource, $UseSharedCache.IsPresent, $ExtraGuestEnv
if ($buildExit -ne 0) {
throw "Build command failed (exit $buildExit)"
+1
View File
@@ -79,6 +79,7 @@ if ($aggressiveMode) {
# ── Helper: purge old subdirectories under a base dir ─────────────────────────
function Remove-OldJobDirs {
[CmdletBinding(SupportsShouldProcess)]
param([string]$BaseDir, [string]$Label)
if (-not (Test-Path $BaseDir)) {
+1 -1
View File
@@ -109,7 +109,7 @@ if (Test-Path $cooldownFile) {
try {
$raw = Get-Content $cooldownFile -Raw -ErrorAction SilentlyContinue
if ($raw) { $restartLog = @($raw | ConvertFrom-Json) }
} catch { }
} catch { Write-Debug "[Watch-RunnerHealth] Restart log unreadable — starting fresh: $_" }
}
# Prune timestamps older than 1 hour
+2
View File
@@ -29,6 +29,8 @@ function New-CISessionOption {
[System.Management.Automation.Remoting.PSSessionOption]
#>
[OutputType([System.Management.Automation.Remoting.PSSessionOption])]
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '',
Justification = 'New-CISessionOption creates an in-memory PSSessionOption object; no system state is changed.')]
[CmdletBinding()]
param()
New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck