feat(sprint2): fix critical and high-priority bugs
C1: composite action selects Linux/Windows template from GITEA_CI_LINUX_TEMPLATE_PATH
based on resolved guest OS; build-nsis.yml adds belt-and-suspenders guest-os param.
C2: redact ExtraGuestEnv secrets in Invoke-RemoteBuild.ps1 (envPrefix never logged).
H2: composite action outputs block corrected; duplicate artifact-name input removed.
H3: Linux Mode 2 PAT now uses http.extraHeader (mirrors Windows branch, drops GIT_CLONE_URL).
H6: Setup-Host.ps1 directory array updated to canonical layout (adds State, ip-leases, keys,
Cache\pip, WinBuild2025/2022/LinuxBuild2404; removes obsolete WinBuild).
H7: Backup-CITemplate.ps1 default TemplatePath -> WinBuild2025; -AllTemplates switch added.
H8: Setup-Host.ps1 adds StrictHostKeyChecking=accept-new and Step 8 to replicate SSH config
to LocalSystem profile for act_runner service account.
H11: runner/config.yaml adds GITEA_CI_SCRIPT_ROOT env var; action.yml uses it in place of
hardcoded N:\ path.
Also: Invoke-RemoteBuild.ps1 skips 7-Zip/Compress-Archive when artifact source dir does not
exist (fixes burn-in smoke runs with exit-0 build command and no output).
This commit is contained in:
+69
-2
@@ -114,13 +114,20 @@ if (-not $ActRunnerConfigYaml) { $ActRunnerConfigYaml = Join-Path $scriptDir 'ru
|
|||||||
# ── Step 1: Create F:\CI\ directory tree ─────────────────────────────────────
|
# ── Step 1: Create F:\CI\ directory tree ─────────────────────────────────────
|
||||||
Write-Step "Creating CI directory tree under $CIRoot"
|
Write-Step "Creating CI directory tree under $CIRoot"
|
||||||
|
|
||||||
|
# Canonical layout — see AGENTS.md §Struttura directory host
|
||||||
$dirs = @(
|
$dirs = @(
|
||||||
"$CIRoot\BuildVMs",
|
"$CIRoot\BuildVMs",
|
||||||
"$CIRoot\Artifacts",
|
"$CIRoot\Artifacts",
|
||||||
"$CIRoot\Logs",
|
"$CIRoot\Logs",
|
||||||
"$CIRoot\Templates\WinBuild",
|
"$CIRoot\Templates\WinBuild2025",
|
||||||
|
"$CIRoot\Templates\WinBuild2022",
|
||||||
|
"$CIRoot\Templates\LinuxBuild2404",
|
||||||
|
"$CIRoot\State",
|
||||||
|
"$CIRoot\State\ip-leases",
|
||||||
|
"$CIRoot\keys",
|
||||||
"$CIRoot\act_runner\logs",
|
"$CIRoot\act_runner\logs",
|
||||||
"$CIRoot\Cache\NuGet",
|
"$CIRoot\Cache\NuGet",
|
||||||
|
"$CIRoot\Cache\pip",
|
||||||
"$CIRoot\RunnerWork",
|
"$CIRoot\RunnerWork",
|
||||||
"$CIRoot\ISO"
|
"$CIRoot\ISO"
|
||||||
)
|
)
|
||||||
@@ -306,6 +313,7 @@ Host gitea-ci
|
|||||||
Port $GiteaSSHPort
|
Port $GiteaSSHPort
|
||||||
User git
|
User git
|
||||||
IdentityFile ~/.ssh/id_rsa
|
IdentityFile ~/.ssh/id_rsa
|
||||||
|
StrictHostKeyChecking accept-new
|
||||||
"@
|
"@
|
||||||
|
|
||||||
$existingConfig = ''
|
$existingConfig = ''
|
||||||
@@ -318,7 +326,66 @@ Host gitea-ci
|
|||||||
} else {
|
} else {
|
||||||
Add-Content -Path $sshConfig -Value $aliasBlock -Encoding UTF8
|
Add-Content -Path $sshConfig -Value $aliasBlock -Encoding UTF8
|
||||||
Write-OK "SSH alias 'gitea-ci' written to $sshConfig"
|
Write-OK "SSH alias 'gitea-ci' written to $sshConfig"
|
||||||
Write-Warn "Ensure your public SSH key is registered in Gitea under User Settings → SSH Keys."
|
Write-Warn "Ensure your public SSH key is registered in Gitea under User Settings -> SSH Keys."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Step 8: Replicate SSH config to LocalSystem profile ──────────────────────
|
||||||
|
# act_runner runs as LocalSystem; its SSH home differs from the interactive
|
||||||
|
# user's USERPROFILE. Copy .ssh/config and the private key so that
|
||||||
|
# 'ssh gitea-ci' works when invoked from act_runner jobs.
|
||||||
|
if ($SkipSSHConfig) {
|
||||||
|
Write-Host "`n=== SSH LocalSystem replication SKIPPED (-SkipSSHConfig) ===" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Step "Replicating SSH config to LocalSystem profile (H8 — act_runner fix)"
|
||||||
|
|
||||||
|
$localSystemSsh = Join-Path $env:SystemRoot 'System32\config\systemprofile\.ssh'
|
||||||
|
$sourceSshDir = Join-Path $env:USERPROFILE '.ssh'
|
||||||
|
$sourceConfigPath = Join-Path $sourceSshDir 'config'
|
||||||
|
$sourceKeyPath = Join-Path $sourceSshDir 'id_rsa'
|
||||||
|
$sourceKnownHosts = Join-Path $sourceSshDir 'known_hosts'
|
||||||
|
|
||||||
|
if (-not (Test-Path $localSystemSsh)) {
|
||||||
|
New-Item -ItemType Directory -Path $localSystemSsh -Force | Out-Null
|
||||||
|
Write-OK "Created $localSystemSsh"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy config
|
||||||
|
if (Test-Path $sourceConfigPath) {
|
||||||
|
Copy-Item -Path $sourceConfigPath -Destination (Join-Path $localSystemSsh 'config') -Force
|
||||||
|
Write-OK "SSH config copied to $localSystemSsh\config"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warn "Source SSH config not found at $sourceConfigPath — skipping config copy."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy private key and lock down permissions
|
||||||
|
if (Test-Path $sourceKeyPath) {
|
||||||
|
$destKeyPath = Join-Path $localSystemSsh 'id_rsa'
|
||||||
|
Copy-Item -Path $sourceKeyPath -Destination $destKeyPath -Force
|
||||||
|
|
||||||
|
$acl = New-Object System.Security.AccessControl.FileSecurity
|
||||||
|
$acl.SetAccessRuleProtection($true, $false)
|
||||||
|
$acl.AddAccessRule(
|
||||||
|
(New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||||
|
'NT AUTHORITY\SYSTEM', 'FullControl', 'Allow')))
|
||||||
|
$acl.AddAccessRule(
|
||||||
|
(New-Object System.Security.AccessControl.FileSystemAccessRule(
|
||||||
|
'BUILTIN\Administrators', 'FullControl', 'Allow')))
|
||||||
|
Set-Acl -Path $destKeyPath -AclObject $acl
|
||||||
|
Write-OK "SSH private key copied and ACLs locked to SYSTEM/Administrators at $destKeyPath"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warn "SSH private key not found at $sourceKeyPath — skipping key copy."
|
||||||
|
Write-Warn "Generate a key with: ssh-keygen -t rsa -b 4096 -f $sourceKeyPath -N ''"
|
||||||
|
Write-Warn "Then re-run Setup-Host.ps1."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy known_hosts if it exists (avoids first-connect host-key prompt)
|
||||||
|
if (Test-Path $sourceKnownHosts) {
|
||||||
|
Copy-Item -Path $sourceKnownHosts -Destination (Join-Path $localSystemSsh 'known_hosts') -Force
|
||||||
|
Write-OK "known_hosts copied to LocalSystem profile."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,6 +129,17 @@ inputs:
|
|||||||
are never echoed to the log.
|
are never echoed to the log.
|
||||||
required: false
|
required: false
|
||||||
default: '{}'
|
default: '{}'
|
||||||
|
|
||||||
|
use-shared-cache:
|
||||||
|
description: >
|
||||||
|
Set to "true" to redirect NuGet and pip caches to VMware shared folders
|
||||||
|
on the host (F:\CI\Cache\NuGet and F:\CI\Cache\pip).
|
||||||
|
Requires Set-TemplateSharedFolders.ps1 to have been run on the template
|
||||||
|
and VMware Tools HGFS driver present in the guest.
|
||||||
|
required: false
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
outputs:
|
||||||
artifact-path:
|
artifact-path:
|
||||||
description: >
|
description: >
|
||||||
Host-side directory where Invoke-CIJob.ps1 placed collected artifacts.
|
Host-side directory where Invoke-CIJob.ps1 placed collected artifacts.
|
||||||
@@ -162,12 +173,13 @@ runs:
|
|||||||
INPUT_JOB_ID_SUFFIX: ${{ inputs.job-id-suffix }}
|
INPUT_JOB_ID_SUFFIX: ${{ inputs.job-id-suffix }}
|
||||||
INPUT_REPO_URL: ${{ inputs.repo-url }}
|
INPUT_REPO_URL: ${{ inputs.repo-url }}
|
||||||
INPUT_EXTRA_GUEST_ENV_JSON: ${{ inputs.extra-guest-env-json }}
|
INPUT_EXTRA_GUEST_ENV_JSON: ${{ inputs.extra-guest-env-json }}
|
||||||
|
INPUT_USE_SHARED_CACHE: ${{ inputs.use-shared-cache }}
|
||||||
run: |
|
run: |
|
||||||
#Requires -Version 5.1
|
#Requires -Version 5.1
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
$ciScriptsDir = 'N:\Code\Workspace\Local-CI-CD-System\scripts'
|
$ciScriptsDir = $env:GITEA_CI_SCRIPT_ROOT
|
||||||
|
|
||||||
# Build job ID — append suffix when provided (matrix disambiguation)
|
# Build job ID — append suffix when provided (matrix disambiguation)
|
||||||
$jobId = '${{ github.run_id }}-${{ github.run_attempt }}'
|
$jobId = '${{ github.run_id }}-${{ github.run_attempt }}'
|
||||||
@@ -199,6 +211,37 @@ runs:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Resolve guest OS: honor explicit input; for Auto, infer from RUNNER_LABELS
|
||||||
|
$resolvedGuestOS = $env:INPUT_GUEST_OS
|
||||||
|
if ($resolvedGuestOS -eq 'Auto') {
|
||||||
|
if ($env:RUNNER_LABELS -match 'linux') {
|
||||||
|
$resolvedGuestOS = 'Linux'
|
||||||
|
} else {
|
||||||
|
$resolvedGuestOS = 'Windows'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Select template path: explicit input overrides OS-based runner env var
|
||||||
|
$resolvedTemplatePath = $env:INPUT_TEMPLATE_PATH
|
||||||
|
if (-not $resolvedTemplatePath) {
|
||||||
|
if ($resolvedGuestOS -eq 'Linux') {
|
||||||
|
$resolvedTemplatePath = $env:GITEA_CI_LINUX_TEMPLATE_PATH
|
||||||
|
} else {
|
||||||
|
$resolvedTemplatePath = $env:GITEA_CI_TEMPLATE_PATH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Select snapshot name: explicit input > OS-based default
|
||||||
|
# GITEA_CI_SNAPSHOT_NAME applies to Windows (template-refresh override)
|
||||||
|
$resolvedSnapshotName = $env:INPUT_SNAPSHOT_NAME
|
||||||
|
if (-not $resolvedSnapshotName) {
|
||||||
|
if ($resolvedGuestOS -eq 'Linux') {
|
||||||
|
$resolvedSnapshotName = 'BaseClean-Linux'
|
||||||
|
} else {
|
||||||
|
$resolvedSnapshotName = if ($env:GITEA_CI_SNAPSHOT_NAME) { $env:GITEA_CI_SNAPSHOT_NAME } else { 'BaseClean' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Build parameter hashtable for Invoke-CIJob.ps1
|
# Build parameter hashtable for Invoke-CIJob.ps1
|
||||||
$params = @{
|
$params = @{
|
||||||
JobId = $jobId
|
JobId = $jobId
|
||||||
@@ -207,14 +250,15 @@ runs:
|
|||||||
Commit = '${{ github.sha }}'
|
Commit = '${{ github.sha }}'
|
||||||
BuildCommand = $env:INPUT_BUILD_COMMAND
|
BuildCommand = $env:INPUT_BUILD_COMMAND
|
||||||
GuestArtifactSource = $env:INPUT_ARTIFACT_SOURCE
|
GuestArtifactSource = $env:INPUT_ARTIFACT_SOURCE
|
||||||
GuestOS = $env:INPUT_GUEST_OS
|
GuestOS = $resolvedGuestOS
|
||||||
Configuration = $env:INPUT_CONFIGURATION
|
Configuration = $env:INPUT_CONFIGURATION
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($env:INPUT_SUBMODULES -eq 'true') { $params['Submodules'] = $true }
|
if ($env:INPUT_SUBMODULES -eq 'true') { $params['Submodules'] = $true }
|
||||||
if ($env:INPUT_USE_GIT_CLONE -eq 'true') { $params['UseGitClone'] = $true }
|
if ($env:INPUT_USE_GIT_CLONE -eq 'true') { $params['UseGitClone'] = $true }
|
||||||
if ($env:INPUT_TEMPLATE_PATH) { $params['TemplatePath'] = $env:INPUT_TEMPLATE_PATH }
|
if ($env:INPUT_USE_SHARED_CACHE -eq 'true') { $params['UseSharedCache'] = $true }
|
||||||
if ($env:INPUT_SNAPSHOT_NAME) { $params['SnapshotName'] = $env:INPUT_SNAPSHOT_NAME }
|
if ($resolvedTemplatePath) { $params['TemplatePath'] = $resolvedTemplatePath }
|
||||||
|
if ($resolvedSnapshotName) { $params['SnapshotName'] = $resolvedSnapshotName }
|
||||||
if ($extraGuestEnv.Count -gt 0) { $params['ExtraGuestEnv'] = $extraGuestEnv }
|
if ($extraGuestEnv.Count -gt 0) { $params['ExtraGuestEnv'] = $extraGuestEnv }
|
||||||
|
|
||||||
& "$ciScriptsDir\Invoke-CIJob.ps1" @params
|
& "$ciScriptsDir\Invoke-CIJob.ps1" @params
|
||||||
@@ -243,4 +287,4 @@ runs:
|
|||||||
path: ${{ steps.invoke-ci.outputs.artifact-path }}\*.log
|
path: ${{ steps.invoke-ci.outputs.artifact-path }}\*.log
|
||||||
retention-days: 3
|
retention-days: 3
|
||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ jobs:
|
|||||||
build-command: 'python build_plugin.py --final --dist-dir dist'
|
build-command: 'python build_plugin.py --final --dist-dir dist'
|
||||||
artifact-source: 'dist'
|
artifact-source: 'dist'
|
||||||
submodules: 'true'
|
submodules: 'true'
|
||||||
|
guest-os: ${{ matrix.target == 'linux' && 'Linux' || 'Windows' }}
|
||||||
# SSH URL alias configured in host ~/.ssh/config — same for both runners
|
# SSH URL alias configured in host ~/.ssh/config — same for both runners
|
||||||
repo-url: 'ssh://gitea-ci/Simone/nsis-plugin-nsinnounp.git'
|
repo-url: 'ssh://gitea-ci/Simone/nsis-plugin-nsinnounp.git'
|
||||||
# Suffix disambiguates artifact dirs: F:\CI\Artifacts\{run_id}-{attempt}-windows
|
# Suffix disambiguates artifact dirs: F:\CI\Artifacts\{run_id}-{attempt}-windows
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ runner:
|
|||||||
GITEA_CI_LINUX_TEMPLATE_PATH: "F:\\CI\\Templates\\LinuxBuild2404\\LinuxBuild2404.vmx"
|
GITEA_CI_LINUX_TEMPLATE_PATH: "F:\\CI\\Templates\\LinuxBuild2404\\LinuxBuild2404.vmx"
|
||||||
# SSH private key for Linux guest authentication
|
# SSH private key for Linux guest authentication
|
||||||
GITEA_CI_SSH_KEY_PATH: "F:\\CI\\keys\\ci_linux"
|
GITEA_CI_SSH_KEY_PATH: "F:\\CI\\keys\\ci_linux"
|
||||||
|
# Root directory of the local-ci-system repository on this host (scripts\ subdir)
|
||||||
|
GITEA_CI_SCRIPT_ROOT: "N:\\Code\\Workspace\\Local-CI-CD-System\\scripts"
|
||||||
|
|
||||||
# Optional: load additional env vars from a file (one KEY=VALUE per line)
|
# Optional: load additional env vars from a file (one KEY=VALUE per line)
|
||||||
# env_file: "F:\\CI\\runner.env"
|
# env_file: "F:\\CI\\runner.env"
|
||||||
|
|||||||
@@ -11,13 +11,18 @@
|
|||||||
|
|
||||||
Restore procedure (after a bad refresh):
|
Restore procedure (after a bad refresh):
|
||||||
Stop-Service act_runner
|
Stop-Service act_runner
|
||||||
Remove-Item 'F:\CI\Templates\WinBuild' -Recurse -Force
|
Remove-Item 'F:\CI\Templates\WinBuild2025' -Recurse -Force
|
||||||
Copy-Item 'F:\CI\Backups\Template_<stamp>' 'F:\CI\Templates\WinBuild' -Recurse
|
Copy-Item 'F:\CI\Backups\Template_<stamp>' 'F:\CI\Templates\WinBuild2025' -Recurse
|
||||||
Start-Service act_runner
|
Start-Service act_runner
|
||||||
|
|
||||||
.PARAMETER TemplatePath
|
.PARAMETER TemplatePath
|
||||||
Directory containing the template VM files (.vmx, .vmdk, snapshots).
|
Directory containing the template VM files (.vmx, .vmdk, snapshots).
|
||||||
Default: F:\CI\Templates\WinBuild
|
Default: F:\CI\Templates\WinBuild2025
|
||||||
|
|
||||||
|
.PARAMETER AllTemplates
|
||||||
|
Back up every subdirectory under F:\CI\Templates\ in a single pass.
|
||||||
|
When set, TemplatePath is ignored. Backup folder names include the template
|
||||||
|
directory name: Template_<DirName>_<timestamp>.
|
||||||
|
|
||||||
.PARAMETER BackupBaseDir
|
.PARAMETER BackupBaseDir
|
||||||
Directory under which timestamped backup folders are created.
|
Directory under which timestamped backup folders are created.
|
||||||
@@ -40,28 +45,28 @@
|
|||||||
|
|
||||||
# Keep 5 backups
|
# Keep 5 backups
|
||||||
.\Backup-CITemplate.ps1 -KeepCount 5
|
.\Backup-CITemplate.ps1 -KeepCount 5
|
||||||
|
|
||||||
|
# Back up all three templates
|
||||||
|
.\Backup-CITemplate.ps1 -AllTemplates
|
||||||
#>
|
#>
|
||||||
[CmdletBinding(SupportsShouldProcess)]
|
[CmdletBinding(SupportsShouldProcess)]
|
||||||
param(
|
param(
|
||||||
[string] $TemplatePath = 'F:\CI\Templates\WinBuild',
|
[string] $TemplatePath = 'F:\CI\Templates\WinBuild2025',
|
||||||
|
|
||||||
[string] $BackupBaseDir = 'F:\CI\Backups',
|
[string] $BackupBaseDir = 'F:\CI\Backups',
|
||||||
|
|
||||||
[ValidateRange(1, 20)]
|
[ValidateRange(1, 20)]
|
||||||
[int] $KeepCount = 3,
|
[int] $KeepCount = 3,
|
||||||
|
|
||||||
[switch] $SkipRunnerStop
|
[switch] $SkipRunnerStop,
|
||||||
|
|
||||||
|
[switch] $AllTemplates
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
if (-not (Test-Path $TemplatePath -PathType Container)) {
|
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
||||||
throw "Template directory not found: $TemplatePath"
|
|
||||||
}
|
|
||||||
|
|
||||||
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
|
|
||||||
$backupDest = Join-Path $BackupBaseDir "Template_$timestamp"
|
|
||||||
|
|
||||||
if (-not (Test-Path $BackupBaseDir)) {
|
if (-not (Test-Path $BackupBaseDir)) {
|
||||||
New-Item -ItemType Directory -Path $BackupBaseDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $BackupBaseDir -Force | Out-Null
|
||||||
@@ -82,36 +87,54 @@ if (-not $SkipRunnerStop) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
# ── Step 2: Copy template directory ───────────────────────────────────────
|
# ── Steps 2+3: Backup and prune one or all templates ──────────────────────
|
||||||
if ($PSCmdlet.ShouldProcess($TemplatePath, "Copy to $backupDest")) {
|
$templatesToBackup = @()
|
||||||
Write-Host "[Backup-CITemplate] Copying $TemplatePath -> $backupDest ..."
|
if ($AllTemplates) {
|
||||||
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
$templatesToBackup = @(Get-ChildItem 'F:\CI\Templates' -Directory -ErrorAction SilentlyContinue)
|
||||||
Copy-Item -Path $TemplatePath -Destination $backupDest -Recurse -Force
|
if ($templatesToBackup.Count -eq 0) { throw 'No template directories found under F:\CI\Templates' }
|
||||||
$sw.Stop()
|
} else {
|
||||||
|
if (-not (Test-Path $TemplatePath -PathType Container)) {
|
||||||
# Size sum without Measure-Object.Sum (Set-StrictMode safe)
|
throw "Template directory not found: $TemplatePath"
|
||||||
$backupFiles = Get-ChildItem $backupDest -Recurse -File -ErrorAction SilentlyContinue
|
}
|
||||||
$totalBytes = [long]0
|
$templatesToBackup = @(Get-Item $TemplatePath)
|
||||||
if ($backupFiles) { foreach ($f in $backupFiles) { $totalBytes += $f.Length } }
|
|
||||||
$sizeMB = [math]::Round($totalBytes / 1MB, 0)
|
|
||||||
|
|
||||||
Write-Host "[Backup-CITemplate] Backup complete: $backupDest ($sizeMB MB, $($sw.Elapsed.TotalSeconds.ToString('F0'))s)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Step 3: Prune old backups ──────────────────────────────────────────────
|
foreach ($tmpl in $templatesToBackup) {
|
||||||
$existing = Get-ChildItem $BackupBaseDir -Directory -ErrorAction SilentlyContinue |
|
$nameTag = if ($AllTemplates) { "$($tmpl.Name)_$timestamp" } else { $timestamp }
|
||||||
Where-Object { $_.Name -like 'Template_????????_??????' } |
|
$backupDest = Join-Path $BackupBaseDir "Template_$nameTag"
|
||||||
Sort-Object LastWriteTime -Descending
|
|
||||||
|
|
||||||
if ($existing.Count -gt $KeepCount) {
|
# ── Step 2: Copy template directory ───────────────────────────────────
|
||||||
$toRemove = $existing | Select-Object -Skip $KeepCount
|
if ($PSCmdlet.ShouldProcess($tmpl.FullName, "Copy to $backupDest")) {
|
||||||
foreach ($old in $toRemove) {
|
Write-Host "[Backup-CITemplate] Copying $($tmpl.FullName) -> $backupDest ..."
|
||||||
if ($PSCmdlet.ShouldProcess($old.FullName, 'Remove old backup')) {
|
$sw = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
Write-Host "[Backup-CITemplate] Pruning old backup: $($old.Name)"
|
Copy-Item -Path $tmpl.FullName -Destination $backupDest -Recurse -Force
|
||||||
Remove-Item $old.FullName -Recurse -Force
|
$sw.Stop()
|
||||||
}
|
|
||||||
|
# Size sum without Measure-Object.Sum (Set-StrictMode safe)
|
||||||
|
$backupFiles = Get-ChildItem $backupDest -Recurse -File -ErrorAction SilentlyContinue
|
||||||
|
$totalBytes = [long]0
|
||||||
|
if ($backupFiles) { foreach ($f in $backupFiles) { $totalBytes += $f.Length } }
|
||||||
|
$sizeMB = [math]::Round($totalBytes / 1MB, 0)
|
||||||
|
|
||||||
|
Write-Host "[Backup-CITemplate] Backup complete: $backupDest ($sizeMB MB, $($sw.Elapsed.TotalSeconds.ToString('F0'))s)"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Step 3: Prune old backups for this template ────────────────────────
|
||||||
|
$prunePattern = if ($AllTemplates) { "Template_$($tmpl.Name)_????????_??????" } else { 'Template_????????_??????' }
|
||||||
|
$existing = Get-ChildItem $BackupBaseDir -Directory -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.Name -like $prunePattern } |
|
||||||
|
Sort-Object LastWriteTime -Descending
|
||||||
|
|
||||||
|
if ($existing.Count -gt $KeepCount) {
|
||||||
|
$toRemove = $existing | Select-Object -Skip $KeepCount
|
||||||
|
foreach ($old in $toRemove) {
|
||||||
|
if ($PSCmdlet.ShouldProcess($old.FullName, 'Remove old backup')) {
|
||||||
|
Write-Host "[Backup-CITemplate] Pruning old backup: $($old.Name)"
|
||||||
|
Remove-Item $old.FullName -Recurse -Force
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Write-Host "[Backup-CITemplate] Retained $KeepCount backup(s) for $($tmpl.Name)."
|
||||||
}
|
}
|
||||||
Write-Host "[Backup-CITemplate] Retained $KeepCount backup(s)."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
@@ -136,6 +136,10 @@ param(
|
|||||||
# SSH username (used when GuestOS=Linux).
|
# SSH username (used when GuestOS=Linux).
|
||||||
[string] $SshUser = 'ci_build',
|
[string] $SshUser = 'ci_build',
|
||||||
|
|
||||||
|
# Persistent known_hosts file for SSH calls (CI jobs).
|
||||||
|
# Pass an empty string to disable host-key checking (template provisioning only).
|
||||||
|
[string] $SshKnownHostsFile = 'F:\CI\State\known_hosts',
|
||||||
|
|
||||||
# Work directory inside the Linux guest.
|
# Work directory inside the Linux guest.
|
||||||
[string] $GuestLinuxWorkDir = '/opt/ci/build',
|
[string] $GuestLinuxWorkDir = '/opt/ci/build',
|
||||||
|
|
||||||
@@ -176,6 +180,7 @@ if ($GuestOS -eq 'Linux') {
|
|||||||
|
|
||||||
# Step 1: Clean work dir (parent permissions handled by template setup)
|
# Step 1: Clean work dir (parent permissions handled by template setup)
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile `
|
||||||
-Command "rm -rf '$GuestLinuxWorkDir' && mkdir -p '$GuestLinuxWorkDir'"
|
-Command "rm -rf '$GuestLinuxWorkDir' && mkdir -p '$GuestLinuxWorkDir'"
|
||||||
|
|
||||||
if ($hostCloneMode) {
|
if ($hostCloneMode) {
|
||||||
@@ -193,10 +198,12 @@ if ($GuestOS -eq 'Linux') {
|
|||||||
try {
|
try {
|
||||||
Write-Host "[Invoke-RemoteBuild] Copying source archive to guest:$guestTar ..."
|
Write-Host "[Invoke-RemoteBuild] Copying source archive to guest:$guestTar ..."
|
||||||
Copy-SshItem -Source $hostTar -Destination $guestTar `
|
Copy-SshItem -Source $hostTar -Destination $guestTar `
|
||||||
-IP $IPAddress -User $SshUser -KeyPath $SshKeyPath -Direction ToGuest
|
-IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile -Direction ToGuest
|
||||||
|
|
||||||
Write-Host "[Invoke-RemoteBuild] Extracting source in guest ($GuestLinuxWorkDir) ..."
|
Write-Host "[Invoke-RemoteBuild] Extracting source in guest ($GuestLinuxWorkDir) ..."
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile `
|
||||||
-Command "tar -xzf '$guestTar' -C '$GuestLinuxWorkDir' && rm -f '$guestTar'"
|
-Command "tar -xzf '$guestTar' -C '$GuestLinuxWorkDir' && rm -f '$guestTar'"
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -208,36 +215,41 @@ if ($GuestOS -eq 'Linux') {
|
|||||||
$cloneCmd = "git clone --depth 1 --branch '$CloneBranch'"
|
$cloneCmd = "git clone --depth 1 --branch '$CloneBranch'"
|
||||||
if ($CloneSubmodules) { $cloneCmd += ' --recurse-submodules' }
|
if ($CloneSubmodules) { $cloneCmd += ' --recurse-submodules' }
|
||||||
|
|
||||||
# PAT injection for private repos
|
|
||||||
$cloneTargetUrl = $CloneUrl
|
|
||||||
if ($GiteaCredentialTarget -ne '') {
|
|
||||||
try {
|
|
||||||
Import-Module CredentialManager -ErrorAction Stop
|
|
||||||
$pat = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
|
|
||||||
if ($pat) {
|
|
||||||
$cloneTargetUrl = $CloneUrl -replace '(https?://)', ('$1' + [uri]::EscapeDataString($pat.UserName) + ':' + [uri]::EscapeDataString($pat.GetNetworkCredential().Password) + '@')
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Warning "[Invoke-RemoteBuild] Could not load PAT from Credential Manager ($GiteaCredentialTarget) — cloning without auth (public repo?)."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$cloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'" # non-authed URL for logging
|
$cloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'" # non-authed URL for logging
|
||||||
Write-Host "[Invoke-RemoteBuild] Cloning $CloneUrl into guest $GuestLinuxWorkDir ..."
|
Write-Host "[Invoke-RemoteBuild] Cloning $CloneUrl into guest $GuestLinuxWorkDir ..."
|
||||||
|
|
||||||
if ($cloneTargetUrl -ne $CloneUrl) {
|
# PAT injection via git http.extraHeader — PAT never appears in argv or /proc/environ
|
||||||
# PAT injected via env var inside SSH session — never in args
|
$authHeader = ''
|
||||||
$envCloneCmd = "export GIT_CLONE_URL='$cloneTargetUrl'; git clone --depth 1 --branch '$CloneBranch'"
|
if ($GiteaCredentialTarget -ne '') {
|
||||||
if ($CloneSubmodules) { $envCloneCmd += ' --recurse-submodules' }
|
try {
|
||||||
$envCloneCmd += " `"`$GIT_CLONE_URL`" '$GuestLinuxWorkDir'; unset GIT_CLONE_URL"
|
Import-Module CredentialManager -ErrorAction Stop
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath -Command $envCloneCmd
|
$credObj = Get-StoredCredential -Target $GiteaCredentialTarget -ErrorAction Stop
|
||||||
|
if ($credObj) {
|
||||||
|
$b64 = [Convert]::ToBase64String(
|
||||||
|
[System.Text.Encoding]::UTF8.GetBytes(
|
||||||
|
"$($credObj.UserName):$($credObj.GetNetworkCredential().Password)"))
|
||||||
|
$authHeader = "Authorization: Basic $b64"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Warning "[Invoke-RemoteBuild] Could not load PAT from Credential Manager ($GiteaCredentialTarget) — cloning without auth (public repo?)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($authHeader -ne '') {
|
||||||
|
$authCloneCmd = "git -c credential.helper= -c 'http.extraHeader=$authHeader' clone --depth 1 --branch '$CloneBranch'"
|
||||||
|
if ($CloneSubmodules) { $authCloneCmd += ' --recurse-submodules' }
|
||||||
|
$authCloneCmd += " '$CloneUrl' '$GuestLinuxWorkDir'"
|
||||||
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile -Command $authCloneCmd
|
||||||
} else {
|
} else {
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath -Command $cloneCmd
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile -Command $cloneCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
# Checkout specific commit if requested
|
# Checkout specific commit if requested
|
||||||
if ($CloneCommit -ne '') {
|
if ($CloneCommit -ne '') {
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile `
|
||||||
-Command "git -C '$GuestLinuxWorkDir' checkout '$CloneCommit'"
|
-Command "git -C '$GuestLinuxWorkDir' checkout '$CloneCommit'"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -254,16 +266,20 @@ if ($GuestOS -eq 'Linux') {
|
|||||||
} else {
|
} else {
|
||||||
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && make"
|
$buildCmd = "${envPrefix}cd '$GuestLinuxWorkDir' && make"
|
||||||
}
|
}
|
||||||
Write-Host "[Invoke-RemoteBuild] Running build: $buildCmd"
|
$displayCmd = if ($BuildCommand -ne '') { $BuildCommand } else { 'make' }
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath -Command $buildCmd
|
Write-Host "[Invoke-RemoteBuild] Running build (env vars redacted): cd '$GuestLinuxWorkDir' && $displayCmd"
|
||||||
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile -Command $buildCmd
|
||||||
|
|
||||||
# Step 4: Stage artifact tree under $GuestLinuxOutputDir for SCP collection
|
# Step 4: Stage artifact tree under $GuestLinuxOutputDir for SCP collection
|
||||||
$outDir = $GuestLinuxOutputDir
|
$outDir = $GuestLinuxOutputDir
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile `
|
||||||
-Command "rm -rf '$outDir' && mkdir -p '$outDir'"
|
-Command "rm -rf '$outDir' && mkdir -p '$outDir'"
|
||||||
|
|
||||||
$artSrc = if ($GuestArtifactSource -ne '') { $GuestArtifactSource } else { 'dist' }
|
$artSrc = if ($GuestArtifactSource -ne '') { $GuestArtifactSource } else { 'dist' }
|
||||||
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
Invoke-SshCommand -IP $IPAddress -User $SshUser -KeyPath $SshKeyPath `
|
||||||
|
-KnownHostsFile $SshKnownHostsFile `
|
||||||
-Command "if [ -d '$GuestLinuxWorkDir/$artSrc' ]; then cp -r '$GuestLinuxWorkDir/$artSrc/.' '$outDir/'; elif [ -e '$GuestLinuxWorkDir/$artSrc' ]; then cp '$GuestLinuxWorkDir/$artSrc' '$outDir/'; else echo '[Invoke-RemoteBuild] WARNING: artifact source not found: $artSrc' >&2; fi"
|
-Command "if [ -d '$GuestLinuxWorkDir/$artSrc' ]; then cp -r '$GuestLinuxWorkDir/$artSrc/.' '$outDir/'; elif [ -e '$GuestLinuxWorkDir/$artSrc' ]; then cp '$GuestLinuxWorkDir/$artSrc' '$outDir/'; else echo '[Invoke-RemoteBuild] WARNING: artifact source not found: $artSrc' >&2; fi"
|
||||||
|
|
||||||
Write-Host "[Invoke-RemoteBuild] Linux build complete."
|
Write-Host "[Invoke-RemoteBuild] Linux build complete."
|
||||||
@@ -432,14 +448,19 @@ try {
|
|||||||
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$srcPath = Join-Path $workDir $artifactSrc
|
$srcPath = Join-Path $workDir $artifactSrc
|
||||||
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
if (-not (Test-Path $srcPath)) {
|
||||||
if (Test-Path $7zip) {
|
Write-Host "[Build] Artifact source '$srcPath' not found — skipping packaging."
|
||||||
Write-Host "[Build] Compressing artifacts with 7-Zip..."
|
|
||||||
& $7zip a -mmt=on -mx1 $artifactZip $srcPath 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
|
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
||||||
|
if (Test-Path $7zip) {
|
||||||
|
Write-Host "[Build] Compressing artifacts with 7-Zip..."
|
||||||
|
& $7zip a -mmt=on -mx1 $artifactZip $srcPath 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Compress-Archive -Path $srcPath -DestinationPath $artifactZip -Force
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,14 +504,19 @@ try {
|
|||||||
if (-not (Test-Path $archiveDir)) {
|
if (-not (Test-Path $archiveDir)) {
|
||||||
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $archiveDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
if (-not (Test-Path $outputDir)) {
|
||||||
if (Test-Path $7zip) {
|
Write-Host "[Build] Output dir '$outputDir' not found — skipping packaging."
|
||||||
Write-Host "[Build] Compressing output with 7-Zip..."
|
|
||||||
& $7zip a -mmt=on -mx1 $artifactZip "$outputDir\*" 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force
|
$7zip = 'C:\Program Files\7-Zip\7z.exe'
|
||||||
|
if (Test-Path $7zip) {
|
||||||
|
Write-Host "[Build] Compressing output with 7-Zip..."
|
||||||
|
& $7zip a -mmt=on -mx1 $artifactZip "$outputDir\*" 2>&1 | Where-Object { $_ -notmatch '^7-Zip' } | ForEach-Object { Write-Host $_ }
|
||||||
|
if ($LASTEXITCODE -ne 0) { throw "7-Zip failed" }
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Compress-Archive -Path "$outputDir\*" -DestinationPath $artifactZip -Force
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -124,6 +124,20 @@ if (Test-Path $leasesDir) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Stale vm-start.lock cleanup ─────────────────────────────────────────────
|
||||||
|
# The lock is released by Invoke-CIJob.ps1 normally. On host crash it persists;
|
||||||
|
# the OS-level lock is gone after reboot but the file blocks subsequent IP-acquire
|
||||||
|
# retries for 10 minutes and confuses operators. Remove if older than 30 minutes.
|
||||||
|
$lockFile = 'F:\CI\State\vm-start.lock'
|
||||||
|
$lockCutoff = (Get-Date).AddMinutes(-30)
|
||||||
|
if ((Test-Path $lockFile) -and (Get-Item $lockFile).LastWriteTime -lt $lockCutoff) {
|
||||||
|
if ($PSCmdlet.ShouldProcess($lockFile, 'Remove stale vm-start.lock')) {
|
||||||
|
$lockAgeMin = [int]((Get-Date) - (Get-Item $lockFile).LastWriteTime).TotalMinutes
|
||||||
|
Remove-Item $lockFile -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Host "[RetentionPolicy] Removed stale vm-start.lock (age: ${lockAgeMin} min)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||||
$freeGBAfter = if ($drive) { [math]::Round((Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue).Free / 1GB, 1) } else { $freeGB }
|
$freeGBAfter = if ($drive) { [math]::Round((Get-PSDrive -Name $DriveLetter -ErrorAction SilentlyContinue).Free / 1GB, 1) } else { $freeGB }
|
||||||
$delta = [math]::Round($freeGBAfter - $freeGB, 1)
|
$delta = [math]::Round($freeGBAfter - $freeGB, 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user