diff --git a/SUIVI.md b/SUIVI.md index 2788b28..dcaad7f 100644 --- a/SUIVI.md +++ b/SUIVI.md @@ -1710,6 +1710,7 @@ engageantes : | Date | Événement | |---|---| +| 07/08/2026 | **Le one-liner d'installation mourait sur sa première commande, chez qui ne l'avait pas ouvert en administrateur — c'est-à-dire chez presque tout le monde.** Signalé sur le forum des supermarchés coopératifs, capture à l'appui : bannière affichée, puis `iex : Impossible de convertir la valeur «System.String» en type «System.Management.Automation.SwitchParameter»`, `MetadataError`, `RuntimeException,Microsoft.PowerShell.Commands.InvokeExpressionCommand`. **La position accusait `iex`, au caractère 96 de la ligne tapée** — soit exactement le premier caractère de `iex` dans la commande publiée, vérifié en comptant —, donc ni le fichier, ni la ligne, ni la variable. **Troisième défaut de la famille du 01/08/2026, dans une troisième forme.** `bootstrap.ps1` déclare `[switch]$Relaunched` et écrivait, quarante lignes plus bas, `$relaunched = Join-Path $env:TEMP 'openscale-bootstrap.ps1'` : les noms de variables PowerShell sont **insensibles à la casse**, ces deux-là sont la même, et celle d'un paramètre est **typée** — un chemin rangé dans une `[switch]` lève. `$ErrorActionPreference = 'Stop'`, posé trois lignes après le `param`, transformait l'erreur en terminating et la faisait ressortir attribuée à l'`Invoke-Expression` appelant, ce qui explique le message illisible. **Pourquoi personne ne l'avait vu** : la ligne vit dans la branche d'auto-élévation, qui ne s'exécute **que** dans une console non élevée — l'en-tête du script dit lui-même « neuf fois sur dix » —, et une installation lancée depuis une fenêtre administrateur la saute entièrement. **Reproduit avant de toucher quoi que ce soit**, au banc et non par lecture : le script réel passé à `iex` dans une console non élevée, `Invoke-WebRequest` et `Start-Process` bouchonnés, rend l'erreur du forum **au message, à la catégorie et à l'identifiant près** ; le même banc sur le fichier corrigé va jusqu'à « l'installation continue dans la nouvelle fenetre ». La variable s'appelle `$relaunchedScript`. **Le garde-fou ferme la famille au lieu du cas** : `TestNoLocalVariableCollidesWithAParameterByCaseAlone` refuse, dans **tous** les `.ps1` du dépôt, une affectation à un nom qui ne diffère d'un paramètre déclaré que par la casse. **La règle porte sur la casse et non sur le fait d'affecter un paramètre**, parce que ce dépôt en affecte exprès — `$AccountPassword` reçoit ce qui vient d'être tapé, `$Pilot` s'allume sur une réponse : ceux-là écrivent le nom qu'ils ont déclaré, et une casse qui diverge est quelqu'un qui croit ouvrir une variable neuve. **La première version du test comptait 15 signalements dont 14 faux** — un `-Directory` d'une fonction contre un `$directory` de trois autres, qui sont quatre portées et pas une collision —, donc il suit désormais les portées : il n'attribue une affectation qu'aux paramètres de la fonction qui la contient, ou du script si aucune. `if`, `foreach` et `try` n'ouvrent **pas** de portée, ce qui est précisément par où le défaut est passé. **Deuxième vraie collision trouvée par le test**, dans `make.ps1` : `$version = Or-Else $Version …` écrasait le paramètre `-Version` au lieu de le compléter — sans lever, une chaîne entrant dans une `[string]`, mais en effaçant ce que l'opérateur avait demandé. **3 115 tests Go** (3 103 verts, 12 écartés) sur 35 paquets, 0 échec, `go vet` et `gofmt` sans remarque. **Ce qui n'est PAS fait, et n'est pas à croire fait** : aucun poste n'a été installé pour de bon — le banc s'arrête à l'appel d'élévation, et tout ce qui suit (release, empreinte, les trois questions, `install.ps1`) reste non exercé sur ce chemin ; et le test lit les scripts par un découpage de texte et non par l'analyseur de PowerShell — il neutralise les chaînes d'une ligne, **pas les here-strings**, dont `common.ps1` porte un de 60 lignes. Celui-là ne le trompe pas, son corps étant équilibré en accolades, mais **par chance et non par construction** : vérifié en posant une collision de l'autre côté, que le test attribue bien à la fonction qui la contient | | 02/08/2026 | **La CI validait une pull request en 3 min 55 ; elle valide en 1 min 43** (PR #47), et les deux causes ont été mesurées avant d'être touchées. **(1) Les montages de test payaient le coût argon2 d'un vrai login.** `internal/web` mettait **59,5 s** dans la passe `-race` — le paquet le plus lourd du dépôt, à lui seul la moitié de la passe. Ses tests écrivaient leurs empreintes avec `HashSecret`, donc à 64 MiB, t=3, p=2 : le coût d'un login sur l'i3 du poste, payé 34 fois deux, plus 21 vérifications. Or `VerifySecret` relit m, t et p **dans la chaîne stockée** — `TestVerificationReadsTheCostFromTheStoredHash` énonçait déjà cette propriété — donc une empreinte écrite au coût minimal se vérifie au coût minimal, sans qu'aucun chemin de production bouge : `HashSecret` garde ses 64 MiB, `TestArgon2idRoundTrip` les épingle toujours. **59,5 s → 9,0 s en CI**, et 21,3 s → 2,9 s sur poste de développement, ce qui allège autant `make test`. **(2) Huit étapes en série qui ne se devaient rien.** Le travail « Tests et frontières » enchaînait 33 s de `go vet`, 2 min 13 de `-race`, 51 s de passe sans cgo, puis les gardes, pendant que les trois autres travaux avaient fini depuis deux minutes. Découpé en `race`, `test` et `guards`. **Le critère est écrit dans `ci.yml` parce qu'il n'est pas esthétique** : une étape ne peut changer de travail que si elle ne lit rien de celui qu'elle quitte — vrai des quatre gardes, faux des planchers de couverture, qui lisent le profil de la passe sans cgo et restent avec elle. **Le temps-runner cumulé baisse aussi** (423 s → 407 s) : le découpage ajoute un `setup-go` par travail, argon2 en rendait davantage. **Ce qui reste** : `race` plafonne à 1 min 33, et l'écart avec le « moins de 10 s » de §16.4 n'est plus dans un paquet mais réparti sur quatre — `station` 16,7 s, `cmd/openscale` 16,0 s, `store` 13,7 s, `web` 9,0 s. **(3) Dans la foulée** (PR #48), `ci.yml` reçoit un `concurrency` : pousser un correctif sur une branche en validation annule le run qui juge l'état qu'on vient de remplacer. **Pas sur `main`**, où chaque commit est un état livrable dont le verdict est une trace — même arbitrage que `docs.yml`, conclusion inverse. Ce n'est pas une minute gagnée sur un run, c'est de la place sous la limite de travaux simultanés, **seule limite d'Actions qui s'applique ici** : le dépôt est public, donc les runners hébergés ne décomptent aucune minute. | | 01/08/2026 | **Le filet ERR-UI-01 prenait un avertissement du navigateur pour un plantage, et rechargeait l'écran client sans fin.** Signalé depuis le poste pilote : la grille réglée sur dix colonnes depuis l'écran d'administration, enregistrée, et *« ça marche plus, Une erreur est survenue est affichée »*. **Le poste, lui, allait parfaitement bien** — `config validate` sans faute, `/readyz` à `ready:true`, `doctor` sans échec : c'est l'**écran** qui tombait, et c'est le journal technique qui l'a dit, pas l'écran. **43 entrées `ERR-UI-01`, toutes identiques**, détail `ResizeObserver loop completed with undelivered notifications`, **une toutes les 5,12 s** — la valeur de `RELOAD_AFTER_S`, donc la cadence du filet lui-même. Ce message **n'est pas une exception** : c'est l'avis qu'un navigateur émet quand un cycle mesure → style → mesure ne converge pas dans la frame. Aucune exception levée, aucune pile. Mais il arrive par le **même événement `error` sur `window`** qu'une vraie exception, et `installErrorNet` attrapait tout ce qui passait : voile, rechargement à 5 s, remesure, même avis. **Le premier diagnostic était faux et le journal l'a corrigé** : les dix colonnes n'étaient pas la cause — la boucle est repartie à 18:20:47 UTC et a tenu huit minutes **avec la grille en automatique**. Le déclencheur du jour cachait la vraie portée : *n'importe quel* hoquet de mise en page — un écran rebranché, une rotation — blanchissait un poste en libre-service et le rechargeait sans fin, devant un client. **La règle posée** : un événement `error` **sans exception derrière** (`e.error` nul) dont le message commence par `ResizeObserver loop` ne lève pas le voile et ne programme aucun rechargement. Préfixe et non phrase entière, la queue variant d'un navigateur à l'autre ; `e.error` nul en plus du préfixe, sans quoi une vraie `TypeError` qui *nomme* `ResizeObserver` serait avalée. **Le taire entièrement a été écarté** : cette ligne est la seule qui ait nommé le défaut, et une grille qui ne converge pas reste un symptôme. Elle part donc par **sa propre route**, `POST /api/v1/ui/layout-notice`, journalisée **`ERR-UI-02` niveau `warn`** — « La grille de l'écran client n'a pas convergé ; l'écran reste utilisable » —, **une fois par chargement de page**, un avis se répétant à chaque frame. **La journaliser en `ERR-UI-01` a été écarté aussi** : la ligne était fausse deux fois — ce n'est ni une erreur, ni du JavaScript de ce dépôt — et elle atterrissait dans le fichier de diagnostic qu'un bénévole envoie au support. Ce dépôt sait déjà ce que coûte une ligne rouge sur un poste sain : on apprend à ignorer le rouge. Le préfixe `ERR-` reste correct malgré le niveau, `ERR-CAT-05` s'écrivant déjà en `warn` dans `localdrop.go`. **Pourquoi ça a vécu, et c'est la vraie leçon** : `main.ts` monte l'application à l'import, donc **rien ne pouvait exercer le filet sans démarrer un écran entier** ; et `web/test/setup.ts` remplace `ResizeObserver` par une classe qui n'observe rien tandis que jsdom ne fait aucune mise en page, donc **l'événement réel n'existait nulle part dans la suite**. Le filet vit désormais dans `web/src/lib/error-net.ts`, et son banc **pose** l'événement tel que le navigateur l'écrit au lieu de l'attendre d'une mesure. **Vérifié en cassant le correctif** : filtre neutralisé, les 4 tests qui portent le défaut tombent et les 9 autres tiennent ; côté Go, la route absente donne `405, attendu 202` avant, `202` après. **13 tests web, 1 test Go**, `882 passed` sur la suite web, `go test ./...` sans échec, `svelte-check` à 0 erreur, budget client 80 558 o gzip sur 112 640. **Puis vérifié sur le poste et pas seulement au banc** : front embarqué régénéré, binaire posé, événements posés dans le bundle réellement servi — l'avis donne voile **absent** et une ligne `ERR-UI-02`/`warn`, une `TypeError` donne le voile, `ERR-UI-01`/`error` et le rechargement. Écran client ouvert, compteur `sessionStorage` à **1 seul chargement**, cadence 5,12 s disparue. **Ce qui n'est PAS fait** : le cycle mesure → style → mesure de `Grid.svelte` n'est **pas** corrigé — `$effect` lit `.name-box → clientWidth` dans `measuredWidthPx`, qui alimente `tileScale`, qui pilote `--tile-pad`, qui change la largeur de ce même `.name-box` ; il n'a pas de point fixe, et la ligne `ERR-UI-02` à chaque chargement en est la trace. Il ne se voit pas — le navigateur saute une livraison et garde la dernière mise en page valide — mais les commentaires du fichier décrivent, pour cette famille exacte, des noms ajustés à une largeur puis dessinés dans une autre : **plausible ici, non mesuré**. `TROUBLESHOOTING.md` ne nomme pas `ERR-UI-02`, et n'en part pas comme d'un symptôme — il n'y en a pas | | 01/08/2026 | **Un poste installé en mode pilote n'avait aucun moyen écrit d'être allumé.** Signalé depuis le poste, une fois l'installation enfin passée : *« j'ai installé en mode pilote, et je ne sais pas comment lancer l'app »*. Le mode pilote installe le service en démarrage **`demand`** — c'est ce qui laisse l'application Access relançable en deux minutes, donc c'est délibéré — et `install.ps1` saute alors le `service start` **et la vérification `/healthz`**. Mais son message de fin, lui, ne distinguait pas les deux modes : il demandait à **tout le monde**, comme recette obligatoire, de « REDÉMARRER LA MACHINE et vérifier que le poste revient **SEUL** sur l'écran client ». **Un poste pilote ne revient jamais seul, par construction** : l'écran promettait donc l'inverse de ce que l'installeur venait de faire, et ni `INSTALLATION.md` ni `TROUBLESHOOTING.md` ne nommaient la commande à taper. **Deux raccourcis sur le Bureau public**, posés en pilote **seulement** : « Demarrer le poste » et « Arreter le poste ». **Le Bureau public et non celui d'un compte** — l'installeur tourne dans la session d'un technicien, le poste dans celle du compte `openscale`, et un raccourci posé sur le premier n'existerait jamais pour le second. **Le raccourci de démarrage n'ouvre PAS l'écran client, et c'est le code qui l'a décidé** : le superviseur du kiosque réinterroge le poste depuis sa page d'attente et bascule tout seul — « le poste répond de nouveau : retour à l'écran client » (`internal/kiosk/supervisor.go`) —, donc un `openscale kiosk` de plus ouvrirait un **second** navigateur par-dessus le premier, et en élevé. **L'élévation est un octet du fichier** : `WScript.Shell` n'a pas cette propriété, c'est le bit `0x20` de l'octet `0x15` de l'en-tête `.lnk` qui met le bouclier et déclenche l'invite ; sans lui, « Démarrer » répondrait « accès refusé » à un bénévole. Le raccourci passe par PowerShell et non par le binaire, demande l'**état après l'action** et attend une touche : `service start` écrit une ligne puis rend la main, et Windows referme la console avec elle — le bénévole voyait une fenêtre clignoter sans savoir si le poste était parti. **Posé et retiré dans les deux modes** : réinstaller en production un poste qui était en pilote emporte deux boutons qui ne veulent plus rien dire, et la désinstallation les emporte aussi, sans quoi ils lanceraient un binaire supprimé. **Mesuré sur un faux Bureau** : les deux `.lnk` sont créés avec la bonne cible, les bons arguments, l'icône du binaire et `ELEVE = True`, puis retirés. **Trois tests**, éprouvés en les cassant, dont celui qui interdit la promesse « revient SEUL » dans la branche pilote — et qui a d'abord accusé **son propre commentaire de garde**, lequel cite la promesse pour expliquer le défaut : il lit désormais par `codeOnly`, exactement ce que l'en-tête de `codeOnly` annonce depuis le premier jour. **Ce qui n'est PAS fait** : `TROUBLESHOOTING.md` ne part toujours pas du symptôme « le poste pilote n'affiche rien » | diff --git a/deploy/powershell_test.go b/deploy/powershell_test.go index ad60d15..6a1444a 100644 --- a/deploy/powershell_test.go +++ b/deploy/powershell_test.go @@ -130,6 +130,175 @@ func TestNoScriptConstantIsSilentlyReassigned(t *testing.T) { } } +// TestNoLocalVariableCollidesWithAParameterByCaseAlone is the third fault of the same +// family, and the one that reached a station. +// +// bootstrap.ps1 declared `[switch]$Relaunched` and, forty lines into the elevation branch, +// wrote `$relaunched = Join-Path $env:TEMP 'openscale-bootstrap.ps1'`. Those are the SAME +// variable — PowerShell compares names without case — and a parameter variable is TYPED, so +// the path went into a switch and the assignment threw: « Impossible de convertir la valeur +// "System.String" en type "System.Management.Automation.SwitchParameter" ». With +// $ErrorActionPreference = 'Stop' the message came back out attributed to `iex`, at +// character 96 of the one-liner, naming neither the file nor the line — the installation +// looked broken from the first command. +// +// It only fired on a NON-elevated console, which is the branch that copies the script into +// %TEMP% to relaunch it. Anyone testing from an administrator window skipped it entirely, +// which is how it shipped. +// +// The rule is about CASE and not about assigning to a parameter, because assigning to one is +// legitimate and this repository does it on purpose: bootstrap.ps1 fills `$AccountPassword` +// with what was typed, and turns `$Pilot` on from an answer. Those write the name they +// declared. A name that differs only in case is somebody believing they opened a new local +// variable — the intent is visible in the spelling, and that is what is forbidden. +// +// SCOPE is what makes this test mean something rather than merely fire. common.ps1 has a +// `-Directory` in one function and a `$directory` in three others; those are four separate +// scopes and not one collision — an assignment inside a function opens a LOCAL, which +// shadows the parameter of a sibling without ever touching it. The rule therefore compares +// an assignment against the parameters of the scope that governs it, and against no other. +// Measured before it did: fifteen findings, fourteen of them in another function. +func TestNoLocalVariableCollidesWithAParameterByCaseAlone(t *testing.T) { + assignment := regexp.MustCompile(`^\$(\w+)\s*=[^=]`) + + checked := 0 + for _, script := range powerShellScripts(t) { + lines := strings.Split(codeOnly(readFile(t, script)), "\n") + owner := owningScopes(lines) + declared, where := parametersByScope(lines, owner) + if len(declared) == 0 { + continue + } + checked++ + + for number, line := range lines { + match := assignment.FindStringSubmatch(strings.TrimSpace(line)) + if match == nil { + continue + } + name := match[1] + parameter, isParameter := declared[owner[number]][strings.ToLower(name)] + if !isParameter || parameter == name { + continue + } + t.Errorf("%s, ligne %d : $%s et le paramètre -%s déclaré ligne %d sont la MÊME "+ + "variable — PowerShell compare les noms sans la casse — et celle d'un "+ + "paramètre est typée : cette affectation lèvera « Impossible de convertir la "+ + "valeur … » à l'exécution.\n %s", + script, number+1, name, parameter, + where[owner[number]][strings.ToLower(name)], strings.TrimSpace(line)) + } + } + + if checked == 0 { + t.Fatal("aucun script PowerShell ne déclare de paramètre : ce test ne prouve plus rien") + } +} + +// owningScopes names, for each line, the function whose body holds it — empty for the body +// of the script itself. +// +// Only `function` opens a scope. `if`, `foreach` and `try` also open braces and DO NOT: a +// variable assigned inside them belongs to the enclosing scope, which is precisely how +// bootstrap.ps1 wrote a path into a switch from inside its elevation branch. +func owningScopes(lines []string) []string { + declaration := regexp.MustCompile(`(?i)^function\s+([\w-]+)`) + type frame struct { + name string + below int + } + var stack []frame + current := func() string { + if len(stack) == 0 { + return "" + } + return stack[len(stack)-1].name + } + + owner := make([]string, len(lines)) + depth := 0 + for number, line := range lines { + code := withoutStringLiterals(line) + owner[number] = current() + opening := declaration.FindStringSubmatch(strings.TrimSpace(code)) + before := depth + depth += strings.Count(code, "{") - strings.Count(code, "}") + if opening != nil { + // The declaration line itself belongs to the enclosing scope, and the brace may + // sit on it or on the next one — the frame closes when the depth comes back + // under what it was before the function was named, either way. + stack = append(stack, frame{name: opening[1], below: before}) + continue + } + for len(stack) > 0 && depth <= stack[len(stack)-1].below { + stack = stack[:len(stack)-1] + } + } + return owner +} + +// parametersByScope collects the parameters each scope declares, keyed by lowercase name so +// a lookup answers the question PowerShell itself asks, and holding the spelling so the +// failure can show both. +func parametersByScope(lines []string, owner []string) (map[string]map[string]string, map[string]map[string]int) { + // A parameter name is followed by a comma, the closing parenthesis, or its default + // value. `$script:DataRoot` in a default is not one: what follows its name is a colon. + name := regexp.MustCompile(`\$(\w+)\s*(?:[,)=]|$)`) + opener := regexp.MustCompile(`(?i)^param\s*\(`) + + declared := map[string]map[string]string{} + where := map[string]map[string]int{} + for number, line := range lines { + code := withoutStringLiterals(line) + if !opener.MatchString(strings.TrimSpace(code)) { + continue + } + scope := owner[number] + if declared[scope] == nil { + declared[scope] = map[string]string{} + where[scope] = map[string]int{} + } + depth := 0 + for cursor := number; cursor < len(lines); cursor++ { + inside := withoutStringLiterals(lines[cursor]) + for _, match := range name.FindAllStringSubmatch(inside, -1) { + declared[scope][strings.ToLower(match[1])] = match[1] + where[scope][strings.ToLower(match[1])] = cursor + 1 + } + depth += strings.Count(inside, "(") - strings.Count(inside, ")") + if depth <= 0 { + break + } + } + } + return declared, where +} + +// withoutStringLiterals blanks out what is inside quotes, so a brace or a parenthesis in a +// message never moves the depth counters above. +// +// « '{0}-{1}{2}' » is a real format string of common.ps1, and « ) » closes a sentence in +// half the messages of these scripts. +func withoutStringLiterals(line string) string { + var out strings.Builder + var quote rune + for _, letter := range line { + switch { + case quote == 0 && (letter == '\'' || letter == '"'): + quote = letter + out.WriteRune(letter) + case quote != 0 && letter == quote: + quote = 0 + out.WriteRune(letter) + case quote != 0: + out.WriteRune(' ') + default: + out.WriteRune(letter) + } + } + return out.String() +} + // TestEveryPowerShellScriptCarriesTheMarkWindowsPowerShellNeeds is the encoding contract, // and it exists because v0.1 shipped without it. // diff --git a/deploy/windows/bootstrap.ps1 b/deploy/windows/bootstrap.ps1 index 0799aef..41f06c5 100644 --- a/deploy/windows/bootstrap.ps1 +++ b/deploy/windows/bootstrap.ps1 @@ -223,15 +223,23 @@ if (-not (Test-Elevated)) { 'Executer en tant qu''administrateur.' } - $relaunched = Join-Path $env:TEMP 'openscale-bootstrap.ps1' - if ($PSCommandPath) { Copy-Item -LiteralPath $PSCommandPath -Destination $relaunched -Force } + # ★ CE NOM N'EST PAS « $relaunched », ET C'EST LA PANNE DU 07/08/2026. Les noms de + # variables PowerShell sont insensibles à la casse : $relaunched ÉTAIT le paramètre + # -Relaunched declaré en haut de ce fichier, dont la variable est TYPÉE [switch]. Y ranger + # un chemin levait « Impossible de convertir la valeur "System.String" en type + # "…SwitchParameter" », que $ErrorActionPreference = 'Stop' faisait ressortir sur « iex », + # au caractère 96 du one-liner, sans nommer ni ce fichier ni cette ligne. Et comme tout ce + # bloc ne s'exécute QUE dans une console non élevée — le cas de neuf postes sur dix —, une + # installation lancée depuis une fenêtre administrateur ne le voyait jamais. + $relaunchedScript = Join-Path $env:TEMP 'openscale-bootstrap.ps1' + if ($PSCommandPath) { Copy-Item -LiteralPath $PSCommandPath -Destination $relaunchedScript -Force } else { # Lancé par « irm | iex » : ce script n'existe nulle part sur le disque, et la seule # façon d'en obtenir une copie fidèle est de le redemander à l'adresse d'où il vient. - Invoke-WebRequest -Uri $script:RawUrl -OutFile $relaunched -UseBasicParsing + Invoke-WebRequest -Uri $script:RawUrl -OutFile $relaunchedScript -UseBasicParsing } - $arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $relaunched, '-Relaunched') + $arguments = @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $relaunchedScript, '-Relaunched') if ($Pilot) { $arguments += '-Pilot' } if ($SkipAutoLogon) { $arguments += '-SkipAutoLogon' } if ($Yes) { $arguments += '-Yes' } diff --git a/make.ps1 b/make.ps1 index b8cdce2..d56cdb4 100644 --- a/make.ps1 +++ b/make.ps1 @@ -73,10 +73,14 @@ function Or-Else($value, $fallback) { return $value } -$version = Or-Else $Version (Or-Else (git describe --tags --always --dirty 2>$null) 'dev') +# « $resolvedVersion » et non « $version » : ce dernier EST le paramètre -Version, les noms +# de variables PowerShell étant insensibles à la casse. L'écrire ici marchait — une chaîne +# dans une [string] —, mais effaçait ce que l'opérateur avait demandé, et c'est la même +# mécanique qui a cassé l'installation d'un poste le 07/08/2026. +$resolvedVersion = Or-Else $Version (Or-Else (git describe --tags --always --dirty 2>$null) 'dev') $commit = Or-Else (git rev-parse --short HEAD 2>$null) 'unknown' $date = Or-Else (git log -1 --format=%cI 2>$null) 'unknown' -$ldflags = "-s -w -X main.version=$version -X main.commit=$commit -X main.date=$date" +$ldflags = "-s -w -X main.version=$resolvedVersion -X main.commit=$commit -X main.date=$date" # Write-Utf8NoBom écrit un fichier texte SANS marque d'ordre des octets. # @@ -252,7 +256,7 @@ function Invoke-Build { $env:CGO_ENABLED = '0' go build -trimpath -ldflags $ldflags -o bin/openscale.exe ./cmd/openscale Assert-Success 'go build' - Write-Host "build : bin/openscale.exe ($version)" + Write-Host "build : bin/openscale.exe ($resolvedVersion)" } function Invoke-Front { @@ -313,7 +317,7 @@ function Invoke-Release { $os, $arch = $target -split '/' $ext = if ($os -eq 'windows') { '.exe' } else { '' } $deployDir = if ($os -eq 'windows') { 'deploy/windows' } else { 'deploy/linux' } - $name = "openscale-$version-$os-$arch" + $name = "openscale-$resolvedVersion-$os-$arch" $stage = Join-Path $staging $name New-Item -ItemType Directory -Force $stage | Out-Null