Commandes Castor¶
Référence exhaustive des cibles Castor définies dans castor.php à la racine du dépôt. Toutes les commandes qualité et développement du projet passent par Castor — à l'exception de git et glab, qui s'utilisent directement.
Chaque cible ci-dessous s'invoque depuis la racine du projet :
Les cibles sans namespace s'invoquent simplement castor <nom>. Toutes les cibles qui exécutent quelque chose en PHP, Composer ou bin/console le font à l'intérieur du conteneur php via docker compose exec — vous n'avez rien à installer côté hôte.
Stack Docker¶
Toutes les cibles qui pilotent le cycle de vie des conteneurs Docker vivent dans le namespace docker: — pour ne pas confondre castor docker:build (image PHP), castor tailwind:build (CSS) et castor docs:build (site mkdocs).
castor docker:up¶
Démarre la stack Docker en dev (FrankenPHP + Postgres + Redis + RabbitMQ + Typesense + Gotenberg + Mailpit).
| Option | Type | Défaut | Effet |
|---|---|---|---|
--build |
flag | false |
Reconstruit les images avant de démarrer. |
--no-wait |
flag | --wait actif par défaut |
Ne pas attendre que les healthchecks soient verts avant de rendre la main. |
castor docker:down¶
Arrête la stack Docker, sans supprimer les volumes. Vos données (Postgres, Redis, etc.) sont préservées.
castor docker:logs¶
Affiche les logs des conteneurs en continu (docker compose logs -f --tail=200). Sans argument, suit tous les services ; passez un nom de service pour filtrer.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
service |
string | '' |
Nom du service Docker Compose à suivre. Vide = tous les services. |
castor docker:sh¶
Ouvre un shell bash interactif dans le conteneur php (docker compose exec php bash).
castor docker:build¶
Reconstruit les images Docker avec --pull pour récupérer les dernières bases.
Dépendances applicatives¶
castor install¶
Installe les dépendances applicatives dans le conteneur php, en enchaînant trois étapes :
composer install --no-interaction— paquets PHP dansvendor/.bin/console importmap:install— assets JavaScript gérés par Symfony Asset Mapper.bin/console tailwind:build— compilation du CSS Tailwind verspublic/.
À lancer après chaque git pull qui modifie composer.lock, importmap.php ou les sources Tailwind (assets/styles/).
castor reinstall¶
Supprime composer.lock et vendor/, puis résout les dépendances depuis le composer.json courant (équivalent d'un composer update complet) et réinstalle l'importmap. Utile quand le lock est désynchronisé des contraintes, typiquement après une montée de version de PHP ou Symfony.
Opération destructive sur le lock
composer.lock est supprimé puis régénéré : les versions résolues peuvent changer. À utiliser en connaissance de cause.
castor tailwind:build¶
Compile le CSS Tailwind de assets/styles/app.css vers public/. La cible install l'appelle déjà en fin de parcours ; utilisez cette cible directement pour recompiler après avoir modifié vos styles.
| Option | Type | Défaut | Effet |
|---|---|---|---|
--watch |
flag | false |
Recompile en continu dès qu'un fichier source change. |
castor php¶
Lance une commande php dans le conteneur php. Pratique pour inspecter la version ou exécuter un script ponctuel.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
arg |
string | --version |
Arguments passés à php. |
castor console¶
Lance une commande Symfony Console dans le conteneur php.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
arg |
string | list |
Commande Symfony Console à exécuter. |
castor composer¶
Lance une commande Composer dans le conteneur php.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
arg |
string | list |
Commande Composer à exécuter. |
castor cache:clear¶
Vide le cache Symfony de l'environnement courant (bin/console cache:clear). À appeler après un changement de configuration ou de service pour forcer la reconstruction du conteneur de services.
Qualité¶
castor cs:fix¶
Applique PHP-CS-Fixer sur le code PHP.
| Option | Type | Défaut | Effet |
|---|---|---|---|
--dry-run |
flag | false |
Affiche le diff sans modifier les fichiers. |
castor phpstan¶
Lance PHPStan en analyse statique.
| Option | Type | Défaut | Effet |
|---|---|---|---|
--level |
int | 8 |
Niveau de sévérité PHPStan. |
castor lint¶
Vérifie la syntaxe et la cohérence du projet en enchaînant quatre sous-étapes :
bin/console lint:twig templates/— syntaxe des templates Twig.bin/console lint:yaml config/— validité des fichiers YAML de configuration.bin/console lint:container— cohérence du conteneur de services Symfony.castor lint:shell—shellchecksur les scripts du devcontainer.
castor lint:shell¶
Lance shellcheck --severity=warning --shell=bash sur tous les scripts shell de .devcontainer/*.sh (init-firewall, post-create, post-start, mate-mcp, healthchecks, supervisor) et sur les hooks Claude sous .claude/hooks/*.sh. Un bug syntaxique silencieux dans un hook ne remonte qu'au prochain déclenchement (logs des hooks peu visibles) — shellcheck statique le détecte avant exécution.
Si shellcheck est absent du conteneur (image antérieure à l'ajout dans Dockerfile), la cible échoue avec un message actionnable demandant de relancer castor docker:build.
Inclus dans castor lint
Cette cible est la 4ᵉ sous-étape de castor lint. L'invocation directe n'a d'intérêt que pour débugger un problème spécifique aux scripts shell, sans rejouer les linters Twig/YAML/conteneur.
Doctrine¶
castor doctrine:schema:validate¶
Vérifie que le schéma BDD est synchronisé avec les entités Doctrine. Cette cible est l'un des garde-fous de CLAUDE.md — elle doit passer en permanence.
castor doctrine:migrate¶
Applique les migrations Doctrine en attente, en mode --all-or-nothing (une seule transaction, rollback total en cas d'échec).
castor doctrine:diff¶
Génère une migration Doctrine à partir du diff entre les entités et le schéma BDD courant.
castor fixtures:load¶
Charge les fixtures DoctrineFixturesBundle en base de données. Sans --append, la base est purgée puis rechargée depuis zéro (comportement par défaut).
| Option | Type | Défaut | Effet |
|---|---|---|---|
--append |
flag | false |
Ajoute les fixtures sans vider la base au préalable. |
Données supprimées
Sans --append, toutes les données existantes sont effacées. À réserver à l'environnement de développement.
Pas nécessaire avant les tests
tests/bootstrap.php exécute déjà database:create, migrations:migrate puis fixtures:load au démarrage de PHPUnit — castor test:unit, castor test:e2e et castor test:coverage partent toujours d'une base fixturée à neuf. La cible reste utile pour rejouer les fixtures sur la base de dev (repeuplement manuel lors d'une exploration locale). Détail dans Exécuter les tests — Préparation automatique de la BDD.
Tests¶
castor test:unit¶
Lance PHPUnit sur les suites unit et integration.
| Option | Type | Défaut | Effet |
|---|---|---|---|
--filter |
string | '' |
Filtre PHPUnit pour ne lancer qu'un test précis. |
castor test:e2e¶
Lance PHPUnit sur la suite e2e (tests Panther, pages uniquement).
| Option | Type | Défaut | Effet |
|---|---|---|---|
--filter |
string | '' |
Filtre PHPUnit. |
castor test:coverage¶
Lance PHPUnit avec un rapport de couverture. Suites unit + integration par défaut. Le rapport est textuel dans le terminal ; avec --html, un rapport HTML est généré dans var/coverage/.
| Option | Type | Défaut | Effet |
|---|---|---|---|
--with-e2e |
flag | false |
Inclut la suite e2e (Panther) dans le run de couverture. Vérifie d'abord que la stack est up et que le conteneur php est healthy — sinon exit 1 avec un message explicite. |
--html |
flag | false |
Génère un rapport HTML dans var/coverage/ au lieu du rapport textuel. |
Messenger¶
castor messenger:consume¶
Lance le worker Messenger dans le conteneur php avec une limite de temps de 3 600 secondes (bin/console messenger:consume $transport --time-limit=3600). En production, le redémarrage est géré par la restart policy Docker — ne pas relancer manuellement.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
transport |
string | async |
Nom du transport Messenger à consommer. |
Raccourcis¶
castor qa¶
Enchaîne cs:fix → phpstan → lint → doctrine:schema:validate dans l'ordre imposé par CLAUDE.md. À lancer avant toute délégation au reviewer.
castor test¶
Enchaîne test:unit → test:bash → test:e2e.
castor all¶
Checklist applicative complète avant clôture, à lancer depuis l'intérieur du devcontainer : enchaîne cs:fix → phpstan → lint → ts:check → doctrine:schema:validate → test:unit → test:bash → test:e2e. Aligné avec la checklist documentée dans CLAUDE.md (ordre choisi pour que chaque étape travaille sur du code stabilisé par la précédente).
C'est l'équivalent de la checklist dynamique affichée par le hook stop-checklist.sh quand Claude clôture une tâche côté devcontainer. Pour le pendant côté hôte (infrastructure), voir castor host:check.
castor host:check¶
Checklist côté hôte : valide les fichiers d'infrastructure que le devcontainer n'est pas censé modifier (ou ne peut pas — verrous Claude). Refuse l'exécution depuis l'intérieur du devcontainer. Cinq sous-étapes :
lint:shell—shellchecksur.devcontainer/*.shet.claude/hooks/*.sh. Identique à la sous-étape 4 decastor lint, ré-exécutée ici parce que ces scripts sont la cible privilégiée d'édition hôte.docker compose config -q— valide la fusioncompose.yaml+compose.override.yaml(+compose.devcontainer.yamlquand le mode devcontainer est actif). Exit non nul si typo, référence cassée ou type incorrect.jq empty .devcontainer/devcontainer.json— valide que le fichier est du JSON parsable (les commentaires JSON5 sont tolérés parjqen--, ici on attend du JSON pur).hadolintsurDockerfile—docker run --rm -i hadolint/hadolint:latest hadolint --failure-threshold error - < Dockerfile. Seuls les niveauxerror/fatalfont échouer la cible ; leswarningetinfo(notammentDL4006hérité dedunglas/symfony-docker) sont remontés à l'écran sans bloquer. L'image hadolint est pullée au premier appel (~3 s), instantanée ensuite.- HEAD HTTP sur les URLs construites depuis les
ARG *_VERSIONdu Dockerfile — Castor, glab, tarball Node etSHASUMS256.txtNode. Les URLs sont reconstruites parToolsUpgrader::buildDockerfileUrls()(testée unitairement) pour garantir qu'on probe exactement les URLs utilisées par lesRUN curl …du Dockerfile. Codes acceptés :200,301,302(les Releases GitHub/GitLab redirigent en HEAD). Quatre requêtes lancées en parallèle via background jobs bash, timeout 5 s par URL. Modèle de panne adressé : une release upstream supprimée, déplacée ou renommée — le prochaindocker buildtéléchargerait une page d'erreur HTML, lesha256sum -crejetterait silencieusement le fichier corrompu, message cryptique. Le check attrape la rupture avant le rebuild.
Coût observé : ~0.8 s en régime établi (hadolint en cache + 4 HEAD HTTP en parallèle). Premier appel : ~3.5 s (pull de l'image hadolint, ~15 Mo).
C'est l'équivalent de la checklist dynamique affichée par le hook stop-checklist.sh quand Claude clôture une tâche côté hôte. Le partage de responsabilités hôte/devcontainer est détaillé dans Choix d'isolation — checklist de clôture.
Côté devcontainer
Lancée depuis l'intérieur, la cible exit 1 avec un message qui pointe vers castor all (la checklist applicative). La raison : host:check valide des fichiers en deny-list Claude (Dockerfile, compose*.yaml) et des scripts .devcontainer/*.sh typiquement édités depuis l'hôte. Passer la checklist hôte depuis le devcontainer donnerait l'illusion d'avoir validé quelque chose qui n'a pas été testé dans le bon environnement.
Diagnostic¶
castor doctor¶
Vérifie l'environnement de développement et imprime une matrice OK/KO ligne par ligne. À utiliser à l'onboarding (un nouveau développeur veut savoir si son setup est complet) ou pour un diagnostic rapide quand un test E2E échoue ou que la stack semble lente.
La cible enchaîne quatre familles de checks :
- Binaires hôte requis : présence de
docker,jq,glabetgitdans lePATH. - Plugin Docker Compose v2 :
docker compose versiondoit répondre. - État de chaque service de la stack (
php,database,redis,rabbitmq,typesense,gotenberg,mailer,docs) : le service doit être présent, sonStatedoit êtrerunninget sonHealthdoit êtrehealthy(les services sans healthcheck remontent une chaîne vide pourHealthet sont tolérés). - Migrations Doctrine à jour :
bin/console doctrine:migrations:up-to-datedoit passer. Ce check n'est exécuté que si le servicephpestrunning— sinon il serait systématiquement KO sans information utile.
Aucun argument. Le code de sortie est 0 si tous les checks passent, 1 dès qu'au moins un check échoue.
Depuis l'intérieur du Dev Container
Lancée depuis l'intérieur, la cible refuse de tourner (elle pilote l'état des conteneurs côté hôte) et le message d'erreur pointe vers castor devcontainer:doctor — le pendant interne, qui couvre l'amorçage, dnsmasq, l'ipset, FrankenPHP sur 127.0.0.1, la résolution intra-stack et les migrations Doctrine.
Dev Container¶
castor devcontainer:firewall-reload¶
Recharge le pare-feu interne du devcontainer en wrappant sudo /app/.devcontainer/init-firewall.sh. À utiliser après avoir modifié la baseline .devcontainer/baseline-domains.txt ou l'override .devcontainer/whitelist.local.txt (cf. Whitelister un domaine) — ou en diagnostic si une connexion sortante est inexplicablement bloquée.
La cible refuse de tourner depuis l'hôte : sur l'hôte, init-firewall.sh toucherait les iptables du système (sudoers différents, droits root réels) et n'a pas de sens. Hors devcontainer, la cible exit 1 avec un message explicite.
À l'intérieur, le sudo ne demande pas de mot de passe : une entrée /etc/sudoers.d/init-firewall autorise app à lancer uniquement ce script en privilégié (cf. Convention sudo).
Le script affiche en fin d'exécution une série de smoke-tests :
example.comdoit être bloqué (KO bloquant attendu) — preuve que la policyOUTPUT DROPest bien posée ;gitlab.com/,symfony.com/,packagist.org/packages.jsonetregistry.npmjs.org/-/pingsont testés en non bloquant — un avertissement est émis pour chaque domaine injoignable (drift de whitelist ou panne externe) sans empêcher le pare-feu d'être actif ;- la résolution intra-stack (
database,redis,rabbitmq,typesense,gotenberg,mailer) est testée en bloquant — un échec signale un DNAT cassé ou un service Compose à l'arrêt.
Appel direct possible en debug
En cas de pépin sur Castor (PHP cassé, fixtures perdues), l'appel direct sudo /app/.devcontainer/init-firewall.sh reste utilisable depuis un terminal du devcontainer — la cible Castor n'est qu'un wrapper.
castor devcontainer:whitelist-add¶
Ajoute un domaine à la whitelist locale (.devcontainer/whitelist.local.txt, gitignored), puis recharge le pare-feu (sudo /app/.devcontainer/init-firewall.sh). Restreinte à l'intérieur du devcontainer.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
domain |
string | — (obligatoire) | Domaine à autoriser, sans schéma ni chemin (ex. raw.githubusercontent.com). |
Étapes effectuées par la cible :
- Validation du format — confronté à la regex partagée
FirewallWhitelist::DOMAIN_REGEX(FQDN, pas d'URL, pas de wildcard*.foo, pas d'IP littérale, pas d'espace). Refus avec message actionnable si invalide. - Dédoublonnage baseline — si le domaine est déjà dans
.devcontainer/baseline-domains.txt(ou couvert comme sous-domaine d'une entrée baseline, puisque dnsmasq matche/domain/), la cible n'écrit rien et renvoie une notenote. - Dédoublonnage local — si le domaine est déjà présent dans
whitelist.local.txt, idem. - Append — ajoute le domaine en fin de fichier (préserve les commentaires et lignes existantes).
- Reload — exécute
sudo /app/.devcontainer/init-firewall.sh.
castor devcontainer:whitelist-remove¶
Retire un domaine de la whitelist locale (.devcontainer/whitelist.local.txt) puis recharge le pare-feu. Ne touche pas à la baseline — pour retirer un domaine de la baseline versionnée, éditer .devcontainer/baseline-domains.txt à la main et commiter.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
domain |
string | — (obligatoire) | Domaine à retirer de whitelist.local.txt. |
Le fichier est réécrit en conservant à l'identique commentaires et lignes vides — seul le domaine demandé est supprimé. Si la ligne n'existe pas, la cible affiche une note et n'exécute pas le reload.
castor devcontainer:whitelist-list¶
Liste l'état effectif de la whitelist du pare-feu : domaines baseline (.devcontainer/baseline-domains.txt) + domaines locaux (.devcontainer/whitelist.local.txt) dans un tableau Origine | Domaine, et — si la cible tourne depuis l'intérieur du devcontainer — le nombre d'IPs actuellement résolues dans l'ipset allowed-domains.
Marche aussi depuis l'hôte : la lecture des deux fichiers ne dépend ni du pare-feu ni de la capability NET_ADMIN. Seule la colonne « ipset alimenté » est skippée — l'ipset allowed-domains n'existe que dans le devcontainer (créé par init-firewall.sh).
Sortie typique depuis l'intérieur :
- un tableau
Origine | Domaine(origine =baselineoulocal), - un total
X baseline + Y locaux = Z domaine(s) whitelisté(s), - une ligne
ipset allowed-domains : N IP(s) actuellement résolues(N = 0 signale que dnsmasq n'alimente pas l'ipset — typiquement après un kill/destroy manuel ; voir Debugger le pare-feu), - si
whitelist.local.txtcontient des lignes invalides (URL avec schéma, wildcard, IP littérale, …), un blocAVERTISSEMENTles liste explicitement plutôt que de les laisser noyées dans les logs.
Pourquoi un total d'IPs et pas un mapping domaine → IPs
L'ipset enregistre toutes les IPs résolues sans distinction du domaine source. Attribuer une IP à un domaine demanderait de corréler avec les logs dnsmasq — pas nécessaire pour le diagnostic courant. Si N = 0, c'est que dnsmasq ne résout pas ou plus ; si N > 0, c'est une question de regex ou de connectivité sur le domaine précis.
castor devcontainer:doctor¶
Diagnostic interne du devcontainer — pendant symétrique de castor doctor qui, lui, s'applique côté hôte. Restreinte à l'intérieur du devcontainer.
Checks effectués, dans l'ordre :
- Amorçage initial (post-create.sh) — lit
.devcontainer/.bootstrap.okou.bootstrap.failed. KO si le marqueur.failedexiste OU si aucun des deux n'existe (pas d'amorçage enregistré). Premier check car presque tous les autres dépendent d'unvendor/cohérent. Pour rejouer l'amorçage et basculer.failed→.ok, utilisercastor devcontainer:bootstrap. - Process
dnsmasqactif —pgrep -x dnsmasq. - Pare-feu sortant intact (
OUTPUT DROP+ ipset alimenté) — délégué àsudo -n /app/.devcontainer/firewall-healthcheck.sh, le wrapper read-only déjà utilisé par le healthcheck Docker du servicephp. Le wrapper enchaîne trois vérifications bloquantes : policyOUTPUT DROPtoujours posée (détecte uniptables -Fmanuel oublié), ipsetallowed-domainsexistant, ipset alimenté (Number of entries:≥ 1). Le passage par le wrapper plutôt que par unsudo ipset listdirect est ce qui rend ce check fonctionnel : seulsinit-firewall.shetfirewall-healthcheck.shsont autorisés en NOPASSWD par le sudoers (cf. Conventionsudo). Verdict aligné sur le healthcheck Docker — pas de divergence possible entredevcontainer:doctorvert et conteneurunhealthy. - Sudoers
init-firewallprésent (chmod 0440) — vérifie l'existence de/etc/sudoers.d/init-firewallET son mode0440(sudo refuse silencieusement les fichiers sudoers non chmod 0440 — un mauvais mode mène à un échec de reload sans message explicite). - Binaires
castor,glab,node,npm,claudedisponibles — résolus dans lePATHviaSymfony\Component\Process\ExecutableFinder. Détecte un upgrade-tools cassé, un rebuild incomplet ou unPATHcorrompu. whitelist.local.txtparsable (si présent) — chaque ligne non commentée est confrontée àFirewallWhitelist::isValidDomain(). Les lignes rejetées sont remontées explicitement plutôt que de rester noyées dans/tmp/post-start.log.- FrankenPHP répond sur
http://127.0.0.1—curl -fsIavec timeout 5 s. - Résolution intra-stack —
getent hostspourdatabase,redis,rabbitmq,typesense,gotenberg,mailer. - Migrations Doctrine à jour —
bin/console doctrine:migrations:up-to-date, exécuté uniquement sidatabaserépond (sinon faux négatif).
Sortie OK/KO aligné colonne par colonne. Exit 1 au moindre KO.
castor devcontainer:bootstrap¶
Rejoue l'amorçage post-create (castor install — composer install + importmap:install + tailwind:build) et bascule le marqueur d'état :
- en cas de succès : supprime
.devcontainer/.bootstrap.faileds'il existe et écrit.devcontainer/.bootstrap.ok(JSON{status, at}) — la première ligne decastor devcontainer:doctorrepasse au vert. - en cas d'échec : écrit
.devcontainer/.bootstrap.failed(JSON{status, exit_code, at}) et exit1.
À utiliser quand castor install a raté au premier démarrage du devcontainer (réseau, contrainte composer, npm offline) — post-create.sh n'avorte volontairement pas dans ce cas, le devcontainer s'ouvre, et la friction est rattrapée par cette cible. Restreinte à l'intérieur du devcontainer ; hors devcontainer, lancer directement castor install.
castor devcontainer:check-tools¶
Alias lisible de castor devcontainer:upgrade-tools --check — vérifie si une nouvelle version d'un outil figé dans le Dockerfile (Castor, glab, Claude Code, Node) ou un tag plus récent de l'image base dunglas/frankenphp:1-php8.5 est disponible, sans rien modifier. Exit 2 si drift (ARG outil ou image base), exit 0 sinon. Découvrable directement par castor list (l'option --check de upgrade-tools ne l'est pas), ce qui en fait une cible utilisable telle quelle en CI pour notifier qu'un bump est dispo.
Comme upgrade-tools, restreinte à l'hôte (cf. Cibles qui refusent l'intérieur) — api.github.com, github.com et hub.docker.com ne sont pas whitelistés par le pare-feu interne.
Équivalence stricte
castor devcontainer:check-tools appelle simplement devcontainer_upgrade_tools(apply: false, check: true). Aucune différence de comportement avec castor devcontainer:upgrade-tools --check — c'est uniquement une question de découvrabilité.
Drift image base FrankenPHP — bump manuel
Le tag de l'image base est porté par une instruction FROM dunglas/frankenphp:<tag>, pas par un ARG. upgrade-tools ne sait manipuler que les ARG ; un tag plus récent est donc signalé en warning (non bloquant en --apply, compté dans l'exit 2 de --check) et le développeur édite la ligne FROM à la main après lecture du changelog upstream. Surveiller cette image couvre aussi indirectement les paquets apt et install-php-extensions livrés via la base — un bump de la base entraîne implicitement leur mise à jour. La sélection du tag candidat est isolée dans ToolsUpgrader::selectFrankenphpCandidate() (testable unitairement) : tuples (franken-major, php-major, php-minor) strictement supérieurs au courant, à l'exclusion des « régressions PHP » (un bump franken-major qui dégraderait la majeure PHP).
castor devcontainer:frankenphp-restart¶
Relance le supervisor FrankenPHP après un bail-out (le supervisor abandonne après MAX_CRASHES redémarrages consécutifs sans stabilisation, cf. .devcontainer/frankenphp-supervisor.sh) ou un kill manuel. Restreinte à l'intérieur du devcontainer — hors devcontainer, FrankenPHP est piloté par l'entrypoint Docker (castor docker:up / docker:down suffisent).
Étapes effectuées par la cible :
- Idempotence — tue d'abord le supervisor (
pkill -f frankenphp-supervisor.sh) puis FrankenPHP lui-même (pkill -f 'frankenphp run'). Tuer uniquement le supervisor ne suffit pas : son enfant FrankenPHP continue de tourner et le nouveau supervisor planterait sur « address already in use » en tentant de re-bind:80. Les deuxpkillsont autorisés à échouer (aucun process à tuer = état nominal post-bail-out). - Relance en arrière-plan —
bash -c 'nohup /app/.devcontainer/frankenphp-supervisor.sh >var/log/devcontainer/frankenphp.log 2>&1 &'. Lebash -cest nécessaire pour que&soit interprété comme un fork, pas comme un argument littéral denohup. Le fichier est lisible directement depuis l'hôte avectail -f var/log/devcontainer/frankenphp.log—/appest bind-mounté vers la racine du repo, doncvar/log/devcontainer/frankenphp.logcôté conteneur =var/log/devcontainer/frankenphp.logcôté hôte (même fichier via bind-mount).
Aucun argument. Pas de mode --check : la cible est purement curative.
Pour vérifier la relance, consulter var/log/devcontainer/frankenphp.log (tail -f var/log/devcontainer/frankenphp.log, lisible aussi bien depuis l'hôte que depuis l'intérieur du conteneur) ou rejouer castor devcontainer:doctor qui teste la réponse HTTP de FrankenPHP sur 127.0.0.1. Le supervisor signale un bail-out par la ligne ${MAX_CRASHES} crashs consécutifs sans stabilisation (>${STABLE_AFTER}s) — abandon. dans les logs.
Quand l'utiliser
Symptôme typique : castor devcontainer:doctor rapporte « FrankenPHP répond sur http://127.0.0.1 ✗ KO » alors que le pare-feu et les services Compose sont up. Inspecter var/log/devcontainer/frankenphp.log pour confirmer un bail-out, corriger la cause (config Caddyfile, code PHP qui boucle au boot) puis relancer cette cible.
castor devcontainer:upgrade-tools¶
Vérifie les dernières versions disponibles des outils CLI figés dans le Dockerfile (Castor, glab, Claude Code, Node) et met à jour les ARG correspondants — version et SHA-256 associé pour les outils distribués en binaire vérifié (CASTOR_SHA256, GLAB_SHA256, NODE_SHA256). Affiche un tableau diff ARG | Actuel | Dernier | Δ (↑ si une mise à jour est disponible, = si à jour) puis applique en place via une regex sur Dockerfile — commentaires et structure sont préservés. Met aussi à jour les versions affichées dans docs/reference/devcontainer.md.
Surveille en complément un drift sur l'image de base FrankenPHP du Dockerfile (instruction FROM dunglas/frankenphp:<tag>) — signalé en warning, jamais appliqué automatiquement (le bump cible une instruction FROM, hors mécanisme ARG géré par ToolsUpgrader). Le développeur lit le changelog upstream puis édite la ligne à la main. Couvre indirectement les paquets apt et install-php-extensions livrés via la base.
Sources interrogées :
| Outil | API |
|---|---|
| Castor | https://api.github.com/repos/jolicode/castor/releases/latest (version) + https://github.com/jolicode/castor/releases/download/${tag}/castor.linux-amd64.phar (binaire pour calculer le SHA-256). |
| glab | https://gitlab.com/api/v4/projects/gitlab-org%2Fcli/releases?per_page=1 (version) + tarball GitLab (SHA-256). |
| Claude Code | https://registry.npmjs.org/@anthropic-ai/claude-code/latest (SHA non managé — npm valide nativement dist.integrity SRI). |
| Node.js | Détection LTS via https://cdn.jsdelivr.net/gh/nodejs/Release@main/schedule.json (majeure la plus haute avec lts ≤ aujourd'hui ≤ end), résolution de la dernière patch via https://nodejs.org/dist/index.json, SHA-256 lu dans https://nodejs.org/dist/v${version}/SHASUMS256.txt. |
| Image base FrankenPHP | https://hub.docker.com/v2/repositories/dunglas/frankenphp/tags/?page_size=100&name=php8. (Docker Hub API publique). Filtré côté serveur sur le pattern php8. pour éviter les 13k+ tags. Sélection du candidat déléguée à ToolsUpgrader::selectFrankenphpCandidate() (logique pure, testable). |
La cible est restreinte à l'hôte : api.github.com (Castor) et github.com (téléchargement du binaire pour calculer le SHA-256) ne sont pas whitelistés par le pare-feu du devcontainer. Lancée depuis l'intérieur, la cible exit avec un message explicite (assert_outside_container('devcontainer:upgrade-tools')). Lancée depuis l'hôte, elle utilise le réseau de la machine — sans contrainte de whitelist.
La liste exacte des ARG gérés est définie par ToolsUpgrader::TOOLS dans src/DevContainer/ToolsUpgrader.php (versions : CASTOR_VERSION, GLAB_VERSION, CLAUDE_CODE_VERSION, NODE_VERSION) et ToolsUpgrader::SHA_TOOLS (SHA associés : CASTOR_SHA256, GLAB_SHA256, NODE_SHA256). Le SHA est recalculé uniquement si la version change — un même tag republie au même hash chez ces fournisseurs.
| Option | Type | Défaut | Effet |
|---|---|---|---|
--check |
flag | false |
Mode CI : exit 2 si au moins une mise à jour est disponible (ARG outil ou drift image base FrankenPHP), exit 0 sinon. N'écrit rien dans le Dockerfile et ne demande pas de confirmation. |
--apply |
flag | false |
Skippe la confirmation interactive (io()->confirm) et applique directement les mises à jour. |
Sans option, la cible est interactive : elle affiche le diff et demande une confirmation avant d'écrire le Dockerfile.
# Diff + confirmation interactive
castor devcontainer:upgrade-tools
# CI : exit 2 si drift détecté (utilisable pour générer une notification)
castor devcontainer:upgrade-tools --check
# Application non interactive (script, automatisation)
castor devcontainer:upgrade-tools --apply
Pattern d'usage typique¶
castor devcontainer:upgrade-tools --check # détecter le drift (exit 2 si maj dispo)
castor devcontainer:upgrade-tools --apply # appliquer les bumps dans le Dockerfile
castor docker:build # rebuilder l'image avec les nouvelles versions
git commit Dockerfile # versionner les nouveaux ARG
Versions figées par design
Les versions de Castor, glab, Claude Code et Node (y compris la patch) sont volontairement figées dans le Dockerfile pour garantir un environnement reproductible. La cible devcontainer:upgrade-tools est l'outil officiel pour faire évoluer ces versions sans aller chercher manuellement le numéro courant sur GitHub, GitLab, npm ou le schedule Node — et sans risquer un mismatch version/SHA-256. Voir Référence du Dev Container pour la liste des ARG concernés.
Documentation¶
castor docs:build¶
Construit le site de documentation statique dans ./site, avec --strict (toute erreur mkdocs devient fatale). Utilise l'image custom définie dans mkdocs/Dockerfile — qui ajoute deux plugins par-dessus squidfunk/mkdocs-material :
git-revision-date-localized— affiche la date de dernière modification de chaque page (lue dans l'historique git).social— génère automatiquement les images Open Graph de partage pour chaque page (mkdocs-material[imaging]+ Pillow + CairoSVG).
La cible enchaîne deux étapes : docker compose build docs (idempotent — ne reconstruit que si mkdocs/Dockerfile change) puis docker compose run --rm docs build --strict.
Depuis l'intérieur du Dev Container
Lancée depuis l'intérieur, la cible refuse de tourner (elle pilote Docker côté hôte) et le message d'erreur suggère curl -sIf http://localhost:8000/ pour valider rapidement que le service mkdocs en live-reload répond, sans rebuild. Le strict mode (validation des liens et des snippets pymdownx.snippets) reste réservé à l'hôte.
Intégration continue¶
castor ci:local¶
Valide — et, au besoin, exécute — la pipeline GitLab CI en local, via gitlab-ci-local. À lancer après tout changement dans .gitlab-ci.yml ou .gitlab/ci/, avant de pousser, pour attraper une erreur de configuration (include cassé, !reference invalide, needs orphelin, rules incohérentes) sans consommer un cycle de pipeline distante.
| Argument | Type | Défaut | Effet |
|---|---|---|---|
job |
string | '' |
Nom d'un job GitLab CI à exécuter. Vide = --list-all : valide la config et liste tous les jobs sans rien exécuter (rapide, sans Docker). |
# Validation + liste de tous les jobs (rapide, sans exécution ni Docker)
castor ci:local
# Exécution d'un job précis (nécessite le socket Docker → hôte uniquement)
castor ci:local quality:phpstan
Sans argument, la cible lance gitlab-ci-local --list-all. Le flag --list-all (et non --list) est délibéré : --list ne montre que les jobs actifs dans le contexte git courant ; sur une branche hors MR, main ou tag, toutes les rules sont fausses et la table ressort vide. --list-all liste tous les jobs, indépendamment des rules, ce qui valide la structure complète de la pipeline.
Ce que la validation vérifie réellement : le parsing de la config, la fusion des include / !reference / needs et la validation du schéma JSON de .gitlab-ci.yml. C'est là tout son intérêt — elle attrape les includes cassés, les !reference invalides et les erreurs de structure sans exécuter le moindre job.
Avec un nom de job, la cible exécute réellement ce job : gitlab-ci-local lance alors des conteneurs Docker « frères » (Docker-out-of-Docker). Le socket Docker est monté (/var/run/docker.sock) et le projet est monté au même chemin que sur l'hôte ($cwd:$cwd) pour que les bind-mounts des jobs frères pointent vers des chemins hôte valides. Cette variante nécessite donc un terminal hôte (cf. avertissement ci-dessous).
Dans les deux cas, la cible utilise un conteneur éphémère node:22 (Debian) — pas la variante node:22-alpine : Alpine n'embarque ni git ni bash, que gitlab-ci-local appelle en interne (sinon erreur spawn git ENOENT / spawn bash ENOENT).
Prérequis : les fichiers d'include doivent être suivis par git
gitlab-ci-local ne lit que les fichiers suivis par git. Un nouveau fichier d'include (ex. un nouveau .gitlab/ci/*.yml) ou une nouvelle version de la config doit être git addé (au minimum dans l'index, pas forcément commité) avant de lancer castor ci:local — sinon l'outil échoue sur Local include file cannot be found. Réflexe à avoir dès qu'on ajoute un include et qu'on veut le valider localement.
Cible HÔTE uniquement pour exécuter un job
castor ci:local <job> refuse de tourner dans le devcontainer : le socket Docker n'y est pas monté, gitlab-ci-local ne pourrait pas lancer les conteneurs de jobs. Lancée depuis l'intérieur avec un nom de job, elle exit avec un message explicite. La validation seule (castor ci:local sans argument) fonctionne en revanche depuis le devcontainer comme depuis l'hôte — elle n'a pas besoin de Docker.
Dossier .gitlab-ci-local/ généré
gitlab-ci-local crée un répertoire .gitlab-ci-local/ à la racine du dépôt (état, artifacts et logs locaux des runs). Il est gitignoré — aucune action requise.
Garde-fou de config, pas un substitut à la pipeline réelle
gitlab-ci-local ne reproduit pas parfaitement l'environnement de GitLab.com. Ne fonctionnent pas comme en réel : les services:, les variables secrètes (GITLAB_TOKEN…), certaines variables prédéfinies, et les jobs pages / release: / registry / schedule. C'est un outil de validation de la configuration (structure, includes, !reference, needs, rules) — la vérité reste la pipeline distante, qui doit passer avant merge (cf. Pipeline GitLab CI).
Ordre recommandé avant une clôture de tâche¶
Deux séquences distinctes selon où tu codes — le hook stop-checklist.sh détecte automatiquement le contexte (KIREXO_INSIDE_CONTAINER=1 ou /.dockerenv) et affiche la checklist correspondante. Le rationale du partage est dans Choix d'isolation — checklist de clôture.
Côté devcontainer — checklist applicative¶
Aligné avec CLAUDE.md :
castor cs:fix # style : peut modifier les fichiers → en premier
castor phpstan # analyse statique : sur du code stabilisé par cs:fix
castor lint # Twig, YAML, conteneur Symfony, shellcheck
castor ts:check # typage TypeScript
castor doctrine:schema:validate # schéma BDD : dépend du conteneur
castor test:unit # tests unitaires + intégration : rapides
castor test:bash # fonctions pures de init-firewall.lib.sh via bats
castor test:e2e # tests E2E Panther : les plus lents, en dernier
Raccourci équivalent : castor all.
Chaque étape élimine une classe de problèmes pour que la suivante travaille sur du code propre. Inverser cs:fix et phpstan invaliderait le résultat de PHPStan dès que le fixer modifie un fichier.
Côté hôte — checklist d'infrastructure¶
castor lint:shell # shellcheck sur .devcontainer/ et .claude/hooks/
docker compose config -q # validation des compose*.yaml empilés
jq empty .devcontainer/devcontainer.json # validation JSON
docker run --rm -i hadolint/hadolint:latest hadolint --failure-threshold error - < Dockerfile
# + HEAD HTTP sur les URLs construites depuis les ARG *_VERSION du Dockerfile
Raccourci équivalent : castor host:check.
La checklist hôte est volontairement plus courte : seuls les fichiers d'infrastructure (Dockerfile, compose*.yaml, scripts .devcontainer/, devcontainer.json) sont validés. Pas de check de vitalité applicative — l'app ne tourne pas forcément sur l'hôte (stack peut être down, c'est attendu).