Skip to content

Commit abc45ba

Browse files
jsboigeclaude
andauthored
fix(runners,#15105): integrite + maintenance bornee du cache _work persistant (#15273)
actions/checkout pose gc.auto=0 dans le depot du slot : correct pour un workspace jetable, faux depuis #14285 qui l'a rendu persistant. Deux defauts mesures firsthand sur ai-01 (2026-09-07) : croissance des packs sans borne (slot 1 : 264 packs dont 263 promisor) et zero precondition d'integrite (slot 7 : 1471 refs de zero octet, gate en loterie 1-sur-8 sans que aucun organe ne le nomme). - work_cache_health.sh (nouveau) : passe integrite (refs cassees lues sur le CANAL stderr de for-each-ref -- rc=0 mesure, seul le warning nomme), reparation par retrait des fichiers vides de .git/refs et .git/logs UNIQUEMENT (le marqueur .promisor vide est legitime sous objects/ : hors perimetre par construction), purge du clone si le depot reste muet ; passe maintenance (repack -ad au-dela de COURSIA_RUNNER_CACHE_PACK_THRESHOLD, defaut 16, mesure avant/apres au journal). Tout git passe par -c safe.directory='*'. - entrypoint.sh : hook job-started avant l'enregistrement du runner -- aucun job en vol ne paie, bornes d'I/O du conteneur heritees, zero plomberie hote. - supervise.sh : knob transmis au conteneur ; garde de fraicheur #14801 etendue aux DEUX scripts embarques (entrypoint + work_cache_health). - tests : test_work_cache_health.sh sur fixtures git reelles (clone partiel blob:none fidele a la flotte), chaque garde avec son controle negatif ; test 20 guards : ecart work_cache_health.sh refuse au demarrage. Mesures (WSL git 2.43 = version de l'image flotte) : - accumulation reproduite : 1 pack par fetch (1 -> 11 en 10 fetches) - repack -ad sur clone au filtre reellement honore : 5 packs promisor -> 1, reseau nul, .promisor preserve, lazy-fetch + fetch incremental OK apres - refs de zero octet : for-each-ref rc=0 + warning stderr ; reparation par perimetre refs/logs ; fichiers vides d'objects/ intacts - dubious ownership : rc=128 et 0 refs sans stderr ; safe.directory=* lit Harness : 40 PASS/0 FAIL guards + 22 PASS/0 FAIL cache, en WSL ET Git Bash. Co-authored-by: Claude-Code <noreply@anthropic.com>
1 parent 5706e1a commit abc45ba

7 files changed

Lines changed: 598 additions & 17 deletions

File tree

‎scripts/ci/docker/linux-runner/Dockerfile‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,9 @@ RUN echo "${GH_SHA256} /tmp/gh.tar.gz" | sha256sum -c - \
7676
USER runner
7777
WORKDIR /opt/runner
7878

79+
# work_cache_health.sh : moitie executante de #15105, sourcee par l'entrypoint
80+
# au demarrage de chaque conteneur (controle du cache _work persistant).
81+
COPY --chown=runner:runner work_cache_health.sh /opt/runner/work_cache_health.sh
7982
COPY --chown=runner:runner entrypoint.sh /opt/runner/entrypoint.sh
8083
RUN chmod +x /opt/runner/entrypoint.sh
8184

‎scripts/ci/docker/linux-runner/entrypoint.sh‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,22 @@ for gitdir in "$ACTIONS_RUNNER_INPUT_WORK"/*/*/.git; do
4949
done
5050
# ---------------------------------------------------------------------------
5151

52+
# --- Sante du cache de depot persistant (#15105) ---------------------------
53+
# Meme hook job-started que le bloc sparse ci-dessus : le volume _work survit
54+
# au conteneur, l'entrypoint est le seul point qui s'execute avant
55+
# l'enregistrement du runner -- donc avant qu'un job n'attrape le cache.
56+
# Deux passes par clone : integrite (refs cassees -> reparation ou purge,
57+
# le gate de la flotte etait une loterie 1-sur-8 sans que rien le nomme),
58+
# puis maintenance (repack -ad si le compte de packs depasse le seuil ;
59+
# gc.auto=0 fait que rien d'autre ne consolide jamais). Le detail et les
60+
# trois controles positifs (safe.directory, stderr, .promisor) sont dans
61+
# work_cache_health.sh.
62+
WCH_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
63+
# shellcheck disable=SC1091
64+
. "$WCH_DIR/work_cache_health.sh"
65+
wch_check_workdir "$ACTIONS_RUNNER_INPUT_WORK" "${RUNNER_WORK_CACHE_PACK_THRESHOLD:-16}"
66+
# ---------------------------------------------------------------------------
67+
5268
cd /opt/runner
5369
# --disableupdate : le conteneur est --rm et le runner --ephemeral (un seul
5470
# job puis mort). Un self-update n'y est donc jamais CONSERVE -- GitHub ordonne

‎scripts/ci/docker/linux-runner/persist/README.md‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,11 +217,43 @@ Les journaux des **conteneurs** sont bornes ailleurs, par `daemon.json`
217217
(`log-driver: local`, 10 Mo x 3). Deux mecanismes distincts, ecrits a deux endroits
218218
differents, qui remplissaient chacun le meme disque.
219219

220+
## Maintenance du cache `_work` (#15105)
221+
222+
`actions/checkout` pose `gc.auto = 0` dans le depot du slot : correct pour un
223+
workspace jetable, faux depuis que #14285 rend ce workspace persistant. Chaque
224+
job depose un pack promisor de plus (slot 1 : **264 packs** au diagnostic,
225+
compte croissant sans plafond), et un arret brutal laisse des refs de ZERO
226+
octet qui transforment le gate en loterie -- le slot 7 en portait 1471.
227+
228+
Deux passes vivent dans l'entrypoint du conteneur (avant l'enregistrement du
229+
runner : aucun job en vol ne les paie, et elles tournent sous les bornes d'I/O
230+
du conteneur lui-meme) :
231+
232+
- **Integrite, inconditionnelle** : refs cassees detectees sur le canal stderr
233+
de `for-each-ref` (rc=0 -- seul le warning nomme la ref), reparees par
234+
retrait des fichiers vides de `.git/refs` et `.git/logs` UNIQUEMENT (un
235+
marqueur `.promisor` vide est legitime et vit sous `.git/objects` :
236+
hors perimetre par construction), et purge du clone si le depot reste muet.
237+
- **Repack, seuille** : `COURSIA_RUNNER_CACHE_PACK_THRESHOLD` (defaut **16**,
238+
0 = desactive) descend au conteneur ; au-dela, `git repack -ad` consolide
239+
avec la mesure avant/apres au journal. Non inerte au meme titre que
240+
`COURSIA_RUNNER_LOG_MAX_BYTES` : son absence est une croissance de disque
241+
sans borne, pas un plafond qu'une machine n'a pas demande. Sur un clone
242+
partiel `blob:none`, le repack est sur (mesure sur fixture au filtre
243+
reellement honore : 5 packs promisor -> 1, marqueur preserve, lazy-fetch et
244+
fetch incremental intacts).
245+
246+
La garde de fraicheur #14801 lit desormais **deux** scripts embarques
247+
(`entrypoint.sh` ET `work_cache_health.sh` qu'il source) : un correctif merge
248+
mais non rebuild sur l'un ou l'autre est refuse au demarrage du pool.
249+
220250
## Voir aussi
221251

222252
- [`../supervise.sh`](../supervise.sh) -- le superviseur, ses trois pools et ses bornes
223-
- [`../test_supervise_guards.sh`](../test_supervise_guards.sh) -- 18 tests, chaque garde avec son controle negatif
253+
- [`../test_supervise_guards.sh`](../test_supervise_guards.sh) -- 20 tests, chaque garde avec son controle negatif
254+
- [`../test_work_cache_health.sh`](../test_work_cache_health.sh) -- sante du cache `_work` sur fixtures git reelles (#15105)
224255
- [`../../../../../docs/ci/self-hosted-runners.md`](../../../../../docs/ci/self-hosted-runners.md) -- vue d'ensemble du parc
225256
- **#15091** -- revision du design du superviseur (traffic disque, isolation)
226257
- **#14385** -- volumes `_work` par slot -- les waiters n'en montent pas, le test 17 le garde
227258
- **#14801** -- fraicheur de l'image, verifiee par empreinte de l'entrypoint
259+
- **#15105** -- integrite et maintenance du cache `_work` persistant

‎scripts/ci/docker/linux-runner/supervise.sh‎

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,16 @@ BACKOFF_MAX_SEC="${COURSIA_RUNNER_BACKOFF_MAX_SEC:-300}"
232232
# jitter la disperse.
233233
BACKOFF_JITTER_PCT="${COURSIA_RUNNER_BACKOFF_JITTER_PCT:-25}"
234234

235+
# Seuil de packs du cache _work persistant (#15105). Au-dela, l'entrypoint du
236+
# conteneur repack le clone (gc.auto=0 pose par actions/checkout : rien
237+
# d'autre ne consolide jamais -- slot 1 : 264 packs, compte croissant a
238+
# chaque job). NON inerte au meme titre que LOG_MAX_BYTES : son absence est
239+
# une croissance de disque sans borne, pas un plafond qu'une machine n'a pas
240+
# demande. 0 = desactive. Le knob descend au conteneur par -e ; la passe
241+
# integrite (refs cassees) est, elle, inconditionnelle -- cf
242+
# work_cache_health.sh et le bloc entrypoint #15105.
243+
CACHE_PACK_THRESHOLD="${COURSIA_RUNNER_CACHE_PACK_THRESHOLD:-16}"
244+
235245
mkdir -p "$STATE_DIR"
236246

237247
# Git Bash (MSYS) sous Windows reecrit les arguments de forme /posix/path des
@@ -255,21 +265,24 @@ RUNNER_CTX="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
255265
# daemon docker-ce WSL construite 5 h AVANT le merge, jamais rebatie), et ce
256266
# silence a produit les rouges fantomes du sparse-checkout empoisonne. Le
257267
# demarrage d'un pool est le seul point qui s'execute inconditionnellement
258-
# (un job annule ne joue aucun step post) : on y compare le sha256 du
259-
# entrypoint.sh de CE checkout a celui embarque dans l'image. La lecture
268+
# (un job annule ne joue aucun step post) : on y compare le sha256 de CHAQUE
269+
# script embarque de CE checkout (entrypoint.sh, et depuis #15105
270+
# work_cache_health.sh qu'il source) a celui porte par l'image. La lecture
260271
# cote image passe par `docker run --entrypoint sha256sum` -- le Dockerfile
261-
# place le script a /opt/runner/entrypoint.sh et MSYS_NO_PATHCONV (exporte
262-
# plus haut) protege l'argument POSIX sous Git Bash.
272+
# place les scripts sous /opt/runner/ et MSYS_NO_PATHCONV (exporte plus haut)
273+
# protege l'argument POSIX sous Git Bash.
263274
assert_image_fresh() {
264275
local image="$1" build_cmd="$2"
265-
local repo_sha img_sha
266-
repo_sha="$(sha256sum "$RUNNER_CTX/entrypoint.sh" 2>/dev/null | awk '{print $1}')"
267-
[ -n "$repo_sha" ] || die "entrypoint.sh introuvable a cote de supervise.sh ($RUNNER_CTX) -- lancer depuis un checkout du depot"
268-
img_sha="$(docker run --rm --entrypoint sha256sum "$image" /opt/runner/entrypoint.sh 2>/dev/null | awk '{print $1}')"
269-
[ -n "$img_sha" ] || die "lecture de /opt/runner/entrypoint.sh dans $image impossible (docker run --entrypoint sha256sum)"
270-
[ "$repo_sha" = "$img_sha" ] || die "image $image PERIMEE : entrypoint.sh du checkout ($repo_sha) != entrypoint embarque ($img_sha).
276+
local f repo_sha img_sha
277+
for f in entrypoint.sh work_cache_health.sh; do
278+
repo_sha="$(sha256sum "$RUNNER_CTX/$f" 2>/dev/null | awk '{print $1}')"
279+
[ -n "$repo_sha" ] || die "$f introuvable a cote de supervise.sh ($RUNNER_CTX) -- lancer depuis un checkout du depot"
280+
img_sha="$(docker run --rm --entrypoint sha256sum "$image" /opt/runner/$f 2>/dev/null | awk '{print $1}')"
281+
[ -n "$img_sha" ] || die "lecture de /opt/runner/$f dans $image impossible (docker run --entrypoint sha256sum)"
282+
[ "$repo_sha" = "$img_sha" ] || die "image $image PERIMEE : $f du checkout ($repo_sha) != version embarquee ($img_sha).
271283
Un correctif merge mais non deploye est indiscernable d'un correctif absent (#14801, #14385). Reconstruire :
272284
$build_cmd"
285+
done
273286
}
274287

275288
# --- Bornes d'I/O : resolution du device et des drapeaux docker -------------
@@ -564,6 +577,7 @@ slot_loop() {
564577
-v "$TOOLCACHE_VOLUME":"$TOOLCACHE_MOUNT" \
565578
-v "${vol_prefix}-${slot}":"$WORK_MOUNT" \
566579
-e RUNNER_TOOL_CACHE="$TOOLCACHE_MOUNT" \
580+
-e RUNNER_WORK_CACHE_PACK_THRESHOLD="$CACHE_PACK_THRESHOLD" \
567581
-e ACTIONS_RUNNER_INPUT_TOKEN="$token" \
568582
-e ACTIONS_RUNNER_INPUT_URL="https://github.com/$REPO" \
569583
-e ACTIONS_RUNNER_INPUT_NAME="$name" \

‎scripts/ci/docker/linux-runner/test_supervise_guards.sh‎

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,13 +31,19 @@ ko() { echo " FAIL: $1"; echo "FAIL $1" >> "$RESULTS"; }
3131

3232
# Stubs docker + gh + ps. Le stub docker simule une image A JOUR pour le
3333
# garde de fraicheur #14801 : au probe `run --entrypoint sha256sum`, il rend
34-
# le sha256 du VRAI entrypoint.sh sibling (bake a la generation du stub).
35-
# STUB_IMG_ENTRYPOINT_SHA force un ecart pour tester le refus (test 9).
34+
# le sha256 du VRAI script sibling demande (entrypoint.sh, et depuis #15105
35+
# work_cache_health.sh -- le garde lit les DEUX, le stub dispatche sur le
36+
# chemin passe en argument). STUB_IMG_ENTRYPOINT_SHA / STUB_IMG_HEALTH_SHA
37+
# forcent un ecart pour tester le refus (tests 9 et 20).
3638
REPO_ENTRYPOINT_SHA="$(sha256sum "$SCRIPT_DIR/entrypoint.sh" 2>/dev/null | awk '{print $1}')"
39+
REPO_HEALTH_SHA="$(sha256sum "$SCRIPT_DIR/work_cache_health.sh" 2>/dev/null | awk '{print $1}')"
3740
cat > "$TEST_DIR/bin/docker" <<STUB
3841
#!/usr/bin/env bash
3942
if [ "\$1" = "run" ]; then
40-
echo "\${STUB_IMG_ENTRYPOINT_SHA:-$REPO_ENTRYPOINT_SHA} /opt/runner/entrypoint.sh"
43+
case "\$*" in
44+
*'/opt/runner/entrypoint.sh') echo "\${STUB_IMG_ENTRYPOINT_SHA:-$REPO_ENTRYPOINT_SHA} /opt/runner/entrypoint.sh" ;;
45+
*'/opt/runner/work_cache_health.sh') echo "\${STUB_IMG_HEALTH_SHA:-$REPO_HEALTH_SHA} /opt/runner/work_cache_health.sh" ;;
46+
esac
4147
exit 0
4248
fi
4349
exit 0
@@ -121,9 +127,13 @@ echo "Test 3 : start --force leve sentinel (Defaut 2 avec --force)"
121127
export COURSIA_RUNNER_NAME_PREFIX="test-prefix-C"
122128
export COURSIA_RUNNER_STATE_DIR="$TEST_DIR/state-C"
123129
touch "$TEST_DIR/state-C/stop"
124-
timeout --kill-after=1 2 bash "$SCRIPT_DIR/supervise.sh" start 1 --force >/dev/null 2>"$TEST_DIR/last.err" &
130+
timeout --kill-after=1 4 bash "$SCRIPT_DIR/supervise.sh" start 1 --force >/dev/null 2>"$TEST_DIR/last.err" &
125131
TPID=$!
126-
sleep 0.5
132+
# 1.5 s : le garde de fraicheur #15105 lit DEUX scripts embarques, soit
133+
# deux probes docker de plus avant le rm du sentinel -- sous Git Bash ou
134+
# chaque fork de stub coute ~100 ms, 0.5 s coupaient parfois AVANT le rm
135+
# et le test echouait sur une question de delai, pas d'intention.
136+
sleep 1.5
127137
if [ ! -f "$TEST_DIR/state-C/stop" ]; then
128138
ok "sentinel leve par start --force"
129139
else
@@ -539,7 +549,10 @@ echo "Test 17 : waiters -- toolcache monte, et JAMAIS de volume _work (#15091)"
539549
cat > "$TEST_DIR/bin17/docker" <<STUB
540550
#!/usr/bin/env bash
541551
if [ "\$1" = "run" ] && printf '%s' "\$*" | grep -q -- '--entrypoint sha256sum'; then
542-
echo "$REPO_ENTRYPOINT_SHA /opt/runner/entrypoint.sh"
552+
case "\$*" in
553+
*'/opt/runner/entrypoint.sh') echo "$REPO_ENTRYPOINT_SHA /opt/runner/entrypoint.sh" ;;
554+
*'/opt/runner/work_cache_health.sh') echo "$REPO_HEALTH_SHA /opt/runner/work_cache_health.sh" ;;
555+
esac
543556
exit 0
544557
fi
545558
if [ "\$1" = "run" ]; then
@@ -683,6 +696,29 @@ echo "Test 19 : cmd_stop rend != 0 quand le sentinel n'a PAS pu etre pose (#1509
683696
)
684697
echo ""
685698

699+
# --- Test 20 : garde de fraicheur -- health script perime refuse (#15105) ---
700+
# Le garde lit DEUX fichiers depuis #15105 (work_cache_health.sh est source
701+
# par l'entrypoint). Le controle positif du COTE garde : un ecart sur le
702+
# SEUL fichier ajoute doit refuser exactement comme un ecart d'entrypoint --
703+
# sinon la porte que le nouveau fichier ouvre serait garde par personne.
704+
echo "Test 20 : start refuse si work_cache_health.sh de l'image != checkout (#15105)"
705+
(
706+
cd "$SCRIPT_DIR"
707+
unset PS_OUTPUT
708+
mkdir -p "$TEST_DIR/state-20"
709+
STUB_IMG_HEALTH_SHA=e000000000000000000000000000000000000000000000000000000000000000e
710+
export STUB_IMG_HEALTH_SHA
711+
rc="$(run_supervise 'start 1' 'test-prefix-20' "$TEST_DIR/state-20" 2>&1 | head -1 | sed 's/rc=//')"
712+
err="$(cat "$TEST_DIR/last.err")"
713+
if [ "$rc" != "0" ] && echo "$err" | grep -q "PERIMEE" && echo "$err" | grep -q "work_cache_health.sh"; then
714+
ok "health script perime refuse, fichier FAUTIF nomme (rc=$rc)"
715+
else
716+
ko "refus sur work_cache_health.sh attendu, rc=$rc err=$err"
717+
fi
718+
unset STUB_IMG_HEALTH_SHA
719+
)
720+
echo ""
721+
686722
# --- Verdict agrege ---------------------------------------------------------
687723
# `|| echo 0` serait un piege ici, et il l'a ete : `grep -c` IMPRIME "0" avant
688724
# de sortir 1 quand il ne trouve rien, donc le repli SUFFIXE un second zero au

0 commit comments

Comments
 (0)