LOT Q0 : socle du démonstrateur Quelque part en 2035

Scaffold Vite + TypeScript + Three.js (scène vide : sol + boîte, marqueur de build),
store et encodage de l'état dans l'URL (5 tests), lint de frontières entre modules,
pipeline d'extraction Rhino (graine du 26/09) et première géométrie v0,
deploy.sh statique (releases + lien symbolique + smoke), AGENTS.md, RUNBOOK, licences MIT + CC BY-SA 4.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Jules Nény
2026-09-27 15:21:40 +02:00
co-authored by Claude Fable 5.1
commit 2e249f1027
33 changed files with 2026 additions and 0 deletions
Executable
+84
View File
@@ -0,0 +1,84 @@
#!/usr/bin/env bash
# deploy.sh — Quelque part en 2035 (quartier.trans-former.fr), site statique.
#
# Pipeline : build LOCAL (aucun binaire natif : le build x86_64 sert un VPS arm64 sans problème)
# → tar de dist/ → scp → /opt/quartier/releases/<hash>/ → bascule du lien /opt/quartier/current
# → smoke LOCAL sur le VPS (Host: quartier.trans-former.fr, marqueur exigé, bloquant)
# → smoke public (informatif tant que DNS/certificat ne sont pas là) → garde 3 releases.
#
# Usage : ./deploy.sh (déploie HEAD ; refuse un arbre non commité)
# ./deploy.sh --force (déploie l'arbre courant même non commité, marqueur suffixé -dirty)
# Retour arrière : ssh vps-hetzner 'ln -sfn /opt/quartier/releases/<hash> /opt/quartier/current'
# ⚠ Ne JAMAIS lancer depuis une copie du repo dans Dropbox.
set -euo pipefail
VPS_HOST="${VPS_HOST:-vps-hetzner}"
DEST="/opt/quartier"
DOMAINE="quartier.trans-former.fr"
GARDER=3
case "$(pwd)" in *Dropbox*) echo "ÉCHEC : jamais depuis Dropbox"; exit 1;; esac
HASH="$(git rev-parse --short HEAD)"
if git status --porcelain | grep -q .; then
if [ "${1:-}" = "--force" ]; then HASH="${HASH}-dirty"; else
echo "ÉCHEC : arbre de travail non propre. Commit d'abord, ou --force (marqueur ${HASH}-dirty)."; exit 1; fi
fi
echo "==> Build local, marqueur ${HASH}"
rm -rf dist
QUARTIER_BUILD_HASH="${HASH}" npm run build --silent
test -f dist/index.html && test -f dist/build.json || { echo "ÉCHEC : dist incomplet"; exit 1; }
POIDS="$(du -sk dist | cut -f1)"
echo " dist : ${POIDS} Ko"
echo "==> Transfert vers ${VPS_HOST}:${DEST}/releases/${HASH}"
tar -C dist -czf "/tmp/quartier-${HASH}.tgz" .
scp -q "/tmp/quartier-${HASH}.tgz" "${VPS_HOST}:/tmp/"
rm -f "/tmp/quartier-${HASH}.tgz"
ssh "${VPS_HOST}" bash -s -- "${HASH}" "${DEST}" "${DOMAINE}" "${GARDER}" <<'EOF'
set -euo pipefail
HASH="$1"; DEST="$2"; DOMAINE="$3"; GARDER="$4"
REL="${DEST}/releases/${HASH}"
rm -rf "${REL}"; mkdir -p "${REL}"
tar -C "${REL}" -xzf "/tmp/quartier-${HASH}.tgz"; rm -f "/tmp/quartier-${HASH}.tgz"
chmod -R a+rX "${REL}"
PREC="$(readlink -f "${DEST}/current" 2>/dev/null || true)"
ln -sfn "${REL}" "${DEST}/current"
echo "==> Smoke local (fichier en place, puis Caddy en TLS local avec --resolve)"
grep -q "\"hash\":\"${HASH}\"" "${DEST}/current/build.json" || {
echo "ÉCHEC : ${DEST}/current/build.json ne porte pas ${HASH}"
if [ -n "${PREC}" ]; then ln -sfn "${PREC}" "${DEST}/current"; echo " retour arrière sur ${PREC}"; fi
exit 1
}
# Caddy redirige http → https (308) : on interroge donc le vhost en TLS, en local, sans dépendre du DNS.
# Sans certificat (DNS absent), la poignée de main TLS échoue : on l'accepte comme « route posée, certificat en attente ».
CODE="$(curl -sk -m 10 --resolve "${DOMAINE}:443:127.0.0.1" -o /tmp/quartier-smoke.json -w '%{http_code}' "https://${DOMAINE}/build.json" || echo 000)"
if [ "${CODE}" = "200" ] && grep -q "\"hash\":\"${HASH}\"" /tmp/quartier-smoke.json; then
echo " marqueur ${HASH} servi par Caddy (TLS local)"
else
HTTP="$(curl -s -m 10 -o /dev/null -w '%{http_code}' -H "Host: ${DOMAINE}" "http://127.0.0.1/build.json" || echo 000)"
if [ "${HTTP}" = "308" ]; then
echo " Caddy route bien ${DOMAINE} (308 → https) ; certificat pas encore émis (TLS local : ${CODE}). Fichier vérifié sur disque."
else
echo "ÉCHEC smoke : TLS local ${CODE}, http ${HTTP}"
if [ -n "${PREC}" ]; then ln -sfn "${PREC}" "${DEST}/current"; echo " retour arrière sur ${PREC}"; fi
exit 1
fi
fi
echo "==> Rotation : garde ${GARDER} releases"
cd "${DEST}/releases"
ls -1t | grep -v "^bootstrap$" | tail -n +$((GARDER + 1)) | while read -r r; do
[ "$(readlink -f "${DEST}/current")" = "$(readlink -f "${r}")" ] || { rm -rf "${r}"; echo " supprimé ${r}"; }
done
EOF
echo "==> Smoke public (informatif)"
PUB="$(curl -s -m 10 -o /dev/null -w '%{http_code}' "https://${DOMAINE}/build.json" || true)"
if [ "${PUB}" = "200" ]; then
echo " https://${DOMAINE}/build.json → 200 : $(curl -s -m 10 "https://${DOMAINE}/build.json")"
else
echo " https://${DOMAINE} → ${PUB} (DNS ou certificat pas encore en place : item MOA Q-A)"
fi
echo "OK — ${HASH} en place."