Files
local-ci-cd-system/AGENTS.md
T
Simone 30fd3120a6 docs: record the Cloudflare user-agent trap (AGENTS.md #18)
Every Gitea API call from Python 403s with "error code: 1010" unless a
User-Agent is set, including endpoints that need no auth — and it looks like a
token/scope problem, which is where this went wrong once. Registered next to
the other environment traps, with the two-command check that tells them apart.
2026-07-25 15:28:29 +02:00

23 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.

  18. Gitea è dietro Cloudflare: Python-urllib riceve 403 prima di arrivare a Gitea — qualunque chiamata a https://gitea.emulab.it/api/v1/... fatta con urllib.request senza User-Agent esplicito torna 403 con body error code: 1010 (Cloudflare "browser banned"), su ogni endpoint, incluso /api/v1/version che non richiede autenticazione. Sembra identico a un problema di token/scope e manda la diagnosi fuori strada. Verifica in due mosse: curl -s https://gitea.emulab.it/api/v1/version risponde 200 (curl manda il suo UA) mentre la stessa chiamata in Python risponde 403; gli header di risposta mostrano server: cloudflare + cf-ray. Fix: header User-Agent esplicito — qualunque stringa normale passa (vedi operations/executions.py::_USER_AGENT). Corollario generale: quando un endpoint HTTP risponde 4xx, leggere il body prima di ipotizzare la causa — Gitea scrive il permesso mancante (required=[read:user], token scope=read:admin,read:repository), Cloudflare scrive il suo codice.