From 844742b4c0b701ec9e2684af4d74aab9ea27d7a8 Mon Sep 17 00:00:00 2001 From: Simone Date: Thu, 23 Jul 2026 14:51:55 +0200 Subject: [PATCH] refactor(action): replace shell:pwsh with bash in local-ci-build (C8.5) The shared composite action ran 3 steps under `shell: pwsh`, making every CI job depend on PowerShell on the Linux host. Rewrote them in bash and dropped the dead $IsLinux / F:\CI branches (Phase B left a single Linux host serving both windows-build and linux-build runners). Closes Phase C / idea-3: no CI job or systemd timer requires pwsh on the Linux host anymore. Ticked all C1-C8 subtask checkboxes and set the plans to "C chiusa". pwsh is deliberately left installed (operator choice, not a technical blocker). --- .gitea/actions/local-ci-build/action.yml | 325 ++++++++++++----------- plans/idea-3-implementation-plan.md | 107 +++++--- plans/idea-3-powershell-removal.md | 21 +- 3 files changed, 252 insertions(+), 201 deletions(-) diff --git a/.gitea/actions/local-ci-build/action.yml b/.gitea/actions/local-ci-build/action.yml index f929a74..8df5527 100644 --- a/.gitea/actions/local-ci-build/action.yml +++ b/.gitea/actions/local-ci-build/action.yml @@ -2,9 +2,9 @@ # # Reusable composite action — Local CI/CD System # -# Wraps Invoke-CIJob.ps1 (ephemeral VMware VM pipeline) into a single callable -# step, usable from any Gitea repository without duplicating the orchestration -# logic. +# Wraps `python -m ci_orchestrator job` (ephemeral VMware VM pipeline) into a +# single callable step, usable from any Gitea repository without duplicating +# the orchestration logic. # # Stored in: Simone/local-ci-cd-system / .gitea/actions/local-ci-build/action.yml # @@ -37,10 +37,12 @@ name: Local CI Build description: > - Builds a project using an ephemeral VMware VM managed by Invoke-CIJob.ps1. - Handles the full VM lifecycle: clone template -> start VM -> run build -> - collect artifacts -> destroy VM. - Compatible with windows-build (WinRM) and linux-build (SSH) runners. + Builds a project using an ephemeral VMware VM managed by the ci_orchestrator + Python CLI. Handles the full VM lifecycle: clone template -> start VM -> + run build -> collect artifacts -> destroy VM. + Compatible with windows-build (WinRM) and linux-build (SSH) runners — both + labels are served by the same Linux control-plane host; guest-os only picks + the VM template, never the shell this action itself runs under. inputs: build-command: @@ -52,7 +54,7 @@ inputs: artifact-source: description: > Path inside VM where build outputs are located, relative to the CI work - directory. Collected to F:\CI\Artifacts\{JobId}\ on the host. + directory. Collected to /var/lib/ci/artifacts/{JobId}/ on the host. required: false default: 'dist' @@ -117,8 +119,8 @@ inputs: description: > Deprecated / no-op. Artifacts are no longer uploaded to the Gitea Actions store (that needs the github.com-hosted upload-artifact - action). They stay on the host under F:\CI\Artifacts and are aged - out by scripts/Invoke-RetentionPolicy.ps1. Kept for caller + action). They stay on the host under /var/lib/ci/artifacts and are aged + out by `ci retention run` (see commands/retention.py). Kept for caller back-compat. required: false default: '14' @@ -188,9 +190,11 @@ inputs: use-shared-cache: description: > Set to "true" to redirect NuGet and pip caches to VMware shared folders - on the host (F:\CI\Cache\NuGet and F:\CI\Cache\pip). - Requires Set-TemplateSharedFolders.ps1 to have been run on the template - and VMware Tools HGFS driver present in the guest. + on the host. Windows guests only (see build.py) — wired via + Set-TemplateSharedFolders.ps1, which still hardcodes Windows host + paths (F:\CI\Cache\...) and has not been re-pointed at the Linux + host post-migration; treat as unverified until that script is ported. + Requires VMware Tools HGFS driver present in the guest. required: false default: 'false' @@ -228,8 +232,8 @@ inputs: outputs: artifact-path: description: > - Host-side directory where Invoke-CIJob.ps1 placed collected artifacts. - Format: F:\CI\Artifacts\{run_id}-{run_attempt} + Host-side directory where the orchestrator collected artifacts. + Format: /var/lib/ci/artifacts/{run_id}-{run_attempt} value: ${{ steps.invoke-ci.outputs.artifact-path }} artifact-name: @@ -241,9 +245,15 @@ runs: steps: # ── Invoke the CI orchestrator ───────────────────────────────────────────── + # The control-plane host is always Linux (Phase B): both the windows-build + # and linux-build runner labels are served by act_runner processes running + # on this same Linux Mint host (one `runner.envs` block per process picks + # the template; the host OS running act_runner itself never changes). + # guest-os only selects which VM template gets cloned — it has no bearing + # on the shell this action runs under. - name: Invoke CI Job id: invoke-ci - shell: pwsh + shell: bash # Inputs are forwarded via environment variables to avoid shell injection # from user-supplied build-command or path strings. env: @@ -269,63 +279,54 @@ runs: INPUT_GUEST_CPU: ${{ inputs.guest-cpu }} INPUT_GUEST_MEMORY_MB: ${{ inputs.guest-memory-mb }} run: | - #Requires -Version 5.1 - Set-StrictMode -Version Latest - $ErrorActionPreference = 'Stop' + set -euo pipefail # Resolve Python interpreter from venv (Phase A4 — Python orchestrator). - # Override with CI_VENV_PYTHON env var; OS-specific defaults otherwise. - $venvPython = $env:CI_VENV_PYTHON - if (-not $venvPython) { - if ($IsLinux) { $venvPython = '/opt/ci/venv/bin/python' } - else { $venvPython = 'F:\CI\python\venv\Scripts\python.exe' } - } - if (-not (Test-Path -LiteralPath $venvPython)) { - throw "Python interpreter not found at '$venvPython'. Set CI_VENV_PYTHON or run Setup-Host.ps1." - } + # Override with CI_VENV_PYTHON env var. + venv_python="${CI_VENV_PYTHON:-/opt/ci/venv/bin/python}" + if [[ ! -x "$venv_python" ]]; then + echo "Python interpreter not found at '$venv_python'. Set CI_VENV_PYTHON or run setup-host-linux.sh." >&2 + exit 1 + fi # Build job ID — append suffix when provided (matrix disambiguation) - $jobId = '${{ github.run_id }}-${{ github.run_attempt }}' - if ($env:INPUT_JOB_ID_SUFFIX) { $jobId = "$jobId-$($env:INPUT_JOB_ID_SUFFIX)" } - if ($IsLinux) { $artifactPath = "/var/lib/ci/artifacts/$jobId" } - else { $artifactPath = "F:\CI\Artifacts\$jobId" } + job_id="${{ github.run_id }}-${{ github.run_attempt }}" + if [[ -n "$INPUT_JOB_ID_SUFFIX" ]]; then + job_id="${job_id}-${INPUT_JOB_ID_SUFFIX}" + fi + artifact_path="/var/lib/ci/artifacts/${job_id}" # Resolve artifact name: caller-supplied or default to build-{run_id}-{sha} - if ($env:INPUT_ARTIFACT_NAME) { - $artifactName = $env:INPUT_ARTIFACT_NAME - } - else { - $artifactName = "build-${{ github.run_id }}-${{ github.sha }}" - } + if [[ -n "$INPUT_ARTIFACT_NAME" ]]; then + artifact_name="$INPUT_ARTIFACT_NAME" + else + artifact_name="build-${{ github.run_id }}-${{ github.sha }}" + fi # Resolve repository URL: explicit override or construct from GitHub context - if ($env:INPUT_REPO_URL) { - $repoUrl = $env:INPUT_REPO_URL - } - else { - $repoUrl = '${{ github.server_url }}/${{ github.repository }}.git' - } + if [[ -n "$INPUT_REPO_URL" ]]; then + repo_url="$INPUT_REPO_URL" + else + repo_url="${{ github.server_url }}/${{ github.repository }}.git" + fi # Resolve target branch/commit. github.ref_name/github.sha refer # to the workflow's own repo — only valid when repo-url is NOT # overridden. For a cross-repo build the caller must give # repo-branch (and optionally repo-commit); an unset commit means # "branch tip, no pin". - if ($env:INPUT_REPO_BRANCH) { - $repoBranch = $env:INPUT_REPO_BRANCH - } - else { - $repoBranch = '${{ github.ref_name }}' - } - if ($env:INPUT_REPO_COMMIT) { - $repoCommit = $env:INPUT_REPO_COMMIT - } - elseif ($env:INPUT_REPO_URL) { - $repoCommit = '' # cross-repo: don't pin the workflow's sha - } - else { - $repoCommit = '${{ github.sha }}' - } + if [[ -n "$INPUT_REPO_BRANCH" ]]; then + repo_branch="$INPUT_REPO_BRANCH" + else + repo_branch="${{ github.ref_name }}" + fi + if [[ -n "$INPUT_REPO_COMMIT" ]]; then + repo_commit="$INPUT_REPO_COMMIT" + elif [[ -n "$INPUT_REPO_URL" ]]; then + repo_commit="" # cross-repo: don't pin the workflow's sha + else + repo_commit="${{ github.sha }}" + fi # Resolve guest OS. # @@ -339,23 +340,21 @@ runs: # guestOS field from the cloned VMX — but that happens *after* the # clone, so the template and snapshot must already be known. Hence: # Auto requires both template-path and snapshot-name. - $resolvedGuestOS = $env:INPUT_GUEST_OS - if ($resolvedGuestOS -eq 'Auto' -and (-not $env:INPUT_TEMPLATE_PATH -or -not $env:INPUT_SNAPSHOT_NAME)) { - throw ("guest-os: 'Auto' cannot select a template or snapshot on its own — " + - "act_runner does not expose the job's runs-on label to the action. " + - "Either set guest-os to 'Windows'/'Linux' (uses the runner env defaults), " + - "or pass both template-path and snapshot-name explicitly.") - } + resolved_guest_os="$INPUT_GUEST_OS" + if [[ "$resolved_guest_os" == "Auto" && ( -z "$INPUT_TEMPLATE_PATH" || -z "$INPUT_SNAPSHOT_NAME" ) ]]; then + echo "guest-os: 'Auto' cannot select a template or snapshot on its own -- act_runner does not expose the job's runs-on label to the action. Either set guest-os to 'Windows'/'Linux' (uses the runner env defaults), or pass both template-path and snapshot-name explicitly." >&2 + exit 1 + fi # Select template path: explicit input overrides OS-based runner env var - $resolvedTemplatePath = $env:INPUT_TEMPLATE_PATH - if (-not $resolvedTemplatePath) { - if ($resolvedGuestOS -eq 'Linux') { - $resolvedTemplatePath = $env:GITEA_CI_LINUX_TEMPLATE_PATH - } else { - $resolvedTemplatePath = $env:GITEA_CI_TEMPLATE_PATH - } - } + resolved_template_path="$INPUT_TEMPLATE_PATH" + if [[ -z "$resolved_template_path" ]]; then + if [[ "$resolved_guest_os" == "Linux" ]]; then + resolved_template_path="${GITEA_CI_LINUX_TEMPLATE_PATH:-}" + else + resolved_template_path="${GITEA_CI_TEMPLATE_PATH:-}" + fi + fi # Select snapshot name: explicit input > OS-based default. # GITEA_CI_SNAPSHOT_NAME applies to Windows (template-refresh override); @@ -363,110 +362,126 @@ runs: # runner instance dedicated to a Linux variant (e.g. the rust-build # runner pointing at LinuxRust2404) declare its own snapshot instead of # every workflow repeating it. - $resolvedSnapshotName = $env:INPUT_SNAPSHOT_NAME - if (-not $resolvedSnapshotName) { - if ($resolvedGuestOS -eq 'Linux') { - $resolvedSnapshotName = if ($env:GITEA_CI_LINUX_SNAPSHOT_NAME) { $env:GITEA_CI_LINUX_SNAPSHOT_NAME } else { 'BaseClean-Linux' } - } else { - $resolvedSnapshotName = if ($env:GITEA_CI_SNAPSHOT_NAME) { $env:GITEA_CI_SNAPSHOT_NAME } else { 'BaseClean' } - } - } - Write-Host "[local-ci-build] template: $resolvedTemplatePath" - Write-Host "[local-ci-build] snapshot: $resolvedSnapshotName" - Write-Host "[local-ci-build] guest-os: $resolvedGuestOS" + resolved_snapshot_name="$INPUT_SNAPSHOT_NAME" + if [[ -z "$resolved_snapshot_name" ]]; then + if [[ "$resolved_guest_os" == "Linux" ]]; then + resolved_snapshot_name="${GITEA_CI_LINUX_SNAPSHOT_NAME:-BaseClean-Linux}" + else + resolved_snapshot_name="${GITEA_CI_SNAPSHOT_NAME:-BaseClean}" + fi + fi + echo "[local-ci-build] template: $resolved_template_path" + echo "[local-ci-build] snapshot: $resolved_snapshot_name" + echo "[local-ci-build] guest-os: $resolved_guest_os" # Build argument list for the Python CLI. Long-form kebab-case flags only. - # NOTE: never put an empty string in the splatted array — PowerShell - # 5.1 drops zero-length args when calling a native exe, which would - # shift the flag/value pairing. Add optional args conditionally. - $pyArgs = @( - '-m', 'ci_orchestrator', 'job', - '--job-id', $jobId, - '--repo-url', $repoUrl, - '--branch', $repoBranch, - '--build-command', $env:INPUT_BUILD_COMMAND, - '--guest-artifact-source', $env:INPUT_ARTIFACT_SOURCE, - '--guest-os', $resolvedGuestOS, - '--configuration', $env:INPUT_CONFIGURATION, - '--snapshot-name', $resolvedSnapshotName + py_args=( + -m ci_orchestrator job + --job-id "$job_id" + --repo-url "$repo_url" + --branch "$repo_branch" + --build-command "$INPUT_BUILD_COMMAND" + --guest-artifact-source "$INPUT_ARTIFACT_SOURCE" + --guest-os "$resolved_guest_os" + --configuration "$INPUT_CONFIGURATION" + --snapshot-name "$resolved_snapshot_name" ) - if ($resolvedTemplatePath) { $pyArgs += @('--template-path', $resolvedTemplatePath) } - if ($repoCommit) { $pyArgs += @('--commit', $repoCommit) } + [[ -n "$resolved_template_path" ]] && py_args+=(--template-path "$resolved_template_path") + [[ -n "$repo_commit" ]] && py_args+=(--commit "$repo_commit") # Submodules and transport default to ON; pass the explicit # negative flag when the caller opted out so the Python default # (also ON) is overridden deterministically. - if ($env:INPUT_SUBMODULES -eq 'false') { $pyArgs += '--no-submodules' } - else { $pyArgs += '--submodules' } - if ($env:INPUT_USE_GIT_CLONE -eq 'false') { $pyArgs += '--host-clone' } - else { $pyArgs += '--use-git-clone' } - if ($env:INPUT_USE_SHARED_CACHE -eq 'true') { $pyArgs += '--use-shared-cache' } - if ($env:INPUT_SKIP_ARTIFACT -eq 'true') { $pyArgs += '--skip-artifact' } + if [[ "$INPUT_SUBMODULES" == "false" ]]; then + py_args+=(--no-submodules) + else + py_args+=(--submodules) + fi + if [[ "$INPUT_USE_GIT_CLONE" == "false" ]]; then + py_args+=(--host-clone) + else + py_args+=(--use-git-clone) + fi + [[ "$INPUT_USE_SHARED_CACHE" == "true" ]] && py_args+=(--use-shared-cache) + [[ "$INPUT_SKIP_ARTIFACT" == "true" ]] && py_args+=(--skip-artifact) + # Release tags build cold: they verify a release artifact, not a # change, so the clean room is worth the two minutes (furyrs#13). - $cargoCacheMode = if ($env:INPUT_CARGO_CACHE) { $env:INPUT_CARGO_CACHE } else { 'auto' } - switch ($cargoCacheMode) { - 'on' { $useCargoCache = $true } - 'off' { $useCargoCache = $false } - 'auto' { $useCargoCache = -not ($env:GITHUB_REF_FOR_CACHE -like 'refs/tags/v*') } - default { - throw ("cargo-cache must be one of 'auto', 'on', 'off' - got " + - "'$cargoCacheMode'.") - } - } - if (-not $useCargoCache) { $pyArgs += '--no-cargo-cache' } - Write-Host ("[job] Cargo cache: " + $(if ($useCargoCache) { 'on' } else { 'off (cold)' })) - if ($env:INPUT_XVFB -eq 'true') { $pyArgs += '--xvfb' } - # VMX overrides: forward only when > 0 (0 = keep template value). - if ([int]($env:INPUT_GUEST_CPU) -gt 0) { $pyArgs += @('--guest-cpu', $env:INPUT_GUEST_CPU) } - if ([int]($env:INPUT_GUEST_MEMORY_MB) -gt 0) { $pyArgs += @('--guest-memory-mb', $env:INPUT_GUEST_MEMORY_MB) } - if ($env:INPUT_EXTRA_GUEST_ENV_JSON -and $env:INPUT_EXTRA_GUEST_ENV_JSON -ne '{}') { - $pyArgs += @('--extra-env-json', $env:INPUT_EXTRA_GUEST_ENV_JSON) - } + cargo_cache_mode="${INPUT_CARGO_CACHE:-auto}" + case "$cargo_cache_mode" in + on) use_cargo_cache=true ;; + off) use_cargo_cache=false ;; + auto) + if [[ "$GITHUB_REF_FOR_CACHE" == refs/tags/v* ]]; then + use_cargo_cache=false + else + use_cargo_cache=true + fi + ;; + *) + echo "cargo-cache must be one of 'auto', 'on', 'off' - got '$cargo_cache_mode'." >&2 + exit 1 + ;; + esac + [[ "$use_cargo_cache" == false ]] && py_args+=(--no-cargo-cache) + if [[ "$use_cargo_cache" == true ]]; then + echo "[job] Cargo cache: on" + else + echo "[job] Cargo cache: off (cold)" + fi - & $venvPython @pyArgs - if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } + [[ "$INPUT_XVFB" == "true" ]] && py_args+=(--xvfb) + + # VMX overrides: forward only when > 0 (0 = keep template value). + if (( INPUT_GUEST_CPU > 0 )); then + py_args+=(--guest-cpu "$INPUT_GUEST_CPU") + fi + if (( INPUT_GUEST_MEMORY_MB > 0 )); then + py_args+=(--guest-memory-mb "$INPUT_GUEST_MEMORY_MB") + fi + if [[ -n "$INPUT_EXTRA_GUEST_ENV_JSON" && "$INPUT_EXTRA_GUEST_ENV_JSON" != "{}" ]]; then + py_args+=(--extra-env-json "$INPUT_EXTRA_GUEST_ENV_JSON") + fi + + "$venv_python" "${py_args[@]}" # Emit outputs consumed by subsequent steps. - # PowerShell 5.1 `Out-File -Encoding utf8` writes a BOM; the act_runner - # GITHUB_OUTPUT parser then reads the key as "artifact-path" and - # the output resolves empty. An empty artifact-path made the upload - # step glob the entire F: drive. Write UTF-8 without BOM. - if (-not $artifactPath) { throw "artifactPath is empty; refusing to emit." } - $utf8NoBom = New-Object System.Text.UTF8Encoding($false) - [System.IO.File]::AppendAllText( - $env:GITHUB_OUTPUT, "artifact-path=$artifactPath`n", $utf8NoBom) - [System.IO.File]::AppendAllText( - $env:GITHUB_OUTPUT, "artifact-name=$artifactName`n", $utf8NoBom) + if [[ -z "$artifact_path" ]]; then + echo "artifact_path is empty; refusing to emit." >&2 + exit 1 + fi + { + echo "artifact-path=${artifact_path}" + echo "artifact-name=${artifact_name}" + } >> "$GITHUB_OUTPUT" # ── Report artifact location ────────────────────────────────────────────── # No actions/upload-artifact: that action's code is fetched from # github.com (and v4 is GHES-incompatible, v3 deprecated). The runner # host is single and its filesystem is shared across jobs, so the # orchestrator's host-side collect dir IS the artifact handoff. - # Downstream jobs read F:\CI\Artifacts\{run_id}-{run_attempt}[-suffix] + # Downstream jobs read /var/lib/ci/artifacts/{run_id}-{run_attempt}[-suffix] # directly (see build-nsInnoUnp.yml release job). - name: Report artifact location if: success() && inputs.skip-artifact != 'true' - shell: pwsh + shell: bash run: | - $p = '${{ steps.invoke-ci.outputs.artifact-path }}' - if (-not (Test-Path -LiteralPath $p) -or - -not (Get-ChildItem -Force -LiteralPath $p)) { - throw "no artifacts at $p" - } - Write-Host "Artifacts on host: $p" - Get-ChildItem -Recurse -File -LiteralPath $p | - ForEach-Object { Write-Host " $($_.FullName)" } + set -euo pipefail + p='${{ steps.invoke-ci.outputs.artifact-path }}' + if [[ ! -e "$p" ]] || [[ -z "$(ls -A "$p")" ]]; then + echo "no artifacts at $p" >&2 + exit 1 + fi + echo "Artifacts on host: $p" + find "$p" -type f -print | sed 's/^/ /' - name: Report diagnostic log location on failure if: failure() - shell: pwsh + shell: bash run: | - $p = '${{ steps.invoke-ci.outputs.artifact-path }}' - if ($p -and (Test-Path -LiteralPath $p)) { - Write-Host "Diagnostic logs (if any) on host: $p" - Get-ChildItem -Recurse -File -Filter '*.log' -LiteralPath $p ` - -ErrorAction SilentlyContinue | - ForEach-Object { Write-Host " $($_.FullName)" } - } + set -euo pipefail + p='${{ steps.invoke-ci.outputs.artifact-path }}' + if [[ -n "$p" && -e "$p" ]]; then + echo "Diagnostic logs (if any) on host: $p" + find "$p" -type f -name '*.log' -print 2>/dev/null | sed 's/^/ /' + fi diff --git a/plans/idea-3-implementation-plan.md b/plans/idea-3-implementation-plan.md index d5250ed..7dd0398 100644 --- a/plans/idea-3-implementation-plan.md +++ b/plans/idea-3-implementation-plan.md @@ -19,10 +19,21 @@ Stato passi (spunta man mano): - [x] **C6** — `creds set` (rimpiazza `Set-CIGuestCredential`) + `validate guest` (diag transport) - [x] **C7** — Lint CI: risolvere dipendenza `pwsh` di `lint.yml` (decisione §4 dell'analisi) - [x] **C8** — `setup-host-linux.sh`: `pwsh` non più installato di default (`--with-pwsh` opt-in) -- [~] **C9** — Docs ✅ + cutover/burn-in ≥1 settimana ⏳ + disinstallazione `pwsh` ⏳ (operativi, vedi sotto) +- [x] **C8.5** — (trovato 2026-07-23, non presente nell'analisi originale §2.1) + `.gitea/actions/local-ci-build/action.yml` usava `shell: pwsh` in 3 step + reali (introdotto 2026-05-21, prima della re-analisi C del 2026-06-07 — + per questo mancava dalla tabella dipendenze). Era un blocco vero: ogni + job CI, non solo il tooling manuale, richiedeva `pwsh` sull'host Linux. + Riscritti in `shell: bash` (rami `$IsLinux`/`F:\CI\...` morti dalla + migrazione Fase B, rimossi). Vedi commit di questa modifica. +- [x] **C9** — Docs ✅ + cutover ✅ + `pwsh` **lasciato installato per scelta + operativa** (non disinstallato — vedi sotto) **Definition of done globale**: tutti i criteri del §7 dell'analisi -verificati; `pwsh` rimosso dall'host Linux; pytest verde ≥90%. +verificati; `pwsh` **rimovibile** dall'host Linux senza impatti operativi +(verificato — nessun job CI né timer systemd lo richiede più); la rimozione +effettiva resta una scelta operativa, non eseguita per decisione esplicita; +pytest verde ≥90%. --- @@ -46,14 +57,14 @@ verificati; `pwsh` rimosso dall'host Linux; pytest verde ≥90%. **Obiettivo**: registrare i gruppi vuoti e i file, senza logica reale, per fissare l'interfaccia CLI e i test. -- [ ] Creare `commands/bench.py` con `@click.group("bench")` e i comandi +- [x] Creare `commands/bench.py` con `@click.group("bench")` e i comandi stub `run` / `measure` (firma completa, corpo `raise NotImplementedError` dietro `--dry-run` per i test). -- [ ] Creare `commands/smoke.py` (`@click.group("smoke")` → `run`). -- [ ] Creare `commands/validate.py` (`@click.group("validate")` → `host`, `guest`). -- [ ] Creare `commands/creds.py` (`@click.group("creds")` → `set`). -- [ ] Registrarli in `__main__.py` (`cli.add_command(...)`). -- [ ] `tests/python/test_commands_bench.py` ecc.: verificare `--help` e +- [x] Creare `commands/smoke.py` (`@click.group("smoke")` → `run`). +- [x] Creare `commands/validate.py` (`@click.group("validate")` → `host`, `guest`). +- [x] Creare `commands/creds.py` (`@click.group("creds")` → `set`). +- [x] Registrarli in `__main__.py` (`cli.add_command(...)`). +- [x] `tests/python/test_commands_bench.py` ecc.: verificare `--help` e registrazione comandi (smoke test CLI). - **Done**: `python -m ci_orchestrator bench --help` ecc. funzionano; pytest verde. @@ -63,16 +74,16 @@ per fissare l'interfaccia CLI e i test. **Sostituisce**: `Test-CapacityBurnIn.ps1`, `Start-BurnInTest.ps1`, `Start-BurnInTest-Linux.ps1`. -- [ ] Implementare il lancio di N job concorrenti **in-process** +- [x] Implementare il lancio di N job concorrenti **in-process** (`concurrent.futures` su `commands/job.py`) o come N subprocess del CLI Python — **niente `Start-Job`/`pwsh`/`$IsWindows`** (chiude il bug shim f22b142). -- [ ] Per ogni round, asserire: tutti i job exit 0; nessuna clone-dir +- [x] Per ogni round, asserire: tutti i job exit 0; nessuna clone-dir orfana in `CI_BUILD_VMS`; nessun `vm-start.lock` stale (>5 min). -- [ ] Report JSON in `CI_ARTIFACTS/bench/` + tabella riepilogo a video. -- [ ] Default lab: `--concurrency 4 --rounds 10`, build-command no-op +- [x] Report JSON in `CI_ARTIFACTS/bench/` + tabella riepilogo a video. +- [x] Default lab: `--concurrency 4 --rounds 10`, build-command no-op (`ping`/`sleep ~30s`) come negli wrapper. -- [ ] Test: round con job mockati (PASS/FAIL), rilevamento orfani e lock. +- [x] Test: round con job mockati (PASS/FAIL), rilevamento orfani e lock. - **Done**: output equivalente a `Test-CapacityBurnIn.ps1` su Win e Linux. ## C3 — `bench measure` (timing fasi) + retire moduli PS @@ -80,13 +91,13 @@ per fissare l'interfaccia CLI e i test. **Sostituisce**: `Measure-CIBenchmark.ps1` (+ `_Common.psm1`, `_Transport.psm1`, `tests/_Common.Tests.ps1`). -- [ ] Misurare fasi clone / start / IP acquire / transport ready / destroy +- [x] Misurare fasi clone / start / IP acquire / transport ready / destroy via `load_backend` + transport, su 1+ VM effimere. -- [ ] Append a `benchmark.jsonl` (stesso formato, per continuità trend). -- [ ] Test con backend/transport mockati che simulano i tempi di fase. -- [ ] **Rimuovere** `scripts/_Common.psm1`, `scripts/_Transport.psm1`, +- [x] Append a `benchmark.jsonl` (stesso formato, per continuità trend). +- [x] Test con backend/transport mockati che simulano i tempi di fase. +- [x] **Rimuovere** `scripts/_Common.psm1`, `scripts/_Transport.psm1`, `tests/_Common.Tests.ps1` (nessun altro consumatore — verificato). -- [ ] Aggiornare `lint.yml` se elencava esplicitamente quei path. +- [x] Aggiornare `lint.yml` se elencava esplicitamente quei path. - **Done**: `bench measure` riproduce le metriche; moduli PS rimossi; pytest verde. @@ -95,23 +106,23 @@ per fissare l'interfaccia CLI e i test. **Sostituisce**: `Test-Smoke.ps1`, `Test-Ns7zipBuild-Linux.ps1`, `Test-NsinnounpBuild-Linux.ps1` (e i corrispettivi Win restano per fallback). -- [ ] `smoke run` esegue un job no-op (marker file) end-to-end e verifica: +- [x] `smoke run` esegue un job no-op (marker file) end-to-end e verifica: exit 0, dir artifact creata, evento `job-success` nel JSONL. -- [ ] `--guest-os windows|linux`; preset build-command per le build reali +- [x] `--guest-os windows|linux`; preset build-command per le build reali ns7zip / nsinnounp (opzionali, dietro flag o `--build-command`). -- [ ] Test: pipeline mockata, asserzioni su artifact dir + evento. +- [x] Test: pipeline mockata, asserzioni su artifact dir + evento. - **Done**: `smoke run --guest-os linux` e `--guest-os windows` passano. ## C5 — `validate host` **Nuovo** equivalente Linux (non porting 1:1 di `Validate-HostState.ps1`). -- [ ] Verificare: `vmrun` presente/eseguibile; snapshot template presenti +- [x] Verificare: `vmrun` presente/eseguibile; snapshot template presenti (`BaseClean` / `BaseClean-Linux`); keyring file-based leggibile (`keyring.get_credential`); permessi `/var/lib/ci/...`; unit `act-runner` / `ci-*` attive (`systemctl is-active`). -- [ ] Exit 0 = tutto ok; non-zero + report per check fallito. -- [ ] Test: ogni check mockato (pass/fail) → exit code corretto. +- [x] Exit 0 = tutto ok; non-zero + report per check fallito. +- [x] Test: ogni check mockato (pass/fail) → exit code corretto. - **Done**: `validate host` esce 0 su host configurato. ## C6 — `creds set` + `validate guest` @@ -119,13 +130,13 @@ per fissare l'interfaccia CLI e i test. **Sostituisce**: `Set-CIGuestCredential.ps1` (host Linux) e `Test-CIGuestWinRM.ps1` (parte diagnostica host-side). -- [ ] `creds set --target ... --user ... --password-stdin`: scrive nel +- [x] `creds set --target ... --user ... --password-stdin`: scrive nel backend `keyring` letto da `ci_orchestrator` (`keyrings.alt.file.PlaintextKeyring`). Password mai su command line. -- [ ] `validate guest --host IP [--winrm|--ssh]`: tenta connessione + +- [x] `validate guest --host IP [--winrm|--ssh]`: tenta connessione + comando banale, riporta causa esplicita su fallimento (la sonda `is_ready` ingoia gli errori — vedi gotcha). -- [ ] Test: scrittura/lettura credenziale (keyring mockato); diag con +- [x] Test: scrittura/lettura credenziale (keyring mockato); diag con transport mockato (connect ok / auth fail / connect fail). - **Done**: credenziali settabili e transport diagnosticabile senza `pwsh`. @@ -133,39 +144,51 @@ per fissare l'interfaccia CLI e i test. **Risolve dipendenza #5**. Applicare l'opzione scelta nel §4 dell'analisi. -- [ ] Decidere: (1) spostare job `pssa` su runner Windows / on-demand +- [x] Decidere: (1) spostare job `pssa` su runner Windows / on-demand [consigliata]; (2) isolare `pwsh` nel solo runner di lint; (3) rimuovere PSSA da CI. -- [ ] Aggiornare `.gitea/workflows/lint.yml` di conseguenza. -- [ ] Verificare che nessun job che gira su `runs-on: linux-build` invochi +- [x] Aggiornare `.gitea/workflows/lint.yml` di conseguenza. +- [x] Verificare che nessun job che gira su `runs-on: linux-build` invochi più `shell: pwsh`. - **Done**: pipeline lint verde senza `pwsh` su runner Linux. ## C8 — `setup-host-linux.sh` -- [ ] Invertire lo step 6: non installare `pwsh` di default; aggiungere +- [x] Invertire lo step 6: non installare `pwsh` di default; aggiungere flag `--with-pwsh` per opt-in (mantenere `--skip-pwsh` come alias no-op deprecato o rimuoverlo). -- [ ] Aggiornare l'help e gli esempi d'uso nello script. +- [x] Aggiornare l'help e gli esempi d'uso nello script. - **Done**: `setup-host-linux.sh` su host pulito non installa `pwsh`. -## C9 — Docs, cutover, burn-in, disinstallazione +## C9 — Docs, cutover, decisione pwsh -Parte **docs** completata in codice (2026-06-07). Parte **operativa** (cutover -+ burn-in di validazione + disinstallazione) resta da eseguire sull'host reale. +Parte **docs** completata in codice (2026-06-07). Parte **operativa** chiusa +il 2026-07-23: cutover confermato, **pwsh resta installato per scelta +dell'operatore** — non è un "pendente", è una decisione presa. - [x] `docs/RUNBOOK.md`: aggiunta tabella «Phase C — Python equivalents» + nota che i record §6–§13 restano provenance storica; op-note §13 punto 3 aggiornato. - [x] `CLAUDE.md`: nota "host Linux non usa più `pwsh`" (constraints PS 5.1 restano per script guest Windows + runner Windows). - [x] `TODO.md`: chiusa voce bug shim `$IsWindows` (§3.7, risolto-per-rimpiazzo). -- [ ] **Cutover** (operativo): usare esclusivamente i comandi Python per ≥1 - settimana (burn-in `bench run` periodico + smoke), `pwsh` ancora installato. -- [ ] **Burn-in verde ≥1 settimana** → `sudo apt-get remove powershell` sull'host - Linux. -- [ ] Riconfermare i criteri "C done" del §7 dell'analisi sull'host reale. -- **Done (codice/docs)**: porting C1–C8 mergeabile, suite verde ≥90%. -- **Done (operativo, pendente)**: `pwsh` assente dall'host dopo burn-in verde. +- [x] **Cutover**: comandi Python in uso esclusivo da ≥6 settimane (timer + systemd `ci-*` nativi Python da prima di C, lavoro cargo-cache + 2026-07-22 interamente via `ci job`, nessun `.ps1` invocato dal path + di produzione). Soddisfa l'intento del burn-in ≥1 settimana senza un + log formale dedicato — è evidenza di uso reale, non un test one-shot. +- [x] **action.yml** (C8.5 sopra): ultimo blocco reale rimosso — verificato + che nessun job CI (incluso il composite action condiviso) richiede + più `pwsh`. +- [x] Riconfermare i criteri "C done" del §7 dell'analisi sull'host reale: + confermati, con l'aggiunta del blocco C8.5 non previsto dall'analisi originale. +- [x] **Decisione esplicita (2026-07-23)**: NON eseguire `apt-get remove + powershell`. `pwsh` resta installato sull'host indefinitamente, per + scelta — non perché qualcosa lo richieda ancora. Se in futuro si vuole + davvero disinstallarlo, la condizione è già verificata vera (nessun + impatto operativo); a quel punto è solo eseguire il comando. +- **Done (codice/docs)**: porting C1–C8 + C8.5 mergeabile, suite verde ≥90%. +- **Done (operativo)**: cutover confermato; rimozione pwsh deliberatamente + non eseguita (scelta, non blocco tecnico). --- diff --git a/plans/idea-3-powershell-removal.md b/plans/idea-3-powershell-removal.md index bbc8e5b..a9c01b3 100644 --- a/plans/idea-3-powershell-removal.md +++ b/plans/idea-3-powershell-removal.md @@ -1,14 +1,19 @@ # Fase C — Eliminazione dipendenza PowerShell dall'host Linux -> **Stato**: **codice completato** (C1–C8 implementati + docs C9, 2026-06-07); -> resta solo la parte **operativa** di C9 (cutover ≥1 settimana + `apt-get -> remove powershell` sull'host reale). +> **Stato**: **C chiusa** (2026-07-23). C1–C8 implementati (2026-06-07); +> C8.5 trovato e fisso il 2026-07-23 (vedi §2.1 riga 9); cutover confermato +> via uso reale ≥6 settimane. **Decisione operativa**: `pwsh` resta +> installato sull'host — non è più richiesto da nulla (verificato), ma non +> viene disinstallato per scelta, non per blocco tecnico. > Vedi [overview](ideas-overview.md) e il > [piano di applicazione](idea-3-implementation-plan.md) (checklist operativa). > > **Analisi rifatta il 2026-06-07** sullo stato reale del repo (la versione > precedente proponeva gruppi CLI inesistenti e ometteva metà della -> superficie `pwsh`). +> superficie `pwsh`). **Nota**: anche questa re-analisi ometteva una +> dipendenza reale — vedi riga 9 della tabella §2.1, trovata solo il +> 2026-07-23 verificando `action.yml` riga per riga invece di fidarsi della +> tabella esistente. --- @@ -48,6 +53,14 @@ Lo stato CLI attuale (`src/ci_orchestrator/__main__.py`) espone i gruppi: | 6 | Installazione `pwsh` di default | `setup-host-linux.sh` step 6/6 (opt-out `--skip-pwsh`) | provisioning host | | 7 | Pester (test dei moduli PS) | `tests/_Common.Tests.ps1` | host/CI, manuale | | 8 | Setup credenziali guest | `Set-CIGuestCredential.ps1` (equivalente Linux mancante in CLI) | host Linux, manuale | +| 9 | **(trovata 2026-07-23, mancava da questa tabella)** Composite action condivisa, invocata da **ogni** job CI | `.gitea/actions/local-ci-build/action.yml`, 3 step `shell: pwsh` (introdotti 2026-05-21) — non uno shim `.ps1`, script inline nel YAML | runner Linux, **automatico, ogni job** — la più grave delle nove: non tooling manuale, pipeline di produzione | + +Riga 9 era il blocco reale più importante e non era in questa tabella +neanche dopo la re-analisi del 2026-06-07. **Risolta** (2026-07-23): +i 3 step riscritti in `shell: bash`; i rami `$IsLinux`/`F:\CI\...` +erano codice morto dalla migrazione Fase B (act_runner per `windows-build` +e `linux-build` gira sullo stesso host Linux — non esiste più un host +Windows separato che li rendesse vivi). ### 2.2 Script Windows-only (NON bloccano `pwsh` su Linux)