chore: initial commit local CI/CD system (e2e verified, production-ready)

Sistema CI locale basato su VMware Workstation + Gitea Actions + act_runner.
Testato e2e con nsis-plugin-nsinnounp (MSBuild + Python, 4 configurazioni parallele).

- scripts/: Invoke-CIJob, Invoke-RemoteBuild, New/Remove-BuildVM, Wait-VMReady, Get-BuildArtifacts
- runner/: act_runner config (windows-build label, capacity 4)
- gitea/workflows/: build-nsis.yml (template per progetti MSBuild/Python)
- template/: script di provisioning template VM (VS BuildTools 2026, .NET SDK 10, Python 3.13)
- docs/: ARCHITECTURE, CI-FLOW, OPTIMIZATION, BEST-PRACTICES, Setup-GiteaSSH

Verificato: e2e-009 SUCCESS in 02:09, cleanup automatico VM confermato.
This commit is contained in:
Simone
2026-05-08 23:25:50 +02:00
commit dd238121b3
21 changed files with 3574 additions and 0 deletions
+193
View File
@@ -0,0 +1,193 @@
# Architecture — Local CI/CD System
## Overview
A fully local, isolated CI/CD pipeline running on a Windows host using:
- **Gitea** as the self-hosted Git server + CI platform
- **act_runner** as the job executor (runs on the Windows host)
- **VMware Workstation** for ephemeral build VMs (linked clones)
- **WinRM / PowerShell Remoting** for communication with build VMs
- **MSBuild / dotnet CLI** executing inside the guest VM only
The runner host **never executes build tools directly**. Its only role is orchestration: creating, driving, and destroying VMs.
---
## System Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ DEVELOPER WORKSTATION │
│ │
│ git push → Gitea (self-hosted, LAN) │
└───────────────────────────┬─────────────────────────────────────────────┘
│ HTTP/HTTPS (Gitea Actions API)
┌─────────────────────────────────────────────────────────────────────────┐
│ WINDOWS HOST (i9-10900X · 64 GB RAM · NVMe SSD) │
│ │
│ ┌───────────────────┐ polls job queue ┌──────────────────┐ │
│ │ Gitea Server │◄────────────────────────►│ act_runner │ │
│ │ (Actions API) │ reports status/logs │ (Windows svc) │ │
│ └───────────────────┘ └────────┬─────────┘ │
│ │ │
│ calls Invoke-CIJob.ps1 │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ Orchestrator Scripts (PowerShell) │ │
│ │ │ │
│ │ New-BuildVM.ps1 ──► vmrun.exe (linked clone from template) │ │
│ │ Wait-VMReady.ps1 ─► Test-WSMan poll loop │ │
│ │ Invoke-RemoteBuild.ps1 ► PSSession + Invoke-Command │ │
│ │ Get-BuildArtifacts.ps1 ► Copy-Item -FromSession │ │
│ │ Remove-BuildVM.ps1 ───► vmrun stop + deleteVM │ │
│ └───────────────────────────────┬───────────────────────────────────┘ │
│ │ vmrun.exe (VMware CLI) │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────────┐ │
│ │ VMware Workstation │ │
│ │ │ │
│ │ Template VM ──snapshot "BaseClean"──┐ │ │
│ │ ├── Clone_Job_001.vmx │ │
│ │ ├── Clone_Job_002.vmx │ │
│ │ └── Clone_Job_003.vmx │ │
│ └───────────────────────────────────────────────────────────────────┘ │
│ │ WinRM :5985 (192.168.11.0/24) │
└──────────────────────────────────┼──────────────────────────────────────┘
┌────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Build VM #1 │ │ Build VM #2 │ │ Build VM #N │
│ (ephemeral) │ │ (ephemeral) │ │ (ephemeral) │
│ │ │ │ │ │
│ VS Build Tools │ │ VS Build Tools │ │ VS Build Tools │
│ .NET SDK 8 │ │ .NET SDK 8 │ │ .NET SDK 8 │
│ Git │ │ Git │ │ Git │
│ WinRM enabled │ │ WinRM enabled │ │ WinRM enabled │
│ │ │ │ │ │
│ > git clone │ │ > git clone │ │ > git clone │
│ > dotnet build │ │ > dotnet build │ │ > dotnet build │
│ > artifacts │ │ > artifacts │ │ > artifacts │
│ │ │ │ │ │
│ [DESTROYED] │ │ [DESTROYED] │ │ [DESTROYED] │
└──────────────────┘ └──────────────────┘ └──────────────────┘
```
---
## Component Descriptions
### Gitea (Self-hosted Git + CI)
- Hosts Git repositories
- Provides Gitea Actions (GitHub Actions-compatible YAML workflow engine)
- Manages job queue and reports build status to commits/PRs
- Stores build artifacts uploaded by act_runner
### act_runner (Job Executor)
- Runs as a Windows service on the host
- Polls Gitea Actions API for pending jobs
- Executes `.gitea/workflows/*.yml` steps
- **Configured with `container.enabled: false`** — does NOT use Docker
- Each step runs as a PowerShell command on the host (orchestration only)
- Supports `capacity: N` for concurrent job execution (N parallel VMs)
### VMware Workstation + vmrun CLI
- Manages the template VM and all ephemeral clones
- `vmrun.exe` is the command-line interface for all VM lifecycle operations
- **Linked clones** are used: each build VM is a delta clone from a frozen template snapshot
- Clone creation: ~515 seconds (vs 60120 seconds for full clone)
- Disk footprint: ~25 GB per clone (vs 4080 GB for full clone)
- Tradeoff: template snapshot must remain intact
### WinRM / PowerShell Remoting
- Default transport for remote build execution inside VMs
- Port: **5985** (HTTP, in-lab use); migrate to 5986/HTTPS for hardened setups
- Authentication: **Basic** (unencrypted — acceptable for isolated lab)
- Used for:
- Transferring source: `Compress-Archive` on host → WinRM copy → `Expand-Archive` in guest
- Running build commands (`Invoke-Command`)
- Packaging artifacts (`Compress-Archive` in guest) and collecting via `Copy-Item -FromSession`
### Build Toolchain (inside VM only)
- Visual Studio Build Tools **2026** (MSBuild 18.5.4 / toolset v145)
- .NET SDK **10.0.203**
- Python **3.13.3** (per build script personalizzati come `build_plugin.py`)
- Git (per repo clone inside VM)
- NuGet CLI (optional, dotnet restore covers most cases)
---
## Network Topology
```
Host NIC (LAN) → 192.168.1.x → Gitea server (10.10.20.11:3100), developer machines
VMware VMnet8 (NAT) → 192.168.79.0/24 → Build VMs (DHCP, internet access via NAT)
Build VMs:
Clone_Job_001 → 192.168.79.131
Clone_Job_002 → 192.168.79.132
...
Clone_Job_N → 192.168.79.1xx
WinRM port 5985 (HTTP) on each VM IP.
VMs have internet access via NAT — required for pip/nuget during build.
```
> **Note:** VMnet8 NAT gives VMs internet access. The host detects the VM IP via
> `vmrun getGuestIPAddress` after boot — no static IP assignment needed.
---
## VM Lifecycle
```
1. TEMPLATE (static, locked)
└─ Snapshot: "BaseClean" ───────────────────────┐
2. CLONE CREATION │
vmrun clone <template.vmx> <clone.vmx> linked ──►│
Duration: ~515 seconds │
3. START │
vmrun start <clone.vmx> nogui │
Duration: ~2040 seconds boot │
4. READINESS CHECK │
Poll: getState=running → ping → Test-WSMan │
Duration: ~3090 seconds total │
5. BUILD │
Copy script → Invoke-Command → collect artifacts │
Duration: varies (seconds to minutes) │
6. DESTROY │
vmrun stop hard -> vmrun deleteVM → Remove-Item │
Duration: ~510 seconds │
Result: zero disk footprint remaining ▼
```
---
## Resource Limits & Parallel Capacity
| Resource | Host Total | Per VM (est.) | Max VMs |
| --------------- | ---------------------- | --------------- | ---------------- |
| RAM | 64 GB | ~68 GB | 810 |
| CPU | 20 threads (i9-10900X) | ~4 vCPU | 5 |
| NVMe IOPS | High | Clone delta I/O | ~68 |
| **Recommended** | — | — | **4 concurrent** |
`act_runner` `capacity: 4` is the conservative default. Raise to 6 after benchmarking.
---
## Security Boundaries
- Build VMs are on VMnet8 NAT (192.168.79.0/24) — host reaches VMs, VMs have internet access
- WinRM credentials are stored in Windows Credential Manager on host (not hardcoded)
- Each VM is fully destroyed after the job — no state carries between builds
- Runner host does not expose any ports to the build VM network
- Source code is transferred via zip (Compress-Archive → WinRM copy → Expand-Archive in guest) — no git clone inside VM
+281
View File
@@ -0,0 +1,281 @@
# Best Practices — Stability, Security & Operations
## 1. Credential Management
### Do NOT store credentials in scripts or config files
The scripts use `-GuestCredentialTarget` (a Windows Credential Manager target name)
rather than plaintext username/password parameters. Store credentials once:
```powershell
# Run on host (once, before first CI job)
cmdkey /generic:BuildVMGuest /user:MACHINENAME\ci_build /pass:YourStrongPassword
```
Retrieve in scripts via the `CredentialManager` PowerShell module:
```powershell
Install-Module CredentialManager -Scope CurrentUser
$cred = Get-StoredCredential -Target 'BuildVMGuest'
```
### Rotate credentials quarterly
1. Update password in the template VM (requires rebuilding `BaseClean` snapshot)
2. Update Windows Credential Manager on the host:
```
cmdkey /generic:BuildVMGuest /user:MACHINENAME\ci_build /pass:NewPassword
```
3. No script changes required — they reference the target name, not the password.
---
## 2. WinRM Security
### Current setup (HTTP / port 5985)
Acceptable for an **isolated lab network** where:
- Build VMs are on a dedicated host-only VMware network (192.168.11.0/24 — VMnet11 dedicated CI)
- No external traffic reaches port 5985
- Credentials are managed via Windows Credential Manager
### Recommended upgrade: WinRM over HTTPS (port 5986)
```powershell
# Inside the template VM (before taking BaseClean snapshot):
# Create self-signed certificate
$cert = New-SelfSignedCertificate `
-Subject "CN=$(hostname)" `
-CertStoreLocation Cert:\LocalMachine\My `
-KeyUsage DigitalSignature, KeyEncipherment `
-KeyAlgorithm RSA `
-KeyLength 2048
# Create HTTPS WinRM listener
New-WSManInstance WinRM/Config/Listener `
-SelectorSet @{ Transport = 'HTTPS'; Address = '*' } `
-ValueSet @{ Hostname = $(hostname); CertificateThumbprint = $cert.Thumbprint }
# Allow port 5986
New-NetFirewallRule -Name 'WinRM-HTTPS-CI' -DisplayName 'WinRM HTTPS CI' `
-Direction Inbound -Protocol TCP -LocalPort 5986 -Action Allow
```
```powershell
# On the HOST (in scripts), use HTTPS session options:
$sessionOptions = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$session = New-PSSession -ComputerName $ip -Port 5986 -UseSSL `
-Credential $cred -SessionOption $sessionOptions
```
> `-SkipCACheck` is acceptable for a self-signed cert in an isolated lab. Do NOT
> use this against externally accessible machines.
---
## 3. act_runner Service Stability
### Windows Service Recovery Policy
The `Install-Runner.ps1` script configures automatic service restart on failure:
- Restart after 1st failure: 5 seconds
- Restart after 2nd failure: 10 seconds
- Restart after subsequent: 30 seconds
Verify in Services → act_runner → Properties → Recovery tab.
### Monitor the service
```powershell
# Check service status
Get-Service act_runner | Select-Object Status, StartType
# View last 50 log lines
Get-EventLog -LogName Application -Source act_runner -Newest 50 | Format-List
# Restart if needed
Restart-Service act_runner
```
### Scheduled health check (optional)
Create a scheduled task that verifies the runner appears "Online" in Gitea via API:
```powershell
# Check runner status via Gitea API every 15 minutes
$response = Invoke-RestMethod `
-Uri "http://gitea.local/api/v1/admin/runners" `
-Headers @{ Authorization = "token $env:GITEA_API_TOKEN" }
$runnerOnline = $response | Where-Object { $_.name -eq 'local-windows-runner' -and $_.status -eq 'online' }
if (-not $runnerOnline) {
# Send alert (email, webhook, etc.) or restart service
Restart-Service act_runner
}
```
---
## 4. Template VM Integrity
The "BaseClean" snapshot is the foundation of every build. If it is corrupted,
**all builds fail immediately**.
### Protection measures
1. **Never power on the template VM for reasons other than planned maintenance.**
Configure VMware Workstation to prevent accidental starts: right-click → Settings →
Options → Advanced → disable "Allow background snapshots".
2. **Backup the parent VMDK before any template changes:**
```powershell
# Before any template maintenance
$templateDir = 'F:\CI\Templates\WinBuild'
$backupDir = "F:\CI\Backups\Template_$(Get-Date -Format yyyyMMdd)"
Copy-Item $templateDir $backupDir -Recurse
```
3. **Keep a list of all current linked clones** before refreshing the snapshot.
If any clone exists when you modify the parent, it may break.
Check: `vmrun list` — should return no build VMs during maintenance window.
4. **Version the snapshot name** to make rollback easy:
Instead of reusing "BaseClean", name snapshots `BaseClean_20260101`.
Update `config.yaml` `envs.GITEA_CI_SNAPSHOT_NAME` when rotating.
---
## 5. Orphaned VM Cleanup
If the host loses power mid-job or act_runner crashes, ephemeral VMs may not be
destroyed. Run this cleanup script on host startup or as a daily scheduled task:
```powershell
# Cleanup-OrphanedBuildVMs.ps1
$vmrun = 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe'
$cloneBase = 'F:\CI\BuildVMs'
$maxAgeHours = 4 # No job should run longer than 4 hours
Get-ChildItem $cloneBase -Directory |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-$maxAgeHours) } |
ForEach-Object {
$vmx = Get-ChildItem $_.FullName -Filter '*.vmx' | Select-Object -First 1
if ($vmx) {
Write-Host "Cleaning orphan: $($vmx.FullName)"
& $vmrun -T ws stop $vmx.FullName hard 2>$null
& $vmrun -T ws deleteVM $vmx.FullName 2>$null
}
Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
}
```
---
## 6. Gitea Repository Configuration
### Required repository settings for workflows to run
1. Enable Actions for the repository: Settings → Repository → Actions → Enable
2. Add secrets if needed: Settings → Secrets and Variables → Actions
3. Protect main branch: Settings → Branches → Branch protection rules
### Workflow file location
Workflows **must** be at `.gitea/workflows/*.yml` (not `.github/workflows/`).
```
your-repo/
└── .gitea/
└── workflows/
└── build.yml ← copy from gitea/workflow-example.yml
```
---
## 7. Logging & Observability
### act_runner logs
The runner daemon writes to stdout (captured by the Windows service manager).
Increase verbosity for debugging:
```yaml
# runner/config.yaml
log:
level: debug # change from "info" to "debug"
format: text
```
### Per-job build logs
`Invoke-CIJob.ps1` outputs timestamped phase banners. act_runner captures all
stdout/stderr and uploads it to Gitea Actions → job log viewer.
For persistent local logs:
```powershell
# In your workflow YAML, redirect output to a log file:
- name: Build in ephemeral VM
shell: pwsh
run: |
.\scripts\Invoke-CIJob.ps1 ... *>&1 | Tee-Object -FilePath "F:\CI\Logs\job-${{ github.run_id }}.log"
```
### Windows Event Log
act_runner (when installed as a service) writes events to Windows Event Log →
Application source "act_runner". Check with:
```powershell
Get-EventLog -LogName Application -Source '*runner*' -Newest 20
```
---
## 8. Network Isolation Verification
After setting up the host-only VMware network, verify that build VMs cannot
reach the internet (important for supply-chain security):
```powershell
# From inside a build VM via WinRM:
Invoke-Command -Session $session -ScriptBlock {
$result = Test-Connection 8.8.8.8 -Count 1 -Quiet
if ($result) {
Write-Warning "VM has internet access — check VMware network adapter type"
} else {
Write-Host "VM correctly isolated (no internet)"
}
}
```
Build VMs should only reach:
- The host (for WinRM connection)
- Gitea server (for git clone, if reachable via host-only network)
- NuGet cache share (host-side shared folder)
---
## 9. Updating the Build Toolchain
When a new .NET SDK or VS Build Tools version is released:
1. **During a maintenance window** (no CI jobs running):
```
vmrun list ← must be empty
```
2. Boot the template VM
3. Run updates:
```powershell
# Update .NET SDK
& "C:\Users\ci_build\AppData\Local\Microsoft\dotnet\dotnet-install.ps1" -Channel 8.0
# Update VS Build Tools via Visual Studio Installer
"C:\Program Files (x86)\Microsoft Visual Studio\Installer\vs_installer.exe" update --quiet --norestart
```
4. Verify tools work (run a test build manually)
5. Shut down VM
6. Take new snapshot: `BaseClean_$(Get-Date -Format yyyyMMdd)`
7. Update `SnapshotName` in `runner/config.yaml`
8. Delete the old snapshot after confirming new one works for 1 week
+254
View File
@@ -0,0 +1,254 @@
# CI Pipeline Flow — Step by Step
## Trigger to Artifact: Complete Pipeline Walk-Through
---
### Step 0 — Prerequisites (one-time setup)
Before any CI job runs, the following must be in place:
| Component | State |
| ------------------ | ----------------------------------------------------------------------- |
| Gitea server | Running, accessible on LAN |
| act_runner | Registered, running as Windows service on host |
| Template VM | Provisioned, snapshot "BaseClean" taken, VM powered off |
| WinRM | Enabled on template VM (inherits to all clones) |
| vmrun.exe | Present at `C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe` |
| `F:\CI\BuildVMs\` | Directory exists and writable |
| `F:\CI\Artifacts\` | Directory exists and writable |
---
### Step 1 — Developer Pushes Code
```
Developer → git push origin main
```
- Git push lands on Gitea server
- Gitea evaluates `.gitea/workflows/build.yml` in the repository
- If the workflow trigger matches (e.g. `on: push` to `main`), Gitea enqueues a new **Actions job**
- Job is tagged with the label defined in the workflow (`runs-on: windows-build`)
---
### Step 2 — act_runner Picks Up the Job
```
act_runner (host) ─polling─► Gitea Actions API
◄─job data─
```
- act_runner polls the Gitea API approximately every 510 seconds
- Detects a pending job matching its labels (`windows-build`)
- Downloads the workflow YAML and job parameters
- If `capacity` slots are available, starts job execution immediately
- If all slots are busy, job waits in Gitea queue until a slot frees
---
### Step 3 — act_runner Executes Workflow Steps
The workflow YAML defines steps that run as PowerShell commands **on the host**.
The first substantive step calls `Invoke-CIJob.ps1`:
```yaml
- name: Build in ephemeral VM
shell: pwsh
run: |
.\scripts\Invoke-CIJob.ps1 `
-JobId "${{ github.run_id }}" `
-RepoUrl "${{ env.GITEA_REPO_URL }}" `
-Branch "${{ github.ref_name }}" `
-Commit "${{ github.sha }}"
```
---
### Step 4 — Invoke-CIJob.ps1: Clone VM
```
New-BuildVM.ps1
vmrun.exe -T ws clone <template.vmx> <clone.vmx> linked -snapshot "BaseClean"
```
- Generates unique clone name: `Clone_{JobId}_{yyyyMMdd_HHmmss}`
- Creates clone directory under `F:\CI\BuildVMs\`
- **Linked clone** creation takes ~515 seconds
- The template snapshot "BaseClean" is **not modified**
- Returns the full path to the new `.vmx` file
---
### Step 5 — Start VM
```
vmrun.exe -T ws start <clone.vmx> nogui
```
- Starts the cloned VM in headless (no GUI) mode
- VM boots from the "BaseClean" state: clean OS, build tools installed, WinRM enabled
- Boot takes ~2040 seconds
---
### Step 6 — Wait for VM Readiness
```
Wait-VMReady.ps1 -VMPath <clone.vmx> -IPAddress <dynamic-NAT-ip> -TimeoutSeconds 300
```
IP is discovered dynamically via `vmrun getGuestIPAddress` (VMnet8 NAT, subnet 192.168.79.0/24).
No hardcoded IPs — each clone gets a DHCP-assigned address from VMware NAT.
Three-phase readiness check (retried every 5 seconds, up to 300s timeout):
```
Phase 1: vmrun getState → must return "running"
Phase 2: Test-Connection (ICMP ping) → must succeed
Phase 3: Test-WSMan → WinRM listener must respond
```
- Total wait is typically **3090 seconds** after `vmrun start`
- If timeout exceeded, VM is destroyed and job fails with a clear error message
- On success, IP and credentials are ready for PSSession
---
### Step 7 — Remote Build Execution
```
Invoke-RemoteBuild.ps1
-IPAddress <dynamic-NAT-ip> # from vmrun getGuestIPAddress
-Credential (from Windows Credential Manager)
-BuildCommand 'python build_plugin.py --final --dist-dir dist'
-GuestArtifactSource 'dist'
```
Sequence inside the PSSession:
```
1. New-PSSession → open WinRM session to VM
2. Compress-Archive (host) → copy zip to C:\CI\build.zip (guest) → Expand-Archive to C:\CI\build\
(source code è già clonato sull'host in Phase 1 — non si fa git clone nella VM)
3. Invoke-Expression "$BuildCommand" in C:\CI\build\
Esempio: python build_plugin.py --final --dist-dir dist
a. Build 4 configurazioni MSBuild in parallelo (x86-unicode, x64-unicode, cli-x86, cli-x64)
b. Raccoglie artifact in C:\CI\build\dist\
c. Cleanup build dir (--final)
4. Compress-Archive C:\CI\build\$GuestArtifactSource C:\CI\output\artifacts.zip
5. Cattura exit code; se non-zero il job fallisce con errore chiaro
6. Remove-PSSession
```
- Output standard e stderr dalla build sono streamati all'host e catturati da act_runner per i log Gitea Actions
- Se la build fallisce (exit code ≠ 0), il controllo passa al blocco `finally`
---
### Step 8 — Collect Artifacts
```
Get-BuildArtifacts.ps1
-IPAddress <dynamic-NAT-ip>
-Credential (same as above)
-GuestArtifactPath C:\CI\output\artifacts.zip
-HostArtifactDir F:\CI\Artifacts\{JobId}\
```
- Apre PSSession e copia `C:\CI\output\artifacts.zip` dall'host
- Valida che il file esista e sia non-vuoto
- Il zip contiene la struttura `dist/` con tutti gli artifact di build
---
### Step 9 — Destroy VM
```
Remove-BuildVM.ps1 -VMPath <clone.vmx>
```
Sequence:
```
1. vmrun stop <clone.vmx> soft (graceful shutdown attempt)
2. Wait 5 seconds
3. vmrun getState → if still "running":
vmrun stop <clone.vmx> hard (force off)
4. vmrun deleteVM <clone.vmx> (removes VMX + VMDK delta files)
5. Remove-Item <clone dir> -Recurse (removes any leftover files)
```
- VM is gone from disk within ~510 seconds
- No state, no temp files, no credentials remain in the clone
- Template VM and "BaseClean" snapshot are unaffected
> **Note:** This step runs in a `finally` block inside `Invoke-CIJob.ps1`, so it
> executes even if Steps 68 failed. A build VM is **never** left running.
---
### Step 10 — Report Status & Upload Artifacts
```
act_runner ─► Gitea Actions API (job status: success / failure)
act_runner ─► Gitea artifact store (uploads F:\CI\Artifacts\{JobId}\*)
```
- act_runner reads the exit code from `Invoke-CIJob.ps1`
- Reports `success` or `failure` to Gitea
- Uploads artifacts using `actions/upload-artifact` step in the workflow YAML
- Build log (stdout from all steps) is stored in Gitea Actions UI
- Commit/PR status badge is updated
---
## Parallel Build Flow
When multiple pushes happen concurrently (or multiple PRs are open):
```
act_runner (capacity: 4)
├── Job #1 ──► Clone_Job_001 (192.168.79.x) → build → destroy
├── Job #2 ──► Clone_Job_002 (192.168.79.x) → build → destroy
├── Job #3 ──► Clone_Job_003 (192.168.79.x) → build → destroy
└── Job #4 ──► Clone_Job_004 (192.168.79.x) → build → destroy
(Job #5 waits in Gitea queue)
```
Note: each clone gets a dynamic IP from VMware NAT DHCP (192.168.79.0/24). IP is discovered
automatically via `vmrun getGuestIPAddress` — no static assignment required.
- Each job creates its own independently named clone
- Clones share the parent "BaseClean" snapshot (read-only CoW)
- No filesystem or process overlap between concurrent builds
- act_runner enforces the `capacity` limit
---
## Failure Scenarios
| Failure Point | Behavior |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| VM clone fails | `Invoke-CIJob.ps1` throws; no VM to destroy; job fails immediately |
| VM never reaches ready state (timeout) | `Wait-VMReady.ps1` throws; `finally` destroys the (running) VM |
| Build fails inside VM | `Invoke-RemoteBuild.ps1` throws; `finally` collects partial artifacts (if any), destroys VM |
| Artifact collection fails | Job marked failed; VM is still destroyed in `finally` |
| Host machine loses power mid-job | `act_runner` resumes on reboot, marks in-flight job as failed; orphan VMs must be cleaned up manually (see TODO.md) |
---
## Timing Summary (typical build)
| Phase | Typical Duration |
| ---------------------------- | -------------------- |
| Gitea → act_runner poll | 510 s |
| Linked clone creation | 515 s |
| VM boot | 2040 s |
| WinRM readiness | 1030 s (after boot) |
| git clone (from local Gitea) | 210 s |
| dotnet restore + build | project-dependent |
| Artifact collection | 210 s |
| VM destruction | 510 s |
| **Overhead total** | **~60120 s** |
+197
View File
@@ -0,0 +1,197 @@
# Optimization Strategies
## 1. Linked Clone Disk Layout
**Goal:** Minimize clone creation time and I/O contention between concurrent VMs.
### Recommended NVMe Partition Layout
```
NVMe SSD (e.g. 2TB)
├── C:\ — Windows host OS, VMware Workstation installation
├── F:\CI\
│ ├── Templates\ — Template VM VMX + base VMDK (parent snapshot)
│ ├── BuildVMs\ — Ephemeral linked clone VMs (delta VMDKs)
│ ├── Artifacts\ — Collected build artifacts (per job)
│ ├── Cache\ — NuGet / npm cache (see §4)
│ └── RunnerWork\ — act_runner workspace (checkout, step scripts)
```
**Why separate directories matter:**
- Template VMDK reads (CoW base) and clone delta writes happen simultaneously
- Keeping them on the same fast NVMe avoids I/O stalls; a separate spinning disk for
the clone directory would bottleneck clone creation
- Artifacts and cache dirs have sequential I/O patterns; they can share space
---
## 2. Snapshot Strategy
### Tiered Snapshot Model
```
Tier 0: Base OS Install (Windows only, no tools)
└── Snapshot: "OSBase" — rarely updated (yearly or on major Windows updates)
Tier 1: Build Toolchain
└── Snapshot: "BaseClean" ← CLONE SOURCE (weekly refresh)
Includes: VS Build Tools 2022, .NET SDK, Git, WinRM config
Requirement: ALL linked clones must reference this exact snapshot
```
### Refresh Schedule
| Snapshot | Refresh Frequency | Trigger |
|----------|------------------|-------|
| `OSBase` | Quarterly | Windows cumulative update |
| `BaseClean` | Weekly/Monthly | .NET SDK patch, security update, VS update |
> **Note:** Windows Server 2025 KMS lease = 180 giorni. Prima della scadenza:
> boot template su VMnet8 (NAT) → `slmgr /ato` → spegni → nuovo snapshot `BaseClean`.
---
## 3. Parallel Build Capacity
### RAM Budget (i9-10900X · 64 GB)
| Component | RAM Usage |
|-----------|----------|
| Windows host OS | ~4 GB |
| VMware Workstation | ~0.5 GB |
| Gitea server | ~0.51 GB |
| act_runner service | ~100 MB |
| Each build VM (idle) | ~23 GB |
| Each build VM (active MSBuild/Python) | ~68 GB |
| **Headroom target (20%)** | ~13 GB |
```
Available for VMs: 64 - 4 - 0.5 - 1 - 0.1 - 13 = ~45 GB
Max VMs at peak: 45 / 8 = ~5.6 → safe limit = 4
```
**Recommendation:** `capacity: 4` in `config.yaml`.
### CPU Budget (i9-10900X: 10 cores / 20 threads)
- Template VM: 4 vCPU configurati
- Con 4 VM parallele: 4 × 4 = 16 thread, lascia 4 per host OS / Gitea / runner
- `build_plugin.py` usa `get_optimal_thread_count()` che rileva i core della VM e divide per il numero di build parallele (fix TRK0002)
### VM Configuration (template VMX)
```ini
numvcpus = "4"
cpuid.coresPerSocket = "2"
memsize = "6144" # 6 GB RAM per VM
scsi0.virtualDev = "pvscsi"
ethernet0.virtualDev = "vmxnet3"
```
---
## 4. NuGet / Package Cache on Host
**Problem:** Each build VM does a fresh `dotnet restore`, re-downloading NuGet packages every time.
**Solution:** Map a host-side NuGet cache directory as a shared folder into each VM.
### Setup (host side)
```
F:\CI\Cache\NuGet\ ← shared folder on host
```
### VMware Shared Folder Configuration (in template VMX)
Add to `WinBuild.vmx` before taking the `BaseClean` snapshot:
```ini
sharedFolder0.present = "TRUE"
sharedFolder0.enabled = "TRUE"
sharedFolder0.readAccess = "TRUE"
sharedFolder0.writeAccess = "TRUE"
sharedFolder0.hostPath = "F:\\CI\\Cache\\NuGet"
sharedFolder0.guestName = "nuget-cache"
sharedFolder.maxNum = "1"
```
Inside the VM, the shared folder appears as `\\vmware-host\Shared Folders\nuget-cache`.
### NuGet Cache Redirect (in `Invoke-RemoteBuild.ps1`)
Add to the remote build ScriptBlock:
```powershell
$env:NUGET_PACKAGES = '\\vmware-host\Shared Folders\nuget-cache'
dotnet restore --packages $env:NUGET_PACKAGES
```
**Result:** First build per package downloads once; subsequent builds read from the host cache.
Cache is shared across all concurrent VMs (NuGet packages are safe for concurrent reads).
---
## 5. Clone Pre-Warming
For latency-sensitive pipelines, keep N pre-warmed clones ready in "booted and idle" state.
**Tradeoff:** Saves ~4590 seconds of startup per job, but clones remain running (consuming RAM) until used.
**Implementation sketch:**
```powershell
# Pre-warmer job (separate scheduled task on host)
# Runs between CI jobs to maintain a pool of warm VMs
$poolSize = 2 # Keep 2 VMs warm at all times
# Check current running clones
$runningClones = Get-ChildItem F:\CI\WarmPool\ -Filter *.vmx
if ($runningClones.Count -lt $poolSize) {
# Create and start new clone in warm pool
$vmx = .\New-BuildVM.ps1 -TemplatePath $template -CloneBaseDir F:\CI\WarmPool -JobId "warm-$(Get-Random)"
vmrun -T ws start $vmx nogui
}
```
> **Note:** Pre-warming is an advanced optimization. Start without it and add
> only if CI overhead (startup time) is a demonstrated bottleneck.
---
## 6. Artifact Storage Management
Build artifacts accumulate quickly. Automate cleanup:
```powershell
# Scheduled task: run daily
# Remove artifact dirs older than 30 days
Get-ChildItem 'F:\CI\Artifacts' -Directory |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-30) } |
Remove-Item -Recurse -Force
# Remove orphaned clone directories (VMs that were not properly deleted)
Get-ChildItem 'F:\CI\BuildVMs' -Directory |
Where-Object { $_.LastWriteTime -lt (Get-Date).AddHours(-4) } |
ForEach-Object {
$vmx = Get-ChildItem $_.FullName -Filter *.vmx | Select-Object -First 1
if ($vmx) {
vmrun -T ws stop $vmx.FullName hard 2>$null
}
Remove-Item $_.FullName -Recurse -Force
}
```
---
## 7. NUMA & CPU Affinity (Advanced)
The i9-10900X is a single NUMA node CPU (no NUMA effects). CPU pinning is not needed.
However, you can set CPU affinity per VM group to reduce vCPU migration overhead:
```powershell
# Optional: pin act_runner process to cores 0-3 (leave build VMs on 4-19)
$runnerPid = (Get-Process act_runner).Id
Start-Process -FilePath 'cmd' -ArgumentList "/c start /affinity 0xF /b /wait powershell" -NoNewWindow
```
For most workloads, Windows scheduler handles this well — skip affinity unless profiling shows issues.
+101
View File
@@ -0,0 +1,101 @@
# Configurazione accesso SSH a Gitea
**Target**: clone da `git@gitea.emulab.it:Simone/nsis-plugin-nsinnounp.git`
**Gitea**: `http://10.10.20.11:3100` / `https://gitea.emulab.it`
---
## 1. Genera la chiave SSH sull'host Windows
```powershell
# Da PowerShell normale (non admin necessario)
ssh-keygen -t ed25519 -C "ci-build@WS1-W11" -f "$env:USERPROFILE\.ssh\id_ci_gitea"
```
- Passphrase: **lascia vuota** (il CI gira non-interattivo)
- Produce due file:
- `~\.ssh\id_ci_gitea` — chiave privata (non condividere mai)
- `~\.ssh\id_ci_gitea.pub` — chiave pubblica da aggiungere a Gitea
---
## 2. Aggiungi la chiave pubblica a Gitea
1. Apri `http://10.10.20.11:3100` → Login come **Simone**
2. Menu utente (in alto a destra) → **Settings****SSH / GPG Keys**
3. Clicca **Add Key**
4. Incolla il contenuto di `~\.ssh\id_ci_gitea.pub`:
```powershell
Get-Content "$env:USERPROFILE\.ssh\id_ci_gitea.pub" | clip
```
5. **Key Name**: `WS1-CI-Host`
6. Salva → **Add Key**
---
## 3. Configura SSH client sull'host
> **Attenzione**: `10.10.20.11` è già usato nel file `~\.ssh\config` con `User root` (entry `HomeAssistant`).
> Non usare `Host 10.10.20.11` direttamente — usare un alias dedicato `gitea-ci`.
Aggiungi in fondo a `~\.ssh\config`:
```powershell
$entry = @"
Host gitea-ci
HostName 10.10.20.11
Port 2222
User git
IdentityFile $env:USERPROFILE\.ssh\id_ci_gitea
IdentitiesOnly yes
"@
Add-Content "$env:USERPROFILE\.ssh\config" $entry
```
---
## 4. Verifica connessione
```powershell
ssh -T gitea-ci
```
Risposta attesa:
```
Hi there, Simone! You've successfully authenticated with the key named ci-build@WS1-W11, but Gitea does not provide shell access.
```
> **Nota**: exit code 1 è **normale** — Gitea non fornisce shell, ma l'autenticazione è avvenuta.
---
## 5. ~~Trova la porta SSH di Gitea~~
**Porta confermata: 2222** (verificata con `Test-NetConnection -ComputerName 10.10.20.11 -Port 2222`).
---
## 6. URL SSH da usare in Invoke-CIJob.ps1
Usare l'alias `gitea-ci` definito in `~\.ssh\config`:
```powershell
-RepoUrl 'ssh://gitea-ci/Simone/nsis-plugin-nsinnounp.git'
```
Oppure con IP/porta espliciti (equivalente):
```powershell
-RepoUrl 'ssh://git@10.10.20.11:2222/Simone/nsis-plugin-nsinnounp.git'
```
---
## 7. Aggiungi host a known_hosts (evita prompt interattivo)
```powershell
ssh-keyscan -p 2222 10.10.20.11 >> "$env:USERPROFILE\.ssh\known_hosts"
```