Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion .devcontainer/post-create.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,36 @@ pip install --no-cache-dir -r handbook/requirements.txt
# `npm ci` et non `npm install` : les versions sont gelées dans package-lock.json (§14.1),
# et `ci` est DÉTERMINISTE — il ÉCHOUE sur un lock désynchronisé au lieu de le réparer
# en silence, là où `install` l'aurait accepté et modifié sans le dire.
npm --prefix web ci
#
# La seconde tentative ne couvre PAS « npm a échoué », elle couvre UNE panne nommée, mesurée
# sur le premier lancement de ce conteneur : « spawnSync .../esbuild/bin/esbuild ETXTBSY »
# dans le postinstall d'esbuild. npm pose un lien dur vers le binaire de plateforme qu'il
# vient d'extraire puis l'exécute, pendant que sa propre phase reify peut encore tenir cet
# inode ouvert en écriture — et Linux refuse d'exécuter un fichier ouvert en écriture. C'est
# une course entre deux morceaux de npm, elle ne dit rien du lock, et elle frappe un volume
# web/node_modules NEUF : le premier lancement d'un contributeur, précisément.
#
# Ce que coûte de ne pas la rattraper dépasse la minute perdue : la CLI n'exécute
# postCreateCommand qu'à la CRÉATION du conteneur. Le conteneur à moitié préparé reste, le
# `devcontainer up` suivant répond « success » sans rien préparer, et dev.sh annonce « Poste
# prêt » sur un web/node_modules vide (voir la sortie de secours qu'il nomme désormais).
#
# Le filtre sur ETXTBSY est ce qui garde `ci` déterministe : tout autre échec — un lock
# désynchronisé au premier chef — sort ici, à la première tentative, sans être répété.
if ! npm_failure=$(npm --prefix web ci 2>&1); then
printf '%s\n' "$npm_failure"
case "$npm_failure" in
*ETXTBSY*)
echo ''
echo 'ETXTBSY : course connue entre les écritures de npm et son propre postinstall.'
echo 'Seconde et dernière tentative, sur un cache déjà chaud.'
npm --prefix web ci
;;
*)
exit 1
;;
esac
fi

echo ''
echo 'Poste prêt. Ce que vous pouvez rejouer ici :'
Expand Down
1 change: 1 addition & 0 deletions SUIVI.md

Large diffs are not rendered by default.

32 changes: 32 additions & 0 deletions deploy/devcontainer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,38 @@ func TestThePostCreateScriptDeclaresTheWorkspaceASafeDirectory(t *testing.T) {
}
}

// TestThePostCreateScriptRetriesNpmCiOnTheETXTBSYRace guards the repair of the only first
// launch this container has ever failed, and the failure did not look like what it was.
//
// `npm ci` died in esbuild's postinstall on « spawnSync .../esbuild/bin/esbuild ETXTBSY »:
// npm hard-links the platform binary it has just extracted, then executes it while its own
// reify phase may still hold that inode open for writing — and Linux refuses to execute a
// file open for writing. It is a race between two parts of npm, not a broken package-lock,
// and it strikes a FRESH web/node_modules volume, i.e. exactly a contributor's first run.
//
// What it costs is worse than the lost minute: the CLI runs postCreateCommand ONLY when it
// CREATES the container. The half-prepared container stays, a second `devcontainer up`
// reports success without preparing anything, and dev.sh announces « Poste prêt » over an
// empty web/node_modules — see TestBothDevScriptsSayHowToRedoAFailedPreparation, which
// holds the other half of this repair.
//
// The retry is deliberately NARROW: only an output naming ETXTBSY is retried, so a genuinely
// desynchronised lock file keeps failing on the first attempt, loudly, the way §14.1 wants.
func TestThePostCreateScriptRetriesNpmCiOnTheETXTBSYRace(t *testing.T) {
script := readFile(t, postCreateScript)
if !strings.Contains(script, "ETXTBSY") {
t.Error("post-create.sh ne nomme plus ETXTBSY : la seconde tentative de `npm ci` " +
"ne vise plus la panne mesurée, et un premier lancement sur un volume " +
"web/node_modules neuf peut de nouveau mourir sur une course interne à npm")
}
if attempts := strings.Count(codeOnly(script), "npm --prefix web ci"); attempts < 2 {
t.Errorf("post-create.sh appelle `npm --prefix web ci` %d fois : sans seconde "+
"tentative, la course ETXTBSY laisse un conteneur à moitié préparé que la CLI "+
"ne repréparera jamais — elle ne rejoue postCreateCommand qu'à la création",
attempts)
}
}

// TestThePostCreateCommandRunsTheScriptThisBenchReads: the bench above is worth nothing if
// devcontainer.json stops calling the file it inspects.
func TestThePostCreateCommandRunsTheScriptThisBenchReads(t *testing.T) {
Expand Down
24 changes: 24 additions & 0 deletions deploy/parity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,30 @@ func TestDevScriptsCheckTheSameThings(t *testing.T) {
checkTheReasonIsWritten(t, devGroupReasonException)
}

// TestBothDevScriptsSayHowToRedoAFailedPreparation holds a trap neither script could show
// on its own, and that a first launch actually fell into.
//
// `devcontainer up` runs postCreateCommand ONLY when it CREATES the container. A preparation
// that dies half-way — `npm ci` on the ETXTBSY race of
// TestThePostCreateScriptRetriesNpmCiOnTheETXTBSYRace, a network cut during `pip install` —
// leaves the container in place, and the next plain `devcontainer up` answers
// {"outcome":"success"} without preparing anything. Both scripts would then print « Poste
// prêt » over an empty web/node_modules, and the contributor's first `make front-check`
// would fail on something that names neither the container nor its preparation.
//
// Naming --remove-existing-container is the whole repair: it is the only way back to a
// container the CLI will prepare again.
func TestBothDevScriptsSayHowToRedoAFailedPreparation(t *testing.T) {
const wayOut = "--remove-existing-container"
for _, script := range []string{devScriptPath, devPowerShellScriptPath} {
if !strings.Contains(readFile(t, script), wayOut) {
t.Errorf("%s ne nomme pas %q : après une préparation échouée, relancer le script "+
"tel quel répondrait « Poste prêt » sur un conteneur à moitié préparé — la CLI "+
"ne rejoue postCreateCommand qu'à la CRÉATION du conteneur", script, wayOut)
}
}
}

// --- Les lecteurs ---------------------------------------------------------------------

// dashedSpelling renders a PowerShell parameter the way a sh script spells it.
Expand Down
11 changes: 11 additions & 0 deletions dev.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,19 @@ Write-Host ' devcontainer est disponible.'
Write-Host ''
Write-Host '3. Tout est présent -- lancement du conteneur de développement'

# Le message d'échec dit un piège qu'on ne devine pas, et c'est le même des deux côtés (voir
# le commentaire équivalent de dev.sh) : la CLI n'exécute postCreateCommand qu'à la CRÉATION
# du conteneur. Une préparation qui meurt en route laisse le conteneur EN PLACE, et le
# « devcontainer up » suivant répond {"outcome":"success"} sans rien préparer -- ce script
# annoncerait « Poste prêt » sur un web/node_modules vide.
devcontainer up --workspace-folder .
if ($LASTEXITCODE -ne 0) {
Write-Host ''
Write-Host " Le lancement a échoué. Si la construction de l'image est passée et que"
Write-Host " c'est la PRÉPARATION qui a lâché, ne relancez pas cette commande telle"
Write-Host ' quelle : la CLI ne rejoue la préparation que sur un conteneur NEUF.'
Write-Host ' Repartez de :'
Write-Host ' devcontainer up --workspace-folder . --remove-existing-container'
exit $LASTEXITCODE
}

Expand Down
16 changes: 15 additions & 1 deletion dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,21 @@ echo ' devcontainer est disponible.'

echo ''
echo '3. Tout est présent -- lancement du conteneur de développement'
devcontainer up --workspace-folder .
# L'échec n'est pas laissé à « set -e », parce que le message qui manque ici est celui d'un
# piège qu'on ne devine pas : la CLI n'exécute postCreateCommand qu'à la CRÉATION du
# conteneur. Une préparation qui meurt en route -- npm, pip, une coupure réseau -- laisse le
# conteneur EN PLACE, et le « devcontainer up » suivant répond {"outcome":"success"} sans
# rien préparer ; ce script annoncerait alors « Poste prêt » sur un web/node_modules vide, et
# le premier « make front-check » du contributeur échouerait sur un message qui ne parle ni
# du conteneur ni de sa préparation. Repartir d'un conteneur neuf est la seule sortie.
if ! devcontainer up --workspace-folder .; then
echo ''
echo " Le lancement a échoué. Si la construction de l'image est passée et que c'est la"
echo ' PRÉPARATION qui a lâché, ne relancez pas cette commande telle quelle : la CLI ne'
echo ' rejoue la préparation que sur un conteneur NEUF. Repartez de :'
echo ' devcontainer up --workspace-folder . --remove-existing-container'
exit 1
fi

echo ''
echo 'Poste prêt. Ce que vous pouvez rejouer depuis ce conteneur :'
Expand Down
13 changes: 13 additions & 0 deletions handbook/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,19 @@ Vous pouvez alors rejouer, avant de pousser, tout ce que la CI vérifie **sauf u
les scripts d'installation sous Windows PowerShell 5.1, qu'aucun conteneur Linux ne peut
exécuter. C'est le job `scripts` de la CI qui les juge, à chaque pull request.

!!! warning "Si le lancement échoue APRÈS la construction de l'image"

La CLI n'exécute la préparation du conteneur — `postCreateCommand` : golangci-lint,
mkdocs, `npm ci` — qu'à la **création** de celui-ci. Une préparation qui meurt en route
laisse donc le conteneur en place, et un second `dev.sh` répondrait « Poste prêt » sans
rien préparer, sur un `web/node_modules` vide. Repartez d'un conteneur neuf :

```bash
devcontainer up --workspace-folder . --remove-existing-container
```

Les deux scripts le disent aussi au moment où ils échouent.

!!! note "Sous Windows, si les compilations traînent"

Le dépôt reste sur votre disque Windows et le conteneur le lit à travers un montage :
Expand Down