Files
local-ci-cd-system/AGENTS.md
T
Simone 9f78742111 docs(cache): end-to-end numbers, operator runbook, and a vmrun gotcha
Measured through the real pipeline, not the unit tests: a Rust job goes from
186s to 84s (build phase 150s -> 47s), and 75s with the source modified, which
is the scenario CI actually runs. The entry is 645 MB and costs 8s to restore.

The test that decides whether this ships is row 4: a deliberate type error in
fury-core, built warm. The cache restored 645 MB of dependencies and the build
still went red with E0308, exit 1, publishing nothing. A cache that can turn a
red build green is the worst failure a CI system has because it is silent --
re-run that row whenever the prune rules change.

AGENTS.md #17 records something found while validating: `vmrun list` is
per-user. Run as the wrong user it answers "Total running VMs: 0" while a clone
is very much alive, which makes the "no VMs running" check before a
deploy --force (#14) unreliable. `ps | grep vmware-vmx` is user-independent and
was already the recommended diagnosis.
2026-07-22 22:15:32 +02:00

22 KiB

AGENTS.md — Environment Reference

Questo file descrive l'ambiente su cui e' costruito il progetto. Leggerlo prima di generare o modificare qualsiasi script.

Istruzione per gli agenti: questo file va tenuto aggiornato. Se durante il lavoro si scopre un errore nuovo, un vincolo non documentato, un pattern che ha causato problemi o una best-practice utile, aggiungerla nella sezione "Errori frequenti da evitare" o in una sezione apposita. Non aggiungere dettagli ovvi o gia' coperti; solo informazioni che impedirebbero errori concreti in sessioni future.


Host

Parametro Valore
OS Windows 11
Hardware i9-10900X, 64 GB RAM, NVMe SSD
PowerShell 5.1 (Windows PowerShell — NON Core/7)
VMware Workstation Pro, vmrun.exe in C:\Program Files (x86)\VMware\VMware Workstation\
SSH client ssh.exe / scp.exe built-in Windows 11 (PATH)
act_runner v1.0.2 — F:\CI\act_runner\act_runner.exe
Gitea http://10.10.20.11:3100 / https://gitea.emulab.it

PowerShell 5.1 — Vincoli critici

Tutti gli script devono girare su Windows PowerShell 5.1. Le seguenti sintassi sono disponibili solo in PS 7+ e NON vanno usate:

Costrutto PS 7+ Alternativa PS 5.1 corretta
$x ??= 'default' if ($null -eq $x) { $x = 'default' }
$a ?? $b if ($null -ne $a) { $a } else { $b }
cmd1 && cmd2 cmd1; if ($LASTEXITCODE -eq 0) { cmd2 }
cmd1 || cmd2 cmd1; if ($LASTEXITCODE -ne 0) { cmd2 }
$x is [int] $x -is [int]
Ternary $a ? $b : $c if ($a) { $b } else { $c }
foreach-object con && pipeline classico con -ErrorAction Stop

Ogni script deve avere in testa:

#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

Python development

L'orchestratore CI vive in src/ci_orchestrator/ (package ci-orchestrator, entry point python -m ci_orchestrator). Tutta la logica applicativa nuova va scritta in Python, non in PowerShell. Gli script PS in scripts/ sono ridotti a shim a 3 righe verso la CLI Python e sono mantenuti solo per compatibilita' con Task Scheduler e caller esterni.

Parametro Valore
Python 3.11+ richiesto (requires-python in pyproject.toml)
Venv produzione F:\CI\python\venv\ — install non-editable: pip install . (act_runner=LocalSystem; -e rompe con No module named ci_orchestrator). Ri-deploy a ogni modifica codice.
Venv dev locale .venv\ (NON committare; gitignored)
Install (dev locale) python -m pip install -e .[dev] — editable solo nel .venv dev
Test python -m pytest tests\python -q --cov=ci_orchestrator --cov-fail-under=90
Lint python -m ruff check src tests\python
Type-check python -m mypy --strict src

Convenzioni Python (enforced da ruff + mypy --strict):

  • Type hints obbligatori su funzioni pubbliche e helper esportati. Evitare Any se esiste un tipo concreto/Protocol; Any solo per fixture pytest e mock.
  • Niente variabili globali. Lo state vive in oggetti passati esplicitamente (factory backends.load_backend(config), Config dataclass).
  • Niente print(): usare click.echo (rispetta il flag -q di click e il redirect err=True per stderr).
  • Niente subprocess.run(..., shell=True). Niente shell-out per cose che esistono come libreria Python (paramiko per SSH, pypsrp per WinRM, keyring per credenziali).
  • Encoding stdio sempre UTF-8: i moduli Python lo assumono e runner/config.yaml esporta PYTHONIOENCODING=utf-8 ad act_runner.

Mappatura script PowerShell → sotto-comando Python (Fase A2/A3/A4 + C). I .ps1 qui sotto sono archiviati in scripts/archived/ (congelati, esenti da lint, nessun caller vivo — l'action Gitea chiama Python direttamente). Non chiamarli né estenderli; vedi scripts/archived/README.md.

PowerShell archiviato Python sub-command
Wait-VMReady.ps1 python -m ci_orchestrator wait-ready
New-BuildVM.ps1 python -m ci_orchestrator vm new
Remove-BuildVM.ps1 python -m ci_orchestrator vm remove
Cleanup-OrphanedBuildVMs.ps1 python -m ci_orchestrator vm cleanup
Invoke-RemoteBuild.ps1 python -m ci_orchestrator build run
Get-BuildArtifacts.ps1 python -m ci_orchestrator artifacts collect
Watch-DiskSpace.ps1 python -m ci_orchestrator monitor disk
Watch-RunnerHealth.ps1 python -m ci_orchestrator monitor runner
Get-CIJobSummary.ps1 python -m ci_orchestrator report job
Invoke-CIJob.ps1 python -m ci_orchestrator job
Start-BurnInTest.ps1 / -Linux python -m ci_orchestrator bench run
Test-CapacityBurnIn.ps1 python -m ci_orchestrator bench run
Test-Smoke.ps1 python -m ci_orchestrator smoke run
Validate-HostState.ps1 python -m ci_orchestrator validate host
Set-CIGuestCredential.ps1 python -m ci_orchestrator creds set

Restano in PowerShell (girano sull'host Windows fuori dal flusso CI o nel guest, non ancora portati): Setup-Host.ps1, scripts/Register-CIScheduledTasks.ps1, scripts/Backup-CITemplate.ps1, scripts/Invoke-RetentionPolicy.ps1, scripts/Set-TemplateSharedFolders.ps1, scripts/Test-CIGuestWinRM.ps1, gli harness e2e scripts/Test-E2E-Section3.3.ps1 / Test-Ns7zipBuild-Linux.ps1 / Test-NsinnounpBuild*.ps1, tutti i template/Deploy-*.ps1, template/Prepare-*.ps1, template/Install-CIToolchain-*.ps1.

Test gate: coverage totale ≥90% (alzato in A5, poi a 90%). Nuovi moduli devono includere unit test con mock; gli errori frequenti di questa stessa sezione sono coperti da tests/python/test_agents_errors.py come regression test — non rimuoverli senza aggiornare la voce in "Errori frequenti da evitare" e motivare la rimozione.

Closeout fasi A: vedi plans/A1-closeout.md, plans/A2-closeout.md, plans/A3-closeout.md, plans/A4-closeout.md, plans/A5-closeout.md per stato per-fase, decisioni prese e voci pendenti a carico utente.


Struttura directory host (fissa)

F:\CI\
  act_runner\          — binario e stato act_runner
  Templates\
    WinBuild2025\      — template Windows Server 2025
    WinBuild2022\      — template Windows Server 2022
    LinuxBuild2404\    — template Ubuntu 24.04
  BuildVMs\            — clone efimeri (creati/distrutti per ogni job)
  Artifacts\           — artifact raccolti (per job-id)
  ISO\                 — ISO/VMDK di bootstrap (cache download)
  keys\
    ci_linux           — chiave SSH privata per guest Linux (no passphrase)
    ci_linux.pub       — chiave pubblica corrispondente

VM Template

Windows (WinBuild2025 — primario)

Parametro Valore
OS Windows Server 2025
VMX F:\CI\Templates\WinBuild2025\WinBuild2025.vmx
Snapshot clone BaseClean
Transport WinRM HTTPS porta 5986, cert self-signed (lab-only)
Credenziali Windows Credential Manager target BuildVMGuest
Rete VMnet8 NAT — 192.168.79.0/24
Toolchain VS Build Tools 2026 (MSBuild 18.5 / v145), .NET SDK 10, Python 3.13, Git, 7-Zip

Variante WinBuild2022: path F:\CI\Templates\WinBuild2022\WinBuild2022.vmx, stesso snapshot BaseClean.

Linux (LinuxBuild2404)

Parametro Valore
OS Ubuntu 24.04 LTS (cloud image)
VMX F:\CI\Templates\LinuxBuild2404\LinuxBuild2404.vmx
Snapshot clone BaseClean-Linux
Transport SSH porta 22, utente ci_build, chiave F:\CI\keys\ci_linux
Rete VMnet8 NAT — 192.168.79.0/24
Toolchain GCC, G++, Clang, CMake, Ninja, Python 3, Git, 7-Zip
CI dirs /opt/ci/{build,output,scripts,cache} — owner ci_build

Linux Rust (LinuxRust2404)

Variante di LinuxBuild2404 per workspace Rust (es. app Tauri): stessa cloud image e stesso layout guest, provisioning via template/Install-CIToolchain-LinuxRust2404.sh che aggiunge le librerie webview/GTK di Tauri v2 e una toolchain Rust system-wide sotto /opt/rust (rustup + rustfmt + clippy + binario cargo-audit; il DB advisory di cargo-audit NON e' bakato, viene fetchato a runtime).

Parametro Valore
OS Ubuntu 24.04 LTS (cloud image)
VMX LinuxRust2404/LinuxRust2404.vmx (sotto la dir templates)
Snapshot clone BaseClean-LinuxRust
Transport SSH porta 22, utente ci_build, stessa chiave di LinuxBuild2404
Toolchain quella di LinuxBuild2404 + webview/GTK Tauri + Rust in /opt/rust
Comandi ci template deploy-rust / ci template prepare-rust

--rust-channel (default stable) controlla la toolchain bakata: pinnare una versione esatta (es. 1.97.1) per uno snapshot deterministico; con stable rustup riscarica a runtime ad ogni nuova stable. Rigenerare lo snapshot quando cambia il pin o la baseline apt di sicurezza.

Due vincoli sull'albero /opt/rust, entrambi regressioni gia' occorse:

  1. chown DOPO l'installazione. Lo script gira come root, quindi tutto cio' che rustup/cargo creano nasce root-owned: un chown fatto solo prima copre le due directory vuote e nient'altro. Senza il chown finale ci_build non puo' scrivere ne' in RUSTUP_HOME ne' in CARGO_HOME/registry, e ogni build Rust con dipendenze esterne muore con Permission denied (os error 13). Gli assert devono verificare la scrivibilita' (sudo -u ci_build test -w), non solo test -x sui binari: altrimenti il template rotto passa la sua build e fallisce nei job dei consumatori.
  2. /etc/profile.d da solo non basta. L'orchestratore esegue i comandi con paramiko exec_command + sh -c (transport/ssh.py, commands/build.py): shell non di login, profile.d non viene mai sorgiato. RUSTUP_HOME/CARGO_HOME vanno anche in /etc/environment (letto da pam_env all'apertura della sessione SSH) e i proxy rustup symlinkati in /usr/local/bin.

Transport layer

  • Windows guest: WinRM (New-PSSession con New-CISessionOption da _Common.psm1). Usare Invoke-Command -Session, Copy-Item -ToSession / -FromSession.
  • Linux guest: SSH/SCP via ssh.exe / scp.exe. Usare le funzioni di scripts/_Transport.psm1: Invoke-SshCommand, Copy-SshItem, Test-SshReady.
  • vmrun.exe: sempre invocato tramite Invoke-Vmrun da _Common.psm1 (wrapper uniforme che gestisce $LASTEXITCODE e output).

NON modificare il branch WinRM esistente quando si lavora sul branch Linux e viceversa.


act_runner / Gitea Actions

  • Labels runner: windows-build:host, linux-build:host
  • Env vars iniettate in ogni job (da runner/config.yaml):
    • GITEA_CI_TEMPLATE_PATH — VMX Windows template
    • GITEA_CI_LINUX_TEMPLATE_PATH — VMX Linux template
    • GITEA_CI_CLONE_BASE_DIR — F:\CI\BuildVMs
    • GITEA_CI_ARTIFACT_DIR — F:\CI\Artifacts
    • GITEA_CI_GUEST_CRED_TARGET — BuildVMGuest
    • GITEA_CI_SSH_KEY_PATH — F:\CI\keys\ci_linux
    • CI_PYTHON_LAUNCHER — absolute path al Python di sistema usato per bootstrap del venv produzione (F:\CI\python\venv). act_runner gira come SYSTEM con PATH sanitizzato (no python/py.exe risolvibili), quindi i workflow Python leggono questo env come single source of truth. Aggiornare solo quando si fa upgrade dell'interprete.

Regole di stile (PSScriptAnalyzer)

Configurazione in PSScriptAnalyzerSettings.psd1. Regole principali:

  • Indentazione: 4 spazi (no tab)
  • Write-Host e' permesso (output catturato da act_runner; Write-Output interferisce con i return value)
  • Invoke-Expression vietato
  • Funzioni state-changing devono supportare -WhatIf / ShouldProcess
  • Credenziali solo come [PSCredential], mai plain-text string
  • Nessuna variabile globale ($global:)

Errori frequenti da evitare

  1. Sintassi PS 7+ in script PS 5.1 — vedi tabella sopra.

  2. $LASTEXITCODE non controllato dopo vmrun, ssh, scp — controllare sempre e fare throw se non zero.

  3. Percorsi hardcoded senza parametro — ogni path critico deve avere un parametro con default documentato.

  4. Modificare entrambi i branch (WinRM e SSH) nello stesso script — mantenerli separati con if ($GuestOS -eq 'Linux').

  5. Usare New-PSSession / WinRM verso host Linux — Linux usa solo SSH tramite _Transport.psm1.

  6. Non rimuovere il seed ISO dal VMX dopo il primo boot Linux — ide1:0.present = "FALSE" deve essere settato dopo cloud-init.

  7. [ordered]@{} non disponibile in PS 2 — non e' un problema qui (PS 5.1), ma ricordare che [ordered] e' PS 3+.

  8. ForEach-Object -Parallel — disponibile solo in PS 7+; usare foreach classico o Start-Job.

  9. Snapshot template catturato con VM accesa — produce file *.vmem / *.vmsn con memoria; vmrun clone ... linked -snapshot=<name> rifiuta con Error: The virtual machine should not be powered on. It is already running. anche se vmrun list mostra 0 VM. Catturare snapshot SOLO da stato fully powered-off (verificare assenza di .vmem nella dir template).

  10. vmrun getGuestIPAddress per check "VM running" — richiede VMware Tools attivi nel guest; in headless Tools puo' impiegare 30-60s a rispondere e il check appare bloccato. Usare vmrun list e cercare il path del VMX nell'output per is_running. Per recuperare l'IP usare getGuestIPAddress -wait (blocca internamente finché Tools registrano l'IP) e filtrare le righe che iniziano con "Error:" perché vmrun in modalità headless può tornare exit code 0 con "Error: The VMware Tools are not running..." su stdout invece di usare un exit code non-zero.

  11. Machine-id identico tra clone Linux — le cloud image Ubuntu hanno un /etc/machine-id fisso. VMware DHCP assegna l'IP tramite DHCP client-id derivato da machine-id: se tutti i clone hanno lo stesso machine-id ottengono lo stesso IP e si bloccano per "IP collision". Fix nel template (prima di prendere lo snapshot): sudo truncate -s 0 /etc/machine-id && sudo rm -f /var/lib/dbus/machine-id && sudo shutdown -h now, poi ricreare lo snapshot BaseClean-Linux da stato powered-off.

  12. & nativecmd 2>$null NON sopprime stderr in PS 5.1 con $ErrorActionPreference='Stop' — la stderr del processo nativo viene convertita in ErrorRecord nel pipeline di PowerShell; sotto 'Stop' questo diventa un errore terminante PRIMA che 2>$null possa scartarlo. Esempio: ssh-keygen -R <ip> -f <file> stampa "Host X not found in file" su stderr → eccezione terminante. Fix: usare $ErrorActionPreference = 'SilentlyContinue' attorno alla chiamata oppure 2>&1 | Out-Null. Per i job CI su VM ephemere locali, usare direttamente StrictHostKeyChecking=no UserKnownHostsFile=NUL (default in _Transport.psm1 con KnownHostsFile='') — nessun ssh-keygen necessario.

  13. Quote WinRM/desktop-heap troppo basse per builds Windows paralleli — sintomo: MSBuild error MSB6003: CL.exe could not be run. System.ComponentModel.Win32Exception (0x80004005): Not enough quota is available to process this command (oppure le altre tool MSVC: link.exe, mt.exe) quando il build interno fa fan-out parallelo (es. matrix x86/x64/x86-ansi, MSBuild /maxcpucount). Causa: il plugin endpoint Microsoft.PowerShell del guest ha MaxMemoryPerShellMB=150 di default e l'heap del desktop non-interattivo (Session 0) ha SharedSection 3rd field=768 KB — entrambi insufficienti per più CL.exe concorrenti via WinRM. Non emerge in pre-migration Win→Win perché Invoke-Command su PSSession localhost-style eredita quote più larghe; emerge appena il path passa per pypsrp Linux→Win. Fix nel template (Deploy-WinBuild2025.ps1 / 2022 lo fa, Install-CIToolchain lo valida): Set-Item WSMan:\localhost\Plugin\Microsoft.PowerShell\Quotas\MaxMemoryPerShellMB 2048, Set-Item WSMan:\localhost\Shell\MaxProcessesPerShell 100, registry HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\SubSystems\Windows patch del 3° campo SharedSection a 4096, reboot (csrss legge SharedSection solo al boot), nuovo BaseClean. Dettagli e procedura completa in docs/WINDOWS-TEMPLATE-SETUP.md § "WinRM quotas e desktop heap".

  14. deploy-* --force lanciato mentre la VM target è ACCESA — corrompe la VM in esecuzione e produce un timeout allo Step 7 che sembra un problema di rete. Occorso il 2026-07-22 su LinuxRust2404, con un prepare-rust ancora in corso sulla stessa VMX. Meccanica: (a) Step 1 vede <disk>.vmdk.lck e lo cancella come "stale artifact" — non era stale, apparteneva al processo vivo; (b) Step 4 riscrive il VMDK base sotto il guest in esecuzione, che muore; (c) Step 6 lancia il power-on, il nuovo vmware-vmx muore subito perché la VMX è già occupata, ma il controllo [OK] VM appears in vmrun list passa lo stesso perché vede l'istanza vecchia; (d) Step 7 interroga per 10 minuti un guest morto e riporta Error: The VMware Tools are not running in the virtual machine. Diagnosi in 5 secondi: ps -eo pid,lstart,etime,args | grep vmware-vmx — se il vmware-vmx vivo è più vecchio del deploy appena lanciato, è questo il caso. Recupero: vmrun stop <vmx> hard, rimuovere i delta orfani <disk>-00000*.vmdk, verificare vmrun list = 0, poi rilanciare. Non interpretarlo come DHCP incastrato (vedi #15) e non risolverlo con un retry automatico: riavviare una VM già corrotta ripete il fallimento con la causa ormai illeggibile.

  15. Step 7 in timeout: distinguere tre cause diverse che danno lo stesso messaggio — getGuestIPAddress risponde The VMware Tools are not running sia quando il guest non ha IP, sia quando ha l'IP ma i Tools non rispondono. Ordine di verifica, dal più veloce: (a) collisione — ps | grep vmware-vmx, processo più vecchio del deploy corrente → vedi #14; (b) DHCP host incastrato — ip -4 addr show vmnet8 deve dare 192.168.79.1/24 e ps -ef | grep vmnet-dhcpd deve mostrarlo vivo; se no, sudo systemctl restart vmware-networks (o vmware-host.service); (c) guest senza lease o Tools non pronti — cercare il MAC della VMX in /etc/vmware/vmnet8/dhcpd/dhcpd.leases e in ip neigh show dev vmnet8: se un lease c'è, il guest è vivo e mancano solo i Tools, quindi sondare TCP/22 direttamente invece di aspettare getGuestIPAddress.

  16. Delta di snapshot orfani dopo --force — deploy-* cancella <vm>.vmsd (il database snapshot) per evitare l'errore di resize sulla catena, ma non cancella i file delta <disk>-00000N.vmdk. Restano su disco con un parentCID che non corrisponde più al base ricreato: innocui (fuori dal percorso di boot, la VMX punta al base con parentCID=ffffffff) ma confondono la diagnosi e vanno rimossi a mano. Verifica della catena: head -c 2000 <disk>.vmdk | strings | grep -E '^CID|parentCID'.

  17. vmrun list è per-utente e mente se lo lanci con l'utente sbagliato — le VM della CI le avvia ci-runner; vmrun -T ws list eseguito come simone (o come root) risponde Total running VMs: 0 anche mentre un clone sta girando, perché vede solo l'istanza VMware dell'utente corrente. Occorso il 2026-07-22 durante la validazione della cache cargo: vmrun list diceva 0 e ps mostrava un vmware-vmx vivo da 19 minuti. Conseguenza pratica: il check "0 VM accese" prima di un deploy --force (vedi #14) non è affidabile se non lo si esegue come ci-runner. Usare sudo -u ci-runner vmrun -T ws list, oppure — meglio, perché indipendente dall'utente — ps -eo pid,lstart,etime,args | grep '[v]mware-vmx', che è anche la diagnosi già raccomandata in #14 e #15.