docs,config: aggiorna AGENTS.md, config.example, HOST-SETUP.md, checklist faseA e fix vari orchestrator (workstation, build, job, wait, winrm, test)
This commit is contained in:
@@ -223,6 +223,6 @@ Configurazione in `PSScriptAnalyzerSettings.psd1`. Regole principali:
|
|||||||
7. **`[ordered]@{}` non disponibile in PS 2** — non e' un problema qui (PS 5.1), ma ricordare che `[ordered]` e' PS 3+.
|
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`.
|
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).
|
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.
|
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.
|
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.
|
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.
|
||||||
|
|||||||
+8
-4
@@ -4,6 +4,14 @@
|
|||||||
# Environment variables (CI_ROOT, CI_TEMPLATES, CI_BUILD_VMS, CI_ARTIFACTS,
|
# Environment variables (CI_ROOT, CI_TEMPLATES, CI_BUILD_VMS, CI_ARTIFACTS,
|
||||||
# CI_KEYS, CI_VMRUN_PATH, CI_SSH_KEY_PATH, CI_GUEST_CRED_TARGET) override
|
# CI_KEYS, CI_VMRUN_PATH, CI_SSH_KEY_PATH, CI_GUEST_CRED_TARGET) override
|
||||||
# anything set here.
|
# anything set here.
|
||||||
|
#
|
||||||
|
# IMPORTANT (TOML rule): top-level keys must appear BEFORE any [section] header.
|
||||||
|
# Keys placed after a [section] belong to that section, not to the root table.
|
||||||
|
|
||||||
|
# Top-level orchestrator settings.
|
||||||
|
vmrun_path = "C:/Program Files (x86)/VMware/VMware Workstation/vmrun.exe"
|
||||||
|
ssh_key_path = "F:/CI/keys/ci_linux"
|
||||||
|
guest_cred_target = "BuildVMGuest"
|
||||||
|
|
||||||
# --------------------------------------------------------------- Windows host
|
# --------------------------------------------------------------- Windows host
|
||||||
[paths]
|
[paths]
|
||||||
@@ -24,7 +32,3 @@ keys = "F:/CI/keys"
|
|||||||
# Phase C hook: backend selector. Only "workstation" is implemented in Phase A.
|
# Phase C hook: backend selector. Only "workstation" is implemented in Phase A.
|
||||||
[backend]
|
[backend]
|
||||||
type = "workstation"
|
type = "workstation"
|
||||||
|
|
||||||
vmrun_path = "C:/Program Files (x86)/VMware/VMware Workstation/vmrun.exe"
|
|
||||||
ssh_key_path = "F:/CI/keys/ci_linux"
|
|
||||||
guest_cred_target = "BuildVMGuest"
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ F:\CI\
|
|||||||
├── keys\
|
├── keys\
|
||||||
│ ├── ci_linux # chiave privata SSH per Linux VMs (ed25519, no passphrase)
|
│ ├── ci_linux # chiave privata SSH per Linux VMs (ed25519, no passphrase)
|
||||||
│ └── ci_linux.pub # chiave pubblica corrispondente
|
│ └── ci_linux.pub # chiave pubblica corrispondente
|
||||||
|
├── config.toml # configurazione Python orchestrator (ssh_key_path, ecc.)
|
||||||
├── act_runner\
|
├── act_runner\
|
||||||
│ ├── act_runner.exe # binario runner Gitea
|
│ ├── act_runner.exe # binario runner Gitea
|
||||||
│ ├── config.yaml # config runner (path, label, capacity)
|
│ ├── config.yaml # config runner (path, label, capacity)
|
||||||
@@ -139,6 +140,22 @@ Allo `Setup-Host.ps1` exit 0:
|
|||||||
```
|
```
|
||||||
Poi **provisiona il template VM Linux** — vedi [LINUX-TEMPLATE-SETUP.md](LINUX-TEMPLATE-SETUP.md)
|
Poi **provisiona il template VM Linux** — vedi [LINUX-TEMPLATE-SETUP.md](LINUX-TEMPLATE-SETUP.md)
|
||||||
|
|
||||||
|
6. **Crea `F:\CI\config.toml`** — necessario affinché il Python orchestrator trovi la chiave SSH e le altre impostazioni:
|
||||||
|
```powershell
|
||||||
|
Copy-Item N:\Code\Workspace\Local-CI-CD-System\config.example.toml F:\CI\config.toml
|
||||||
|
```
|
||||||
|
Il file `config.example.toml` contiene già i path corretti per Windows (`F:\CI\...`). Modificare solo se si usa un `$CIRoot` diverso da `F:\CI`.
|
||||||
|
|
||||||
|
Valori chiave configurati:
|
||||||
|
|
||||||
|
| Chiave | Valore default | Scopo |
|
||||||
|
| -------------------- | ----------------------- | -------------------------------------------- |
|
||||||
|
| `ssh_key_path` | `F:/CI/keys/ci_linux` | Chiave privata per SSH verso Linux guest |
|
||||||
|
| `guest_cred_target` | `BuildVMGuest` | Target Windows Credential Manager (WinRM) |
|
||||||
|
| `backend.type` | `workstation` | Backend VMware (solo `workstation` in Phase A) |
|
||||||
|
|
||||||
|
Le variabili d'ambiente `CI_SSH_KEY_PATH`, `CI_VMRUN_PATH`, `CI_GUEST_CRED_TARGET` sovrascrivono i valori del TOML se presenti.
|
||||||
|
|
||||||
6. **Registra chiave SSH con Gitea**:
|
6. **Registra chiave SSH con Gitea**:
|
||||||
- Gitea UI → User Settings → SSH Keys → Add Key
|
- Gitea UI → User Settings → SSH Keys → Add Key
|
||||||
- Chiave pubblica: `%USERPROFILE%\.ssh\id_rsa.pub`
|
- Chiave pubblica: `%USERPROFILE%\.ssh\id_rsa.pub`
|
||||||
@@ -162,6 +179,7 @@ Allo `Setup-Host.ps1` exit 0:
|
|||||||
| Snapshot name (Windows) | `BaseClean` |
|
| Snapshot name (Windows) | `BaseClean` |
|
||||||
| Snapshot name (Linux) | `BaseClean-Linux` |
|
| Snapshot name (Linux) | `BaseClean-Linux` |
|
||||||
| SSH key (Linux VMs) | `F:\CI\keys\ci_linux` (privata), `F:\CI\keys\ci_linux.pub` |
|
| SSH key (Linux VMs) | `F:\CI\keys\ci_linux` (privata), `F:\CI\keys\ci_linux.pub` |
|
||||||
|
| Orchestrator config TOML | `F:\CI\config.toml` (copia di `config.example.toml`) |
|
||||||
| Clone base dir | `F:\CI\BuildVMs\` |
|
| Clone base dir | `F:\CI\BuildVMs\` |
|
||||||
| Artifact dir | `F:\CI\Artifacts\` |
|
| Artifact dir | `F:\CI\Artifacts\` |
|
||||||
| Log dir | `F:\CI\Logs\` (retention: 30 giorni) |
|
| Log dir | `F:\CI\Logs\` (retention: 30 giorni) |
|
||||||
@@ -196,3 +214,4 @@ Task schedulati (vedi `scripts/Register-CIScheduledTasks.ps1` per il bootstrap):
|
|||||||
| Service `act_runner` non parte | Check `F:\CI\act_runner\logs\stderr.log` |
|
| Service `act_runner` non parte | Check `F:\CI\act_runner\logs\stderr.log` |
|
||||||
| `New-StoredCredential` fail | Verifica modulo CredentialManager importato + scope corretto |
|
| `New-StoredCredential` fail | Verifica modulo CredentialManager importato + scope corretto |
|
||||||
| Job CI fallisce su `Test-WSMan` | TrustedHosts host non include subnet VM, AllowUnencrypted=$false |
|
| Job CI fallisce su `Test-WSMan` | TrustedHosts host non include subnet VM, AllowUnencrypted=$false |
|
||||||
|
| `wait-ready` SSH: `No authentication methods available` | `F:\CI\config.toml` mancante o `ssh_key_path` non configurato. Crea il file (vedi step 6) oppure passa `--ssh-key-path F:\CI\keys\ci_linux` |
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ copiabili. Stop a primo errore: ogni passo è un gate per il successivo.
|
|||||||
Il codice nuovo va installato nel venv che act_runner usa
|
Il codice nuovo va installato nel venv che act_runner usa
|
||||||
(`F:\CI\python\venv\`).
|
(`F:\CI\python\venv\`).
|
||||||
|
|
||||||
- [ ] Aprire PowerShell **come amministratore** sull'host CI.
|
- [x] Aprire PowerShell **come amministratore** sull'host CI.
|
||||||
- [ ] Posizionarsi nella copia git aggiornata del repo (pull del branch
|
- [x] Posizionarsi nella copia git aggiornata del repo (pull del branch
|
||||||
`feature/python-rewrite-and-linux-migration`):
|
`feature/python-rewrite-and-linux-migration`):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
@@ -27,13 +27,13 @@ Il codice nuovo va installato nel venv che act_runner usa
|
|||||||
git pull
|
git pull
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Installare il package in editable nel venv di produzione:
|
- [x] Installare il package in editable nel venv di produzione:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m pip install -e .
|
& 'F:\CI\python\venv\Scripts\python.exe' -m pip install -e .
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Smoke check rapido che la CLI risponda:
|
- [x] Smoke check rapido che la CLI risponda:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator --help
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator --help
|
||||||
@@ -50,19 +50,19 @@ L'act_runner gira come servizio SYSTEM e legge `runner/config.yaml`
|
|||||||
all'avvio. La modifica di A4 (`PYTHONIOENCODING=utf-8`) ha effetto solo
|
all'avvio. La modifica di A4 (`PYTHONIOENCODING=utf-8`) ha effetto solo
|
||||||
dopo restart.
|
dopo restart.
|
||||||
|
|
||||||
- [ ] Trovare il servizio:
|
- [x] Trovare il servizio:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-Service | Where-Object { $_.Name -like '*act*runner*' }
|
Get-Service | Where-Object { $_.Name -like '*act*runner*' }
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Restart (sostituisci `<NomeServizio>` con quello trovato):
|
- [x] Restart (sostituisci `<NomeServizio>` con quello trovato):
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Restart-Service -Name '<NomeServizio>'
|
Restart-Service -Name '<NomeServizio>'
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Verificare che sia partito:
|
- [x] Verificare che sia partito:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
Get-Service -Name '<NomeServizio>'
|
Get-Service -Name '<NomeServizio>'
|
||||||
@@ -76,7 +76,7 @@ dopo restart.
|
|||||||
|
|
||||||
PoC pendente da A1.
|
PoC pendente da A1.
|
||||||
|
|
||||||
- [ ] Clonare manualmente un template Windows:
|
- [x] Clonare manualmente un template Windows:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
||||||
@@ -86,14 +86,14 @@ PoC pendente da A1.
|
|||||||
linked -snapshot=BaseClean -cloneName=smoke-win
|
linked -snapshot=BaseClean -cloneName=smoke-win
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Avviare la VM:
|
- [x] Avviare la VM:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
||||||
-T ws start 'F:\CI\BuildVMs\smoke-win\smoke-win.vmx' nogui
|
-T ws start 'F:\CI\BuildVMs\smoke-win\smoke-win.vmx' nogui
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Eseguire `wait-ready`:
|
- [x] Eseguire `wait-ready`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator wait-ready `
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator wait-ready `
|
||||||
@@ -103,7 +103,7 @@ PoC pendente da A1.
|
|||||||
|
|
||||||
Atteso: exit code `0` entro 3 minuti.
|
Atteso: exit code `0` entro 3 minuti.
|
||||||
|
|
||||||
- [ ] Cleanup VM di smoke:
|
- [x] Cleanup VM di smoke:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator vm remove `
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator vm remove `
|
||||||
@@ -116,7 +116,7 @@ PoC pendente da A1.
|
|||||||
|
|
||||||
Stesso schema, template Linux. PoC pendente da A1.
|
Stesso schema, template Linux. PoC pendente da A1.
|
||||||
|
|
||||||
- [ ] Clonare:
|
- [x] Clonare:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
||||||
@@ -126,7 +126,7 @@ Stesso schema, template Linux. PoC pendente da A1.
|
|||||||
linked -snapshot=BaseClean-Linux -cloneName=smoke-linux
|
linked -snapshot=BaseClean-Linux -cloneName=smoke-linux
|
||||||
```
|
```
|
||||||
|
|
||||||
- [ ] Start + wait-ready:
|
- [x] Start + wait-ready:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
& 'C:\Program Files (x86)\VMware\VMware Workstation\vmrun.exe' `
|
||||||
@@ -139,7 +139,11 @@ Stesso schema, template Linux. PoC pendente da A1.
|
|||||||
|
|
||||||
Atteso: exit code `0`.
|
Atteso: exit code `0`.
|
||||||
|
|
||||||
- [ ] Cleanup:
|
> **Prerequisito scoperto**: `F:\CI\config.toml` deve esistere con `ssh_key_path`
|
||||||
|
> impostato — altrimenti SSH fallisce con `No authentication methods available`.
|
||||||
|
> Creato durante questa validazione; vedi HOST-SETUP.md step 6.
|
||||||
|
|
||||||
|
- [x] Cleanup:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator vm remove `
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator vm remove `
|
||||||
@@ -152,29 +156,44 @@ Stesso schema, template Linux. PoC pendente da A1.
|
|||||||
|
|
||||||
Valida A3+A4 insieme: clone → wait → build → collect → cleanup.
|
Valida A3+A4 insieme: clone → wait → build → collect → cleanup.
|
||||||
|
|
||||||
- [ ] Lanciare un job sul template Windows con uno script trivial:
|
- [x] Lanciare un job sul template Windows con uno script trivial:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator job `
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator job `
|
||||||
--template 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' `
|
--template-path 'F:\CI\Templates\WinBuild2025\WinBuild2025.vmx' `
|
||||||
--snapshot BaseClean `
|
--snapshot-name BaseClean `
|
||||||
--name smoke-job-win `
|
--job-id smoke-job-win `
|
||||||
|
--repo-url 'http://10.10.20.11:3100/Simone/your-repo.git' `
|
||||||
|
--branch main `
|
||||||
--build-command 'echo hello > artifact.txt' `
|
--build-command 'echo hello > artifact.txt' `
|
||||||
--artifact-source 'C:\ci\workspace\artifact.txt' `
|
--guest-artifact-source 'C:\ci\workspace\artifact.txt' `
|
||||||
--artifact-dest 'F:\CI\Artifacts\smoke-job-win'
|
--artifact-base-dir 'F:\CI\Artifacts\smoke-job-win'
|
||||||
```
|
```
|
||||||
|
|
||||||
Atteso: exit code `0`, file `F:\CI\Artifacts\smoke-job-win\artifact.txt` presente,
|
Atteso: exit code `0`, file `F:\CI\Artifacts\smoke-job-win\artifact.txt` presente,
|
||||||
VM smoke-job-win **non** più esistente in `F:\CI\BuildVMs\` (cleanup garantito).
|
VM smoke-job-win **non** più esistente in `F:\CI\BuildVMs\` (cleanup garantito).
|
||||||
|
|
||||||
- [ ] Ripetere su Linux (cambiare `--template`, `--snapshot`,
|
- [x] Ripetere su Linux:
|
||||||
`--build-command 'echo hello > artifact.txt'`,
|
|
||||||
`--artifact-source '/opt/ci/build/artifact.txt'`).
|
|
||||||
|
|
||||||
- [ ] Verificare assenza VM orfane:
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator vm cleanup --dry-run
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator job `
|
||||||
|
--template-path 'F:\CI\Templates\LinuxBuild2404\LinuxBuild2404.vmx' `
|
||||||
|
--snapshot-name BaseClean-Linux `
|
||||||
|
--guest-os linux `
|
||||||
|
--job-id smoke-job-linux `
|
||||||
|
--repo-url 'http://10.10.20.11:3100/Simone/your-repo.git' `
|
||||||
|
--branch main `
|
||||||
|
--build-command 'echo hello > artifact.txt' `
|
||||||
|
--guest-artifact-source 'artifact.txt' `
|
||||||
|
--artifact-base-dir 'F:\CI\Artifacts\smoke-job-linux'
|
||||||
|
```
|
||||||
|
|
||||||
|
Atteso: exit code `0`, `F:\CI\Artifacts\smoke-job-linux\smoke-job-linux\artifact.txt` presente.
|
||||||
|
|
||||||
|
- [x] Verificare assenza VM orfane:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
& 'F:\CI\python\venv\Scripts\python.exe' -m ci_orchestrator vm cleanup --what-if
|
||||||
```
|
```
|
||||||
|
|
||||||
Atteso: nessun candidato.
|
Atteso: nessun candidato.
|
||||||
@@ -260,11 +279,11 @@ Solo dopo che i passi 3–7 sono tutti `[x]` PASS.
|
|||||||
|
|
||||||
| Passo | Descrizione | Stato |
|
| Passo | Descrizione | Stato |
|
||||||
| ----- | ----------------------------------------- | ----- |
|
| ----- | ----------------------------------------- | ----- |
|
||||||
| 1 | Aggiorna venv di produzione | [ ] |
|
| 1 | Aggiorna venv di produzione | [x] |
|
||||||
| 2 | Restart act_runner | [ ] |
|
| 2 | Restart act_runner | [x] |
|
||||||
| 3 | Smoke `wait-ready` Windows | [ ] |
|
| 3 | Smoke `wait-ready` Windows | [x] |
|
||||||
| 4 | Smoke `wait-ready` Linux | [ ] |
|
| 4 | Smoke `wait-ready` Linux | [x] |
|
||||||
| 5 | Smoke `job` end-to-end (Win + Linux) | [ ] |
|
| 5 | Smoke `job` end-to-end (Win + Linux) | [x] |
|
||||||
| 6 | Workflow Gitea (lint + self-test + build) | [ ] |
|
| 6 | Workflow Gitea (lint + self-test + build) | [ ] |
|
||||||
| 7 | Burn-in 4 job × 10 round | [ ] |
|
| 7 | Burn-in 4 job × 10 round | [ ] |
|
||||||
| 8 | Benchmark wall-clock | [ ] |
|
| 8 | Benchmark wall-clock | [ ] |
|
||||||
|
|||||||
@@ -121,11 +121,16 @@ class WorkstationVmrunBackend:
|
|||||||
args: list[str] = [handle.identifier]
|
args: list[str] = [handle.identifier]
|
||||||
if timeout > 0:
|
if timeout > 0:
|
||||||
args.extend(["-wait"])
|
args.extend(["-wait"])
|
||||||
|
try:
|
||||||
result = self._run("getGuestIPAddress", *args, check=False, timeout=timeout or None)
|
result = self._run("getGuestIPAddress", *args, check=False, timeout=timeout or None)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return None
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
return None
|
return None
|
||||||
ip = (result.stdout or "").strip()
|
ip = (result.stdout or "").strip()
|
||||||
return ip or None
|
if not ip or ip.lower().startswith("error:"):
|
||||||
|
return None
|
||||||
|
return ip
|
||||||
|
|
||||||
def list_snapshots(self, handle: VmHandle) -> list[str]:
|
def list_snapshots(self, handle: VmHandle) -> list[str]:
|
||||||
result = self._run("listSnapshots", handle.identifier)
|
result = self._run("listSnapshots", handle.identifier)
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ def _linux_build(
|
|||||||
elif clone_url:
|
elif clone_url:
|
||||||
click.echo(f"[build run] cloning {clone_url} into guest:{workdir}")
|
click.echo(f"[build run] cloning {clone_url} into guest:{workdir}")
|
||||||
cmd = (
|
cmd = (
|
||||||
|
f"GIT_TERMINAL_PROMPT=0 "
|
||||||
f"git clone --depth 1 --branch {_sh_quote(clone_branch)} "
|
f"git clone --depth 1 --branch {_sh_quote(clone_branch)} "
|
||||||
f"{'--recurse-submodules ' if clone_submodules else ''}"
|
f"{'--recurse-submodules ' if clone_submodules else ''}"
|
||||||
f"{_sh_quote(clone_url)} {_sh_quote(workdir)}"
|
f"{_sh_quote(clone_url)} {_sh_quote(workdir)}"
|
||||||
@@ -314,12 +315,14 @@ def _windows_build(
|
|||||||
f"$src = {src_expr}; "
|
f"$src = {src_expr}; "
|
||||||
f"$zip = {_ps_quote(artifact_zip)}; "
|
f"$zip = {_ps_quote(artifact_zip)}; "
|
||||||
"if (-not (Test-Path $src)) { "
|
"if (-not (Test-Path $src)) { "
|
||||||
" Write-Host \"[build run] artifact source not found: $src\"; "
|
" throw \"[build run] artifact source not found: $src\" }; "
|
||||||
" exit 2 } ; "
|
|
||||||
"$dir = Split-Path $zip -Parent; "
|
"$dir = Split-Path $zip -Parent; "
|
||||||
"if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }; "
|
"if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }; "
|
||||||
"if (Test-Path $zip) { Remove-Item $zip -Force }; "
|
"if (Test-Path $zip) { Remove-Item $zip -Force }; "
|
||||||
"Compress-Archive -Path (Join-Path $src '*') -DestinationPath $zip -Force"
|
"if (Test-Path $src -PathType Leaf) { "
|
||||||
|
" Compress-Archive -Path $src -DestinationPath $zip -Force "
|
||||||
|
"} else { "
|
||||||
|
" Compress-Archive -Path (Join-Path $src '*') -DestinationPath $zip -Force }"
|
||||||
)
|
)
|
||||||
t.run(pkg_ps)
|
t.run(pkg_ps)
|
||||||
click.echo(f"[build run] artifact written to {artifact_zip}")
|
click.echo(f"[build run] artifact written to {artifact_zip}")
|
||||||
|
|||||||
@@ -135,14 +135,14 @@ def _wait_running(
|
|||||||
def _wait_for_ip(
|
def _wait_for_ip(
|
||||||
backend: VmBackend, handle: VmHandle, deadline: float, poll: float
|
backend: VmBackend, handle: VmHandle, deadline: float, poll: float
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
while _now() < deadline:
|
while (now := _now()) < deadline:
|
||||||
|
call_timeout = min(poll, deadline - now)
|
||||||
try:
|
try:
|
||||||
ip = backend.get_ip(handle)
|
ip = backend.get_ip(handle, timeout=call_timeout)
|
||||||
except BackendError:
|
except BackendError:
|
||||||
ip = None
|
ip = None
|
||||||
if ip:
|
if ip:
|
||||||
return ip
|
return ip
|
||||||
time.sleep(poll)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -328,7 +328,22 @@ def job(
|
|||||||
"""
|
"""
|
||||||
# Reserved for future host-side clone variant; the Python port always
|
# Reserved for future host-side clone variant; the Python port always
|
||||||
# provisions sources via in-guest ``git clone``.
|
# provisions sources via in-guest ``git clone``.
|
||||||
del submodules, use_git_clone, gitea_credential_target
|
del submodules, use_git_clone
|
||||||
|
|
||||||
|
# Inject Gitea credentials into HTTP(S) clone URL so git doesn't prompt.
|
||||||
|
authed_repo_url = repo_url
|
||||||
|
if repo_url and repo_url.startswith(("http://", "https://")):
|
||||||
|
from urllib.parse import urlparse, urlunparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
gitea_cred = KeyringCredentialStore().get(gitea_credential_target)
|
||||||
|
parsed = urlparse(repo_url)
|
||||||
|
host = parsed.hostname or ""
|
||||||
|
port_part = f":{parsed.port}" if parsed.port else ""
|
||||||
|
netloc = f"{gitea_cred.username}:{gitea_cred.password}@{host}{port_part}"
|
||||||
|
authed_repo_url = urlunparse(parsed._replace(netloc=netloc))
|
||||||
|
except Exception:
|
||||||
|
pass # no credential stored — try unauthenticated (public repo)
|
||||||
|
|
||||||
config: Config = load_config()
|
config: Config = load_config()
|
||||||
if not template_path:
|
if not template_path:
|
||||||
@@ -463,7 +478,7 @@ def job(
|
|||||||
workdir="/opt/ci/build",
|
workdir="/opt/ci/build",
|
||||||
output_dir="/opt/ci/output",
|
output_dir="/opt/ci/output",
|
||||||
host_source_dir=None,
|
host_source_dir=None,
|
||||||
clone_url=repo_url,
|
clone_url=authed_repo_url,
|
||||||
clone_branch=branch,
|
clone_branch=branch,
|
||||||
clone_commit=commit,
|
clone_commit=commit,
|
||||||
clone_submodules=False,
|
clone_submodules=False,
|
||||||
@@ -479,7 +494,7 @@ def job(
|
|||||||
workdir="C:\\CI\\build",
|
workdir="C:\\CI\\build",
|
||||||
artifact_zip="C:\\CI\\output\\artifacts.zip",
|
artifact_zip="C:\\CI\\output\\artifacts.zip",
|
||||||
host_source_dir=None,
|
host_source_dir=None,
|
||||||
clone_url=repo_url,
|
clone_url=authed_repo_url,
|
||||||
clone_branch=branch,
|
clone_branch=branch,
|
||||||
clone_commit=commit,
|
clone_commit=commit,
|
||||||
clone_submodules=False,
|
clone_submodules=False,
|
||||||
|
|||||||
@@ -61,14 +61,14 @@ def _wait_running(
|
|||||||
def _wait_for_ip(
|
def _wait_for_ip(
|
||||||
backend: VmBackend, handle: VmHandle, deadline: float, poll: float
|
backend: VmBackend, handle: VmHandle, deadline: float, poll: float
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
while time.monotonic() < deadline:
|
while (now := time.monotonic()) < deadline:
|
||||||
|
call_timeout = min(poll, deadline - now)
|
||||||
try:
|
try:
|
||||||
ip = backend.get_ip(handle)
|
ip = backend.get_ip(handle, timeout=call_timeout)
|
||||||
except BackendError:
|
except BackendError:
|
||||||
ip = None
|
ip = None
|
||||||
if ip:
|
if ip:
|
||||||
return ip
|
return ip
|
||||||
time.sleep(poll)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -153,6 +153,7 @@ def wait_ready(
|
|||||||
sys.exit(_EXIT_TIMEOUT_RUNNING)
|
sys.exit(_EXIT_TIMEOUT_RUNNING)
|
||||||
|
|
||||||
if ip_address is None:
|
if ip_address is None:
|
||||||
|
click.echo("[wait-ready] waiting for guest IP (VMware Tools must be running)...")
|
||||||
ip = _wait_for_ip(backend, handle, deadline, poll_interval)
|
ip = _wait_for_ip(backend, handle, deadline, poll_interval)
|
||||||
if not ip:
|
if not ip:
|
||||||
click.echo("[wait-ready] timeout waiting for guest IP.", err=True)
|
click.echo("[wait-ready] timeout waiting for guest IP.", err=True)
|
||||||
|
|||||||
@@ -116,11 +116,15 @@ class WinRmTransport:
|
|||||||
stdout, stderr, returncode = client.execute_ps(script)
|
stdout, stderr, returncode = client.execute_ps(script)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
raise TransportConnectError(f"WinRM execute_ps failed: {exc}") from exc
|
raise TransportConnectError(f"WinRM execute_ps failed: {exc}") from exc
|
||||||
# pypsrp returns stderr as a list of PSRP error records in some versions;
|
# pypsrp returns stderr as a PSDataStreams object (newer versions) or a
|
||||||
# normalise to a string.
|
# list of error records (older versions); normalise to a plain string.
|
||||||
stderr_any: Any = stderr
|
stderr_any: Any = stderr
|
||||||
if isinstance(stderr_any, str):
|
if isinstance(stderr_any, str):
|
||||||
stderr_str = stderr_any
|
stderr_str = stderr_any
|
||||||
|
elif hasattr(stderr_any, "error"):
|
||||||
|
# PSDataStreams — extract .error records.
|
||||||
|
error_records = getattr(stderr_any, "error", None) or []
|
||||||
|
stderr_str = "\n".join(str(e) for e in error_records)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
stderr_str = "\n".join(str(s) for s in stderr_any)
|
stderr_str = "\n".join(str(s) for s in stderr_any)
|
||||||
|
|||||||
@@ -96,6 +96,19 @@ def test_get_ip_returns_none_on_failure(monkeypatch: pytest.MonkeyPatch, fake_vm
|
|||||||
assert backend.get_ip(VmHandle("x.vmx")) is None
|
assert backend.get_ip(VmHandle("x.vmx")) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_ip_returns_none_when_tools_not_running(monkeypatch: pytest.MonkeyPatch, fake_vmrun: str) -> None:
|
||||||
|
"""vmrun exits 0 but writes 'Error: ...' to stdout — must return None, not the error string."""
|
||||||
|
monkeypatch.setattr(
|
||||||
|
subprocess,
|
||||||
|
"run",
|
||||||
|
lambda *a, **kw: _make_completed(
|
||||||
|
0, stdout="Error: The VMware Tools are not running in the virtual machine: x.vmx\n"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
backend = WorkstationVmrunBackend(vmrun_path=fake_vmrun)
|
||||||
|
assert backend.get_ip(VmHandle("x.vmx")) is None
|
||||||
|
|
||||||
|
|
||||||
def test_get_ip_returns_address(monkeypatch: pytest.MonkeyPatch, fake_vmrun: str) -> None:
|
def test_get_ip_returns_address(monkeypatch: pytest.MonkeyPatch, fake_vmrun: str) -> None:
|
||||||
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: _make_completed(0, stdout="192.168.79.42\n"))
|
monkeypatch.setattr(subprocess, "run", lambda *a, **kw: _make_completed(0, stdout="192.168.79.42\n"))
|
||||||
backend = WorkstationVmrunBackend(vmrun_path=fake_vmrun)
|
backend = WorkstationVmrunBackend(vmrun_path=fake_vmrun)
|
||||||
|
|||||||
Reference in New Issue
Block a user