diff --git a/deploy/parity_test.go b/deploy/parity_test.go index 1ab2c41..5120c0d 100644 --- a/deploy/parity_test.go +++ b/deploy/parity_test.go @@ -424,6 +424,103 @@ 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", +} + +// 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 (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)) + + // 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) + } + } + + // 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 + } + } + + 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..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 { @@ -92,9 +97,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 +111,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 { diff --git a/dev.ps1 b/dev.ps1 new file mode 100644 index 0000000..537fc9a --- /dev/null +++ b/dev.ps1 @@ -0,0 +1,132 @@ +<# +.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 : 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 + 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' + +# 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 = Test-CommandRuns { docker info } + +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 = Test-CommandRuns { devcontainer --version } + +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..429d593 --- /dev/null +++ b/dev.sh @@ -0,0 +1,145 @@ +#!/bin/sh +# Une seule commande entre un clone et un conteneur de développement lancé, pour qui n'a +# 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 +# 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) + # 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 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' + ;; + *) + 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. 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' 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