Commit Graph
213 Commits
Author SHA1 Message Date
arcodange f250817641 fix(pi2) — Gitea et Postgres avaient zéro plafond, plus maintenant
pi2 tournait à load average ~45 (4 cœurs) pendant qu'un push docker
timeoutait vers le registre. Gitea et Postgres tournent en docker compose
nu, hors k3s — invisibles du scheduler ET sans limite (`docker inspect`
mesurait NanoCPUs=0, Memory=0 pour les deux), donc rien ne les empêchait
de se battre à armes égales avec tout le reste du nœud.

Postgres → 1 CPU / 1024M, Gitea → 1.5 CPU / 1536M (Compose v2 honore
`deploy.resources.limits` hors swarm). Valeurs dérivées d'une mesure au
repos (Postgres 3-5 %, Gitea 12 % CPU) avec de la marge pour les pics —
un filet, pas un dimensionnement pour la charge normale.

Appliqué et vérifié en direct sur pi2 : les deux conteneurs ont recréé
avec les nouvelles limites (`docker inspect` confirme), PostGIS survit
au recreate de Postgres (déjà géré par ce playbook), Gitea sert web (200)
et registre (401 attendu, anonyme) normalement après coup.

Le levier complémentaire (kubelet --system-reserved/--kube-reserved sur
pi2, pour que le SCHEDULER k8s sache que cette place est déjà prise) n'est
pas dans cette PR — plus gros, touche system_k3s.yml pour tout le cluster.
2026-08-11 16:25:20 +02:00
arcodange 1520ecac41 Merge pull request 'feat(postgres) — PostGIS, posé par le playbook et non par une image custom' (#52) from arcodange/postgis-pour-kadans into main 2026-08-08 11:59:30 +02:00
arcodangeandClaude Opus 5 f944fe4bbd feat(postgres) — PostGIS, posé par le playbook et non par une image custom
Kadans doit ranger le contour d'un quartier en vraie géométrie
(geometry(MultiPolygon,4326), ST_Contains, index GiST). L'extension
n'existait nulle part : mesuré sur pi2, `pg_available_extensions` ne
rendait AUCUNE ligne `postgis%`.

Arbitrage fondateur (2026-08-08) : on garde `postgres:16.3-alpine` et on
pose l'extension par Ansible, comme le playbook pose déjà les bases et le
rôle pgbouncer. Pas d'image custom.

⚠ POURQUOI LE RECALAGE DE CHEMINS N'EST PAS FACULTATIF — mesuré, arm64.
`apk add postgis` SEUL réussit, et `CREATE EXTENSION postgis` échoue quand
même :

    ERROR: extension "postgis" is not available
    DETAIL: Could not open extension control file
            "/usr/local/share/postgresql/extension/postgis.control"

Le paquet Alpine vise la disposition d'Alpine (/usr/share/postgresql16,
/usr/lib/postgresql16) ; l'image officielle compile le serveur dans
/usr/local. Les fichiers sont là, le serveur regarde ailleurs. Après
recalage : PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, et un polygone lyonnais qui
fait l'aller-retour ST_GeomFromText → ST_AsGeoJSON.

Ne pas « simplifier » en un `apk add` nu : la simulation dit OK,
l'installation dit OK, et l'extension reste inutilisable.

⚠ INSTALLATION PAR CONTENEUR, PAS PAR VOLUME. `apk add` écrit dans la
couche inscriptible : recréer le conteneur efface PostGIS pendant que les
données gardent leurs colonnes géométriques — toute requête spatiale casse
jusqu'au prochain passage du playbook. D'où l'ordre (déploiement compose
PUIS installation), l'idempotence, et surtout la tâche de vérification.

La vérification ne se contente pas d'un code de retour : elle exige que la
base rende USE_GEOS=1 ET un vrai Point GeoJSON avec son SRID. Un bouchon
qui répondrait une chaîne vide passerait un simple `rc == 0` et ne
prouverait rien — un playbook vert sur une extension absente ferait
atterrir le symptôme dans Kadans, des jours plus tard, déguisé en bug
applicatif.

Vérifié sur le conteneur RÉEL sans le modifier : `apk add --simulate`
résout postgis 3.4.2-r2, et `pg_config` y rend bien les deux chemins que
les variables supposent.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01J4UE4AmX5PAMN6c6Q6Fey9
2026-08-08 09:46:08 +02:00
arcodange b7f7a47a5d Merge pull request 'feat(miroirs) — un dépôt personnel n'est pas une organisation' (#47) from arcodange/mirror-depots-perso into main 2026-07-30 19:55:31 +02:00
arcodange 0cc8213bff Merge remote-tracking branch 'origin/main' into arcodange/mirror-depots-perso
# Conflicts:
#	ansible/arcodange/factory/inventory/group_vars/all/gitea.yml
2026-07-30 19:55:06 +02:00
arcodange 5ad6601c01 Merge pull request 'fix(cicd) — épingler la version du runner : latest + pull: missing ne rafraîchit JAMAIS' (#51) from arcodange/runner-version-epinglee into main 2026-07-30 09:10:57 +02:00
CI BotandClaude Opus 5 6bb27b0e5c feat(cicd) — Gitea 1.27.1 et Gitea Runner 2.3.0 : l'image du runner a CHANGÉ DE NOM
⚠ CORRIGE LE PREMIER JET DE CETTE BRANCHE, qui épinglait `gitea/act_runner:0.3.1`.

`gitea/act_runner` est GELÉE à 0.6.1. Le successeur officiel est `gitea/runner`
(binaire renommé `act_runner` → `gitea-runner`), aujourd'hui en **2.3.0**.
Épingler l'ancien nom nous aurait enfermés dans une image morte — trouvé grâce
aux notes de version de Gitea 1.27 signalées par le fondateur.

VÉRIFIÉ AVANT DE BASCULER — c'est un remplacement DIRECT pour ce compose :
  • entrypoint identique : /sbin/tini -- run.sh
  • mêmes variables lues : CONFIG_FILE, GITEA_INSTANCE_URL,
    GITEA_RUNNER_{REGISTRATION_TOKEN,NAME,LABELS}
  • config.yaml compatible : capacity, labels, cache.*, container.force_pull,
    options, valid_volumes, host.workdir_parent — AUCUNE clé utilisée ici n'a
    disparu (comparé au `gitea-runner generate-config` de la 2.3.0)

La 2.3.0 apporte en prime des réglages qui parlent à nos pannes connues :
`health_check.min_free_disk_space_mb` (les images de runner supprimées quand le
disque se remplit, ADR 20260407) et `state_report_interval` (les tâches tuées en
zombie faute de rapport, factory#50).

GITEA 1.25.5 → 1.27.1 : deux versions mineures, migrations de base
IRRÉVERSIBLES. Sauvegardes du jour VÉRIFIÉES avant, pas supposées :
  /mnt/backups/postgres/backup_20260730.sql.gz  13 Mo, gzip -t OK,
      contient « CREATE DATABASE gitea » (pg_dumpall)
  /mnt/backups/gitea/backup_20260730.gitea.gz   1,7 Go, gzip -t OK
⚠ Le backup Gitea utilise `gitea dump --skip-db` : il ne contient PAS la base.
C'est le dump postgres qui la porte — les deux sont nécessaires.

Changements cassants de 1.27 et leur portée ici, vérifiée :
  • workflows réutilisables externes retirés → AUCUN dans front, kadans-api,
    factory (contrôlé programmatiquement, `uses:` au niveau job)
  • nonce CSP pour scripts inline → concerne les templates personnalisés, nous
    n'en avons pas
  • X-Content-Type-Options: nosniff par défaut

Refs arcodange-org/factory#50

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 09:03:40 +02:00
CI BotandClaude Opus 5 48e3d6827c fix(cicd) — épingler la version du runner : latest + pull: missing ne rafraîchit JAMAIS
Réponse à « qu'est-ce qui nous empêche d'upgrade des deux côtés ? » : rien.
Le playbook déployait `gitea/act_runner:latest` avec `pull: missing`, c'est-à-dire
la pire combinaison possible — un tag FLOTTANT qui n'est JAMAIS rafraîchi. Chaque
hôte garde donc ce que « latest » voulait dire le jour de son premier pull :

  pi1 : sha256:7bdc8d31…  →  v0.3.1
  pi3 : sha256:0f65fa10…  →  v0.2.13

Deux machines censées être équivalentes, deux versions à trois mineures d'écart.
Effets mesurés : le MÊME job, sur la MÊME image de CI, met 511 s sur pi1 et
397 s sur pi3 (114 s d'écart imputables à la machine) ; et pi3 a mal lu la
définition d'un job dont il dépendait (« 'runs-on' key not defined », puis
« No steps found »).

⚠ POURQUOI PAS `latest` + `pull: always`. `latest` vaut aujourd'hui **0.6.1**
(Docker Hub, 30/04/2026), soit 3 à 4 versions mineures devant tout ce qui est
éprouvé ici. Le runner exécute TOUTE la CI de la forge : une montée subie, non
datée et non choisie s'y paie cher. On épingle donc, et on monte délibérément.

⚠ POURQUOI 0.3.1 ET PAS 0.6.1. 0.3.1 est la version que pi1 exécute DÉJÀ avec
succès sur cette forge. Ce changement aligne donc pi3 VERS LE HAUT, sur du
prouvé, sans saut de quatre versions. Passer ensuite à 0.6.1 devient une
modification d'UNE ligne, datée et reculable — c'est tout l'intérêt de la
variable.

⚠ Et `pull: missing` redevient CORRECT avec un tag épinglé : changer la version
change le tag, donc l'image est absente, donc elle est tirée. Aucun besoin de
`pull: always`, qui interrogerait le registre à chaque passage pour rien.

⚠ NE PAS jouer ce playbook pendant qu'une CI tourne : il recrée les conteneurs
de runner et TUE les jobs en vol (journaux perdus). Vérifier `list_runs` avant —
et se rappeler qu'un merge est un déclencheur.

Refs arcodange-org/factory#50

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-30 08:54:43 +02:00
arcodange 1a8b6bf36c Merge pull request 'fix(ci_base_image) — le contexte de build doit être SUR la machine, pas sur le contrôleur' (#49) from arcodange/image-ci-build-distant into main 2026-07-29 23:51:17 +02:00
CI BotandClaude Opus 5 ba791ed055 fix(ci_base_image) — le contexte de build doit être SUR la machine, pas sur le contrôleur
Le playbook 03_cicd est mort sur les deux hôtes :

  "/Users/…/roles/ci_base_image/files/" is not an existing directory

`docker_image_build` s'exécute SUR LA CIBLE : son `path:` est un chemin de la
cible. Je passais `{{ role_path }}/files/`, un chemin du CONTRÔLEUR.

Le motif venait du rôle `playwright`, qui l'emploie LÉGITIMEMENT parce qu'il
construit en local. Recopié pour un build distant, il ne pouvait pas marcher —
et aucune relecture ne l'aurait montré, seule l'exécution le dit.

⚠ L'échec est arrivé AVANT les tâches qui déploient le runner : les deux
runners sont restés `Up 6 days`, rien n'a été cassé. Le seul effet fut un jeton
d'API Gitea créé par le rôle gitea_token, son comportement normal.

Le contexte est désormais déposé sur la machine (`/tmp/ci-base-image`), et la
RECONSTRUCTION DEVIENT CONDITIONNELLE : `never` en régime normal — le playbook
ne rebâtit pas 3,3 Go à chaque passage — mais `always` dès que le Dockerfile a
CHANGÉ sur la machine. Ajouter une bibliothèque devient donc effectif sans avoir
à penser à un drapeau.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 23:41:32 +02:00
arcodange 863ac68065 Merge pull request 'feat(ci): construire l'image des jobs CI lourds sur chaque machine à runner (+ implémente l'épinglage de l'ADR 20260407)' (#48) from arcodange/image-ci-runners into main 2026-07-29 23:33:29 +02:00
CI BotandClaude Opus 5 9f438c4968 fix(ci_base_image) — l'image livrait Node 18 : deux défauts que seule sa CONSTRUCTION a montrés
J'avais écrit dans cette PR « je n'ai pas pu construire l'image, sa base vit
derrière le certificat interne ». C'était une SUPPOSITION NON TESTÉE, et elle est
fausse : `docker pull gitea.arcodange.lab/…/runner-images:ubuntu-latest-ca` passe
sans rien configurer. En la construisant vraiment, deux défauts sont sortis — et
aucun n'était visible à la lecture du Dockerfile.

1. `runner-images:ubuntu-latest-ca` LIVRE NODE 18 (v18.20.8). Or `nuxi` importe
   `node:util.styleText`, absent de Node 18 : c'est la raison d'être du
   `container: node:20-bookworm` de la CI de kadans, que son CLAUDE.md interdit
   de retirer. Sans correctif, basculer la CI sur cette image cassait `nuxt build`
   sur un message parlant d'un import introuvable — jamais d'une version de Node.
   → Node 20 installé depuis NodeSource.

2. ET INSTALLER NE SUFFISAIT PAS. Après l'installation, `node --version` rendait
   TOUJOURS v18.20.8 : l'image de base précuit un node pour le toolcache d'act et
   le met EN TÊTE du PATH.

     which node → /opt/acttoolcache/node/18.20.8/arm64/bin/node
     /usr/bin/node --version → v20.20.2   ← le bon, mais il PERD

   → l'entrée 18 du toolcache est retirée ; la résolution retombe sur
     /usr/bin/node. ⚠ Conséquence assumée : `actions/setup-node` ne trouvera plus
     de Node 18 préinstallé — aucun workflow de kadans ne l'utilise, et l'image
     n'est servie qu'aux jobs qui DEMANDENT le label.

Le Dockerfile porte désormais une ASSERTION DE BUILD
(`node --version | grep -q "^v${NODE_MAJOR}\."`) : l'image ne peut plus se
construire si la résolution redevient mauvaise. Et le rôle vérifie la version au
déploiement (`failed_when`), au lieu de la supposer.

MESURES RÉELLES (construite en linux/arm64, l'architecture des runners) :

  TOTAL                              4,58 Go
  ├─ playwright install chromium     1,01 Go
  ├─ playwright install-deps          405 Mo
  ├─ Node 20 (NodeSource)             183 Mo
  └─ bun                              179 Mo

  Vérifié dans l'image : which node → /usr/bin/node v20.20.2 · bun 1.3.14 ·
  chromium-1228 + headless-shell + ffmpeg · /etc/ci-base.versions cohérent.

⚠ Ce que ces chiffres tranchent : le découpage en RUN séparés N'A PAS suffi à
rendre l'image poussable — 1,01 Go pour la plus grosse couche, soit ~4× les
261 Mo que le registre accepte (runner-images:ubuntu-latest-ca). Le build LOCAL
n'est donc pas une préférence, c'est la seule voie. Mesuré, plus supposé.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 23:09:14 +02:00
CI BotandClaude Opus 5 fdecabf8ca feat(ci): construire l'image des jobs CI lourds sur chaque machine à runner
Les jobs CI du dépôt kadans réinstallent, à CHAQUE exécution, des choses qui
changent tous les trimestres. Mesuré le 2026-07-29 sur le run 628 (runner
ARM64, 2 vCPU / 3 Gio) :

  npm install -g bun                        →    8,6 s
  playwright install --with-deps chromium   →  104,0 s   (apt-get, à chaque run)
                                               ────────
                                                112,6 s jetées par run, sur le
                                                job qui EST le chemin critique

Le cache `actions/cache` ne peut rien contre ces 104 s : il couvre le NAVIGATEUR
(299 Mo déjà mis en cache), pas ses dépendances SYSTÈME — `--with-deps` relance
apt quoi qu'il arrive.

POURQUOI CONSTRUIRE ICI PLUTÔT QUE POUSSER UNE IMAGE. La tentative de publier
l'image au registre a échoué (kadans#224 puis #225) : 3,81 Go dont une couche
unique de 1,36 Go, `docker push` casse en « connection reset by peer » — 7
couches passent, 3 sont retentées 50 fois puis abandonnées. À titre de
comparaison, runner-images:ubuntu-latest-ca (534,7 Mo, plus grosse couche
261 Mo) passe sans problème : la limite est entre 261 Mo et ~500 Mo par couche.
Construire localement supprime le problème — aucune couche ne traverse le
réseau.

Et c'est bien sur CHAQUE machine : avec `capacity: 1`, le parallélisme vient de
plusieurs Raspberry, et `container:` est résolu par le runner. Un job qui
atterrit là où l'image manque échoue AVANT sa première étape.

Trois choix de conception :

1. L'image hérite de runner-images:ubuntu-latest-ca, donc du certificat de la CA
   interne (step-ca). Repartir de node:20-bookworm obligerait à réinjecter le CA
   à la main, et tout job parlant à gitea.arcodange.lab échouerait en TLS.

2. TROIS `RUN` séparés (bun / dépendances système / navigateur), délibérément.
   La version qui a échoué faisait une couche de 1,36 Go. Ne pas les fusionner
   pour « gagner une couche ».

3. L'image est ÉPINGLÉE par un conteneur factice — ce qui implémente enfin la
   section 1 de docs/adr/20260407-docker-storage-gitea-runner.md, restée à
   l'état de proposition : system_docker.yml n'applique que le data-root sur
   disque externe et les log-opts. Sans épinglage, le ramasse-miettes de Docker
   supprime l'image dès que le disque se remplit — panne déjà constatée sur les
   images de runner elles-mêmes.

Le rôle vérifie sa sortie (`docker run … bun --version`) au lieu de supposer que
le build a suffi, et l'image écrit ses versions dans /etc/ci-base.versions pour
que la CI de kadans puisse les confronter à son bun.lock et échouer FORT sur une
dérive, plutôt que de la découvrir en « Executable doesn't exist ».

Nouveau label runner `ci-node-playwright`. `container.force_pull: false` est
déjà en place et devient REQUIS pour ce label : sans lui, act_runner tenterait
un pull d'une image qui n'est dans aucun registre.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-29 20:23:59 +02:00
arcodangeandClaude Opus 5 e84383ee34 feat(miroirs) — un dépôt personnel n'est pas une organisation
Le rôle gitea_repo ne savait viser qu'un propriétaire : l'organisation, des
deux côtés à la fois. Les dépôts qui vivent sous le compte personnel
`arcodange` ne pouvaient donc pas sortir du homelab — ni être balayés par
gitea_sync, qui n'interroge que /orgs/<org>/repos.

Trois séparations, toutes rétrocompatibles (les défauts reconduisent le
comportement org-vers-org des dix dépôts déjà en miroir) :

- le propriétaire côté Gitea (`gitea_repo_owner`) n'est plus le même objet que
  celui d'en face (`github_owner`, `gitlab_owner`) ;
- un compte personnel n'est pas une organisation : GitHub ne crée pas le dépôt
  au même endroit, d'où `github_owner_is_org` qui route vers POST /user/repos ;
- GitLab devient facultatif (`gitea_mirror_gitlab`). Il ne l'était pas : sa
  création attendait un 201 sans ignore_errors, si bien qu'un échec GitLab
  avortait l'itération — y compris la moitié GitHub, qui n'y était pour rien.

Deux défauts corrigés au passage, tous deux silencieux :

- les trois listages de gitea_sync ne paginaient pas (30 chez GitHub, 20 chez
  GitLab). Sous la taille d'une page tout va bien ; au-delà, la différence
  entre forges désigne de FAUX dépôts manquants et le rôle les « répare » ;
- la migration entrante posait `repo_owner: github_organization` pour désigner
  le propriétaire DANS Gitea.

Et un piège découvert en exécutant : un dépôt GitHub créé vide adopte comme
branche par défaut la PREMIÈRE branche que le miroir lui pousse — `kadans` a
atterri sur `arcodange/adr-ddd-front`. Le rôle réaligne désormais sur la
branche par défaut de Gitea ; le miroir étant asynchrone, l'alignement échoue
au run qui crée le dépôt et réussit au suivant, d'où le failed_when permissif.

Ce qui sort du homelab reste un CHOIX : playbooks/07_mirrors.yml parcourt une
liste explicite et relue (`gitea_mirrored_repos`) plutôt que la différence
automatique entre forges, qui recréerait un dépôt supprimé exprès.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-07-27 13:34:26 +02:00
arcodange 726456c5ed Merge pull request 'docs(adr) — stockage objet : le point « non vérifié » est tranché par le réel' (#46) from arcodange/adr-minio-listbucket into main 2026-07-26 11:44:30 +02:00
arcodangeandClaude Opus 5 e0cd93c6d3 docs(adr) — stockage objet : le point « non vérifié » est tranché par le réel
L'ADR annonçait que les noms d'actions MinIO de la politique du provisionneur
venaient de la documentation, pas d'un essai. Le premier apply (kadans,
2026-07-26) a répondu : tout le bloc admin passe, il manquait `s3:ListBucket`
côté S3 — le provider interroge l'existence du bucket avant de le créer.

La conséquence devient un constat, avec ce que ListBucket concède (la vue des
clés) et ce qu'il ne concède pas (leur contenu).

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 11:43:47 +02:00
arcodange fc0dd854f1 Merge pull request 'docs(adr) — stockage objet MinIO : qui déclare quoi, et qui détient quoi' (#45) from arcodange/adr-stockage-objet into main 2026-07-26 10:46:34 +02:00
arcodangeandClaude Opus 5 aabedb0f3f docs(adr) — stockage objet MinIO : qui déclare quoi, et qui détient quoi
Trois questions indépendantes, tranchées lors du branchement de Kadans sur
MinIO (2026-07-26) : qui déclare les buckets d'une app, qui détient les
identifiants capables de les créer, et comment l'app lit les siens.

La décision de fond est du fondateur : CHACUN SON PÉRIMÈTRE. Une application
déclare ses buckets depuis son propre dépôt ; `tools` fournit le serveur, un
module de standardisation et un compte de provisionnement — pas la liste. Une
première version faisait tout porter par l'infra partagée : à ce rythme, chaque
bucket de chaque app devenait une PR sur le dépôt commun.

L'ADR consigne aussi les trois identités et leurs portées (root / provisionneur
/ compte de service), pourquoi la lecture des identifiants est une propriété
inconditionnelle de la plateforme plutôt qu'une déclaration par app, et pourquoi
les octets ne transitent pas par l'API — avec les conséquences que ça impose
(endpoint public, CORS aux origines exactes, pas de basic-auth sur l'ingress S3).

Les alternatives écartées sont listées avec leur motif, dont deux que j'avais
moi-même proposées et qui étaient plus faibles.

Deux limites assumées y figurent : le provisionneur est un secret PARTAGÉ entre
rôles CI (sa compromission permet de créer des buckets, pas de lire des objets),
et les noms d'actions d'administration MinIO n'ont pas été éprouvés contre le
serveur au moment d'écrire.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01CoafGWmRVESaWX819USUUA
2026-07-26 10:21:04 +02:00
arcodange b06b7e79ac Merge pull request 'fix(argocd): url-shortener enfin syncable — ignorer le volumeName épinglé de son PVC' (#44) from arcodange/url-shortener-pvc-sync into main 2026-07-25 10:49:41 +02:00
arcodangeandClaude Fable 5 73bf7d1170 fix(argocd): url-shortener enfin syncable — ignorer le volumeName épinglé de son PVC
L'app url-shortener était en SyncError permanent : son PVC live porte un
spec.volumeName épinglé (rebind du volume Longhorn après le drill coupure de
courant) absent du chart ; chaque sync tentait donc de le vider, refus API
(spec immuable après création), échec en boucle malgré automated+selfHeal.

- apps.yaml : passthrough générique ignoreDifferences + syncOptions par app.
- values.yaml : url-shortener ignore /spec/volumeName du PVC, avec
  RespectIgnoreDifferences=true pour que l'apply réinjecte la valeur live au
  lieu de la vider (le cas d'usage documenté d'ArgoCD pour les champs
  immuables).

Rendu helm vérifié : seule l'Application url-shortener change.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-25 10:47:34 +02:00
arcodange 1ed3154668 Merge pull request 'fix(dns): coredns-custom — importer les blocs *.server à la racine du Corefile (sinon CoreDNS crash)' (#40) from arcodange/coredns-custom-import into main
Reviewed-on: #40
2026-07-24 12:44:33 +02:00
arcodange a59049d436 Merge pull request 'docs(runbook) — corrige 09 : ne pas écrire vaultConnectionRef hors du ns tools' (#43) from arcodange/runbook-fix-connref into main 2026-07-24 11:29:41 +02:00
arcodangeandClaude Opus 4.8 51d01f47c2 docs(runbook) — corrige 09 : NE PAS écrire vaultConnectionRef dans un ns applicatif
Le chapitre « service compagnon » montrait `vaultConnectionRef: default` dans
l'exemple VaultAuth — c'est faux hors du namespace `tools` et ça a réellement bloqué
le déploiement de kadans-api (pods en CreateContainerConfigError, VaultDynamicSecret
sur « VaultConnection default not found »).

VSO résout vaultConnectionRef dans le namespace DU CR ; la VaultConnection `default`
ne vit que dans `tools`. Les apps hors `tools` (erp, webapp) OMETTENT le champ et
laissent VSO retomber sur sa defaultVaultConnection. On retire donc la ligne de
l'exemple + on ajoute un encart WARNING dédié au piège.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
2026-07-24 11:29:17 +02:00
arcodange 97b2f49d49 Merge pull request 'docs(runbook) — chapitre « service compagnon » (namespace + stack partagés)' (#42) from arcodange/runbook-service-compagnon into main 2026-07-24 10:40:13 +02:00
arcodange cb83c03d15 Merge pull request 'feat(argocd) — enregistre kadans-api (app-of-apps, namespace kadans)' (#41) from arcodange/register-kadans-api into main 2026-07-24 10:39:54 +02:00
arcodangeandClaude Opus 4.8 e1167eec27 docs(runbook) — chapitre « service compagnon » (namespace + stack partagés)
Le runbook new-web-app couvre l'app autonome (dépôt/base/Vault/namespace propres,
tout nommé <app>). Il manquait le cas du SERVICE COMPAGNON : un second service qui
partage le namespace — et parfois le stack Vault/DB — d'une app existante (API cœur
à côté de son front, façade d'analyse). Deux précédents vivants non documentés :
kadans-jobs (namespace seul) et kadans-api (namespace + base + Vault).

- Nouvelle page 09-service-compagnon.md : compagnon vs app autonome ; les deux
  formes (sans état / partage Vault+DB) ; le PIÈGE du VaultAuth manquant quand
  l'app primaire ne consomme pas Vault (front statique) → le compagnon pose son
  propre VaultAuth mais avec le rôle+SA du PRIMAIRE ; carte, précédents, delta de
  checklist.
- 07-argocd-register.md : ajoute la ligne `namespace:` aux options (elle existait
  dans values.yaml — kadans-jobs — mais n'était pas documentée) ; corrige le
  callout qui affirmait le namespace « non configurable ».
- conventions.md : note l'exception compagnon à la règle « tout est <app> ».
- README.md : entrée 09 dans l'index + Last Updated.

Vérifié : VaultAuth erp nommé `auth` ; connexion via pgbouncer.tools ; liens
internes tous résolus.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
2026-07-24 10:38:27 +02:00
arcodangeandClaude Opus 4.8 1365c95c2f feat(argocd) — enregistre kadans-api (app-of-apps, namespace kadans)
Ajoute l'API cœur au registre gitea_applications. ArgoCD crée une Application
`kadans-api` (source arcodange/kadans-api, path chart, targetRevision HEAD), sync
automatique prune+selfHeal, image-updater par digest sur :latest — même moule que
les autres apps.

Namespace `kadans` (comme kadans-jobs) : kadans-api partage le stack Vault/DB déjà
en place pour l'app front (VaultAuth `kadans`, rôle Postgres dynamique
postgres/creds/kadans, ServiceAccount `kadans`, policy KV `kadans`). Aucun nouvel
iac/DB/Vault à provisionner.

À merger APRÈS le fix chart kadans-api (VaultAuth + hôte DB pgbouncer.tools) pour
que la première synchro ArgoCD parte d'un chart correct.

helm template rend l'Application kadans-api → repoURL arcodange/kadans-api,
namespace kadans, CreateNamespace, digest.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
2026-07-24 10:28:28 +02:00
arcodangeandClaude Fable 5 ec49706952 fix(dns): import coredns custom *.server blocks at Corefile root — inside .:53 it crashes CoreDNS
The never-yet-applied k3s_dns.yml placed 'import /etc/coredns/custom/*.server'
INSIDE the .:53 server block. *.server files hold full server blocks
(arcodange.lab:53 {…}), which only parse at Corefile root — inside a block
CoreDNS dies at startup with "Unknown directive 'arcodange.lab:53'"
(CrashLoopBackOff, cluster DNS fully down; lived it on 2026-07-24 while
restoring the expired *.arcodange.lab certificate).

Also restores the stock 'loadbalance' plugin dropped by the playbook.

Context: cluster CoreDNS forwarded to the node's resolv.conf, which lists the
ISP box's IPv6 RDNSS next to the Pi-holes — NXDOMAIN roulette for *.lab names.
That's what left step-issuer unable to reach ssl-ca.arcodange.lab:8443 and let
the 24h wildcard cert expire this morning. The (fixed) playbook pins .lab
resolution to the Pi-holes via the coredns-custom ConfigMap; applied live on
2026-07-24, wildcard renewed, strict TLS verified on gitea/argocd/grafana.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 10:13:41 +02:00
arcodange 0612da184c Merge pull request 'fix(cicd): cap act_runner jobs (3g/2cpu) and capacity 2→1 — a build can no longer take down pi1' (#39) from arcodange/runner-limits into main
Reviewed-on: #39
2026-07-24 09:51:08 +02:00
arcodangeandClaude Fable 5 961691d6d2 fix(cicd): cap act_runner jobs (3g/2cpu/pids) and capacity 2→1 — a build can no longer take down pi1
Incident 2026-07-23: an uncapped nuxt generate (3.5G RSS) on pi1 starved the
k3s control-plane and traefik (load >150, no swap, no OOM-kill) — every
*.arcodange.lab endpoint went dark, Gitea included, while Gitea itself was
healthy on pi2. Job containers are spawned via the host docker socket, so
cgroup caps on the job container are the only guardrail.

Applied live on pi1+pi3 via 03_cicd.yml on 2026-07-24 (both runners
re-registered; pi3's runner was down and is back in service).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-24 00:09:47 +02:00
arcodange 1f22669ac6 feat(argocd): apps override destination namespace; kadans-jobs → kadans (#38)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-07-20 10:23:34 +02:00
arcodange 342026a3d9 fix(argocd): stop the document separator gluing itself to the last chart value (#37)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-07-20 09:22:49 +02:00
arcodange 34c1ad697f fix(argocd): let the image-updater trust the lab CA — the last blocker to rollouts (#36)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-07-20 09:11:25 +02:00
arcodangeandClaude Opus 4.8 d3261bc8c4 feat(argocd): enroll kadans-jobs — the Kadans analysis-jobs façade (#35)
kadans-jobs is the tier-2 (homelab, 24/7) piece of the Kadans topology: a job
queue plus the store of published analysis results, decoupling the product from
the volatile Mac worker.

org: arcodange — the repo does not live under the default arcodange-org.
Digest-based image-updater annotations follow the fleet pattern; the
cluster-wide ImageUpdater CR (namePattern "*", useAnnotations) picks them up,
so there is no per-app CR to maintain. The image is already in the registry.

No postgres/iac/terraform.tfvars entry, on purpose: the façade runs a memory
store in v0, so it needs neither a database nor Vault. That is the runbook's
degraded mode — the DB, the Vault JWT role and the app's own iac/ will land
together with the Postgres store.

Chart realigned on the runbook conventions first, in kadans-jobs#1.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_013ws8L74dVZmp97Wu36fm8j
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-07-20 09:07:44 +02:00
arcodange 655ef61fc5 fix(argocd): restore fleet image rollouts — image-updater v1 needs an ImageUpdater CR (#34)
Co-authored-by: Gabriel Radureau <[email protected]>
Co-committed-by: Gabriel Radureau <[email protected]>
2026-07-20 08:58:47 +02:00
arcodange e9d10113aa Merge pull request 'docs(prd): STATUS — erp#39/#40/#54 shipped and merged under the operator's go' (#33) from arcodange/status-morning into main 2026-07-19 09:23:49 +02:00
arcodangeandClaude Fable 5 39809dcec8 docs(prd): STATUS — #39/#40/#54 shipped and merged under the operator's go
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-19 09:23:34 +02:00
arcodange 37b9e183d9 Merge pull request 'docs(prd): STATUS truth-pass — erp#43/#44 shipped, #40 built (bar met), entry points re-baselined' (#32) from arcodange/status-20260719 into main 2026-07-19 00:32:34 +02:00
arcodangeandClaude Fable 5 8587b4ad00 docs(prd): STATUS truth-pass — #43/#44 shipped, #40 built (bar met), entry points re-baselined
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-19 00:32:14 +02:00
arcodange c8ea130ff6 Merge pull request 'docs(prd): cross-family verification rule + STATUS truth-pass (erp#56/#63/#39)' (#31) from arcodange/harness-portability-qa into main 2026-07-18 20:23:13 +02:00
arcodangeandClaude Fable 5 c55714d892 docs(prd): cross-family verification rule + STATUS truth-pass (erp#56/#63/#39)
- qa-strategy › Independent verification: with Mistral (vibe -p,
  mistral-medium-3.5) and Ornith 35B admitted to verifier duty by verdict
  parity (erp#63 evidence, blind-judged), the independent verifier SHOULD be
  a different model family than the builder; journal records which family
  verified what.
- STATUS: #63  (PR erp#69, harness home erp:fleet/harness/), #56 
  (PR erp#68, authored by the Mistral builder bench), #39 built on local
  branch (push+PR = operator step), PR-log rows, P3 flipped to in-progress.

Paired with erp#69 (Closes erp#63).

Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
2026-07-18 20:21:55 +02:00
arcodangeandClaude Fable 5 a4be009e3e chore(argocd): retire le bloc repoServer mort des values image-updater
Ajouté par bda53f29 pour « monter les credentials dans le repo-server »,
mais ces values sont passées au chart argocd-image-updater (HelmChart
kube-system) qui n'a pas de clé repoServer : no-op intégral. Le vrai
repo-server ArgoCD est déployé par l'addon k3s et n'a pas besoin de ce
montage — les credentials repo passent par les secrets étiquetés
argocd.argoproj.io/secret-type.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-16 20:37:58 +02:00
arcodangeandClaude Fable 5 3698ab3c73 fix(argocd): retire usernameSecret/passwordSecret de spec.source — champs inexistants du CRD Application
Ces champs (commit 7aa789d1) n'existent pas dans le schéma Application
d'ArgoCD : l'API server les élague à l'apply, d'où un diff permanent →
factory OutOfSync en boucle (296 tentatives selfHeal) sur kadans,
telegram-gateway et dance-lessons-coach. L'authentification aux repos
privés passe par un secret repo-creds (label
argocd.argoproj.io/secret-type: repo-creds, url préfixe
https://gitea.arcodange.lab/arcodange) — corrigé côté cluster sur le
secret gitea-credentials existant.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-16 20:37:29 +02:00
arcodange bf32afc217 Merge pull request 'docs(prd): contract lifecycle is live — erp#65 phase 1 shipped, erp#67 official-doc skill (T18)' (#29) from arcodange/contract-lifecycle-live into main 2026-07-15 19:57:00 +02:00
arcodangeandClaude Fable 5 92c1be9780 docs(prd): contract lifecycle is live — erp#65 phase 1 shipped, erp#67 official-doc skill (T18 candidate)
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 19:56:28 +02:00
arcodange 96751f9fcd Merge pull request 'docs(prd): harness portability — Claude as escalation tier, not hard dependency' (#28) from arcodange/harness-portability into main 2026-07-15 18:50:20 +02:00
arcodangeandClaude Fable 5 c7320d2811 docs(prd): STATUS — erp#63 harness-portability spike joins P3
Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 18:50:05 +02:00
arcodangeandClaude Fable 5 5aea83749d docs(prd): harness portability — Claude as escalation tier, not hard dependency
Operator direction 2026-07-15: the orchestration layer itself (builder
sessions, cold verifiers) must run on Mistral or hermes+Ornith/MLX too.
The protocol already carries everything in files+issues; new model-fleet
section defines the evidence-gated ladder — verifier roles migrate
first (cross-family refutation is stronger verification), scoped
builders benched on unchanged acceptance gates, Claude default until
the bench says otherwise. D2 row records the direction; spike = erp#63.

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 18:49:05 +02:00
arcodange 72cca8a3b0 Merge pull request 'docs(prd): D8 settled — fleet/ landed with erp#62; STATUS truth-pass' (#27) from arcodange/d8-settled into main 2026-07-15 18:43:57 +02:00
arcodangeandClaude Fable 5 8232ed7987 docs(prd): D8 settled — fleet/ landed with erp#62; STATUS truth-pass
erp#38 shipped 2026-07-15 (PR erp#62, independent locate-test PASS):
D8 row flipped leaning→settled, P1 phase row 🟡 in progress with the
first , cold-start entry point advances to erp#39 (golden set).

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-07-15 18:43:34 +02:00