From cc0df9fb71b2631ff1baf20ccaf0317dd2f391b1 Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Tue, 11 Aug 2026 16:35:15 +0200 Subject: [PATCH 1/5] feat(dev): une commande qui verifie et guide au lieu de quatre MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dev.sh et dev.ps1 amenent un contributeur n'ayant que Docker jusqu'au conteneur de developpement en un seul appel, plutot que quatre commandes documentees dont les pannes de premier lancement ne se racontent pas d'elles-memes : Docker installe mais pas demarre, ou un devcontainer trouvable dans le PATH sans etre executable (mesure sous WSL, ou le PATH de Windows s'invite dans celui de la distribution). Les deux scripts font trois contrôles dans le même ordre — Docker répond, la CLI devcontainer répond, puis « devcontainer up » — testés par exécution réelle et non par simple présence, et n'installent jamais rien eux-mêmes : ils contrôlent, ils nomment, ils lancent. Aucune option, pour garder la surface de parité entre les deux réduite à ces trois contrôles. --- dev.ps1 | 110 +++++++++++++++++++++++++++++++++++++++++++++ dev.sh | 137 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 dev.ps1 create mode 100644 dev.sh diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 0000000..0c8ce32 --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,110 @@ +<# +.SYNOPSIS + Vérifie qu'un poste peut lancer le conteneur de développement, et dit quoi faire sinon. + +.DESCRIPTION + Une seule commande entre un clone et un conteneur de développement lancé, pour qui n'a + que Docker : quatre commandes documentées suffisent à rejouer six des sept contrôles + de la CI (voir .devcontainer/), mais leurs pannes de premier lancement ne se racontent + pas d'elles-mêmes -- Docker installé mais pas démarré, ou la CLI devcontainer absente. + Ce script fait trois contrôles DANS CET ORDRE et s'arrête sur le premier qui échoue, + en disant quoi faire. + + CE SCRIPT N'INSTALLE RIEN -- ni Docker, ni Node. Les installer demande des droits + administrateur, un installeur graphique, et pour Docker Desktop un redémarrage ; un + script du dépôt qui tenterait ça sur la machine de quelqu'un d'autre échouerait en + silence, ce qu'une commande « qui vérifie et guide » existe justement pour éviter. Il + contrôle, il nomme, il lance. + + Pas d'options, et c'est délibéré : ça garde la surface de parité avec dev.sh (voir + deploy/parity_test.go) réduite à trois contrôles, plutôt qu'à une table de réglages à + tenir en phase des deux côtés. + + CE SCRIPT DOIT TOURNER SOUS WINDOWS POWERSHELL 5.1, comme make.ps1 : c'est le seul + PowerShell garanti sur un poste Windows neuf. Aucune syntaxe propre à 7 (`??`, `?.`, + l'opérateur ternaire) n'y est employée. + +.EXAMPLE + .\dev.ps1 +#> + +$ErrorActionPreference = 'Stop' + +Write-Host '1. Docker' + +# « docker info » et non « Get-Command docker » : un Docker installé mais pas démarré est +# le cas ordinaire, et seul « docker info » distingue les deux. Get-Command ne sert +# ci-dessous qu'à CHOISIR le bon message une fois ce contrôle-là en échec. +$dockerReady = $false +try { + docker info *> $null + $dockerReady = ($LASTEXITCODE -eq 0) +} +catch { + $dockerReady = $false +} + +if (-not $dockerReady) { + if (Get-Command docker -ErrorAction SilentlyContinue) { + Write-Host " La commande docker existe mais ne répond pas. Cause la plus probable :" + Write-Host " - Docker Desktop n'est pas démarré : lancez-le depuis le menu Démarrer." + Write-Host " - (WSL2) la distribution Linux qui héberge le moteur n'est pas lancée." + Write-Host '' + Write-Host " Il n'y a pas de groupe docker sous Windows : Docker Desktop expose son" + Write-Host " démon par un named pipe que gère son propre service, pas par les" + Write-Host " permissions d'un groupe Unix comme le fait dev.sh sous Linux." + } + else { + Write-Host " Docker n'est pas installé. Sur cette machine :" + Write-Host ' Installez Docker Desktop :' + Write-Host ' https://docs.docker.com/desktop/setup/install/windows-install/' + } + exit 1 +} +Write-Host ' Docker répond.' + +Write-Host '' +Write-Host '2. La CLI devcontainer' + +# « devcontainer --version » et non « Get-Command devcontainer », pour la même raison que +# le contrôle 1 : une commande présente dans le PATH n'est pas forcément exécutable telle +# quelle -- voir le commentaire équivalent de dev.sh, où c'est mesuré sous WSL avec un +# devcontainer installé côté Windows mais injoignable depuis Linux. +$devcontainerReady = $false +try { + devcontainer --version *> $null + $devcontainerReady = ($LASTEXITCODE -eq 0) +} +catch { + $devcontainerReady = $false +} + +if (-not $devcontainerReady) { + Write-Host " La commande devcontainer est introuvable ou ne fonctionne pas. Installez-la :" + Write-Host ' npm i -g @devcontainers/cli' + if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + Write-Host '' + Write-Host " npm est absent : installez Node d'abord." + Write-Host ' winget install OpenJS.NodeJS.LTS' + Write-Host ' (ou : https://nodejs.org/en/download)' + } + Write-Host '' + Write-Host " Un éditeur qui sait ouvrir un devcontainer (VS Code, Cursor, Windsurf) n'a" + Write-Host " besoin d'aucun Node pour ça : son extension parle au démon Docker directement." + exit 1 +} +Write-Host ' devcontainer est disponible.' + +Write-Host '' +Write-Host '3. Tout est présent -- lancement du conteneur de développement' + +devcontainer up --workspace-folder . +if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE +} + +Write-Host '' +Write-Host 'Poste prêt. Ce que vous pouvez rejouer depuis ce conteneur :' +Write-Host ' devcontainer exec --workspace-folder . make test' +Write-Host ' devcontainer exec --workspace-folder . make front-check' +Write-Host ' devcontainer exec --workspace-folder . mkdocs build --strict' diff --git a/dev.sh b/dev.sh new file mode 100644 index 0000000..ca02aef --- /dev/null +++ b/dev.sh @@ -0,0 +1,137 @@ +#!/bin/sh +# Une seule commande entre un clone et un conteneur de développement lancé, pour qui n'a +# que Docker : quatre commandes documentées suffisent à rejouer six des sept contrôles de +# la CI (voir .devcontainer/), mais leurs pannes de premier lancement ne se racontent pas +# d'elles-mêmes -- Docker installé mais pas démarré, un utilisateur Linux pas encore dans +# le groupe docker (« permission denied ... /var/run/docker.sock », qui exige une +# déconnexion/reconnexion). Ce script fait trois contrôles DANS CET ORDRE et s'arrête sur +# le premier qui échoue, en disant quoi faire. +# +# CE SCRIPT N'INSTALLE RIEN -- ni Docker, ni Node. Les installer demande root, un +# installeur graphique sous Windows, et une déconnexion/reconnexion sous Linux ; un +# script du dépôt qui tenterait ça sur la machine de quelqu'un d'autre échouerait en +# silence, ce qu'une commande « qui vérifie et guide » existe justement pour éviter. Il +# contrôle, il nomme, il lance. +# +# Pas d'options, et c'est délibéré : ça garde la surface de parité avec dev.ps1 (voir +# deploy/parity_test.go) réduite à trois contrôles, plutôt qu'à une table de réglages à +# tenir en phase des deux côtés. +# +# /bin/sh et non bash : un contributeur Debian minimale ou Alpine n'a que dash, et ce +# script n'est pas l'endroit où découvrir qu'un shell manque. +set -eu + +# --- Détection de la distribution, pour la ligne d'installation de Docker ou de Node - +# +# Le responsable du produit travaille sous Arch : une ligne « apt install » montrée à un +# utilisateur Arch est pire qu'aucune ligne, parce qu'elle se tape et échoue. +detect_distro() { + if [ -r /etc/os-release ]; then + info=$( . /etc/os-release && printf '%s %s' "${ID:-}" "${ID_LIKE:-}" ) + case "$info" in + *ubuntu*|*debian*) echo debian; return ;; + *arch*) echo arch; return ;; + *fedora*) echo fedora; return ;; + esac + fi + echo inconnue +} + +docker_install_hint() { + case "$(detect_distro)" in + debian) + echo ' sudo apt-get update && sudo apt-get install -y docker.io' + echo ' sudo systemctl enable --now docker' + ;; + arch) + echo ' sudo pacman -S docker' + echo ' sudo systemctl enable --now docker' + ;; + fedora) + echo ' sudo dnf install -y dnf-plugins-core' + echo ' sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo' + echo ' sudo dnf install -y docker-ce docker-ce-cli containerd.io' + echo ' sudo systemctl enable --now docker' + ;; + *) + if [ "$(uname -s)" = 'Darwin' ]; then + echo ' Installez Docker Desktop pour Mac :' + echo ' https://docs.docker.com/desktop/setup/install/mac-install/' + else + echo " Distribution non reconnue par ce script -- suivez :" + echo ' https://docs.docker.com/engine/install/' + fi + ;; + esac +} + +node_install_hint() { + case "$(detect_distro)" in + debian) echo ' sudo apt-get update && sudo apt-get install -y nodejs npm' ;; + arch) echo ' sudo pacman -S nodejs npm' ;; + fedora) echo ' sudo dnf install -y nodejs npm' ;; + *) + if [ "$(uname -s)" = 'Darwin' ]; then + echo ' brew install node' + else + echo " Distribution non reconnue par ce script -- suivez :" + echo ' https://nodejs.org/en/download' + fi + ;; + esac +} + +echo '1. Docker' +if ! docker info >/dev/null 2>&1; then + if command -v docker >/dev/null 2>&1; then + echo " La commande docker existe mais ne répond pas. Deux causes possibles :" + echo " - Docker n'est pas démarré : démarrez Docker Desktop, ou" + echo ' « sudo systemctl start docker »' + if [ "$(uname -s)" = 'Linux' ]; then + user=$(id -un 2>/dev/null || echo "${LOGNAME:-}") + echo " - votre utilisateur n'est pas dans le groupe docker (le message serait" + echo ' « permission denied ... /var/run/docker.sock »). Corrigez avec :' + echo " sudo usermod -aG docker $user" + echo " PUIS DÉCONNECTEZ-VOUS ET RECONNECTEZ-VOUS : l'appartenance à un groupe" + echo " Unix ne se relit qu'à l'ouverture de session suivante -- un « docker" + echo " info » relancé dans le même terminal échouera encore." + fi + else + echo " Docker n'est pas installé. Sur cette machine :" + docker_install_hint + fi + exit 1 +fi +echo ' Docker répond.' + +echo '' +echo '2. La CLI devcontainer' +# « devcontainer --version » et non « command -v devcontainer », pour la même raison que +# le contrôle 1 : sous WSL, le PATH de Windows s'invite dans celui de la distribution, et +# un « devcontainer » qui existe par ce chemin-là est souvent un binaire Node installé côté +# Windows (par exemple par nvm-windows) -- présent dans le PATH, injoignable au premier +# lancement réel, faute d'un Node accessible depuis Linux. Mesuré sur ce projet. +if ! devcontainer --version >/dev/null 2>&1; then + echo ' La commande devcontainer est introuvable ou ne fonctionne pas. Installez-la :' + echo ' npm i -g @devcontainers/cli' + if ! command -v npm >/dev/null 2>&1; then + echo '' + echo " npm est absent : installez Node d'abord." + node_install_hint + fi + echo '' + echo " Un éditeur qui sait ouvrir un devcontainer (VS Code, Cursor, Windsurf) n'a" + echo " besoin d'aucun Node pour ça : son extension parle au démon Docker directement." + exit 1 +fi +echo ' devcontainer est disponible.' + +echo '' +echo '3. Tout est présent -- lancement du conteneur de développement' +devcontainer up --workspace-folder . + +echo '' +echo 'Poste prêt. Ce que vous pouvez rejouer depuis ce conteneur :' +echo ' devcontainer exec --workspace-folder . make test' +echo ' devcontainer exec --workspace-folder . make front-check' +echo ' devcontainer exec --workspace-folder . mkdocs build --strict' From ba35d816abc05e126c894952be45c1fc803d9ddf Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Tue, 11 Aug 2026 16:35:42 +0200 Subject: [PATCH 2/5] test(deploy): garder dev.sh et dev.ps1 sous les memes gardes que le reste MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois ajouts à deploy/, chacun comblant un trou que le nouveau couple de scripts aurait sinon hérité en silence : - TestTheShellScriptsAreValidAccordingToTheShell (shell_test.go) analyse désormais dev.sh par « sh -n », à côté de .devcontainer/post-create.sh. - TestEveryPowerShellScriptParses (powershell_test.go) s'appuie sur powerShellScripts(t), qui parcourt tout le dépôt, au lieu de son propre glob limité à deploy/windows/*.ps1. Ça fait entrer make.ps1 ET dev.ps1 dans le seul banc qui invoque réellement le parseur PowerShell — make.ps1 n'était vérifié par aucun interprète jusqu'ici, et dev.ps1 aurait hérité du même trou. - TestDevScriptsCheckTheSameThings (parity_test.go) tient la parité entre les deux scripts : les trois mêmes contrôles (Docker, la CLI devcontainer, le lancement) doivent apparaître dans les deux fichiers. L'unique écart légitime — le contrôle du groupe docker, qui n'existe pas sous Windows — est vérifié par la même mécanique que les exceptions de parité des installeurs : la raison doit être écrite, et retrouvée dans le fichier qui n'a pas le contrôle, pas seulement affirmée dans le banc. Les trois ont été vus rougir sur une casse volontaire (syntaxe cassée sur chaque script, contrôle retiré d'un des deux) avant d'être restaurés. --- deploy/parity_test.go | 57 +++++++++++++++++++++++++++++++++++++++ deploy/powershell_test.go | 17 +++++++----- deploy/shell_test.go | 8 +++--- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/deploy/parity_test.go b/deploy/parity_test.go index 1ab2c41..2828492 100644 --- a/deploy/parity_test.go +++ b/deploy/parity_test.go @@ -424,6 +424,63 @@ func TestNeitherInstallerPutsASecretOnTheBinaryCommandLine(t *testing.T) { } } +// devScriptPath and devPowerShellScriptPath are dev.sh and dev.ps1 — the "check and +// guide" pair at the repository root, next to make.ps1. deploy/ tests run one level +// below the root, hence the "..". +var ( + devScriptPath = filepath.Join("..", "dev.sh") + devPowerShellScriptPath = filepath.Join("..", "dev.ps1") +) + +// devGroupReasonException is the one legitimate asymmetry between dev.sh and dev.ps1: +// dev.sh checks for docker-group membership and dev.ps1 does not, because only Linux +// has a docker group to be missing from. Same rule as installerParityExceptions above — +// a silent écart is a carpet, a written one is an arbitrage — so it is held by the same +// helper rather than a new one. +var devGroupReasonException = parityException{ + option: "le contrôle du groupe docker", + carriedBy: devScriptPath, + missingFrom: devPowerShellScriptPath, + why: "dev.sh signale quand l'utilisateur Linux n'est pas dans le groupe docker (« " + + "permission denied … /var/run/docker.sock ») parce que Docker Desktop pour Windows " + + "n'a pas d'équivalent : son démon est exposé par un named pipe que gère son propre " + + "service, pas par les permissions d'un groupe Unix — il n'y a donc rien à détecter " + + "côté PowerShell.", + proof: "groupe docker", +} + +// TestDevScriptsCheckTheSameThings is the parity guard for dev.sh and dev.ps1 — the "one +// command that checks and guides" pair, and neither one an installer. +// +// Both scripts are deliberately option-free (see both headers), so there is no table of +// flags to compare the way installerParity does above. Parity here means the same THREE +// checks, in the same order — Docker answers, the devcontainer CLI is available, then +// devcontainer up is launched — read out of both files as text, the way installerParity's +// neighbours already do: actually RUNNING either script from this bench would need Docker +// and Node on whatever machine runs `go test ./deploy/`, which is exactly what these +// scripts exist to check for instead. +func TestDevScriptsCheckTheSameThings(t *testing.T) { + sh := codeOnly(readFile(t, devScriptPath)) + ps1 := codeOnly(readFile(t, devPowerShellScriptPath)) + + for _, marker := range []string{"docker info", "devcontainer", "devcontainer up"} { + if !strings.Contains(sh, marker) { + t.Errorf("dev.sh ne contient pas %q : un des trois contrôles semble absent, "+ + "alors que dev.ps1 le porte", marker) + } + if !strings.Contains(ps1, marker) { + t.Errorf("dev.ps1 ne contient pas %q : un des trois contrôles semble absent, "+ + "alors que dev.sh le porte", marker) + } + } + + if !strings.Contains(strings.ToLower(sh), devGroupReasonException.proof) { + t.Errorf("dev.sh ne nomme plus %s : la raison écrite dans dev.ps1 excuse un "+ + "contrôle qui n'existe plus", devGroupReasonException.proof) + } + checkTheReasonIsWritten(t, devGroupReasonException) +} + // --- Les lecteurs --------------------------------------------------------------------- // dashedSpelling renders a PowerShell parameter the way a sh script spells it. diff --git a/deploy/powershell_test.go b/deploy/powershell_test.go index e077dc4..d718e47 100644 --- a/deploy/powershell_test.go +++ b/deploy/powershell_test.go @@ -618,14 +618,17 @@ func measurementsOf(output string) map[string]string { // station half-installed, and the typo is found by whoever runs it as administrator on a // Saturday morning. // -// It uses the PowerShell parser itself rather than a heuristic, and it checks the four -// scripts plus the shared file — under EVERY PowerShell installed, because the encoding -// defect above is invisible to PowerShell 7 and fatal to 5.1. +// It uses the PowerShell parser itself rather than a heuristic, and it now checks EVERY +// PowerShell script of the repository — via powerShellScripts, not a glob of its own — +// under EVERY PowerShell installed, because the encoding defect above is invisible to +// PowerShell 7 and fatal to 5.1. +// +// The four installers and common.ps1 were always covered this way. make.ps1 and dev.ps1 +// were not: a glob scoped to windows/*.ps1 never saw either, so the script every Windows +// contributor runs first — make.ps1 — was parsed by no interpreter at all, a hole dev.ps1 +// would otherwise have inherited on day one. func TestEveryPowerShellScriptParses(t *testing.T) { - scripts, err := filepath.Glob(filepath.Join("windows", "*.ps1")) - if err != nil || len(scripts) == 0 { - t.Fatalf("aucun script PowerShell trouvé : %v", err) - } + scripts := powerShellScripts(t) body := `$ErrorActionPreference = 'Stop' $failed = 0 diff --git a/deploy/shell_test.go b/deploy/shell_test.go index 275fcba..051bf5a 100644 --- a/deploy/shell_test.go +++ b/deploy/shell_test.go @@ -92,9 +92,10 @@ func TestNoLinuxArtifactCarriesAWindowsLineEnding(t *testing.T) { // TestTheShellScriptsAreValidAccordingToTheShell runs `sh -n` when a shell is available. // -// .devcontainer/post-create.sh joins the list: it is not under linux/, but a syntax error in -// it is otherwise discovered only after an eight-minute container build, while `sh -n` costs -// nothing and runs on the same Linux CI that already builds this list. +// The list reaches outside linux/ for two files now, and for the same reason each time: a +// syntax error in either is otherwise discovered late — after an eight-minute container +// build for .devcontainer/post-create.sh, on a contributor's first run for dev.sh — while +// `sh -n` costs nothing and runs on the same Linux CI that already builds this list. func TestTheShellScriptsAreValidAccordingToTheShell(t *testing.T) { shell, err := exec.LookPath("sh") if err != nil { @@ -105,6 +106,7 @@ func TestTheShellScriptsAreValidAccordingToTheShell(t *testing.T) { t.Fatalf("aucun script shell trouvé : %v", err) } scripts = append(scripts, filepath.Join("..", ".devcontainer", "post-create.sh")) + scripts = append(scripts, filepath.Join("..", "dev.sh")) for _, script := range scripts { output, err := exec.Command(shell, "-n", script).CombinedOutput() if err != nil { From 89748dfc3464ecf25c07b5c234c643e27ef4d13f Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Tue, 11 Aug 2026 16:59:29 +0200 Subject: [PATCH 3/5] fix(dev): dnf5 sur fedora, buildx sur arch, et un piege de 5.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trois corrections mesurées sur dev.sh et dev.ps1, après relecture : - La recette Fedora de dev.sh employait la syntaxe DNF4 (« --add-repo »). Fedora est passée à DNF5 en version 41, et toutes les versions encore maintenues répondent « argument inconnu » à cette syntaxe — l'utilisateur tape la ligne qu'on lui donne et n'obtient rien. Remplacée par la syntaxe DNF5 (« addrepo --from-repofile »), telle que documentée aujourd'hui. - docker-buildx (Arch) et docker-buildx-plugin (Fedora, déjà corrigé) ajoutés aux deux recettes : sans eux, le contrôle 1 (« docker info ») passe et le contrôle 3 (« devcontainer up », qui construit l'image) tombe sur « BuildKit is enabled but the buildx component is missing or broken ». - Sous Windows PowerShell 5.1 — pas sous pwsh 7, vérifié sous les deux — $ErrorActionPreference = 'Stop' rend TERMINANTE toute écriture sur le flux d'erreur d'une commande native dont la sortie est redirigée, même quand cette commande réussit et n'écrit qu'un avertissement. Reproduit pour de vrai : un « cmd /c "echo err 1>&2 && exit 0" » réussi devient une exception attrapée par le catch sous 5.1. « docker info » sur un moteur WSL2 émet un avertissement sur stderr, et « devcontainer --version » hérite de ceux de Node — les deux auraient été déclarés en panne sous 5.1 alors qu'ils répondent. Une fonction Test-CommandRuns desserre la préférence le temps de l'appel seulement, et la restaure aussitôt ; les deux sondes s'appuient désormais dessus plutôt que sur deux blocs try/catch dupliqués. Deux corrections mineures à côté : les en-têtes des deux scripts recopiaient des nombres (« quatre commandes », « six des sept contrôles ») que rien ne tenait à jour et que le handbook contredisait déjà ; ils disent maintenant le critère et renvoient au handbook. « Deux causes possibles » devient « Causes possibles », le message n'en imprimant qu'une sur macOS. --- dev.ps1 | 64 ++++++++++++++++++++++++++++++++++++++------------------- dev.sh | 28 ++++++++++++++++--------- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/dev.ps1 b/dev.ps1 index 0c8ce32..537fc9a 100644 --- a/dev.ps1 +++ b/dev.ps1 @@ -4,11 +4,12 @@ .DESCRIPTION Une seule commande entre un clone et un conteneur de développement lancé, pour qui n'a - que Docker : quatre commandes documentées suffisent à rejouer six des sept contrôles - de la CI (voir .devcontainer/), mais leurs pannes de premier lancement ne se racontent - pas d'elles-mêmes -- Docker installé mais pas démarré, ou la CLI devcontainer absente. - Ce script fait trois contrôles DANS CET ORDRE et s'arrête sur le premier qui échoue, - en disant quoi faire. + que Docker : le chemin conteneur du guide de démarrage (handbook/getting-started.md) + rejoue déjà, à la main, tout ce que la CI vérifie sauf les scripts d'installation sous + PowerShell 5.1, qu'aucun conteneur Linux ne peut exécuter. Mais ses pannes de premier + lancement ne se racontent pas d'elles-mêmes -- Docker installé mais pas démarré, ou la + CLI devcontainer trouvable sans être exécutable. Ce script fait trois contrôles DANS + CET ORDRE et s'arrête sur le premier qui échoue, en disant quoi faire. CE SCRIPT N'INSTALLE RIEN -- ni Docker, ni Node. Les installer demande des droits administrateur, un installeur graphique, et pour Docker Desktop un redémarrage ; un @@ -30,19 +31,47 @@ $ErrorActionPreference = 'Stop' +# Test-CommandRuns renvoie si l'appel réussit (code de sortie 0), sans jamais laisser une +# ÉCRITURE SUR STDERR transformer un succès en échec. +# +# Sous Windows PowerShell 5.1 -- pas sous pwsh 7 -- $ErrorActionPreference = 'Stop' rend +# TERMINANTE toute écriture sur le flux d'erreur d'une commande NATIVE dont la sortie est +# redirigée (« *> $null » ci-dessous), même quand cette commande réussit et n'écrit là +# qu'un avertissement. Mesuré sur ce poste, sous les deux PowerShell : un +# « cmd /c "echo err 1>&2 && exit 0" » réussi devient une exception attrapée par le +# catch sous 5.1, jamais sous 7. « docker info » sur un moteur WSL2 émet un « WARNING: » +# sur stderr, et « devcontainer --version » hérite de tout avertissement expérimental que +# Node écrit là -- les deux auraient donc été déclarés en panne sous 5.1 alors qu'ils +# répondent. +# +# La préférence est desserrée le temps de CET appel seulement, et remise aussitôt après : +# une commande ABSENTE lève toujours une exception, quelle que soit la préférence, donc la +# branche « non installé » des deux contrôles continue de fonctionner sans elle. +# +# NE PAS « SIMPLIFIER » CE DÉTOUR : il tient la seule différence mesurée entre 5.1 et 7 sur +# ce script, et aucun banc du dépôt ne peut l'exécuter pour le revérifier -- le parseur de +# deploy/powershell_test.go ANALYSE ces scripts, il ne les fait pas tourner. +function Test-CommandRuns([scriptblock]$Command) { + $previous = $ErrorActionPreference + try { + $ErrorActionPreference = 'Continue' + & $Command *> $null + return ($LASTEXITCODE -eq 0) + } + catch { + return $false + } + finally { + $ErrorActionPreference = $previous + } +} + Write-Host '1. Docker' # « docker info » et non « Get-Command docker » : un Docker installé mais pas démarré est # le cas ordinaire, et seul « docker info » distingue les deux. Get-Command ne sert # ci-dessous qu'à CHOISIR le bon message une fois ce contrôle-là en échec. -$dockerReady = $false -try { - docker info *> $null - $dockerReady = ($LASTEXITCODE -eq 0) -} -catch { - $dockerReady = $false -} +$dockerReady = Test-CommandRuns { docker info } if (-not $dockerReady) { if (Get-Command docker -ErrorAction SilentlyContinue) { @@ -70,14 +99,7 @@ Write-Host '2. La CLI devcontainer' # le contrôle 1 : une commande présente dans le PATH n'est pas forcément exécutable telle # quelle -- voir le commentaire équivalent de dev.sh, où c'est mesuré sous WSL avec un # devcontainer installé côté Windows mais injoignable depuis Linux. -$devcontainerReady = $false -try { - devcontainer --version *> $null - $devcontainerReady = ($LASTEXITCODE -eq 0) -} -catch { - $devcontainerReady = $false -} +$devcontainerReady = Test-CommandRuns { devcontainer --version } if (-not $devcontainerReady) { Write-Host " La commande devcontainer est introuvable ou ne fonctionne pas. Installez-la :" diff --git a/dev.sh b/dev.sh index ca02aef..429d593 100644 --- a/dev.sh +++ b/dev.sh @@ -1,11 +1,13 @@ #!/bin/sh # Une seule commande entre un clone et un conteneur de développement lancé, pour qui n'a -# que Docker : quatre commandes documentées suffisent à rejouer six des sept contrôles de -# la CI (voir .devcontainer/), mais leurs pannes de premier lancement ne se racontent pas -# d'elles-mêmes -- Docker installé mais pas démarré, un utilisateur Linux pas encore dans -# le groupe docker (« permission denied ... /var/run/docker.sock », qui exige une -# déconnexion/reconnexion). Ce script fait trois contrôles DANS CET ORDRE et s'arrête sur -# le premier qui échoue, en disant quoi faire. +# que Docker : le chemin conteneur du guide de démarrage (handbook/getting-started.md) +# rejoue déjà, à la main, tout ce que la CI vérifie sauf les scripts d'installation sous +# PowerShell 5.1, qu'aucun conteneur Linux ne peut exécuter. Mais ses pannes de premier +# lancement ne se racontent pas d'elles-mêmes -- Docker installé mais pas démarré, un +# utilisateur Linux pas encore dans le groupe docker (« permission denied ... +# /var/run/docker.sock », qui exige une déconnexion/reconnexion), ou un devcontainer +# trouvable dans le PATH sans être exécutable. Ce script fait trois contrôles DANS CET +# ORDRE et s'arrête sur le premier qui échoue, en disant quoi faire. # # CE SCRIPT N'INSTALLE RIEN -- ni Docker, ni Node. Les installer demande root, un # installeur graphique sous Windows, et une déconnexion/reconnexion sous Linux ; un @@ -44,13 +46,19 @@ docker_install_hint() { echo ' sudo systemctl enable --now docker' ;; arch) - echo ' sudo pacman -S docker' + # docker-buildx est un paquet séparé sur Arch : sans lui, « docker info » répond, + # mais le contrôle 3 (« devcontainer up », qui construit l'image) tombe sur + # « BuildKit is enabled but the buildx component is missing or broken ». + echo ' sudo pacman -S docker docker-buildx' echo ' sudo systemctl enable --now docker' ;; fedora) + # Syntaxe DNF5 : Fedora en est équipée depuis la version 41, et toutes les versions + # encore maintenues répondent « argument inconnu » à la syntaxe DNF4 + # (« --add-repo »). docker-buildx-plugin, même raison que docker-buildx sur Arch. echo ' sudo dnf install -y dnf-plugins-core' - echo ' sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo' - echo ' sudo dnf install -y docker-ce docker-ce-cli containerd.io' + echo ' sudo dnf config-manager addrepo --from-repofile https://download.docker.com/linux/fedora/docker-ce.repo' + echo ' sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin' echo ' sudo systemctl enable --now docker' ;; *) @@ -84,7 +92,7 @@ node_install_hint() { echo '1. Docker' if ! docker info >/dev/null 2>&1; then if command -v docker >/dev/null 2>&1; then - echo " La commande docker existe mais ne répond pas. Deux causes possibles :" + echo " La commande docker existe mais ne répond pas. Causes possibles :" echo " - Docker n'est pas démarré : démarrez Docker Desktop, ou" echo ' « sudo systemctl start docker »' if [ "$(uname -s)" = 'Linux' ]; then From dd4eec8d2304017f520da7811de0dfd373b19e72 Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Tue, 11 Aug 2026 16:59:35 +0200 Subject: [PATCH 4/5] test(deploy): un marqueur qui ne se declare pas vrai tout seul MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestDevScriptsCheckTheSameThings cherchait « devcontainer » pour juger le contrôle 2, et ce mot est un sous-mot de « devcontainer up », le marqueur du contrôle 3 : il était donc vrai par construction dès que le troisième l'était, et ne pouvait pas voir le contrôle 2 disparaître — précisément celui que la CLI --version vient de réparer. Remplacé par « devcontainer --version », le texte que les deux scripts exécutent réellement pour ce contrôle. Une deuxième boucle vérifie maintenant l'ordre des trois marqueurs dans chaque script, ce que le commentaire de la fonction affirmait sans le vérifier. Le message d'échec de la boucle de présence ne suppose plus que l'autre script porte le marqueur : il le constate, avec un message distinct selon qu'un seul côté a perdu le contrôle ou que les deux l'ont perdu ensemble. TestNoShellScriptExitsOnATestThatIsSimplyFalse (shell_test.go) n'analysait que linux/*.sh, alors que dev.sh pose le même set -e et est exactement le genre de script dont une panne de premier lancement doit parler plutôt que sortir en silence sur un test simplement faux. Il rejoint la liste, comme son voisin sh -n l'a fait pour dev.sh dans un lot précédent. --- deploy/parity_test.go | 64 +++++++++++++++++++++++++++++++++++-------- deploy/shell_test.go | 5 ++++ 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/deploy/parity_test.go b/deploy/parity_test.go index 2828492..5120c0d 100644 --- a/deploy/parity_test.go +++ b/deploy/parity_test.go @@ -449,28 +449,68 @@ var devGroupReasonException = parityException{ proof: "groupe docker", } +// devCheckMarkers are the three checks dev.sh and dev.ps1 must both perform, in the +// order they must perform them. +// +// "devcontainer --version" and not the bare "devcontainer": the second is a substring of +// the third ("devcontainer up"), so it is true by construction as soon as the third is — +// it cannot, on its own, catch the second check disappearing. "--version" is what both +// scripts actually run to tell "the CLI is available" from "a command of this name is +// merely on the PATH" (see the comment next to each call site) — using it here means this +// bench would have caught that check going back to a bare presence test. +var devCheckMarkers = []string{"docker info", "devcontainer --version", "devcontainer up"} + // TestDevScriptsCheckTheSameThings is the parity guard for dev.sh and dev.ps1 — the "one // command that checks and guides" pair, and neither one an installer. // // Both scripts are deliberately option-free (see both headers), so there is no table of // flags to compare the way installerParity does above. Parity here means the same THREE -// checks, in the same order — Docker answers, the devcontainer CLI is available, then -// devcontainer up is launched — read out of both files as text, the way installerParity's -// neighbours already do: actually RUNNING either script from this bench would need Docker -// and Node on whatever machine runs `go test ./deploy/`, which is exactly what these -// scripts exist to check for instead. +// checks (devCheckMarkers), in the same order, read out of both files as text, the way +// installerParity's neighbours already do: actually RUNNING either script from this bench +// would need Docker and Node on whatever machine runs `go test ./deploy/`, which is +// exactly what these scripts exist to check for instead. func TestDevScriptsCheckTheSameThings(t *testing.T) { sh := codeOnly(readFile(t, devScriptPath)) ps1 := codeOnly(readFile(t, devPowerShellScriptPath)) - for _, marker := range []string{"docker info", "devcontainer", "devcontainer up"} { - if !strings.Contains(sh, marker) { - t.Errorf("dev.sh ne contient pas %q : un des trois contrôles semble absent, "+ - "alors que dev.ps1 le porte", marker) + // Present on both sides — and the message says what it can actually see rather than + // assuming the side it is not looking at: a check missing from BOTH scripts is a + // different failure from one that fell off a single side, and conflating them would + // have this bench contradict itself on exactly the case where both regressed together. + for _, marker := range devCheckMarkers { + inSh := strings.Contains(sh, marker) + inPs1 := strings.Contains(ps1, marker) + switch { + case inSh && inPs1: + // rien à dire + case inSh && !inPs1: + t.Errorf("dev.ps1 ne contient pas %q, alors que dev.sh le porte : un des trois "+ + "contrôles a disparu d'un seul côté", marker) + case !inSh && inPs1: + t.Errorf("dev.sh ne contient pas %q, alors que dev.ps1 le porte : un des trois "+ + "contrôles a disparu d'un seul côté", marker) + default: + t.Errorf("ni dev.sh ni dev.ps1 ne contiennent %q : un des trois contrôles a "+ + "disparu des deux côtés", marker) } - if !strings.Contains(ps1, marker) { - t.Errorf("dev.ps1 ne contient pas %q : un des trois contrôles semble absent, "+ - "alors que dev.sh le porte", marker) + } + + // Dans le même ordre — sur les marqueurs effectivement trouvés seulement : un marqueur + // absent est déjà signalé ci-dessus, et le juger aussi sur l'ordre ne ferait que + // répéter la même panne sous un second message. + for _, script := range []struct{ name, text string }{{"dev.sh", sh}, {"dev.ps1", ps1}} { + last := -1 + for _, marker := range devCheckMarkers { + at := strings.Index(script.text, marker) + if at < 0 { + continue + } + if at < last { + t.Errorf("%s : %q apparaît avant un contrôle qui devrait le précéder — les "+ + "trois contrôles doivent se succéder dans le même ordre des deux côtés", + script.name, marker) + } + last = at } } diff --git a/deploy/shell_test.go b/deploy/shell_test.go index 051bf5a..8628da3 100644 --- a/deploy/shell_test.go +++ b/deploy/shell_test.go @@ -18,6 +18,10 @@ import ( // TestNoShellScriptExitsOnATestThatIsSimplyFalse guards a trap `sh -n` cannot see, and // that a Saturday-morning installation would find instead. // +// dev.sh joins the list too, for the same reason it joined the `sh -n` bench next to this +// one: it poses `set -e` and is exactly the kind of script a first-run failure must speak +// from, not exit silently on a test that was simply false. +// // Under `set -e`, a standalone `[ … ] && commande` whose TEST is false returns a non-zero // status, and the shell exits. It reads like « fais ceci si », it behaves like « arrête-toi // si ce n'est pas le cas ». It was really in install.sh: an optional file that is not @@ -31,6 +35,7 @@ func TestNoShellScriptExitsOnATestThatIsSimplyFalse(t *testing.T) { if err != nil || len(scripts) == 0 { t.Fatalf("aucun script shell trouvé : %v", err) } + scripts = append(scripts, filepath.Join("..", "dev.sh")) andList := regexp.MustCompile(`^\s*(\[|command\s|test\s).*&&`) for _, script := range scripts { From 085a6b9b6283adfc26fbd61494f7304fbedda68d Mon Sep 17 00:00:00 2001 From: "Fabien F." Date: Tue, 11 Aug 2026 17:04:45 +0200 Subject: [PATCH 5/5] docs(demarrage): reboucher le chemin conteneur sur dev.sh/dev.ps1 Le chemin conteneur du guide de demarrage manquait le git clone, ne disait rien de l'installation de Docker et laissait le contributeur Linux decouvrir seul le blocage du groupe docker au premier lancement. Les scripts dev.sh et dev.ps1 verifient et guident deja pour ca, plateforme par plateforme : la page renvoie desormais vers ces deux commandes plutot que de dupliquer leurs instructions, et garde le detail des commandes devcontainer pour qui prefere le savoir plutot que le croire. Adoucit aussi la revendication non verifiee sur les IDE JetBrains : seuls VS Code et ses forks implementent la specification devcontainer avec certitude. --- handbook/getting-started.md | 44 ++++++++++++++++++++++++++++++++----- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/handbook/getting-started.md b/handbook/getting-started.md index 2d52a7a..8bcfb63 100644 --- a/handbook/getting-started.md +++ b/handbook/getting-started.md @@ -8,13 +8,44 @@ la première fois — les suivantes sont immédiates. | Chemin | Ce qu'il faut sur votre poste | Pour qui | |---|---|---| -| **Conteneur** | Docker, et Node si vous passez par la CLI (Docker seul suffit avec un éditeur qui la porte) | Découverte, contribution ponctuelle, poste qu'on ne veut pas encombrer | +| **Conteneur** | Docker ; le script `dev.sh` / `dev.ps1` dit le reste s'il manque quelque chose (Node, pour la CLI devcontainer) | Découverte, contribution ponctuelle, poste qu'on ne veut pas encombrer | | **Local** | Go, et le reste selon ce que vous touchez | Développement quotidien, mise en route d'une balance ou d'une imprimante réelle | ### Le chemin conteneur La commande `devcontainer`, indépendante de tout éditeur, construit une image qui porte Go, -Node, Python, gcc et golangci-lint aux versions **exactes** de l'intégration continue : +Node, Python, gcc et golangci-lint aux versions **exactes** de l'intégration continue. Une +seule commande, une fois le dépôt cloné, suffit pour l'obtenir : elle contrôle ce qu'il faut +sur votre poste et lance le conteneur, en disant quoi faire si quelque chose manque — Docker +non démarré, groupe `docker` sous Linux, CLI `devcontainer` introuvable. + +=== "Linux · macOS" + + ```bash + git clone https://github.com/lostmind84/OpenScale.git + cd OpenScale + sh dev.sh + ``` + + `sh dev.sh`, pas `./dev.sh` : le fichier est commité en mode `100644`, sans le bit + d'exécution, et `./dev.sh` répondrait « Permission denied ». + +=== "Windows" + + ```powershell + git clone https://github.com/lostmind84/OpenScale.git + cd OpenScale + .\dev.ps1 + ``` + +`dev.sh` et `dev.ps1` **n'installent rien** — ni Docker, ni Node —, délibérément : un +script qui tenterait ça sur la machine de quelqu'un d'autre échouerait en silence. Ils +font trois contrôles dans l'ordre — Docker répond, la CLI `devcontainer` fonctionne, le +conteneur se lance — et s'arrêtent au premier qui échoue en nommant la cause **pour votre +plateforme** (Debian, Arch, Fedora, macOS, Windows), groupe `docker` sous Linux compris, +dont la correction exige une déconnexion/reconnexion. + +Ce que ces scripts font, en détail, pour qui préfère le savoir plutôt que le croire : ```bash npm i -g @devcontainers/cli @@ -25,10 +56,11 @@ devcontainer exec --workspace-folder . make test C'est le chemin réellement vérifié : cette fonctionnalité a été construite, lancée et testée par cette commande et par `docker exec`, sans jamais ouvrir VS Code. -Un éditeur qui porte les conteneurs de développement — VS Code, Cursor, Windsurf, une -JetBrains récente — arrive au même résultat depuis « Reopen in Container » et n'a besoin -que de Docker : l'implémentation du devcontainer est intégrée à l'éditeur. La CLI, elle, -demande en plus Node sur votre poste. Le compromis est réel : à vous de choisir. +Un éditeur qui implémente la spécification devcontainer arrive au même résultat depuis +« Reopen in Container » et n'a besoin que de Docker. VS Code et ses forks — Cursor, +Windsurf — le font avec certitude ; d'autres éditeurs le revendiquent aussi, sans que ce +dépôt l'ait vérifié. La CLI, elle, demande en plus Node sur votre poste. Le compromis est +réel : à vous de choisir. Vous pouvez alors rejouer, avant de pousser, tout ce que la CI vérifie **sauf un point** : les scripts d'installation sous Windows PowerShell 5.1, qu'aucun conteneur Linux ne peut