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
30 changes: 30 additions & 0 deletions .github/workflows/bash-syntax-advisory.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,36 @@ jobs:
done < <(git ls-files '*.sh' | grep -v -E '^(MyIA.AI.Notebooks/GenAI/Vibe-Coding/|docs/archive/)|(^|/)_archive/' | sort)
exit "$FAILED"

runner-script-tests:
name: Runner script behavioural tests
# Complements bash -n (syntax) with execution: the sibling bash tests that
# live alongside the self-hosted Linux runner scripts
# (scripts/ci/docker/linux-runner/test_*.sh). Discovered by glob so a new
# test_*.sh is wired automatically -- a semantic regression of the block
# under test can no longer land unappercu (NanoClaw reserve, PR #15226
# review 16:54:37Z). Runs on the same self-hosted jambe as syntax-check
# (routing #14283 tranche 3c, do not revert independently); it is queued
# while that runner is starved, exactly like syntax-check.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v4
- name: Run linux-runner test_*.sh
run: |
set -euo pipefail
FAILED=0
for t in scripts/ci/docker/linux-runner/test_*.sh; do
[ -e "$t" ] || continue
echo "== $t =="
if bash "$t"; then
echo "OK: $t"
else
echo "::error::runner script test failed: $t"
FAILED=1
fi
done
exit "$FAILED"

shebang-dry-run-advisory:
name: Shebang + dry-run advisory (non-blocking)
# Routage #14283 tranche 3c (ai-01 2026-09-02) : jambe Linux auto-hebergee.
Expand Down
32 changes: 32 additions & 0 deletions scripts/ci/docker/linux-runner/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,38 @@ for gitdir in "$ACTIONS_RUNNER_INPUT_WORK"/*/*/.git; do
done
# ---------------------------------------------------------------------------

# --- Desarmement des refs distantes dangling (slot poisoning checkout) -------
# Le volume _work persiste PAR SLOT (#14285/#14288) : une branche rebasee ou
# force-poussee cote origin laisse refs/remotes/origin/<branche> pointant sur
# un objet absent du depot -- le fetch de actions/checkout (--depth=1, refspec
# EXPLICITE +<sha>:refs/remotes/pull/N/merge) ne met a jour NI ne prune les
# autres refs distantes. Le checkout du job suivant meurt alors avant toute
# etape utile :
# git checkout --force <ref> -> fatal: bad object refs/remotes/origin/<b>
# (incident PR #15089, job 101812399772, 2026-09-07 : chore/11840-iit-zero-pad
# rebasee entre-temps sur un slot persistant). On ne supprime QUE les refs dont
# l'objet manque -- le cache incrementale de #14285 reste conserve (contraire-
# ment au purge rm -rf du cas sparse arme, ~40-51 s/job).
# Complementaire au bloc work_cache_health (#15105) place juste apres : son
# detecteur (for-each-ref par defaut) ne nomme que les refs INPARSABLES
# (fichier zero octet -> reparation ou purge du clone) ; une ref au SHA
# parsable mais d'objet absent tue son scan en fatal non reconnu et son
# compte de refs reste > 0 -- il conclurait "sain". Verifie par mesure :
# for-each-ref defaut -> fatal rc=128 ; format '%(refname) %(objectname)'
# -> liste rc=0. D'ou l'ordre : disarm chirurgical ici, integrite large
# ensuite.
for gitdir in "$ACTIONS_RUNNER_INPUT_WORK"/*/*/.git; do
[ -e "$gitdir" ] || continue
repo="${gitdir%/.git}"
while read -r ref sha; do
if ! git -C "$repo" cat-file -e "$sha^{object}" 2>/dev/null; then
echo "entrypoint: ref distante dangling $ref -- suppression (objet absent)"
git -C "$repo" update-ref -d "$ref" 2>/dev/null || true
fi
done < <(git -C "$repo" for-each-ref --format='%(refname) %(objectname)' refs/remotes 2>/dev/null)
done
# TEST-ENTRYPOINT-DANGLING-REFS-END

# --- Sante du cache de depot persistant (#15105) ---------------------------
# Meme hook job-started que le bloc sparse ci-dessus : le volume _work survit
# au conteneur, l'entrypoint est le seul point qui s'execute avant
Expand Down
103 changes: 103 additions & 0 deletions scripts/ci/docker/linux-runner/test_entrypoint_disarm.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
# Tests du desarmement des refs distantes dangling de entrypoint.sh (famille
# slot-poisoning du volume _work persistant #14285/#14288 ; incident PR #15089,
# job 101812399772 : `git checkout --force` mort en `fatal: bad object
# refs/remotes/origin/chore/11840-iit-zero-pad`).
#
# Methode : on extrait le bloc REEL du script sous test (marqueurs de section)
# et on l'execute contre des depots fixture reels -- pas de stub, git est
# l'outil observe. Une ref saine doit survivre, une ref dangling (objet absent,
# fabriquee par ecriture directe du fichier loose, git refusant de la creer via
# update-ref) doit etre supprimee, le cache/working tree conserves.

set -o pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TEST_DIR="/tmp/entrypoint-disarm-test-$$"
mkdir -p "$TEST_DIR/_work"
LOG="$TEST_DIR/test.log"
: > "$LOG"

RESULTS="$TEST_DIR/results"
: > "$RESULTS"
ok() { echo " PASS: $1"; echo "PASS $1" >> "$RESULTS"; }
ko() { echo " FAIL: $1"; echo "FAIL $1" >> "$RESULTS"; }

# Extraction du bloc reel : du header de section au marqueur END.
if ! sed -n '/# --- Desarmement des refs distantes dangling/,/# TEST-ENTRYPOINT-DANGLING-REFS-END/p' \
"$SCRIPT_DIR/entrypoint.sh" > "$TEST_DIR/block.sh" || \
! grep -q "for-each-ref" "$TEST_DIR/block.sh"; then
echo "FAIL extraction du bloc entrypoint.sh" >&2
exit 1
fi

# Fixture : un repo client sous la layout _work/<org>/<repo> avec un commit,
# une ref saine (objet present) et une ref dangling (sha fabrique, fichier
# loose ecrit directement).
make_fixture() { # $1 = chemin du repo
git init -q "$1"
git -C "$1" -c user.email=t@t -c user.name=t commit -q --allow-empty -m "c1"
echo content > "$1/file.txt"
git -C "$1" add file.txt
git -C "$1" -c user.email=t@t -c user.name=t commit -q -m "c2"
local head_sha
head_sha="$(git -C "$1" rev-parse HEAD)"
mkdir -p "$1/.git/refs/remotes/origin"
echo "$head_sha" > "$1/.git/refs/remotes/origin/keepbranch"
}

REPO_POISONED="$TEST_DIR/_work/jsboige/CoursIA"
REPO_CLEAN="$TEST_DIR/_work/jsboige/OtherRepo"
make_fixture "$REPO_POISONED"
make_fixture "$REPO_CLEAN"
echo "1234567890123456789012345678901234567890" > "$REPO_POISONED/.git/refs/remotes/origin/deadbranch"

# --- Test 1 : la ref dangling est supprimee, la saine conservee -------------
(
out="$(ACTIONS_RUNNER_INPUT_WORK="$TEST_DIR/_work" bash "$TEST_DIR/block.sh" 2>&1)"; rc=$?
echo "$out" >> "$LOG"
if [ "$rc" -ne 0 ]; then ko "T1 le bloc doit sortir 0 (rc=$rc)"; else ok "T1 bloc exit 0"; fi
if git -C "$REPO_POISONED" rev-parse --verify -q refs/remotes/origin/deadbranch >/dev/null; then
ko "T1 la ref dangling doit etre supprimee"
else ok "T1 ref dangling supprimee"; fi
if git -C "$REPO_POISONED" rev-parse --verify -q refs/remotes/origin/keepbranch >/dev/null; then
ok "T1 ref saine conservee"
else ko "T1 la ref saine ne doit PAS etre supprimee"; fi
if echo "$out" | grep -q "deadbranch"; then ok "T1 suppression journalisee"; else ko "T1 aucune trace de la suppression"; fi
)

# --- Test 2 : repo sain = intouché (controle negatif) ------------------------
(
out="$(ACTIONS_RUNNER_INPUT_WORK="$TEST_DIR/_work" bash "$TEST_DIR/block.sh" 2>&1)"; rc=$?
if [ "$rc" -ne 0 ]; then ko "T2 second passage exit 0 (rc=$rc)"; else ok "T2 second passage exit 0"; fi
if [ -n "$out" ]; then ko "T2 rien a signaler sur un depot sain (sort: $out)"; else ok "T2 silencieux sur depot sain"; fi
if git -C "$REPO_CLEAN" rev-parse --verify -q refs/remotes/origin/keepbranch >/dev/null; then
ok "T2 repo temoin intouché"
else ko "T2 le repo temoin a ete modifie"; fi
)

# --- Test 3 : working tree et cache conserves --------------------------------
(
if [ "$(cat "$REPO_POISONED/file.txt" 2>/dev/null)" = "content" ]; then
ok "T3 working tree conserve"
else ko "T3 working tree altere"; fi
if git -C "$REPO_POISONED" status --porcelain 2>/dev/null | grep -q .; then
ko "T3 status sale apres passage"
else ok "T3 status propre"; fi
if [ "$(git -C "$REPO_POISONED" log --oneline 2>/dev/null | wc -l)" -ge 2 ]; then
ok "T3 historique/cache conserves (pas de purge rm -rf)"
else ko "T3 le clone a ete purge (historique absent)"; fi
)

# --- Verdict agrege -----------------------------------------------------------
n_pass="$(grep -c '^PASS' "$RESULTS" || true)"
n_fail="$(grep -c '^FAIL' "$RESULTS" || true)"
echo "============================================================"
echo "entrypoint dangling-refs disarm : $n_pass PASS, $n_fail FAIL"
echo "============================================================"
if [ "$n_fail" -gt 0 ]; then
echo "(logs: $LOG)"
exit 1
fi
rm -rf "$TEST_DIR"
exit 0
Loading