Aller au contenu

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 :

castor <namespace>:<nom> [options]

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:up --build

castor docker:down

Arrête la stack Docker, sans supprimer les volumes. Vos données (Postgres, Redis, etc.) sont préservées.

castor docker:down

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:logs
castor docker:logs php
castor docker:logs database

castor docker:sh

Ouvre un shell bash interactif dans le conteneur php (docker compose exec php bash).

castor docker:sh

castor docker:build

Reconstruit les images Docker avec --pull pour récupérer les dernières bases.

castor docker:build

Dépendances applicatives

castor install

Installe les dépendances applicatives dans le conteneur php, en enchaînant trois étapes :

  1. composer install --no-interaction — paquets PHP dans vendor/.
  2. bin/console importmap:install — assets JavaScript gérés par Symfony Asset Mapper.
  3. bin/console tailwind:build — compilation du CSS Tailwind vers public/.

À lancer après chaque git pull qui modifie composer.lock, importmap.php ou les sources Tailwind (assets/styles/).

castor install

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.

castor reinstall

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 tailwind:build
castor tailwind:build --watch

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 php "--version"
castor php "-r 'echo PHP_INT_MAX;'"

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 console "debug:router"
castor console "cache:clear"

castor composer

Lance une commande Composer dans le conteneur php.

Argument Type Défaut Effet
arg string list Commande Composer à exécuter.
castor composer "require symfony/mailer"
castor composer "update symfony/*"

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.

castor cache:clear

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 cs:fix
castor cs:fix --dry-run

castor phpstan

Lance PHPStan en analyse statique.

Option Type Défaut Effet
--level int 8 Niveau de sévérité PHPStan.
castor phpstan
castor phpstan --level=9

castor lint

Vérifie la syntaxe et la cohérence du projet en enchaînant quatre sous-étapes :

  1. bin/console lint:twig templates/ — syntaxe des templates Twig.
  2. bin/console lint:yaml config/ — validité des fichiers YAML de configuration.
  3. bin/console lint:container — cohérence du conteneur de services Symfony.
  4. castor lint:shellshellcheck sur les scripts du devcontainer.
castor lint

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.

castor lint:shell

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:schema:validate

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:migrate

castor doctrine:diff

Génère une migration Doctrine à partir du diff entre les entités et le schéma BDD courant.

castor doctrine:diff

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.
castor fixtures:load
castor fixtures:load --append

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:unit
castor test:unit --filter=ArticlePublishedHandlerTest

castor test:e2e

Lance PHPUnit sur la suite e2e (tests Panther, pages uniquement).

Option Type Défaut Effet
--filter string '' Filtre PHPUnit.
castor test:e2e

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.
castor test:coverage
castor test:coverage --with-e2e
castor test:coverage --html

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.
castor messenger:consume
castor messenger:consume async

Raccourcis

castor qa

Enchaîne cs:fixphpstanlintdoctrine:schema:validate dans l'ordre imposé par CLAUDE.md. À lancer avant toute délégation au reviewer.

castor qa

castor test

Enchaîne test:unittest:bashtest:e2e.

castor test

castor all

Checklist applicative complète avant clôture, à lancer depuis l'intérieur du devcontainer : enchaîne cs:fixphpstanlintts:checkdoctrine:schema:validatetest:unittest:bashtest: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).

castor all

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 :

  1. lint:shellshellcheck sur .devcontainer/*.sh et .claude/hooks/*.sh. Identique à la sous-étape 4 de castor lint, ré-exécutée ici parce que ces scripts sont la cible privilégiée d'édition hôte.
  2. docker compose config -q — valide la fusion compose.yaml + compose.override.yaml (+ compose.devcontainer.yaml quand le mode devcontainer est actif). Exit non nul si typo, référence cassée ou type incorrect.
  3. jq empty .devcontainer/devcontainer.json — valide que le fichier est du JSON parsable (les commentaires JSON5 sont tolérés par jq en --, ici on attend du JSON pur).
  4. hadolint sur Dockerfiledocker run --rm -i hadolint/hadolint:latest hadolint --failure-threshold error - < Dockerfile. Seuls les niveaux error / fatal font échouer la cible ; les warning et info (notamment DL4006 hérité de dunglas/symfony-docker) sont remontés à l'écran sans bloquer. L'image hadolint est pullée au premier appel (~3 s), instantanée ensuite.
  5. HEAD HTTP sur les URLs construites depuis les ARG *_VERSION du Dockerfile — Castor, glab, tarball Node et SHASUMS256.txt Node. Les URLs sont reconstruites par ToolsUpgrader::buildDockerfileUrls() (testée unitairement) pour garantir qu'on probe exactement les URLs utilisées par les RUN 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 prochain docker build téléchargerait une page d'erreur HTML, le sha256sum -c rejetterait silencieusement le fichier corrompu, message cryptique. Le check attrape la rupture avant le rebuild.
castor host:check

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 :

  1. Binaires hôte requis : présence de docker, jq, glab et git dans le PATH.
  2. Plugin Docker Compose v2 : docker compose version doit répondre.
  3. État de chaque service de la stack (php, database, redis, rabbitmq, typesense, gotenberg, mailer, docs) : le service doit être présent, son State doit être running et son Health doit être healthy (les services sans healthcheck remontent une chaîne vide pour Health et sont tolérés).
  4. Migrations Doctrine à jour : bin/console doctrine:migrations:up-to-date doit passer. Ce check n'est exécuté que si le service php est running — 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.

castor doctor

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).

castor devcontainer:firewall-reload

Le script affiche en fin d'exécution une série de smoke-tests :

  • example.com doit être bloqué (KO bloquant attendu) — preuve que la policy OUTPUT DROP est bien posée ;
  • gitlab.com/, symfony.com/, packagist.org/packages.json et registry.npmjs.org/-/ping sont 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 :

  1. 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.
  2. 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 note note.
  3. Dédoublonnage local — si le domaine est déjà présent dans whitelist.local.txt, idem.
  4. Append — ajoute le domaine en fin de fichier (préserve les commentaires et lignes existantes).
  5. Reload — exécute sudo /app/.devcontainer/init-firewall.sh.
castor devcontainer:whitelist-add raw.githubusercontent.com

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-remove raw.githubusercontent.com

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).

castor devcontainer:whitelist-list

Sortie typique depuis l'intérieur :

  • un tableau Origine | Domaine (origine = baseline ou local),
  • 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.txt contient des lignes invalides (URL avec schéma, wildcard, IP littérale, …), un bloc AVERTISSEMENT les 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 :

  1. Amorçage initial (post-create.sh) — lit .devcontainer/.bootstrap.ok ou .bootstrap.failed. KO si le marqueur .failed existe OU si aucun des deux n'existe (pas d'amorçage enregistré). Premier check car presque tous les autres dépendent d'un vendor/ cohérent. Pour rejouer l'amorçage et basculer .failed.ok, utiliser castor devcontainer:bootstrap.
  2. Process dnsmasq actifpgrep -x dnsmasq.
  3. 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 service php. Le wrapper enchaîne trois vérifications bloquantes : policy OUTPUT DROP toujours posée (détecte un iptables -F manuel oublié), ipset allowed-domains existant, ipset alimenté (Number of entries: ≥ 1). Le passage par le wrapper plutôt que par un sudo ipset list direct est ce qui rend ce check fonctionnel : seuls init-firewall.sh et firewall-healthcheck.sh sont autorisés en NOPASSWD par le sudoers (cf. Convention sudo). Verdict aligné sur le healthcheck Docker — pas de divergence possible entre devcontainer:doctor vert et conteneur unhealthy.
  4. Sudoers init-firewall présent (chmod 0440) — vérifie l'existence de /etc/sudoers.d/init-firewall ET son mode 0440 (sudo refuse silencieusement les fichiers sudoers non chmod 0440 — un mauvais mode mène à un échec de reload sans message explicite).
  5. Binaires castor, glab, node, npm, claude disponibles — résolus dans le PATH via Symfony\Component\Process\ExecutableFinder. Détecte un upgrade-tools cassé, un rebuild incomplet ou un PATH corrompu.
  6. whitelist.local.txt parsable (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.
  7. FrankenPHP répond sur http://127.0.0.1curl -fsI avec timeout 5 s.
  8. Résolution intra-stackgetent hosts pour database, redis, rabbitmq, typesense, gotenberg, mailer.
  9. Migrations Doctrine à jourbin/console doctrine:migrations:up-to-date, exécuté uniquement si database répond (sinon faux négatif).

Sortie OK/KO aligné colonne par colonne. Exit 1 au moindre KO.

castor devcontainer:doctor

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.failed s'il existe et écrit .devcontainer/.bootstrap.ok (JSON {status, at}) — la première ligne de castor devcontainer:doctor repasse au vert.
  • en cas d'échec : écrit .devcontainer/.bootstrap.failed (JSON {status, exit_code, at}) et exit 1.

À 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:bootstrap

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.

castor devcontainer:check-tools

É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 :

  1. 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 deux pkill sont autorisés à échouer (aucun process à tuer = état nominal post-bail-out).
  2. Relance en arrière-planbash -c 'nohup /app/.devcontainer/frankenphp-supervisor.sh >var/log/devcontainer/frankenphp.log 2>&1 &'. Le bash -c est nécessaire pour que & soit interprété comme un fork, pas comme un argument littéral de nohup. Le fichier est lisible directement depuis l'hôte avec tail -f var/log/devcontainer/frankenphp.log/app est bind-mounté vers la racine du repo, donc var/log/devcontainer/frankenphp.log côté conteneur = var/log/devcontainer/frankenphp.log côté hôte (même fichier via bind-mount).

Aucun argument. Pas de mode --check : la cible est purement curative.

castor devcontainer:frankenphp-restart

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.

castor docs:build

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 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).