@noeva/analytics-core 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +4 -0
- package/cli.js +34 -4
- package/contenuto/lib/mcp-locale/ambiente/.python-version +1 -0
- package/contenuto/lib/mcp-locale/ambiente/pyproject.toml +23 -0
- package/contenuto/lib/mcp-locale/ambiente/uv.lock +1103 -0
- package/contenuto/lib/mcp-locale/cartella/CLAUDE.md +52 -0
- package/contenuto/lib/mcp-locale/cartella/LEGGIMI.md +163 -0
- package/contenuto/lib/mcp-locale/cartella/imposta-password.ps1 +40 -0
- package/contenuto/lib/mcp-locale/cartella/installa.ps1 +286 -0
- package/contenuto/lib/mcp-locale/costruisci.py +495 -0
- package/contenuto/lib/mcp-locale/prova.py +93 -0
- package/contenuto/lib/mcp-locale/server/modello_locale.py +167 -0
- package/contenuto/lib/mcp-locale/server/noeva_mcp.py +203 -0
- package/contenuto/lib/modello/consegna.py +134 -0
- package/contenuto/lib/modello/pubblica.py +131 -0
- package/contenuto/lib/snapshot/ascolta.py +213 -0
- package/contenuto/lib/snapshot/impacchetta.py +184 -0
- package/contenuto/lib/snapshot/noeva_snapshot.py +107 -0
- package/contenuto/lib/snapshot/pubblica.py +35 -15
- package/contenuto/lib/snapshot/worker/installa.ps1 +340 -0
- package/core.js +18 -2
- package/modello/repository/github/workflows/ci.yml +53 -3
- package/modello/repository/github/workflows/rilascio-windows.yml +207 -0
- package/modello/repository/gitignore +2 -1
- package/package.json +2 -1
- package/pubblica.js +205 -0
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Installa o aggiorna il worker degli snapshot di {{progetto}} (NOEVA Analytics, ADR 0004),
|
|
2
|
+
# passo per passo, e alla fine dice com'e' andata.
|
|
3
|
+
#
|
|
4
|
+
# powershell -ExecutionPolicy Bypass -File .\installa.ps1 installa o aggiorna
|
|
5
|
+
# powershell -ExecutionPolicy Bypass -File .\installa.ps1 -SoloControllo controlla e basta (dopo un riavvio)
|
|
6
|
+
#
|
|
7
|
+
# Si lancia dalla cartella estratta dallo zip, ENTRANDO IN WINDOWS COME L'UTENTE CHE FARA'
|
|
8
|
+
# GIRARE IL WORKER: le chiavi vanno nelle sue variabili d'ambiente. La copia in
|
|
9
|
+
# C:\NOEVA\snapshot la fa lo script; se c'e' gia' e' un aggiornamento (la cartella vecchia si
|
|
10
|
+
# mette da parte, lavoro\ e le chiavi restano). Alla fine il primo giro parte subito: lo
|
|
11
|
+
# scheduler di NOEVA lo mette in coda e il worker lo esegue qui davanti, poi l'attivita'
|
|
12
|
+
# pianificata prende il posto (ogni {{minuti}} minuti, ascolta --una-volta).
|
|
13
|
+
# Nessun segreto finisce a schermo o nel file dell'esito.
|
|
14
|
+
|
|
15
|
+
param([switch]$SoloControllo)
|
|
16
|
+
|
|
17
|
+
$ErrorActionPreference = 'Stop'
|
|
18
|
+
try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { }
|
|
19
|
+
|
|
20
|
+
$qui = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
21
|
+
$progetto = '{{progetto}}'
|
|
22
|
+
$utenteDb = '{{utente}}'
|
|
23
|
+
$varPassword = '{{password_env}}'
|
|
24
|
+
$dbHost = '{{host}}'
|
|
25
|
+
$dbPorta = {{porta}}
|
|
26
|
+
$minuti = {{minuti}}
|
|
27
|
+
$pipeline = "analytics-snapshot-$progetto"
|
|
28
|
+
$attivita = "NOEVA snapshot $progetto"
|
|
29
|
+
$destinazione = 'C:\NOEVA\snapshot'
|
|
30
|
+
$exe = Join-Path $destinazione 'noeva-snapshot\noeva-snapshot.exe'
|
|
31
|
+
$pacchetto = Join-Path $destinazione "pacchetti\$progetto"
|
|
32
|
+
$modo = 'nuova installazione'
|
|
33
|
+
$esiti = New-Object System.Collections.ArrayList
|
|
34
|
+
$conf = Get-Content (Join-Path $qui 'worker.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
35
|
+
$api = $conf.api.TrimEnd('/')
|
|
36
|
+
$contenitore = $conf.contenitore.TrimEnd('/')
|
|
37
|
+
$workspace = $conf.workspace
|
|
38
|
+
|
|
39
|
+
function Passo($titolo) {
|
|
40
|
+
Write-Host ''
|
|
41
|
+
Write-Host "== $titolo" -ForegroundColor Cyan
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function Esito($passo, $stato, $nota) {
|
|
45
|
+
[void]$esiti.Add([pscustomobject]@{ Passo = $passo; Stato = $stato; Nota = $nota })
|
|
46
|
+
$colore = @{ 'ok' = 'Green'; 'avviso' = 'Yellow'; 'NO' = 'Red'; 'saltato' = 'DarkGray' }[$stato]
|
|
47
|
+
Write-Host (' {0,-7} {1}' -f $stato, $nota) -ForegroundColor $colore
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Chiedi($domanda, [bool]$predefinito = $true) {
|
|
51
|
+
$suggerimento = if ($predefinito) { '(S/n)' } else { '(s/N)' }
|
|
52
|
+
$r = Read-Host " $domanda $suggerimento"
|
|
53
|
+
if (-not $r) { return $predefinito }
|
|
54
|
+
return $r.Trim().ToLower().StartsWith('s')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function ChiediSegreto($domanda) {
|
|
58
|
+
$sicura = Read-Host " $domanda" -AsSecureString
|
|
59
|
+
$bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sicura)
|
|
60
|
+
try { return [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) }
|
|
61
|
+
finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function PortaAperta($h, $p) {
|
|
65
|
+
$c = New-Object Net.Sockets.TcpClient
|
|
66
|
+
try { return $c.BeginConnect($h, $p, $null, $null).AsyncWaitHandle.WaitOne(5000) -and $c.Connected }
|
|
67
|
+
catch { return $false }
|
|
68
|
+
finally { $c.Close() }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Esegui($comando, [string[]]$argomenti) {
|
|
72
|
+
$prima = $ErrorActionPreference
|
|
73
|
+
$ErrorActionPreference = 'Continue'
|
|
74
|
+
try {
|
|
75
|
+
$testo = (& $comando @argomenti 2>&1 | ForEach-Object { "$_" }) -join "`n"
|
|
76
|
+
return [pscustomobject]@{ Codice = $LASTEXITCODE; Testo = $testo }
|
|
77
|
+
} finally { $ErrorActionPreference = $prima }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
# Come Esegui, ma le righe si vedono mentre arrivano (la verifica e il giro durano minuti).
|
|
81
|
+
function EseguiDalVivo($comando, [string[]]$argomenti) {
|
|
82
|
+
$prima = $ErrorActionPreference
|
|
83
|
+
$ErrorActionPreference = 'Continue'
|
|
84
|
+
try {
|
|
85
|
+
$righe = New-Object System.Collections.ArrayList
|
|
86
|
+
& $comando @argomenti 2>&1 | ForEach-Object { $r = "$_"; [void]$righe.Add($r); Write-Host " $r" }
|
|
87
|
+
return [pscustomobject]@{ Codice = $LASTEXITCODE; Testo = ($righe -join "`n") }
|
|
88
|
+
} finally { $ErrorActionPreference = $prima }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Una chiamata all'API NOEVA con la chiave del worker. Cloudflare blocca lo user agent di
|
|
92
|
+
# PowerShell come quello di Python: ci si presenta come noeva-snapshot.
|
|
93
|
+
function ApiNoeva($metodo, $percorso, $corpo) {
|
|
94
|
+
$par = @{
|
|
95
|
+
Method = $metodo; Uri = "$api$percorso"; UseBasicParsing = $true; TimeoutSec = 60
|
|
96
|
+
UserAgent = 'noeva-snapshot (NOEVA Analytics)'
|
|
97
|
+
Headers = @{ Authorization = "Bearer $env:NOEVA_API_KEY"; 'x-noeva-workspace' = $workspace }
|
|
98
|
+
}
|
|
99
|
+
if ($corpo) { $par.Body = ($corpo | ConvertTo-Json -Compress); $par.ContentType = 'application/json' }
|
|
100
|
+
try {
|
|
101
|
+
$r = Invoke-WebRequest @par
|
|
102
|
+
return [pscustomobject]@{ Codice = [int]$r.StatusCode; Corpo = $(if ($r.Content) { $r.Content | ConvertFrom-Json } else { $null }); Errore = '' }
|
|
103
|
+
} catch {
|
|
104
|
+
$codice = 0
|
|
105
|
+
if ($_.Exception.Response) { $codice = [int]$_.Exception.Response.StatusCode }
|
|
106
|
+
$messaggio = if ($_.ErrorDetails -and $_.ErrorDetails.Message) { $_.ErrorDetails.Message } else { $_.Exception.Message }
|
|
107
|
+
return [pscustomobject]@{ Codice = $codice; Corpo = $null; Errore = $messaggio.Substring(0, [Math]::Min(200, $messaggio.Length)) }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function Fine {
|
|
112
|
+
$no = @($esiti | Where-Object { $_.Stato -eq 'NO' }).Count
|
|
113
|
+
$avvisi = @($esiti | Where-Object { $_.Stato -eq 'avviso' }).Count
|
|
114
|
+
$esitoGenerale = if ($no) { "NON COMPLETATA: $no passi da sistemare" } elseif ($avvisi) { "COMPLETATA con $avvisi avvisi" } else { 'COMPLETATA' }
|
|
115
|
+
$quando = Get-Date
|
|
116
|
+
$runner = ''
|
|
117
|
+
$quale = if (Test-Path $exe) { $exe } else { Join-Path $qui 'noeva-snapshot\noeva-snapshot.exe' }
|
|
118
|
+
if (Test-Path $quale) { try { $runner = ((Esegui $quale @('versione')).Testo | ConvertFrom-Json).runner } catch { } }
|
|
119
|
+
$righe = @(
|
|
120
|
+
"worker degli snapshot di $progetto (runner $runner) · $modo · $esitoGenerale",
|
|
121
|
+
"macchina $env:COMPUTERNAME · utente Windows $env:USERDOMAIN\$env:USERNAME · $($quando.ToString('dd/MM/yyyy HH:mm'))",
|
|
122
|
+
''
|
|
123
|
+
) + ($esiti | ForEach-Object { '{0,-7} {1,-20} {2}' -f $_.Stato, $_.Passo, $_.Nota })
|
|
124
|
+
Write-Host ''
|
|
125
|
+
Write-Host '==================== RIEPILOGO ====================' -ForegroundColor Cyan
|
|
126
|
+
$colore = if ($no) { 'Red' } elseif ($avvisi) { 'Yellow' } else { 'Green' }
|
|
127
|
+
Write-Host $righe[0] -ForegroundColor $colore
|
|
128
|
+
$righe | Select-Object -Skip 1 | ForEach-Object { Write-Host $_ }
|
|
129
|
+
$cartellaEsito = if (Test-Path $destinazione) { $destinazione } else { $qui }
|
|
130
|
+
$file = Join-Path $cartellaEsito ("esito-installa-{0}.txt" -f $quando.ToString('yyyyMMdd-HHmm'))
|
|
131
|
+
try {
|
|
132
|
+
$righe | Set-Content -Path $file -Encoding UTF8
|
|
133
|
+
Write-Host ''
|
|
134
|
+
Write-Host "Esito salvato in $file (senza chiavi ne' password): da mandare a chi segue l'installazione."
|
|
135
|
+
} catch { Write-Host "Non riesco a salvare l'esito in $file" -ForegroundColor Yellow }
|
|
136
|
+
if ($no) { exit 1 } else { exit 0 }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Ferma($passo, $nota) {
|
|
140
|
+
Esito $passo 'NO' $nota
|
|
141
|
+
Write-Host ' Mi fermo qui: sistemato questo, rilancia lo script.' -ForegroundColor Red
|
|
142
|
+
Fine
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
Write-Host "NOEVA Analytics · worker degli snapshot di $progetto" -ForegroundColor Cyan
|
|
146
|
+
Write-Host "macchina $env:COMPUTERNAME · utente Windows $env:USERDOMAIN\$env:USERNAME"
|
|
147
|
+
if ($SoloControllo) { $modo = 'solo controllo'; Write-Host 'Solo controllo: non cambio niente.' }
|
|
148
|
+
|
|
149
|
+
# 0. L'utente ----------------------------------------------------------------------
|
|
150
|
+
if (-not $SoloControllo) {
|
|
151
|
+
Passo "0. L'utente Windows del worker"
|
|
152
|
+
Write-Host " Il worker girera' come $env:USERDOMAIN\$env:USERNAME, con le sue variabili d'ambiente."
|
|
153
|
+
Write-Host ' Su un server: un account di servizio dedicato, non amministratore, non una persona.'
|
|
154
|
+
if (Chiedi "E' l'utente giusto?") { Esito 'utente' 'ok' "$env:USERDOMAIN\$env:USERNAME" }
|
|
155
|
+
else { Ferma 'utente' 'entra in Windows come l''utente del worker e rilancia' }
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# 1. La rete -------------------------------------------------------------------------
|
|
159
|
+
Passo '1. La rete: il database e NOEVA, solo in uscita'
|
|
160
|
+
if (PortaAperta $dbHost $dbPorta) { Esito 'rete database' 'ok' "$dbHost`:$dbPorta" }
|
|
161
|
+
else { Ferma 'rete database' "$dbHost`:$dbPorta non risponde: rete o VPN, per l'IT" }
|
|
162
|
+
foreach ($u in @($api, $contenitore)) {
|
|
163
|
+
$h = ([Uri]$u).Host
|
|
164
|
+
if (PortaAperta $h 443) { Esito 'rete NOEVA' 'ok' "$h`:443" }
|
|
165
|
+
else { Ferma 'rete NOEVA' "$h`:443 non risponde: serve l'uscita HTTPS (proxy?), per l'IT" }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# 2. Il driver ODBC --------------------------------------------------------------------
|
|
169
|
+
Passo '2. Il driver ODBC per SQL Server'
|
|
170
|
+
$driver = @(Get-OdbcDriver -Platform 64-bit -ErrorAction SilentlyContinue | Where-Object { $_.Name -match '^ODBC Driver (18|17) for SQL Server$' } | ForEach-Object { $_.Name })
|
|
171
|
+
if ($driver -contains 'ODBC Driver 18 for SQL Server') { Esito 'driver' 'ok' 'ODBC Driver 18 for SQL Server' }
|
|
172
|
+
elseif ($driver -contains 'ODBC Driver 17 for SQL Server') {
|
|
173
|
+
if (-not $SoloControllo) { [Environment]::SetEnvironmentVariable('NOEVA_MSSQL_DRIVER', 'ODBC Driver 17 for SQL Server', 'User') }
|
|
174
|
+
$env:NOEVA_MSSQL_DRIVER = 'ODBC Driver 17 for SQL Server'
|
|
175
|
+
Esito 'driver' 'avviso' 'c''e'' il 17, non il 18: va bene, impostato NOEVA_MSSQL_DRIVER'
|
|
176
|
+
} else {
|
|
177
|
+
Ferma 'driver' 'manca: ODBC Driver 18 for SQL Server x64 da https://aka.ms/downloadmsodbcsql (serve un amministratore)'
|
|
178
|
+
}
|
|
179
|
+
$driverUtente = [Environment]::GetEnvironmentVariable('NOEVA_MSSQL_DRIVER', 'User')
|
|
180
|
+
if ($driverUtente) { $env:NOEVA_MSSQL_DRIVER = $driverUtente }
|
|
181
|
+
|
|
182
|
+
# 3. La cartella -------------------------------------------------------------------------
|
|
183
|
+
Passo "3. La cartella $destinazione"
|
|
184
|
+
$giaInstallata = [bool](Get-ScheduledTask -TaskName $attivita -ErrorAction SilentlyContinue)
|
|
185
|
+
if ($SoloControllo) {
|
|
186
|
+
if (Test-Path $exe) { Esito 'cartella' 'ok' $destinazione } else { Ferma 'cartella' "$exe non c'e'" }
|
|
187
|
+
} elseif ($qui.TrimEnd('\') -ieq $destinazione) {
|
|
188
|
+
$modo = 'sul posto'
|
|
189
|
+
Esito 'cartella' 'ok' "lo script gira gia' da $destinazione"
|
|
190
|
+
} else {
|
|
191
|
+
if (Test-Path $destinazione) {
|
|
192
|
+
$modo = 'aggiornamento'
|
|
193
|
+
# Durante la copia l'attivita' non deve partire: si riaccende al passo 7.
|
|
194
|
+
if ($giaInstallata) { Disable-ScheduledTask -TaskName $attivita | Out-Null }
|
|
195
|
+
$inCorso = @(Get-Process -Name 'noeva-snapshot' -ErrorAction SilentlyContinue)
|
|
196
|
+
if ($inCorso.Count) {
|
|
197
|
+
Write-Host ' Un giro e'' in corso: aspetto che finisca (al massimo 10 minuti).'
|
|
198
|
+
$inCorso | Wait-Process -Timeout 600 -ErrorAction SilentlyContinue
|
|
199
|
+
}
|
|
200
|
+
$vecchia = "$destinazione-prima-$((Get-Date).ToString('yyyyMMdd-HHmm'))"
|
|
201
|
+
try { Rename-Item $destinazione (Split-Path -Leaf $vecchia) }
|
|
202
|
+
catch { Ferma 'cartella' "non riesco a rinominare $destinazione (in uso?): $($_.Exception.Message)" }
|
|
203
|
+
Copy-Item $qui $destinazione -Recurse
|
|
204
|
+
# lavoro\ (gli ultimi giri e ascolta.log) resta: serve a capire i giri passati.
|
|
205
|
+
if (Test-Path (Join-Path $vecchia 'lavoro')) { Copy-Item (Join-Path $vecchia 'lavoro') (Join-Path $destinazione 'lavoro') -Recurse -Force }
|
|
206
|
+
Esito 'cartella' 'ok' "vecchia messa da parte in $vecchia, lavoro\ riportato"
|
|
207
|
+
} else {
|
|
208
|
+
New-Item -ItemType Directory -Force (Split-Path $destinazione) | Out-Null
|
|
209
|
+
Copy-Item $qui $destinazione -Recurse
|
|
210
|
+
Esito 'cartella' 'ok' "copiata in $destinazione"
|
|
211
|
+
}
|
|
212
|
+
Get-ChildItem $destinazione -Recurse | Unblock-File
|
|
213
|
+
if (-not (Test-Path $exe)) { Ferma 'cartella' "$exe non c'e' dopo la copia" }
|
|
214
|
+
}
|
|
215
|
+
New-Item -ItemType Directory -Force (Join-Path $destinazione 'lavoro') | Out-Null
|
|
216
|
+
$v = Esegui $exe @('versione')
|
|
217
|
+
if ($v.Codice -eq 0) { Esito 'eseguibile' 'ok' $v.Testo.Trim() } else { Ferma 'eseguibile' "non parte: $($v.Testo.Trim())" }
|
|
218
|
+
|
|
219
|
+
# 4. La chiave del worker -----------------------------------------------------------------
|
|
220
|
+
Passo '4. La chiave NOEVA del worker'
|
|
221
|
+
$chiave = [Environment]::GetEnvironmentVariable('NOEVA_API_KEY', 'User')
|
|
222
|
+
if ($chiave -and -not $SoloControllo -and -not (Chiedi "La chiave e' gia' salvata per $env:USERNAME. La tengo?")) { $chiave = '' }
|
|
223
|
+
if (-not $chiave) {
|
|
224
|
+
if ($SoloControllo) { Ferma 'chiave' 'manca NOEVA_API_KEY: rilancia senza -SoloControllo' }
|
|
225
|
+
for ($i = 0; $i -lt 3; $i++) {
|
|
226
|
+
$chiave = ChiediSegreto "Chiave NOEVA dell'utente di servizio (noeva_sk_..., non si vede mentre scrivi)"
|
|
227
|
+
if ($chiave.StartsWith('noeva_sk_')) { break }
|
|
228
|
+
Write-Host ' Non sembra una chiave NOEVA (comincia con noeva_sk_).' -ForegroundColor Yellow
|
|
229
|
+
$chiave = ''
|
|
230
|
+
}
|
|
231
|
+
if (-not $chiave) { Ferma 'chiave' 'nessuna chiave valida inserita' }
|
|
232
|
+
[Environment]::SetEnvironmentVariable('NOEVA_API_KEY', $chiave, 'User')
|
|
233
|
+
}
|
|
234
|
+
$env:NOEVA_API_KEY = $chiave
|
|
235
|
+
$chiave = $null
|
|
236
|
+
# La prova della chiave: un biglietto per una versione di prova. Dice in una volta se la chiave
|
|
237
|
+
# vale, se l'utente e' membro del workspace e se il workspace ha il modulo. Non scrive niente.
|
|
238
|
+
$b = ApiNoeva 'POST' '/api/analytics/biglietto-dati' @{ progetto = $progetto; versione = 'prova-installazione'; da = $env:COMPUTERNAME }
|
|
239
|
+
switch ($b.Codice) {
|
|
240
|
+
200 { Esito 'chiave' 'ok' "valida, l'utente e' membro del workspace e il modulo e' attivo" }
|
|
241
|
+
401 { Ferma 'chiave' 'non valida o revocata: rilancia e reinseriscila' }
|
|
242
|
+
403 { Ferma 'chiave' "rifiutata: $($b.Errore) (l'utente e' membro del workspace? il modulo e' abilitato?)" }
|
|
243
|
+
503 { Ferma 'chiave' "l'API non rilascia biglietti (manca ANALYTICS_SEGRETO_DATI): e' da NOEVA, non da qui" }
|
|
244
|
+
default { Ferma 'chiave' "risposta inattesa dall'API ($($b.Codice)): $($b.Errore)" }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
# 5. La password e la verifica ----------------------------------------------------------------
|
|
248
|
+
Passo "5. La password di $utenteDb e la verifica dal vivo"
|
|
249
|
+
$password = [Environment]::GetEnvironmentVariable($varPassword, 'User')
|
|
250
|
+
if ($password -and -not $SoloControllo -and -not (Chiedi "La password e' gia' salvata per $env:USERNAME. La tengo?")) { $password = '' }
|
|
251
|
+
if (-not $password) {
|
|
252
|
+
if ($SoloControllo) { Ferma 'password' "manca $varPassword`: rilancia senza -SoloControllo" }
|
|
253
|
+
for ($i = 0; $i -lt 3 -and -not $password; $i++) { $password = ChiediSegreto "Password di $utenteDb (non si vede mentre scrivi)" }
|
|
254
|
+
if (-not $password) { Ferma 'password' 'nessuna password inserita' }
|
|
255
|
+
[Environment]::SetEnvironmentVariable($varPassword, $password, 'User')
|
|
256
|
+
}
|
|
257
|
+
Set-Item "env:$varPassword" $password
|
|
258
|
+
$password = $null
|
|
259
|
+
$v = EseguiDalVivo $exe @('verifica', '--pacchetto', $pacchetto, '--connessione', (Join-Path $pacchetto 'connessione.json'))
|
|
260
|
+
$avvisiVerifica = @($v.Testo -split "`n" | Where-Object { $_ -match '^\s*avviso\s' }).Count
|
|
261
|
+
if ($v.Codice -eq 0 -and $avvisiVerifica) { Esito 'verifica' 'avviso' "i controlli tornano, $avvisiVerifica avvisi entro la tolleranza (dati che l'ERP riscrive)" }
|
|
262
|
+
elseif ($v.Codice -eq 0) { Esito 'verifica' 'ok' 'connessione e cifre di controllo tornano' }
|
|
263
|
+
else {
|
|
264
|
+
$primoNo = ($v.Testo -split "`n" | Where-Object { $_ -match '^\s*NO\s' } | Select-Object -First 1)
|
|
265
|
+
Ferma 'verifica' $(if ($primoNo) { $primoNo.Trim() } else { "uscita $($v.Codice)" })
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# 6. Il primo giro ------------------------------------------------------------------------------
|
|
269
|
+
if ($SoloControllo) {
|
|
270
|
+
$log = Join-Path $destinazione 'lavoro\ascolta.log'
|
|
271
|
+
if (Test-Path $log) {
|
|
272
|
+
$ultima = Get-Content $log -Tail 40 | Where-Object { $_ -match 'pubblicata|FALLITO' } | Select-Object -Last 1
|
|
273
|
+
Esito 'ultimo giro' $(if ($ultima -match 'FALLITO') { 'NO' } elseif ($ultima) { 'ok' } else { 'avviso' }) $(if ($ultima) { $ultima.Trim() } else { 'nessun giro ancora in ascolta.log' })
|
|
274
|
+
} else { Esito 'ultimo giro' 'avviso' 'ascolta.log non c''e'' ancora' }
|
|
275
|
+
} else {
|
|
276
|
+
Passo '6. Il primo giro, adesso'
|
|
277
|
+
if (Chiedi 'Faccio partire adesso il primo giro (circa 4 minuti)?') {
|
|
278
|
+
$jobs = ApiNoeva 'GET' '/api/scheduled-jobs' $null
|
|
279
|
+
$job = @($jobs.Corpo.data | Where-Object { $_.process_function -eq $pipeline }) | Select-Object -First 1
|
|
280
|
+
if (-not $job) {
|
|
281
|
+
Esito 'schedulazione' 'NO' "nessuna schedulazione '$pipeline' nel workspace: va creata nel pannello NOEVA (tipo ETL)"
|
|
282
|
+
} else {
|
|
283
|
+
Esito 'schedulazione' $(if ($job.enabled) { 'ok' } else { 'avviso' }) ("'{0}', {1} {2}{3}" -f $job.name, $job.cron_expression, $job.timezone, $(if ($job.enabled) { '' } else { ', DISATTIVATA: riattivala dal pannello' }))
|
|
284
|
+
$t = ApiNoeva 'POST' "/api/scheduled-jobs/$($job.id)/trigger" $null
|
|
285
|
+
if ($t.Codice -ne 200) { Esito 'primo giro' 'NO' "non riesco a farlo partire ($($t.Codice)): $($t.Errore)" }
|
|
286
|
+
else {
|
|
287
|
+
Write-Host ' In coda. Lo eseguo qui (i giri arretrati in coda, se ce ne sono, passano tutti):'
|
|
288
|
+
$g = EseguiDalVivo $exe @('ascolta', '--config', (Join-Path $destinazione 'worker.json'), '--una-volta')
|
|
289
|
+
Add-Content -Path (Join-Path $destinazione 'lavoro\ascolta.log') -Value $g.Testo -Encoding UTF8
|
|
290
|
+
$pubblicate = @([regex]::Matches($g.Testo, ': pubblicata (\S+) in') | ForEach-Object { $_.Groups[1].Value })
|
|
291
|
+
$falliti = @($g.Testo -split "`n" | Where-Object { $_ -match 'FALLITO' })
|
|
292
|
+
if ($falliti.Count) { Esito 'primo giro' 'NO' $falliti[0].Trim() }
|
|
293
|
+
elseif ($pubblicate.Count) { Esito 'primo giro' 'ok' "pubblicata la versione $($pubblicate[-1]) ($($pubblicate.Count) giri)" }
|
|
294
|
+
else { Esito 'primo giro' 'avviso' 'coda vuota: il giro lo prendera'' l''attivita'' pianificata, guarda ascolta.log' }
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
} else { Esito 'primo giro' 'saltato' 'lo fara'' l''attivita'' pianificata alla prossima schedulazione' }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
# 7. L'attivita' pianificata --------------------------------------------------------------------
|
|
301
|
+
Passo "7. L'attivita' pianificata '$attivita'"
|
|
302
|
+
if ($SoloControllo) {
|
|
303
|
+
$t = Get-ScheduledTask -TaskName $attivita -ErrorAction SilentlyContinue
|
|
304
|
+
if (-not $t) { Esito 'attivita''' 'NO' 'non installata: rilancia senza -SoloControllo' }
|
|
305
|
+
else {
|
|
306
|
+
$info = Get-ScheduledTaskInfo -TaskName $attivita
|
|
307
|
+
$stato = if ($t.State -eq 'Disabled') { 'NO' } elseif ($info.LastTaskResult -in 0, 267009, 267011) { 'ok' } else { 'avviso' }
|
|
308
|
+
Esito 'attivita''' $stato ("{0}, ultimo avvio {1}, risultato {2}" -f $t.State, $info.LastRunTime, $info.LastTaskResult)
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
Write-Host " Ogni $minuti minuti lancia 'ascolta --una-volta' come $env:USERNAME, anche senza sessione aperta:"
|
|
312
|
+
Write-Host ' per questo Windows chiede la password di questo utente (quella di Windows, non del database).'
|
|
313
|
+
$credenziali = Get-Credential -UserName "$env:USERDOMAIN\$env:USERNAME" -Message "Password Windows di $env:USERNAME"
|
|
314
|
+
if (-not $credenziali) { Ferma 'attivita''' 'nessuna password Windows: senza, l''attivita'' non gira a sessione chiusa' }
|
|
315
|
+
$log = Join-Path $destinazione 'lavoro\ascolta.log'
|
|
316
|
+
$azione = New-ScheduledTaskAction -Execute 'cmd.exe' -WorkingDirectory $destinazione `
|
|
317
|
+
-Argument "/c `"`"$exe`" ascolta --config `"$(Join-Path $destinazione 'worker.json')`" --una-volta >> `"$log`" 2>&1`""
|
|
318
|
+
$quando = New-ScheduledTaskTrigger -Once -At (Get-Date).Date -RepetitionInterval (New-TimeSpan -Minutes $minuti)
|
|
319
|
+
$regole = New-ScheduledTaskSettingsSet -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
|
|
320
|
+
-StartWhenAvailable -DontStopIfGoingOnBatteries -AllowStartIfOnBatteries
|
|
321
|
+
try {
|
|
322
|
+
Register-ScheduledTask -TaskName $attivita -Action $azione -Trigger $quando -Settings $regole `
|
|
323
|
+
-User $credenziali.UserName -Password $credenziali.GetNetworkCredential().Password -RunLevel Limited -Force | Out-Null
|
|
324
|
+
Enable-ScheduledTask -TaskName $attivita | Out-Null
|
|
325
|
+
Esito 'attivita''' 'ok' "ogni $minuti minuti come $($credenziali.UserName); registro in $log"
|
|
326
|
+
} catch {
|
|
327
|
+
Esito 'attivita''' 'NO' "Windows l'ha rifiutata (password giusta?): $($_.Exception.Message)"
|
|
328
|
+
}
|
|
329
|
+
$credenziali = $null
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# 8. La dashboard ------------------------------------------------------------------------------
|
|
333
|
+
if (-not $SoloControllo) {
|
|
334
|
+
Passo '8. La dashboard'
|
|
335
|
+
Write-Host " Apri $contenitore/$progetto/ (login NOEVA) e guarda la data dei dati."
|
|
336
|
+
if (Chiedi 'La data e'' quella di oggi?') { Esito 'dashboard' 'ok' 'dati di oggi' }
|
|
337
|
+
else { Esito 'dashboard' 'avviso' 'da guardare con chi segue l''installazione (la pagina si ricarica?)' }
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
Fine
|
package/core.js
CHANGED
|
@@ -50,7 +50,12 @@ export function leggiConfig(radice) {
|
|
|
50
50
|
if (!Array.isArray(nonGestiti) || nonGestiti.some((p) => typeof p !== 'string')) {
|
|
51
51
|
throw new ErroreConfig('analytics.json: `non_gestiti` deve essere un elenco di percorsi');
|
|
52
52
|
}
|
|
53
|
-
|
|
53
|
+
// Il workspace NOEVA che possiede il progetto: lo nomina il biglietto con cui
|
|
54
|
+
// `analytics pubblica` scrive le pagine. Non e' un segreto, e' un id.
|
|
55
|
+
if (c.workspace !== undefined && typeof c.workspace !== 'string') {
|
|
56
|
+
throw new ErroreConfig('analytics.json: `workspace` deve essere l\'id del workspace NOEVA');
|
|
57
|
+
}
|
|
58
|
+
return { progetto: c.progetto, nonGestiti, ...(c.workspace ? { workspace: c.workspace } : {}) };
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
// --- file gestiti ---------------------------------------------------------
|
|
@@ -78,7 +83,9 @@ export function fileGestiti(config) {
|
|
|
78
83
|
for (const f of readdirSync(join(CONTENUTO, 'assets')).sort()) {
|
|
79
84
|
voci.push({ percorso: `apps/assets/${f}`, sorgente: join(CONTENUTO, 'assets', f) });
|
|
80
85
|
}
|
|
81
|
-
|
|
86
|
+
for (const w of ['ci.yml', 'rilascio-windows.yml']) {
|
|
87
|
+
voci.push({ percorso: `.github/workflows/${w}`, sorgente: join(MODELLO, 'github', 'workflows', w) });
|
|
88
|
+
}
|
|
82
89
|
for (const nome of readdirSync(join(KIT, 'skills')).sort()) {
|
|
83
90
|
const base = join(KIT, 'skills', nome);
|
|
84
91
|
for (const f of fileSotto(base).sort()) {
|
|
@@ -269,6 +276,15 @@ export function verifica(radice, config) {
|
|
|
269
276
|
|
|
270
277
|
// L'ultima versione pubblicata, o null se il registry non risponde o il pacchetto
|
|
271
278
|
// non c'e': il controllo resta in sola lettura e non fallisce per la rete.
|
|
279
|
+
/** a e' una versione piu' nuova di b? (x.y.z; un prerilascio o un formato strano non lo e') */
|
|
280
|
+
export function piuNuova(a, b) {
|
|
281
|
+
const n = (v) => (/^\d+\.\d+\.\d+$/.test(v || '') ? v.split('.').map(Number) : null);
|
|
282
|
+
const [x, y] = [n(a), n(b)];
|
|
283
|
+
if (!x || !y) return false;
|
|
284
|
+
for (let i = 0; i < 3; i++) if (x[i] !== y[i]) return x[i] > y[i];
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
272
288
|
export async function ultimaVersione(fetchImpl = fetch) {
|
|
273
289
|
try {
|
|
274
290
|
const r = await fetchImpl('https://registry.npmjs.org/@noeva%2fanalytics-core', { signal: AbortSignal.timeout(5000) });
|
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
# La verifica del progetto
|
|
2
|
-
# Quali controlli girano lo decide la versione del pacchetto,
|
|
3
|
-
# qui ci sono solo l'installazione e
|
|
1
|
+
# La verifica del progetto e, su main, la pubblicazione (ADR 0005 di NOEVA Analytics).
|
|
2
|
+
# Quali controlli girano lo decide la versione del pacchetto @noeva/analytics-core,
|
|
3
|
+
# non questo file: qui ci sono solo l'installazione e i comandi.
|
|
4
4
|
# File gestito: lo riscrive `analytics upgrade`, non si modifica a mano.
|
|
5
|
+
#
|
|
6
|
+
# `pubblica` porta online, a ogni push su main che passa la verifica, le pagine
|
|
7
|
+
# (apps/) e il modello compilato del commit: su R2, attraverso il contenitore, con
|
|
8
|
+
# un biglietto che l'API NOEVA rilascia alla chiave di un membro del workspace del
|
|
9
|
+
# progetto. Si accende con due impostazioni del repository:
|
|
10
|
+
#
|
|
11
|
+
# secret NOEVA_API_KEY la chiave API NOEVA di un utente del workspace
|
|
12
|
+
# variable ANALYTICS_PUBBLICA "si" quando il contenitore serve questo progetto da R2
|
|
13
|
+
# variable ANALYTICS_URL facoltativa, default https://analytics.noeva.ai
|
|
14
|
+
#
|
|
15
|
+
# I dati no: lo snapshot si genera dove c'e' il database (aggiorna.sh --pubblica
|
|
16
|
+
# sul Mac, o il worker dal cliente) e si pubblica da li'.
|
|
5
17
|
|
|
6
18
|
name: ci
|
|
7
19
|
|
|
@@ -25,3 +37,41 @@ jobs:
|
|
|
25
37
|
node-version: '24'
|
|
26
38
|
- run: npm ci
|
|
27
39
|
- run: npx analytics ci
|
|
40
|
+
|
|
41
|
+
pubblica:
|
|
42
|
+
needs: verifica
|
|
43
|
+
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request' && vars.ANALYTICS_PUBBLICA == 'si'
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
# Una pubblicazione alla volta, e quella in corso non si interrompe: la
|
|
46
|
+
# successiva aspetta e pubblica il main piu' recente.
|
|
47
|
+
concurrency:
|
|
48
|
+
group: pubblica
|
|
49
|
+
cancel-in-progress: false
|
|
50
|
+
env:
|
|
51
|
+
NOEVA_API_KEY: ${{ secrets.NOEVA_API_KEY }}
|
|
52
|
+
URL: ${{ vars.ANALYTICS_URL || 'https://analytics.noeva.ai' }}
|
|
53
|
+
steps:
|
|
54
|
+
- uses: actions/checkout@v4
|
|
55
|
+
- uses: actions/setup-node@v4
|
|
56
|
+
with:
|
|
57
|
+
node-version: '24'
|
|
58
|
+
- run: npm ci
|
|
59
|
+
- name: la chiave API NOEVA c'e'
|
|
60
|
+
run: |
|
|
61
|
+
if [ -z "$NOEVA_API_KEY" ]; then
|
|
62
|
+
echo "::error::manca il secret NOEVA_API_KEY: la chiave di un utente del workspace del progetto"
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
- name: le pagine
|
|
66
|
+
run: npx analytics pubblica --url "$URL"
|
|
67
|
+
# Il modello si compila con wren, che e' Python: le versioni sono quelle del
|
|
68
|
+
# servizio locale (lib/mcp-locale/ambiente del pacchetto).
|
|
69
|
+
- uses: actions/setup-python@v5
|
|
70
|
+
if: hashFiles('wren_project.yml') != ''
|
|
71
|
+
with:
|
|
72
|
+
python-version: '3.11'
|
|
73
|
+
- name: il modello
|
|
74
|
+
if: hashFiles('wren_project.yml') != ''
|
|
75
|
+
run: |
|
|
76
|
+
pip install --quiet "wrenai==0.15.0" "wren-core-py==0.8.0" "pyyaml>=6"
|
|
77
|
+
PYTHON=python npx analytics modello --url "$URL"
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# I due eseguibili Windows del progetto, costruiti e provati su un runner Windows
|
|
2
|
+
# (PyInstaller non fa cross-compilazione), con le ricette del pacchetto
|
|
3
|
+
# @noeva/analytics-core (ADR 0005 di NOEVA Analytics):
|
|
4
|
+
#
|
|
5
|
+
# mcp la cartella per Claude Code sui PC: noeva-<id>.exe, il servizio MCP,
|
|
6
|
+
# con la configurazione di consegna.yml. Il modello lo scarica da se'
|
|
7
|
+
# dall'ultima versione pubblicata (ci.yml, job pubblica); quello nella
|
|
8
|
+
# cartella e' l'ultimo ripiego.
|
|
9
|
+
# snapshot l'eseguibile del worker degli snapshot (noeva-snapshot.exe) e il
|
|
10
|
+
# pacchetto del progetto, per il server del cliente (ADR 0004).
|
|
11
|
+
#
|
|
12
|
+
# Solo manuale. Nessuna credenziale: le cartelle non ne contengono, e il database
|
|
13
|
+
# del cliente da qui non si raggiunge. Si prova fin dove si arriva senza: il driver
|
|
14
|
+
# ODBC 18, il login che fallisce sulla rete, installa.ps1 con PowerShell 5.1.
|
|
15
|
+
#
|
|
16
|
+
# Le cartelle escono come artefatto (7 giorni). Con `rilascia` diventano una
|
|
17
|
+
# versione permanente di questo repository: noeva-<id>-v<versione> e
|
|
18
|
+
# noeva-snapshot-<id>-v<versione>. Una versione non si riscrive.
|
|
19
|
+
# File gestito: lo riscrive `analytics upgrade`, non si modifica a mano.
|
|
20
|
+
|
|
21
|
+
name: rilascio-windows
|
|
22
|
+
|
|
23
|
+
on:
|
|
24
|
+
workflow_dispatch:
|
|
25
|
+
inputs:
|
|
26
|
+
cosa:
|
|
27
|
+
description: "mcp = la cartella per Claude Code sui PC; snapshot = l'eseguibile del worker (ADR 0004)"
|
|
28
|
+
type: choice
|
|
29
|
+
options: [mcp, snapshot]
|
|
30
|
+
default: mcp
|
|
31
|
+
versione:
|
|
32
|
+
description: "La versione (es. 2026.11.6); vuota = la data di oggi"
|
|
33
|
+
required: false
|
|
34
|
+
default: ""
|
|
35
|
+
rilascia:
|
|
36
|
+
description: "Pubblica la versione come release permanente di questo repository"
|
|
37
|
+
type: boolean
|
|
38
|
+
default: false
|
|
39
|
+
|
|
40
|
+
permissions:
|
|
41
|
+
contents: write
|
|
42
|
+
|
|
43
|
+
jobs:
|
|
44
|
+
windows:
|
|
45
|
+
runs-on: windows-latest
|
|
46
|
+
timeout-minutes: 40
|
|
47
|
+
env:
|
|
48
|
+
# Il builder e il server stampano accenti e "·": la console del runner no.
|
|
49
|
+
PYTHONUTF8: "1"
|
|
50
|
+
PYTHONIOENCODING: utf-8
|
|
51
|
+
steps:
|
|
52
|
+
- uses: actions/checkout@v4
|
|
53
|
+
- uses: actions/setup-node@v4
|
|
54
|
+
with:
|
|
55
|
+
node-version: '24'
|
|
56
|
+
- run: npm ci
|
|
57
|
+
- uses: astral-sh/setup-uv@v6
|
|
58
|
+
|
|
59
|
+
- name: Il progetto e le ricette del pacchetto
|
|
60
|
+
id: dove
|
|
61
|
+
shell: pwsh
|
|
62
|
+
run: |
|
|
63
|
+
$id = (Get-Content analytics.json -Raw | ConvertFrom-Json).progetto
|
|
64
|
+
$lib = (npx analytics percorso) + "\lib"
|
|
65
|
+
$versione = "${{ inputs.versione }}"
|
|
66
|
+
if (-not $versione) { $d = Get-Date; $versione = "$($d.Year).$($d.Month).$($d.Day)" }
|
|
67
|
+
"id=$id" >> $env:GITHUB_OUTPUT
|
|
68
|
+
"lib=$lib" >> $env:GITHUB_OUTPUT
|
|
69
|
+
"versione=$versione" >> $env:GITHUB_OUTPUT
|
|
70
|
+
"progetto $id, versione $versione, ricette da $lib (@noeva/analytics-core $(npx analytics versione))"
|
|
71
|
+
|
|
72
|
+
- name: Il driver ODBC 18, come sui PC e sul server del cliente
|
|
73
|
+
shell: pwsh
|
|
74
|
+
run: |
|
|
75
|
+
choco install sqlserver-odbcdriver -y --no-progress | Out-Null
|
|
76
|
+
Get-OdbcDriver -Platform 64-bit | Select-Object Name
|
|
77
|
+
|
|
78
|
+
# --- mcp ------------------------------------------------------------------
|
|
79
|
+
- name: La cartella e l'eseguibile del servizio MCP
|
|
80
|
+
if: ${{ inputs.cosa == 'mcp' }}
|
|
81
|
+
shell: pwsh
|
|
82
|
+
run: uv run --with pyyaml python "${{ steps.dove.outputs.lib }}\mcp-locale\costruisci.py" . --versione "${{ steps.dove.outputs.versione }}"
|
|
83
|
+
|
|
84
|
+
# Il ramo vivo con il driver 18 e una password finta: deve dire "ok driver" e
|
|
85
|
+
# poi fallire sulla rete. Un errore di import o di driver vuol dire una ricetta
|
|
86
|
+
# PyInstaller da correggere.
|
|
87
|
+
- name: Il ramo vivo dell'eseguibile (pyodbc e driver 18, senza rete)
|
|
88
|
+
if: ${{ inputs.cosa == 'mcp' }}
|
|
89
|
+
shell: pwsh
|
|
90
|
+
run: |
|
|
91
|
+
$id = "${{ steps.dove.outputs.id }}"
|
|
92
|
+
$exe = "dist\mcp-locale\noeva-$id\noeva\noeva-$id.exe"
|
|
93
|
+
$conf = Get-Content "dist\mcp-locale\noeva-$id\noeva\noeva.json" -Raw | ConvertFrom-Json
|
|
94
|
+
$vive = $conf.sorgenti.PSObject.Properties | Where-Object { $_.Value.tipo -eq 'mssql' }
|
|
95
|
+
if (-not $vive) { "nessuna sorgente viva: niente da provare"; exit 0 }
|
|
96
|
+
foreach ($v in $vive) {
|
|
97
|
+
if ($v.Value.password_env) { [Environment]::SetEnvironmentVariable($v.Value.password_env, 'non-e-la-password', 'Process') }
|
|
98
|
+
$out = "$env:RUNNER_TEMP\verifica-$($v.Name).txt"
|
|
99
|
+
$p = Start-Process $exe -ArgumentList '--verifica', '--sorgente', $v.Name -NoNewWindow -PassThru -RedirectStandardOutput $out
|
|
100
|
+
$tempo = [Diagnostics.Stopwatch]::StartNew()
|
|
101
|
+
if (-not $p.WaitForExit(180000)) { $p.Kill(); " (fermato dopo 180 s)" }
|
|
102
|
+
$testo = Get-Content $out -Raw
|
|
103
|
+
$testo
|
|
104
|
+
" durata: $([int]$tempo.Elapsed.TotalSeconds) s"
|
|
105
|
+
if ($testo -match 'pyodbc non utilizzabile|non e'' installato') { throw "il ramo vivo non arriva al login: ricetta da correggere" }
|
|
106
|
+
if ($testo -notmatch 'ok driver') { throw "nessun 'ok driver' per $($v.Name)" }
|
|
107
|
+
}
|
|
108
|
+
exit 0
|
|
109
|
+
|
|
110
|
+
- name: installa.ps1 parte e chiude con il riepilogo
|
|
111
|
+
if: ${{ inputs.cosa == 'mcp' }}
|
|
112
|
+
shell: pwsh
|
|
113
|
+
run: |
|
|
114
|
+
$dir = "dist\mcp-locale\noeva-${{ steps.dove.outputs.id }}"
|
|
115
|
+
$out = (powershell -NoProfile -ExecutionPolicy Bypass -File "$dir\installa.ps1" -SoloControllo 2>&1 | Out-String)
|
|
116
|
+
$out
|
|
117
|
+
if ($out -notmatch 'RIEPILOGO' -or $out -notmatch 'NON COMPLETATA') { throw "installa.ps1 non arriva al riepilogo" }
|
|
118
|
+
$esito = Get-ChildItem $dir -Filter 'esito-installa-*.txt'
|
|
119
|
+
if (-not $esito) { throw "installa.ps1 non scrive il file dell'esito" }
|
|
120
|
+
Remove-Item $esito.FullName
|
|
121
|
+
"ok: installa.ps1 gira con PowerShell 5.1 e chiude con l'esito"
|
|
122
|
+
exit 0
|
|
123
|
+
|
|
124
|
+
# --- snapshot -------------------------------------------------------------
|
|
125
|
+
- name: L'eseguibile degli snapshot e il pacchetto del progetto
|
|
126
|
+
if: ${{ inputs.cosa == 'snapshot' }}
|
|
127
|
+
shell: pwsh
|
|
128
|
+
run: |
|
|
129
|
+
uv run --with pyyaml python "${{ steps.dove.outputs.lib }}\snapshot\impacchetta.py" eseguibile .
|
|
130
|
+
uv run --with pyyaml python "${{ steps.dove.outputs.lib }}\snapshot\impacchetta.py" pacchetto .
|
|
131
|
+
|
|
132
|
+
- name: Prova dell'eseguibile fino al login
|
|
133
|
+
if: ${{ inputs.cosa == 'snapshot' }}
|
|
134
|
+
shell: pwsh
|
|
135
|
+
run: |
|
|
136
|
+
$id = "${{ steps.dove.outputs.id }}"
|
|
137
|
+
$exe = "dist\snapshot\noeva-snapshot\noeva-snapshot.exe"
|
|
138
|
+
$pac = "dist\snapshot\pacchetti\$id"
|
|
139
|
+
& $exe versione
|
|
140
|
+
if ($LASTEXITCODE -ne 0) { throw "versione: uscita $LASTEXITCODE" }
|
|
141
|
+
$conf = Get-Content "$pac\connessione.json" -Raw | ConvertFrom-Json
|
|
142
|
+
if ($conf.password_env) { [Environment]::SetEnvironmentVariable($conf.password_env, 'non-e-la-password', 'Process') }
|
|
143
|
+
$out = "$env:RUNNER_TEMP\verifica.txt"
|
|
144
|
+
$p = Start-Process $exe -ArgumentList 'verifica', '--pacchetto', $pac, '--connessione', "$pac\connessione.json" -NoNewWindow -PassThru -RedirectStandardOutput $out -RedirectStandardError "$out.err"
|
|
145
|
+
if (-not $p.WaitForExit(240000)) { $p.Kill(); " (fermato dopo 240 s)" }
|
|
146
|
+
$testo = (Get-Content $out -Raw -Encoding utf8) + (Get-Content "$out.err" -Raw -Encoding utf8)
|
|
147
|
+
$testo
|
|
148
|
+
if ($testo -match 'ModuleNotFoundError|ImportError|No module named|non e'' installato|Data source name not found|manca la password') { throw "l'eseguibile non arriva al login: ricetta da correggere" }
|
|
149
|
+
if ($testo -notmatch 'TCP Provider|Login timeout|timeout|08001|HYT00|network') { throw "atteso un errore di rete al login, non trovato" }
|
|
150
|
+
"ok: l'eseguibile arriva al login, si ferma sulla rete come deve"
|
|
151
|
+
# ascolta (ADR 0004): con un'API che non risponde deve dirlo e uscire con 1.
|
|
152
|
+
$w = Get-Content dist\snapshot\worker.json -Raw | ConvertFrom-Json
|
|
153
|
+
$w.api = 'http://127.0.0.1:9'
|
|
154
|
+
$w | ConvertTo-Json -Depth 5 | Set-Content "$env:RUNNER_TEMP\worker.json" -Encoding utf8
|
|
155
|
+
Copy-Item dist\snapshot\pacchetti "$env:RUNNER_TEMP\pacchetti" -Recurse -Force
|
|
156
|
+
$env:NOEVA_API_KEY = 'noeva_sk_finta'
|
|
157
|
+
$testo = (& $exe ascolta --config "$env:RUNNER_TEMP\worker.json" --una-volta 2>&1 | Out-String)
|
|
158
|
+
$testo
|
|
159
|
+
if ($testo -notmatch "in ascolto per analytics-snapshot-$id" -or $testo -notmatch 'non riesco a chiedere lavoro') { throw "ascolta non parte come deve" }
|
|
160
|
+
$out = (powershell -NoProfile -ExecutionPolicy Bypass -File dist\snapshot\installa.ps1 -SoloControllo 2>&1 | Out-String)
|
|
161
|
+
$out
|
|
162
|
+
if ($out -notmatch 'RIEPILOGO' -or $out -notmatch 'NON COMPLETATA') { throw "installa.ps1 non arriva al riepilogo" }
|
|
163
|
+
Get-ChildItem dist\snapshot -Filter 'esito-installa-*.txt' | Remove-Item
|
|
164
|
+
exit 0
|
|
165
|
+
|
|
166
|
+
- name: Lo zip degli snapshot
|
|
167
|
+
if: ${{ inputs.cosa == 'snapshot' }}
|
|
168
|
+
shell: pwsh
|
|
169
|
+
run: Compress-Archive -Path dist\snapshot\noeva-snapshot, dist\snapshot\pacchetti, dist\snapshot\worker.json, dist\snapshot\*.ps1 -DestinationPath dist\snapshot\noeva-snapshot-windows.zip
|
|
170
|
+
|
|
171
|
+
# --- artefatto e versione permanente --------------------------------------
|
|
172
|
+
- uses: actions/upload-artifact@v4
|
|
173
|
+
with:
|
|
174
|
+
name: ${{ inputs.cosa }}-${{ steps.dove.outputs.versione }}
|
|
175
|
+
path: |
|
|
176
|
+
dist/mcp-locale/*.zip
|
|
177
|
+
dist/snapshot/noeva-snapshot-windows.zip
|
|
178
|
+
retention-days: 7
|
|
179
|
+
|
|
180
|
+
- name: La versione permanente (release)
|
|
181
|
+
if: ${{ inputs.rilascia }}
|
|
182
|
+
shell: pwsh
|
|
183
|
+
env:
|
|
184
|
+
GH_TOKEN: ${{ github.token }}
|
|
185
|
+
run: |
|
|
186
|
+
$id = "${{ steps.dove.outputs.id }}"
|
|
187
|
+
$versione = "${{ steps.dove.outputs.versione }}"
|
|
188
|
+
if ("${{ inputs.cosa }}" -eq "mcp") {
|
|
189
|
+
$tag = "noeva-$id-v$versione"; $titolo = "noeva-$id $versione"
|
|
190
|
+
$zip = "dist\mcp-locale\noeva-$id-$versione.zip"; Copy-Item "dist\mcp-locale\noeva-$id.zip" $zip
|
|
191
|
+
$guida = "docs\installazione-$id-windows.md"
|
|
192
|
+
$note = "Cartella per Claude Code su Windows: noeva-$id.exe, dal commit $env:GITHUB_SHA. Il modello arriva da solo dall'ultima versione pubblicata. Installazione: $guida (allegata); poi ``noeva\noeva-$id.exe --verifica``."
|
|
193
|
+
$latest = "--latest"
|
|
194
|
+
} else {
|
|
195
|
+
$tag = "noeva-snapshot-$id-v$versione"; $titolo = "noeva-snapshot $id $versione"
|
|
196
|
+
$zip = "dist\snapshot\noeva-snapshot-$id-windows-$versione.zip"; Copy-Item dist\snapshot\noeva-snapshot-windows.zip $zip
|
|
197
|
+
$guida = "docs\installazione-snapshot-$id-windows.md"
|
|
198
|
+
$note = "noeva-snapshot.exe per Windows e il pacchetto di $id (snapshot.yml, mdl.json, connessione.json senza password), dal commit $env:GITHUB_SHA. ADR 0004. Installazione: $guida (allegata)."
|
|
199
|
+
# la release in evidenza resta quella della cartella MCP che il cliente usa
|
|
200
|
+
$latest = "--latest=false"
|
|
201
|
+
}
|
|
202
|
+
gh release view $tag 2>$null
|
|
203
|
+
if ($LASTEXITCODE -eq 0) { throw "la versione $tag esiste gia': una versione non si riscrive, scegline un'altra" }
|
|
204
|
+
$file = @($zip)
|
|
205
|
+
if (Test-Path $guida) { $file += $guida }
|
|
206
|
+
gh release create $tag @file --target $env:GITHUB_SHA --title $titolo --notes $note $latest
|
|
207
|
+
"pubblicata $tag"
|