Compare commits
20
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
80532ed9b4 | ||
|
|
d355c9c24e | ||
|
|
adc07f91c3 | ||
|
|
f250817641 | ||
|
|
1520ecac41 | ||
|
|
f944fe4bbd | ||
|
|
b7f7a47a5d | ||
|
|
0cc8213bff | ||
|
|
5ad6601c01 | ||
|
|
6bb27b0e5c | ||
|
|
48e3d6827c | ||
|
|
1a8b6bf36c | ||
|
|
ba791ed055 | ||
|
|
863ac68065 | ||
|
|
9f438c4968 | ||
|
|
fdecabf8ca | ||
|
|
e84383ee34 | ||
|
|
726456c5ed | ||
|
|
e0cd93c6d3 | ||
|
|
fc0dd854f1 |
@@ -9,3 +9,80 @@
|
||||
# so the secret propagation playbook iterates over this list.
|
||||
gitea_secret_propagation_users:
|
||||
- arcodange
|
||||
|
||||
# Dépôts mis en miroir vers GitHub (et GitLab) par playbooks/06_mirrors.yml.
|
||||
# Gitea reste la source ; les forges publiques ne reçoivent qu'une copie poussée.
|
||||
#
|
||||
# `gitea_sync` ne balaie qu'UN propriétaire à la fois et déduit les manques en
|
||||
# comparant les forges : utile pour l'organisation, inadapté ici, où l'on choisit
|
||||
# dépôt par dépôt ce qui sort du homelab. D'où cette liste, explicite et relue.
|
||||
#
|
||||
# owner : propriétaire côté Gitea
|
||||
# github_owner : propriétaire côté GitHub (un compte personnel n'est pas une
|
||||
# organisation — voir github_owner_is_org)
|
||||
# gitlab_namespace : ID numérique du groupe ou du compte GitLab d'accueil
|
||||
gitea_mirrored_repos:
|
||||
- name: kadans
|
||||
owner: arcodange
|
||||
github_owner: arcodange
|
||||
github_owner_is_org: false
|
||||
description: application d'entrainement social de danse
|
||||
- name: kadans-api
|
||||
owner: arcodange
|
||||
github_owner: arcodange
|
||||
github_owner_is_org: false
|
||||
- name: kadans-dossier
|
||||
owner: arcodange
|
||||
github_owner: arcodange
|
||||
github_owner_is_org: false
|
||||
- name: kadans-jobs
|
||||
owner: arcodange
|
||||
github_owner: arcodange
|
||||
github_owner_is_org: false
|
||||
- name: video_analysis
|
||||
owner: arcodange
|
||||
github_owner: arcodange
|
||||
github_owner_is_org: false
|
||||
|
||||
# Espace GitLab qui accueille les dépôts du compte personnel. À renseigner avec
|
||||
# l'ID du namespace « arcodange » sur gitlab.com (Settings → General) : sans lui,
|
||||
# la création GitLab retomberait dans le groupe arcodange-org.
|
||||
gitlab_personal_namespace_id: ~
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# VERSION DU RUNNER GITEA ACTIONS — ÉPINGLÉE, ET C'EST LE POINT.
|
||||
#
|
||||
# Le playbook 03_cicd 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 ce que « latest » voulait dire le jour de son
|
||||
# premier pull — d'où deux machines censées être équivalentes qui divergent
|
||||
# (constaté le 2026-07-30) :
|
||||
#
|
||||
# pi1 : sha256:7bdc8d31… → act_runner v0.3.1
|
||||
# pi3 : sha256:0f65fa10… → act_runner v0.2.13
|
||||
#
|
||||
# Effet mesuré : 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 (v0.2.13) a mal
|
||||
# lu la définition d'un job dont il dépendait (« 'runs-on' key not defined »,
|
||||
# puis « No steps found »).
|
||||
#
|
||||
# ⚠ NE PAS remplacer par `latest` + `pull: always` : `latest` vaut aujourd'hui
|
||||
# 0.6.1, 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. Une version épinglée se relit, se date et se recule.
|
||||
# ⚠ L'IMAGE A CHANGÉ DE NOM. `gitea/act_runner` est gelée à 0.6.1 ; le
|
||||
# successeur officiel est `gitea/runner`, et son binaire s'appelle désormais
|
||||
# `gitea-runner` (plus `act_runner`).
|
||||
# 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é à `gitea-runner generate-config` de la 2.3.0).
|
||||
# Le blog de Gitea 1.27 recommande « Gitea Runner 2.0.0 » ; 2.3.0 est la même
|
||||
# lignée majeure, en plus récent. Gitea reste par ailleurs compatible fil-à-fil
|
||||
# avec les runners plus anciens — il désactive simplement les fonctionnalités
|
||||
# qu'ils n'annoncent pas.
|
||||
gitea_runner_image: "gitea/runner"
|
||||
gitea_runner_version: "2.3.0"
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
gitea_version: 1.25.5
|
||||
# ⚠ Montée 1.25.5 → 1.27.1 : DEUX versions mineures, avec migrations de base
|
||||
# IRRÉVERSIBLES (Gitea ne sait pas redescendre après migration). Sauvegardes du
|
||||
# jour vérifiées avant la bascule (pg_dumpall 13 Mo intègre + archive fichiers
|
||||
# 1,7 Go intègre, /mnt/backups).
|
||||
# Changements cassants relevés dans les notes de version, et leur portée ICI :
|
||||
# • workflows réutilisables externes retirés → AUCUN dans nos trois dépôts (vérifié)
|
||||
# • nonce CSP exigé pour les scripts inline → concerne les templates
|
||||
# personnalisés ; nous n'en avons pas
|
||||
# • X-Content-Type-Options: nosniff par défaut
|
||||
gitea_version: 1.27.1
|
||||
|
||||
gitea_database:
|
||||
db_name: gitea
|
||||
@@ -50,4 +59,14 @@ gitea:
|
||||
volumes:
|
||||
- /home/pi/arcodange/docker_composes/gitea/data:/data
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
# Gitea tourne sur pi2 hors k3s (docker compose nu) : invisible du
|
||||
# scheduler k8s et sans plafond jusqu'ici (`docker inspect` mesurait
|
||||
# NanoCPUs=0, Memory=0). Mesuré à 12 % CPU / 417 Mi au repos — la
|
||||
# limite est un filet pour les pics (gros push, opérations git
|
||||
# lourdes), pas un dimensionnement pour la charge normale.
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1.5"
|
||||
memory: 1536M
|
||||
@@ -19,7 +19,57 @@ postgres:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- /home/pi/arcodange/docker_composes/postgres/data:/var/lib/postgresql/data
|
||||
# Postgres tourne sur pi2 hors k3s (docker compose nu) : invisible du
|
||||
# scheduler k8s et sans plafond jusqu'ici (`docker inspect` mesurait
|
||||
# NanoCPUs=0, Memory=0). Mesuré à 3-5 % CPU au repos — la limite est un
|
||||
# filet, pas un dimensionnement pour la charge normale.
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: "1"
|
||||
memory: 1024M
|
||||
|
||||
pgbouncer:
|
||||
auth_user: &pgbouncer_auth pgbouncer_auth
|
||||
auth_user_password: *pgbouncer_auth
|
||||
auth_user_password: *pgbouncer_auth
|
||||
|
||||
# PostGIS — spatial types for the databases that ask for it.
|
||||
#
|
||||
# Kadans stores neighbourhood ("zone") outlines as real geometry, so it needs
|
||||
# geometry(MultiPolygon,4326), ST_Contains and a GiST index. The image stays
|
||||
# `postgres:16.3-alpine`: the extension is installed INTO the running container
|
||||
# by setup/postgres.yml, exactly like the pgbouncer role and the app databases
|
||||
# are created there. No custom image (founder's call, 2026-08-08).
|
||||
#
|
||||
# WARNING — this is a per-CONTAINER install, not a per-VOLUME one. `apk add`
|
||||
# writes to the container's writable layer, so recreating the container (image
|
||||
# change, `docker compose up --force-recreate`) REMOVES PostGIS while the data
|
||||
# keeps its geometry columns — every spatial query then fails until this
|
||||
# playbook runs again. The install task is therefore idempotent and runs after
|
||||
# every compose deploy, and it is the reason `postgis_verifier` exists below:
|
||||
# a silent absence would look like an application bug.
|
||||
postgis:
|
||||
# Only these databases get the extension. Adding one here is the whole change.
|
||||
databases:
|
||||
- kadans
|
||||
# The Alpine package. Pinned to a MAJOR line, not a patch: postgis 3.x
|
||||
# upgrades within a major are ABI-compatible with a given PostgreSQL major.
|
||||
paquet: postgis
|
||||
# ⚠ WHY THE COPY STEP EXISTS — measured on pi2 (arm64), 2026-08-08.
|
||||
# `apk add postgis` alone SUCCEEDS and `CREATE EXTENSION postgis` still fails:
|
||||
#
|
||||
# ERROR: extension "postgis" is not available
|
||||
# DETAIL: Could not open extension control file
|
||||
# "/usr/local/share/postgresql/extension/postgis.control"
|
||||
#
|
||||
# Alpine's package targets Alpine's own PostgreSQL layout
|
||||
# (/usr/share/postgresql16, /usr/lib/postgresql16), while the official
|
||||
# `postgres:16-alpine` image builds the server into /usr/local. The files are
|
||||
# on disk, the server looks elsewhere. Relocating them makes it work — proven
|
||||
# in a throwaway container: PostGIS 3.4 USE_GEOS=1 USE_PROJ=1, and a Lyon
|
||||
# polygon round-tripping through ST_GeomFromText/ST_AsGeoJSON.
|
||||
#
|
||||
# Do NOT "simplify" this to a bare `apk add`. An `apk add --simulate` reports
|
||||
# OK, the install reports OK, and the extension is still unusable.
|
||||
source_partagee: /usr/share/postgresql16/extension
|
||||
source_lib: /usr/lib/postgresql16
|
||||
@@ -8,6 +8,13 @@ raspberries:
|
||||
ansible_host: pi2.home
|
||||
preferred_ip: 192.168.1.202
|
||||
ansible_ssh_extra_args: '-o StrictHostKeyChecking=no'
|
||||
# Gitea + Postgres tournent ici en docker compose nu, hors k3s (cf.
|
||||
# inventory/group_vars/gitea|postgres) : invisibles du scheduler k8s,
|
||||
# qui croyait donc disposer des 4 cœurs / 7,6 Gi en entier. Réservé
|
||||
# informatif seulement (pas d'--enforce-node-allocatable ajouté) : ça
|
||||
# réduit l'Allocatable annoncé par le kubelet, pas d'éviction ajoutée.
|
||||
kubelet_reserved_args: >-
|
||||
--kubelet-arg=system-reserved=cpu=2,memory=2Gi
|
||||
pi3:
|
||||
ansible_host: pi3.home
|
||||
preferred_ip: 192.168.1.203
|
||||
|
||||
@@ -5,6 +5,13 @@
|
||||
|
||||
roles:
|
||||
- arcodange.factory.gitea_token # generate gitea_api_token used to replace generated token with set name if required
|
||||
# Image de base des jobs CI lourds (Node + Bun + Chromium), construite ICI,
|
||||
# sur chaque machine à runner, puis épinglée contre le ramasse-miettes Docker.
|
||||
# Le même groupe d'hôtes que le runner, et ce n'est pas un détail : avec
|
||||
# `capacity: 1` (ci-dessous), le parallélisme vient de PLUSIEURS machines, et
|
||||
# `container:` est résolu par le runner — un job qui atterrit là où l'image
|
||||
# manque échoue AVANT sa première étape.
|
||||
- arcodange.factory.ci_base_image
|
||||
|
||||
tasks:
|
||||
|
||||
@@ -23,7 +30,14 @@
|
||||
name: arcodange_factory_gitea_action
|
||||
services:
|
||||
gitea_action:
|
||||
image: gitea/act_runner:latest
|
||||
# ⚠ VERSION ÉPINGLÉE (inventory/group_vars/all/gitea.yml), PAS `latest`.
|
||||
# `latest` + `pull: missing` = tag flottant JAMAIS rafraîchi : chaque
|
||||
# hôte gardait ce que « latest » voulait dire à son premier pull, d'où
|
||||
# pi1 en v0.3.1 et pi3 en v0.2.13 sur des machines censées être
|
||||
# équivalentes (114 s d'écart mesurés sur le même job).
|
||||
# Avec un tag épinglé, `pull: missing` redevient CORRECT : changer la
|
||||
# version change le tag, donc l'image est absente, donc elle est tirée.
|
||||
image: "{{ gitea_runner_image }}:{{ gitea_runner_version }}"
|
||||
container_name: gitea_action
|
||||
restart: always
|
||||
environment:
|
||||
@@ -32,7 +46,7 @@
|
||||
http://{{ hostvars[groups.gitea[0]].ansible_host }}:3000
|
||||
GITEA_RUNNER_REGISTRATION_TOKEN: "{{ gitea_runner_token_cmd.stdout }}"
|
||||
GITEA_RUNNER_NAME: arcodange_global_runner_{{ inventory_hostname }}
|
||||
GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
|
||||
GITEA_RUNNER_LABELS: ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca,ci-node-playwright:docker://ci-node-playwright:latest
|
||||
ports:
|
||||
- "43707:43707"
|
||||
networks:
|
||||
@@ -91,6 +105,15 @@
|
||||
labels:
|
||||
- "ubuntu-latest:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca"
|
||||
- "ubuntu-latest-ca:docker://gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca"
|
||||
# Jobs CI lourds (Node + Bun + Chromium préinstallés) —
|
||||
# image construite LOCALEMENT par le rôle ci_base_image, sur
|
||||
# cette machine. Elle n'est volontairement PAS dans le
|
||||
# registre : 3,81 Go dont une couche de 1,36 Go, dont le
|
||||
# push casse en « connection reset by peer » (mesuré
|
||||
# 2026-07-29, kadans#225). `force_pull: false` ci-dessous
|
||||
# est donc REQUIS pour ce label — sans lui, act_runner
|
||||
# tenterait un pull et échouerait.
|
||||
- "ci-node-playwright:docker://ci-node-playwright:latest"
|
||||
|
||||
cache:
|
||||
# Enable cache server to use actions/cache.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
# Miroirs push Gitea → GitHub (et GitLab).
|
||||
#
|
||||
# Gitea est la source ; chaque dépôt listé dans `gitea_mirrored_repos`
|
||||
# (inventory/group_vars/all/gitea.yml) reçoit un miroir push rafraîchi toutes les
|
||||
# 8 h ET à chaque commit. Les dépôts créés en face le sont en PRIVÉ.
|
||||
#
|
||||
# uv run ansible-playbook -i ansible/arcodange/factory/inventory \
|
||||
# ansible/arcodange/factory/playbooks/06_mirrors.yml
|
||||
#
|
||||
# GitHub seulement (tant que l'espace GitLab personnel n'est pas renseigné) :
|
||||
# … -e gitea_mirror_gitlab=false
|
||||
#
|
||||
# Le jeton Gitea est frappé pour la durée du run puis révoqué en post_tasks.
|
||||
|
||||
- name: Mettre les dépôts Gitea en miroir sur GitHub et GitLab
|
||||
hosts: localhost
|
||||
gather_facts: true # gitea_token date son jeton avec ansible_date_time
|
||||
|
||||
roles:
|
||||
- role: arcodange.factory.gitea_token
|
||||
tags:
|
||||
- gitea_mirrors
|
||||
|
||||
tasks:
|
||||
- name: Poser le miroir de chaque dépôt déclaré
|
||||
tags: gitea_mirrors
|
||||
include_role:
|
||||
name: arcodange.factory.gitea_repo
|
||||
apply:
|
||||
tags: gitea_mirrors
|
||||
vars:
|
||||
gitea_repo_name: "{{ mirrored_repo.name }}"
|
||||
gitea_repo_owner: "{{ mirrored_repo.owner }}"
|
||||
gitea_repo_description: "{{ mirrored_repo.description | default('') }}"
|
||||
github_owner: "{{ mirrored_repo.github_owner | default(mirrored_repo.owner) }}"
|
||||
github_owner_is_org: "{{ mirrored_repo.github_owner_is_org | default(true) }}"
|
||||
gitlab_owner: "{{ mirrored_repo.gitlab_owner | default(mirrored_repo.owner) }}"
|
||||
gitlab_namespace_id: >-
|
||||
{{ mirrored_repo.gitlab_namespace
|
||||
| default(gitlab_personal_namespace_id)
|
||||
| default(89826881, true) }}
|
||||
# Ce qui sort du homelab reste privé en face.
|
||||
github_repo_private: true
|
||||
gitlab_repo_visibility: private
|
||||
loop: "{{ gitea_mirrored_repos }}"
|
||||
loop_control:
|
||||
loop_var: mirrored_repo
|
||||
label: "{{ mirrored_repo.owner }}/{{ mirrored_repo.name }}"
|
||||
|
||||
post_tasks:
|
||||
- name: Révoquer le jeton Gitea du run
|
||||
tags:
|
||||
- gitea_mirrors
|
||||
include_role:
|
||||
name: arcodange.factory.gitea_token
|
||||
apply:
|
||||
tags: gitea_mirrors
|
||||
vars:
|
||||
gitea_token_delete: true
|
||||
@@ -25,6 +25,63 @@
|
||||
applications_databases:
|
||||
gitea: "{{ gitea_database }}"
|
||||
|
||||
# ── PostGIS ────────────────────────────────────────────────────────────
|
||||
# Installed INTO the running container rather than baked into a custom
|
||||
# image (founder's call, 2026-08-08). See group_vars/postgres/postgres.yml
|
||||
# for why the copy step is not optional, and for the durability caveat.
|
||||
#
|
||||
# Runs after the compose deploy above ON PURPOSE: if that task recreated
|
||||
# the container, the writable layer is fresh and PostGIS is gone with it.
|
||||
# This is what makes the pair (deploy, install) safe to replay.
|
||||
- name: Install PostGIS into the Postgres container
|
||||
ansible.builtin.shell: |
|
||||
set -eu
|
||||
docker exec {{ postgres_container_name }} sh -c '
|
||||
set -eu
|
||||
apk add --no-cache {{ postgis.paquet }} >/dev/null
|
||||
cp -r {{ postgis.source_partagee }}/* "$(pg_config --sharedir)/extension/"
|
||||
cp -r {{ postgis.source_lib }}/*.so "$(pg_config --pkglibdir)/"
|
||||
'
|
||||
# `apk add` is idempotent and the copies overwrite identical files, so a
|
||||
# replay changes nothing observable. We do not pretend to detect that:
|
||||
# claiming `changed_when: false` outright would hide a REAL first install.
|
||||
register: postgis_installation
|
||||
changed_when: "'Installing' in postgis_installation.stdout"
|
||||
|
||||
- name: Enable PostGIS on the databases that need it
|
||||
ansible.builtin.shell: |
|
||||
docker exec {{ postgres_container_name }} \
|
||||
psql -U postgres -d {{ item }} -tAc 'CREATE EXTENSION IF NOT EXISTS postgis;'
|
||||
loop: "{{ postgis.databases }}"
|
||||
register: postgis_activation
|
||||
changed_when: "'CREATE EXTENSION' in postgis_activation.stdout"
|
||||
|
||||
# ⚠ THE HALF THAT MATTERS. Without it, a botched install leaves a green
|
||||
# playbook and an application that fails at its first spatial query — the
|
||||
# symptom would land in Kadans, days later, looking like an app bug.
|
||||
# We ask the database itself, and we FAIL on anything unexpected.
|
||||
- name: Verify PostGIS answers on every database
|
||||
ansible.builtin.shell: |
|
||||
docker exec {{ postgres_container_name }} psql -U postgres -d {{ item }} -tAc \
|
||||
"SELECT postgis_version() || ' | ' || ST_AsGeoJSON(ST_SetSRID(ST_Point(4.83, 45.76), 4326));"
|
||||
loop: "{{ postgis.databases }}"
|
||||
register: postgis_verifier
|
||||
changed_when: false
|
||||
# Not just "the command exited 0": a real geometry must come back with
|
||||
# the SRID applied. A stub that answered an empty string would pass a
|
||||
# bare rc check — and prove nothing.
|
||||
failed_when: >-
|
||||
postgis_verifier.rc != 0
|
||||
or 'USE_GEOS=1' not in postgis_verifier.stdout
|
||||
or '"type":"Point"' not in postgis_verifier.stdout
|
||||
|
||||
- name: Report the PostGIS version in use
|
||||
ansible.builtin.debug:
|
||||
msg: "PostGIS on {{ item.item }} → {{ item.stdout | trim }}"
|
||||
loop: "{{ postgis_verifier.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item }}"
|
||||
|
||||
- name: Create auth_user for pgbouncer (connection pool component)
|
||||
ansible.builtin.shell: |
|
||||
docker exec -it {{ postgres_container_name }} psql -U postgres -d {{ database }} -tc "{{ pg_instruction.replace('$','\$') }}"
|
||||
|
||||
@@ -34,14 +34,23 @@
|
||||
# ansible.builtin.import_playbook: k3s.orchestration.reset
|
||||
vars:
|
||||
k3s_version: v1.34.3+k3s1
|
||||
# ⚠ PAS de guillemets autour de key=value : `--kubelet-arg="k=v"` (avec
|
||||
# guillemets) fait ressortir un `\=` littéral dans l'ExecStart généré par
|
||||
# k3s-install.sh — kubelet refuse ensuite de démarrer ("unknown flag:
|
||||
# --container-log-max-files\"), boucle de redémarrage jusqu'à NotReady.
|
||||
# Mesuré le 11/08 sur pi2 : latent depuis des mois (le service n'avait pas
|
||||
# redémarré depuis avril, donc jamais régénéré par la version actuelle du
|
||||
# script), révélé par le premier restart forcé par ce playbook. La forme
|
||||
# SANS guillemets (`--kubelet-arg=k=v`) traverse la génération intacte.
|
||||
extra_server_args: >-
|
||||
--docker --disable traefik
|
||||
--kubelet-arg="container-log-max-files=5"
|
||||
--kubelet-arg="container-log-max-size=10Mi"
|
||||
--kubelet-arg=container-log-max-files=5
|
||||
--kubelet-arg=container-log-max-size=10Mi
|
||||
extra_agent_args: >-
|
||||
--docker
|
||||
--kubelet-arg="container-log-max-files=5"
|
||||
--kubelet-arg="container-log-max-size=10Mi"
|
||||
--kubelet-arg=container-log-max-files=5
|
||||
--kubelet-arg=container-log-max-size=10Mi
|
||||
{{ kubelet_reserved_args | default('') }}
|
||||
api_endpoint: "{{ hostvars[groups['server'][0]]['ansible_host'] | default(groups['server'][0]) }}"
|
||||
|
||||
- name: how to reach k3s
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
---
|
||||
# Image de base des jobs CI lourds (Node + Bun + Chromium), construite SUR CHAQUE
|
||||
# machine qui héberge un runner Gitea.
|
||||
#
|
||||
# POURQUOI CONSTRUIRE PLUTÔT QUE POUSSER (mesuré le 2026-07-29, kadans#224/#225) :
|
||||
# une image Node + Playwright + Chromium pèse 3,81 Go, avec une couche unique de
|
||||
# 1,36 Go. Son `docker push` vers gitea.arcodange.lab casse en
|
||||
# « connection reset by peer » : 7 couches passent, 3 sont réinitialisées et
|
||||
# retentées 50 fois avant abandon. À titre de comparaison,
|
||||
# `runner-images:ubuntu-latest-ca` (534,7 Mo, plus grosse couche 261 Mo) passe
|
||||
# sans problème — la limite est donc entre 261 Mo et ~500 Mo par couche.
|
||||
#
|
||||
# Construire localement supprime le problème : aucune couche ne traverse le
|
||||
# réseau. Et comme `capacity: 1` par runner (03_cicd.yml), le parallélisme vient
|
||||
# de PLUSIEURS machines — l'image doit donc exister sur CHACUNE d'elles, ce que
|
||||
# ce rôle garantit.
|
||||
|
||||
# On hérite de l'image de runner maison : elle porte DÉJÀ le certificat de la CA
|
||||
# interne (step-ca). Repartir de `node:20-bookworm` obligerait à réinjecter le CA
|
||||
# à la main, et un job qui parle à gitea.arcodange.lab échouerait en TLS.
|
||||
ci_base_image_from: gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
|
||||
|
||||
ci_base_image_name: ci-node-playwright
|
||||
ci_base_image_tag: latest
|
||||
|
||||
# ⚠ Ces versions doivent suivre le `bun.lock` du dépôt kadans. Le dépôt s'en
|
||||
# protège : l'image écrit ce qu'elle a cuit dans /etc/ci-base.versions, et la CI
|
||||
# de kadans CONFRONTE ce fichier à son lockfile pour échouer FORT plutôt que de
|
||||
# dériver en silence (des navigateurs qui ne correspondent plus au client
|
||||
# Playwright donnent « Executable doesn't exist », loin de la cause).
|
||||
ci_base_image_bun_version: '1.3.14'
|
||||
ci_base_image_playwright_version: '1.61.1'
|
||||
|
||||
# ⚠ NODE 20 EST OBLIGATOIRE, ET CE N'EST PAS UN CONFORT.
|
||||
# `runner-images:ubuntu-latest-ca` livre Node **18** (v18.20.8, constaté en
|
||||
# lançant l'image). Or `nuxi` importe `node:util.styleText`, absent de Node 18 :
|
||||
# c'est la raison d'être du `container: node:20-bookworm` que portait la CI de
|
||||
# kadans, et que son CLAUDE.md interdit de retirer. Sans cette surcharge, tout
|
||||
# job Nuxt basculé sur cette image casse au premier build, sur un message qui
|
||||
# parle d'un import introuvable et jamais d'une version de Node.
|
||||
ci_base_image_node_major: 20
|
||||
|
||||
# Épinglage par conteneur factice — remède décrit par
|
||||
# docs/adr/20260407-docker-storage-gitea-runner.md §1, jusqu'ici resté à l'état
|
||||
# de proposition (system_docker.yml n'applique que le data-root et les log-opts).
|
||||
# Sans lui, le ramasse-miettes de Docker supprime l'image dès que le disque se
|
||||
# remplit, et la CI casse sur une image manquante — panne déjà constatée sur les
|
||||
# images de runner elles-mêmes.
|
||||
ci_base_image_pin: true
|
||||
|
||||
# Où le contexte de build est déposé SUR LA MACHINE CIBLE. `docker_image_build`
|
||||
# s'exécute sur la cible : son `path:` est un chemin de la cible, jamais du
|
||||
# contrôleur. (Première version : `{{ role_path }}/files/` → le playbook mourait
|
||||
# sur « is not an existing directory », sur les deux hôtes.)
|
||||
ci_base_image_contexte: /tmp/ci-base-image
|
||||
|
||||
# Reconstruire même si l'image existe déjà. ⚠ Inutile pour un simple changement
|
||||
# du Dockerfile : le rôle le détecte et reconstruit tout seul (voir tasks/).
|
||||
# Ce drapeau sert aux cas que le Dockerfile ne montre pas — une montée de
|
||||
# `bun.lock` côté kadans, par exemple, qui change les VERSIONS attendues sans
|
||||
# changer le fichier.
|
||||
ci_base_image_force_rebuild: false
|
||||
@@ -0,0 +1,101 @@
|
||||
# Image de base des jobs CI lourds — construite SUR CHAQUE machine à runner.
|
||||
#
|
||||
# Elle cuit une fois pour toutes ce que chaque exécution de CI réinstallait.
|
||||
# Mesures du dépôt kadans (run 628, 2026-07-29, runner ARM64 2 vCPU / 3 Gio) :
|
||||
#
|
||||
# npm install -g bun → 8,6 s par run
|
||||
# playwright install --with-deps chromium → 104,0 s par run (apt-get)
|
||||
# ────────
|
||||
# 112,6 s jetées à CHAQUE run,
|
||||
# sur le job du 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-get` quoi qu'il arrive.
|
||||
#
|
||||
# ⚠ ON HÉRITE DE L'IMAGE DE RUNNER MAISON, ET C'EST ESSENTIEL : elle porte le
|
||||
# certificat de la CA interne (step-ca). Une image repartant de `node:20-bookworm`
|
||||
# ne ferait pas confiance à gitea.arcodange.lab, et tout job qui lui parle
|
||||
# échouerait en TLS.
|
||||
ARG CI_BASE_FROM=gitea.arcodange.lab/arcodange-org/runner-images:ubuntu-latest-ca
|
||||
FROM ${CI_BASE_FROM}
|
||||
|
||||
ARG BUN_VERSION=1.3.14
|
||||
ARG PLAYWRIGHT_VERSION=1.61.1
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Chemin des navigateurs, figé et hors du HOME : un job qui tourne sous un autre
|
||||
# utilisateur doit les retrouver.
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/ms-playwright
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# ⚠ TROIS `RUN` SÉPARÉS, ET C'EST LE POINT DE CONCEPTION DE CE FICHIER.
|
||||
#
|
||||
# La première version faisait `npm i -g bun` puis `playwright install --with-deps`
|
||||
# en deux couches, dont une de 1,36 Go — irrecevable par le registre. Ici, même
|
||||
# si l'on décidait un jour de pousser cette image, chaque couche reste du même
|
||||
# ordre de grandeur que celles qui passent déjà (261 Mo pour la plus grosse de
|
||||
# `runner-images:ubuntu-latest-ca`).
|
||||
#
|
||||
# Ne pas fusionner ces `RUN` pour « gagner une couche » : le gain serait nul et
|
||||
# la couche redeviendrait impossible à transporter.
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
# 0. NODE 20, ET C'EST OBLIGATOIRE — pas une préférence.
|
||||
#
|
||||
# ⚠ `runner-images:ubuntu-latest-ca` livre **Node 18** (v18.20.8, vérifié en le
|
||||
# lançant). Or `nuxi` importe `node:util.styleText`, ABSENT de Node 18 : c'est la
|
||||
# raison d'être du `container: node:20-bookworm` que la CI de kadans portait, et
|
||||
# que son `CLAUDE.md` interdit explicitement de retirer.
|
||||
#
|
||||
# Sans cette couche, une CI qui bascule sur cette image casse au premier `nuxt
|
||||
# build` — et le message parle d'un import introuvable, pas d'une version de Node.
|
||||
# Le défaut a été trouvé en CONSTRUISANT l'image puis en lançant `node --version`
|
||||
# dedans ; aucune lecture du Dockerfile ne l'aurait montré.
|
||||
# ⚠⚠ ET INSTALLER NE SUFFIT PAS — il faut aussi que `node` RÉSOLVE vers le bon.
|
||||
# Constaté en lançant l'image : après l'installation de Node 20 par apt,
|
||||
# `node --version` rendait toujours **v18.20.8**, parce que 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
|
||||
# PATH → /opt/acttoolcache/node/18.20.8/arm64/bin:/usr/local/sbin:/usr/bin:…
|
||||
# /usr/bin/node --version → v20.20.2 ← le bon, mais il PERD
|
||||
#
|
||||
# On retire donc l'entrée 18 du toolcache : le segment de PATH devient inexistant
|
||||
# (inoffensif) et la résolution retombe sur /usr/bin/node, en 20.
|
||||
# ⚠ Conséquence assumée : `actions/setup-node` ne trouvera plus de Node 18
|
||||
# préinstallé dans cette image. Aucun workflow de kadans ne l'utilise, et l'image
|
||||
# n'est servie qu'aux jobs qui DEMANDENT le label `ci-node-playwright`.
|
||||
ARG NODE_MAJOR=20
|
||||
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" -o /tmp/nodesource.sh \
|
||||
&& bash /tmp/nodesource.sh \
|
||||
&& apt-get install -y --no-install-recommends nodejs \
|
||||
&& rm -f /tmp/nodesource.sh \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& rm -rf /opt/acttoolcache/node \
|
||||
&& echo "node résolu : $(which node) $(node --version)" \
|
||||
&& node --version | grep -q "^v${NODE_MAJOR}\."
|
||||
|
||||
# 1. Bun (~180 Mo) — installé APRÈS Node 20, pour que son npm global soit celui
|
||||
# de Node 20 et non celui de Node 18.
|
||||
RUN npm install -g "bun@${BUN_VERSION}" \
|
||||
&& npm cache clean --force
|
||||
|
||||
# 2. Les dépendances SYSTÈME de Chromium — c'est CETTE couche qui rachète les
|
||||
# 104 s d'apt-get de chaque run.
|
||||
RUN npx --yes "playwright@${PLAYWRIGHT_VERSION}" install-deps chromium \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# 3. Le navigateur lui-même, séparé de ses dépendances système : les deux ne
|
||||
# bougent pas au même rythme, et Docker ne réinvalide alors que la bonne.
|
||||
RUN npx --yes "playwright@${PLAYWRIGHT_VERSION}" install chromium
|
||||
|
||||
# La trace opposable de ce qui est réellement cuit ici. La CI de kadans la LIT et
|
||||
# la confronte à son `bun.lock` : une dérive de version doit échouer FORT, avec sa
|
||||
# cause, plutôt que de se manifester par un « Executable doesn't exist » à
|
||||
# vingt minutes de là.
|
||||
RUN printf 'node=%s\nbun=%s\nplaywright=%s\n' \
|
||||
"$(node --version)" "$(bun --version)" "${PLAYWRIGHT_VERSION}" \
|
||||
> /etc/ci-base.versions \
|
||||
&& cat /etc/ci-base.versions
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
# Construit l'image de base des jobs CI sur la machine courante, puis l'épingle.
|
||||
#
|
||||
# À exécuter sur les MÊMES hôtes que le runner Gitea (03_cicd.yml) : comme
|
||||
# `capacity: 1`, le parallélisme vient de plusieurs machines, et un job qui
|
||||
# atterrit sur une machine sans l'image échouerait AVANT sa première étape —
|
||||
# `runs-on`/`container:` est résolu par le runner, pas par le workflow.
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
# ⚠ LE CONTEXTE DE BUILD DOIT ÊTRE SUR LA MACHINE, PAS SUR LE CONTRÔLEUR.
|
||||
#
|
||||
# `docker_image_build` s'exécute SUR LA CIBLE : son `path:` est un chemin de la
|
||||
# cible. La première version passait `{{ role_path }}/files/` — un chemin du
|
||||
# CONTRÔLEUR — et le playbook mourait sur les deux hôtes :
|
||||
#
|
||||
# "/Users/…/roles/ci_base_image/files/" is not an existing directory
|
||||
#
|
||||
# Le motif venait du rôle `playwright`, qui l'utilise LÉGITIMEMENT parce qu'il
|
||||
# construit en local ; recopié tel quel pour un build distant, il ne peut pas
|
||||
# marcher. On copie donc le contexte d'abord.
|
||||
# ══════════════════════════════════════════════════════════════════════════
|
||||
- name: Créer le répertoire de contexte de build sur la machine
|
||||
ansible.builtin.file:
|
||||
path: '{{ ci_base_image_contexte }}'
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Déposer le Dockerfile sur la machine
|
||||
ansible.builtin.copy:
|
||||
src: Dockerfile
|
||||
dest: '{{ ci_base_image_contexte }}/Dockerfile'
|
||||
mode: '0644'
|
||||
register: ci_base_image_dockerfile
|
||||
|
||||
- name: Construire {{ ci_base_image_name }}:{{ ci_base_image_tag }}
|
||||
community.docker.docker_image_build:
|
||||
name: '{{ ci_base_image_name }}'
|
||||
tag: '{{ ci_base_image_tag }}'
|
||||
path: '{{ ci_base_image_contexte }}'
|
||||
# RECONSTRUCTION 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 — c'est ce qui rend l'ajout d'une bibliothèque
|
||||
# effectif sans avoir à penser à un drapeau.
|
||||
rebuild: >-
|
||||
{{ "always"
|
||||
if (ci_base_image_force_rebuild or ci_base_image_dockerfile is changed)
|
||||
else "never" }}
|
||||
args:
|
||||
CI_BASE_FROM: '{{ ci_base_image_from }}'
|
||||
NODE_MAJOR: '{{ ci_base_image_node_major }}'
|
||||
BUN_VERSION: '{{ ci_base_image_bun_version }}'
|
||||
PLAYWRIGHT_VERSION: '{{ ci_base_image_playwright_version }}'
|
||||
register: ci_base_image_build
|
||||
|
||||
# ⚠ CE CONTENEUR NE TOURNE JAMAIS — il ne sert qu'à référencer l'image.
|
||||
# Remède décrit par docs/adr/20260407-docker-storage-gitea-runner.md §1, resté
|
||||
# jusqu'ici à 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, et qui casse la CI de tous les dépôts.
|
||||
#
|
||||
# `state: present` (et non `started`) : Docker considère l'image comme utilisée
|
||||
# tant qu'un conteneur la référence, même à l'arrêt. Aucun CPU, aucune mémoire.
|
||||
- name: Épingler {{ ci_base_image_name }} contre le ramasse-miettes Docker
|
||||
community.docker.docker_container:
|
||||
name: 'pin-{{ ci_base_image_name }}'
|
||||
image: '{{ ci_base_image_name }}:{{ ci_base_image_tag }}'
|
||||
state: present
|
||||
command: ['sh', '-c', 'sleep infinity']
|
||||
auto_remove: false
|
||||
restart_policy: 'no'
|
||||
when: ci_base_image_pin
|
||||
|
||||
# Contrôle de sortie : on VÉRIFIE que l'image répond, plutôt que de supposer que
|
||||
# le build a suffi. Une image construite mais dont `bun` n'est pas dans le PATH
|
||||
# passerait le build et casserait tous les jobs.
|
||||
- name: Vérifier que l'image livre bien Node {{ ci_base_image_node_major }}, bun et chromium
|
||||
ansible.builtin.command:
|
||||
cmd: >-
|
||||
docker run --rm {{ ci_base_image_name }}:{{ ci_base_image_tag }}
|
||||
sh -c "node --version && bun --version && ls /ms-playwright && cat /etc/ci-base.versions"
|
||||
register: ci_base_image_check
|
||||
changed_when: false
|
||||
# ⚠ La version de Node est VÉRIFIÉE, pas supposée : l'image de base en livre
|
||||
# une trop ancienne (18), et une régression silencieuse ici casserait tout job
|
||||
# Nuxt sur un message qui ne nomme pas la cause.
|
||||
failed_when: >-
|
||||
ci_base_image_check.rc != 0
|
||||
or ('v' ~ ci_base_image_node_major ~ '.') not in ci_base_image_check.stdout
|
||||
|
||||
- name: Ce que l'image contient réellement
|
||||
ansible.builtin.debug:
|
||||
var: ci_base_image_check.stdout_lines
|
||||
@@ -6,4 +6,38 @@ gitea_username: arcodange
|
||||
gitea_organization: arcodange-org
|
||||
|
||||
# URL de base du serveur Gitea
|
||||
gitea_base_url: http://{{ groups.gitea[0] }}:3000
|
||||
gitea_base_url: http://{{ groups.gitea[0] }}:3000
|
||||
|
||||
# Propriétaire du dépôt CÔTÉ GITEA. Par défaut l'organisation, pour ne rien
|
||||
# changer aux dépôts déjà en miroir ; à surcharger (« arcodange ») pour les
|
||||
# dépôts qui vivent sous le compte personnel.
|
||||
gitea_repo_owner: "{{ gitea_organization }}"
|
||||
|
||||
# Propriétaires en FACE, forge par forge. Ils suivent le propriétaire Gitea par
|
||||
# défaut, mais un dépôt personnel peut viser un compte personnel.
|
||||
github_owner: "{{ github_organization }}"
|
||||
gitlab_owner: "{{ gitlab_root_group }}"
|
||||
|
||||
# Un compte personnel n'est pas une organisation : GitHub ne crée pas un dépôt
|
||||
# au même endroit (POST /user/repos contre POST /orgs/<org>/repos).
|
||||
github_owner_is_org: true
|
||||
|
||||
# Identifiant du groupe OU de l'utilisateur GitLab qui accueille le projet.
|
||||
# https://gitlab.com/groups/arcodange-org/-/edit
|
||||
gitlab_namespace_id: 89826881
|
||||
|
||||
# Quelles forges recevoir en miroir. GitLab devient facultatif : sans ça, un
|
||||
# échec côté GitLab avorte toute l'itération, y compris la partie GitHub.
|
||||
gitea_mirror_github: true
|
||||
gitea_mirror_gitlab: true
|
||||
|
||||
# Le miroir pousse ; c'est le dépôt d'en face qui doit être privé.
|
||||
github_repo_private: true
|
||||
gitlab_repo_visibility: private
|
||||
|
||||
# Nom d'utilisateur porté par le miroir push (le mot de passe est le jeton).
|
||||
github_mirror_username: "{{ gitea_username }}"
|
||||
gitlab_mirror_username: "{{ gitea_username }}"
|
||||
|
||||
# Cadence de rafraîchissement des miroirs, en plus du push à chaque commit.
|
||||
gitea_mirror_interval: "8h"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
- name: Vérifier si le dépôt existe dans Gitea
|
||||
uri:
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_organization }}/{{ gitea_repo_name }}"
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_repo_owner }}/{{ gitea_repo_name }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "token {{ gitea_api_token }}"
|
||||
@@ -10,26 +10,36 @@
|
||||
|
||||
- name: Vérifier si le dépôt existe sur GitLab
|
||||
uri:
|
||||
url: "https://gitlab.com/api/v4/projects/{{ gitlab_root_group }}%2F{{ gitea_repo_name }}"
|
||||
url: "https://gitlab.com/api/v4/projects/{{ gitlab_owner }}%2F{{ gitea_repo_name }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ gitlab_api_token }}"
|
||||
status_code: 200
|
||||
register: gitlab_repo_check
|
||||
ignore_errors: yes
|
||||
when: gitea_mirror_gitlab | bool
|
||||
|
||||
- name: Vérifier si le dépôt existe sur GitHub
|
||||
uri:
|
||||
url: "https://api.github.com/repos/{{ github_organization }}/{{ gitea_repo_name }}"
|
||||
url: "https://api.github.com/repos/{{ github_owner }}/{{ gitea_repo_name }}"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "token {{ github_api_token }}"
|
||||
status_code: 200
|
||||
register: github_repo_check
|
||||
ignore_errors: yes
|
||||
when: gitea_mirror_github | bool
|
||||
|
||||
# Une tâche sautée n'enregistre pas de « status » : sans le default(0), la
|
||||
# condition suivante explose dès qu'une forge est désactivée.
|
||||
- name: Retenir l'état de chaque forge
|
||||
set_fact:
|
||||
gitlab_repo_present: "{{ (gitlab_repo_check.status | default(0)) == 200 }}"
|
||||
github_repo_present: "{{ (github_repo_check.status | default(0)) == 200 }}"
|
||||
gitea_repo_present: "{{ (gitea_repo_check.status | default(0)) == 200 }}"
|
||||
|
||||
- name: Importer un dépôt GitLab/GitHub vers Gitea
|
||||
when: gitea_repo_check.status != 200 and (gitlab_repo_check.status == 200 or github_repo_check.status == 200)
|
||||
when: not gitea_repo_present and (gitlab_repo_present or github_repo_present)
|
||||
uri:
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/migrate"
|
||||
method: POST
|
||||
@@ -38,16 +48,16 @@
|
||||
status_code: 201
|
||||
body_format: json
|
||||
body:
|
||||
service: "{{ (gitlab_repo_check.status == 200) | ternary('gitlab','github') }}"
|
||||
service: "{{ gitlab_repo_present | ternary('gitlab','github') }}"
|
||||
# URL du dépôt GitHub/GitLab
|
||||
clone_addr: >-
|
||||
{{ (gitlab_repo_check.status == 200) | ternary(gitlab_mirror_url,github_mirror_url) }}
|
||||
{{ gitlab_repo_present | ternary(gitlab_mirror_url,github_mirror_url) }}
|
||||
auth_username: "{{ gitea_username }}" # Nom d'utilisateur pour l'authentification si nécessaire
|
||||
# token d'accès
|
||||
auth_token: >-
|
||||
{{ (gitlab_repo_check.status == 200) | ternary(gitlab_api_token,github_api_token) }}
|
||||
{{ gitlab_repo_present | ternary(gitlab_api_token,github_api_token) }}
|
||||
repo_name: "{{ gitea_repo_name }}" # Nom du dépôt dans Gitea
|
||||
repo_owner: "{{ github_organization }}" # Propriétaire du dépôt dans Gitea (utilisateur ou organisation
|
||||
repo_owner: "{{ gitea_repo_owner }}" # Propriétaire du dépôt dans Gitea (utilisateur ou organisation)
|
||||
mirror: false # Activer le mirroring pour synchroniser les changements
|
||||
register: migration_result
|
||||
|
||||
@@ -66,15 +76,20 @@
|
||||
body:
|
||||
name: "{{ gitea_repo_name }}"
|
||||
path: "{{ gitea_repo_name }}"
|
||||
namespace_id: "{{ gitlab_namespace_id }}" # Remplacez par l'ID du groupe ou de l'utilisateur où le projet doit être créé
|
||||
visibility: "{{ gitlab_repo_visibility | default('private') }}" # Définir la visibilité (private, internal, public)
|
||||
namespace_id: "{{ gitlab_namespace_id }}" # ID du groupe ou de l'utilisateur où le projet doit être créé
|
||||
visibility: "{{ gitlab_repo_visibility }}" # Définir la visibilité (private, internal, public)
|
||||
description: "{{ gitea_repo_description | default('') }}"
|
||||
status_code: 201
|
||||
when: gitlab_repo_check.status != 200
|
||||
when: (gitea_mirror_gitlab | bool) and not gitlab_repo_present
|
||||
|
||||
# Un compte personnel n'a pas d'endpoint /orgs/<nom>/repos : GitHub crée alors
|
||||
# le dépôt sous le compte porteur du jeton, via POST /user/repos.
|
||||
- name: Créer un dépôt sur GitHub si nécessaire
|
||||
uri:
|
||||
url: "https://api.github.com/orgs/{{ github_organization }}/repos"
|
||||
url: >-
|
||||
{{ (github_owner_is_org | bool)
|
||||
| ternary('https://api.github.com/orgs/' ~ github_owner ~ '/repos',
|
||||
'https://api.github.com/user/repos') }}
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "token {{ github_api_token }}"
|
||||
@@ -82,13 +97,13 @@
|
||||
body:
|
||||
name: "{{ gitea_repo_name }}"
|
||||
description: "{{ gitea_repo_description | default('') }}"
|
||||
private: "{{ github_repo_private | default(true) }}" # Définir si le dépôt est privé ou public
|
||||
private: "{{ github_repo_private | bool }}" # Définir si le dépôt est privé ou public
|
||||
status_code: 201
|
||||
when: github_repo_check.status != 200
|
||||
when: (gitea_mirror_github | bool) and not github_repo_present
|
||||
|
||||
- name: Vérifier l'existence des miroirs push sur GitHub et GitLab
|
||||
uri:
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_organization }}/{{ gitea_repo_name }}/push_mirrors"
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_repo_owner }}/{{ gitea_repo_name }}/push_mirrors"
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "token {{ gitea_api_token }}"
|
||||
@@ -97,32 +112,68 @@
|
||||
|
||||
- name: Ajouter un miroir push vers GitHub si nécessaire
|
||||
uri:
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_organization }}/{{ gitea_repo_name }}/push_mirrors"
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_repo_owner }}/{{ gitea_repo_name }}/push_mirrors"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "token {{ gitea_api_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
interval: "8h"
|
||||
interval: "{{ gitea_mirror_interval }}"
|
||||
remote_address: "{{ github_mirror_url }}"
|
||||
remote_username: "{{ gitea_username }}"
|
||||
remote_username: "{{ github_mirror_username }}"
|
||||
remote_password: "{{ github_api_token }}"
|
||||
sync_on_commit: true
|
||||
status_code: 200
|
||||
when: "github_mirror_url not in existing_mirrors.json | map(attribute='remote_address') | list"
|
||||
when:
|
||||
- gitea_mirror_github | bool
|
||||
- github_mirror_url not in existing_mirrors.json | map(attribute='remote_address') | list
|
||||
|
||||
- name: Ajouter un miroir push vers GitLab si nécessaire
|
||||
uri:
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_organization }}/{{ gitea_repo_name }}/push_mirrors"
|
||||
url: "{{ gitea_base_url }}/api/v1/repos/{{ gitea_repo_owner }}/{{ gitea_repo_name }}/push_mirrors"
|
||||
method: POST
|
||||
headers:
|
||||
Authorization: "token {{ gitea_api_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
interval: "8h"
|
||||
interval: "{{ gitea_mirror_interval }}"
|
||||
remote_address: "{{ gitlab_mirror_url }}"
|
||||
remote_username: "{{ gitea_username }}"
|
||||
remote_username: "{{ gitlab_mirror_username }}"
|
||||
remote_password: "{{ gitlab_api_token }}"
|
||||
sync_on_commit: true
|
||||
status_code: 200
|
||||
when: "gitlab_mirror_url not in existing_mirrors.json | map(attribute='remote_address') | list"
|
||||
when:
|
||||
- gitea_mirror_gitlab | bool
|
||||
- gitlab_mirror_url not in existing_mirrors.json | map(attribute='remote_address') | list
|
||||
|
||||
# Un dépôt GitHub créé vide adopte comme branche par défaut la PREMIÈRE branche
|
||||
# que le miroir lui pousse — souvent une branche de travail, pas « main ». 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 plutôt qu'un blocage.
|
||||
- name: Aligner la branche par défaut de GitHub sur celle de Gitea
|
||||
uri:
|
||||
url: "https://api.github.com/repos/{{ github_owner }}/{{ gitea_repo_name }}"
|
||||
method: PATCH
|
||||
headers:
|
||||
Authorization: "token {{ github_api_token }}"
|
||||
body_format: json
|
||||
body:
|
||||
default_branch: "{{ gitea_repo_check.json.default_branch }}"
|
||||
status_code: 200
|
||||
register: github_default_branch
|
||||
failed_when: false
|
||||
when:
|
||||
- gitea_mirror_github | bool
|
||||
- gitea_repo_present
|
||||
- (github_repo_check.json.default_branch | default('')) != gitea_repo_check.json.default_branch
|
||||
|
||||
- name: Signaler une branche par défaut encore désalignée
|
||||
debug:
|
||||
msg: >-
|
||||
La branche par défaut de github.com/{{ github_owner }}/{{ gitea_repo_name }}
|
||||
n'a pas pu être alignée sur « {{ gitea_repo_check.json.default_branch }} » :
|
||||
le miroir ne l'a probablement pas encore poussée. Relancer après la synchro.
|
||||
when:
|
||||
- github_default_branch is defined
|
||||
- github_default_branch is not skipped
|
||||
- (github_default_branch.status | default(0)) != 200
|
||||
|
||||
@@ -3,8 +3,8 @@ gitlab_api_token: '{{ hostvars[groups.gitea[0]].gitea_vault.gitlab_api_token }}'
|
||||
|
||||
github_organization: '{{ gitea_organization }}'
|
||||
gitlab_root_group: '{{ gitea_organization }}'
|
||||
gitlab_namespace_id: 89826881 # https://gitlab.com/groups/arcodange-org/-/edit
|
||||
|
||||
# URLs des miroirs sur GitLab et GitHub
|
||||
gitlab_mirror_url: "https://gitlab.com/{{ gitlab_root_group | default(gitlab_username | default(gitea_username)) }}/{{ gitea_repo_name }}.git"
|
||||
github_mirror_url: "https://github.com/{{ github_organization | default(github_username | default(gitea_username)) }}/{{ gitea_repo_name }}.git"
|
||||
# URLs des miroirs sur GitLab et GitHub — elles suivent le propriétaire visé sur
|
||||
# chaque forge (cf. github_owner / gitlab_owner dans defaults/).
|
||||
gitlab_mirror_url: "https://gitlab.com/{{ gitlab_owner }}/{{ gitea_repo_name }}.git"
|
||||
github_mirror_url: "https://github.com/{{ github_owner }}/{{ gitea_repo_name }}.git"
|
||||
|
||||
@@ -4,4 +4,18 @@ gitea_organization: arcodange-org
|
||||
# URL de base du serveur Gitea
|
||||
gitea_base_url: http://{{ groups.gitea[0] }}:3000
|
||||
|
||||
gitea_token_fact_name: arcodange_factory_gitea_sync_token
|
||||
gitea_token_fact_name: arcodange_factory_gitea_sync_token
|
||||
|
||||
# Propriétaire balayé. Par défaut l'organisation ; mettre « arcodange » et
|
||||
# gitea_sync_owner_is_org à false pour balayer le compte personnel.
|
||||
gitea_sync_owner: "{{ gitea_organization }}"
|
||||
gitea_sync_owner_is_org: true
|
||||
|
||||
# Les trois API paginent (30 par défaut chez GitHub, 20 chez GitLab). Sous la
|
||||
# taille d'une page, la différence entre forges désigne de faux dépôts manquants.
|
||||
gitea_sync_page_size: 100
|
||||
|
||||
# Forges comparées. Balayer une forge qu'on ne veut pas alimenter ferait passer
|
||||
# tous ses dépôts pour « incomplets ».
|
||||
gitea_mirror_github: true
|
||||
gitea_mirror_gitlab: true
|
||||
|
||||
@@ -1,40 +1,71 @@
|
||||
# Un compte personnel n'est pas une organisation : ni GitHub ni GitLab ne
|
||||
# servent ses dépôts au même endroit.
|
||||
- name: Lister les dépôts de l'organisation GitHub
|
||||
uri:
|
||||
url: "https://api.github.com/orgs/{{ github_organization }}/repos"
|
||||
url: >-
|
||||
{{ (gitea_sync_owner_is_org | bool)
|
||||
| ternary('https://api.github.com/orgs/' ~ github_owner ~ '/repos',
|
||||
'https://api.github.com/users/' ~ github_owner ~ '/repos')
|
||||
}}?per_page={{ gitea_sync_page_size }}
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "token {{ github_api_token }}"
|
||||
status_code: 200
|
||||
register: github_repos
|
||||
when: gitea_mirror_github | bool
|
||||
|
||||
- name: Lister les dépôts du groupe GitLab
|
||||
uri:
|
||||
url: "https://gitlab.com/api/v4/groups/{{ gitlab_root_group }}/projects"
|
||||
url: >-
|
||||
{{ (gitea_sync_owner_is_org | bool)
|
||||
| ternary('https://gitlab.com/api/v4/groups/' ~ gitlab_owner ~ '/projects',
|
||||
'https://gitlab.com/api/v4/users/' ~ gitlab_owner ~ '/projects')
|
||||
}}?per_page={{ gitea_sync_page_size }}
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "Bearer {{ gitlab_api_token }}"
|
||||
status_code: 200
|
||||
register: gitlab_repos
|
||||
when: gitea_mirror_gitlab | bool
|
||||
|
||||
- name: Lister les dépôts de l'organisation Gitea
|
||||
uri:
|
||||
url: "{{ gitea_base_url }}/api/v1/orgs/{{ gitea_organization }}/repos"
|
||||
url: >-
|
||||
{{ (gitea_sync_owner_is_org | bool)
|
||||
| ternary(gitea_base_url ~ '/api/v1/orgs/' ~ gitea_sync_owner ~ '/repos',
|
||||
gitea_base_url ~ '/api/v1/users/' ~ gitea_sync_owner ~ '/repos')
|
||||
}}?limit={{ gitea_sync_page_size }}
|
||||
method: GET
|
||||
headers:
|
||||
Authorization: "token {{ gitea_api_token }}"
|
||||
status_code: 200
|
||||
register: gitea_repos
|
||||
|
||||
# Une forge désactivée ne doit pas peser dans la différence : on la remplace par
|
||||
# la liste Gitea elle-même, qui la rend neutre à l'intersection.
|
||||
- name: Établir la liste des dépôts incomplets
|
||||
set_fact:
|
||||
gitea_repo_names: "{{ gitea_repos.json | map(attribute='name') | list }}"
|
||||
github_repo_names: >-
|
||||
{{ (gitea_mirror_github | bool)
|
||||
| ternary(github_repos.json | default([]) | map(attribute='name') | list,
|
||||
gitea_repos.json | map(attribute='name') | list) }}
|
||||
gitlab_repo_names: >-
|
||||
{{ (gitea_mirror_gitlab | bool)
|
||||
| ternary(gitlab_repos.json | default([]) | map(attribute='name') | list,
|
||||
gitea_repos.json | map(attribute='name') | list) }}
|
||||
|
||||
- name: Réduire aux dépôts absents d'au moins une forge
|
||||
set_fact:
|
||||
repos_incomplete: >-
|
||||
{{ (github_repo_names | union(gitlab_repo_names) | union(gitea_repo_names))
|
||||
| difference(github_repo_names | intersect(gitlab_repo_names) | intersect(gitea_repo_names)) }}
|
||||
|
||||
- name: Synchroniser
|
||||
include_role:
|
||||
name: arcodange.factory.gitea_repo
|
||||
vars:
|
||||
github_repo_names: "{{ github_repos.json | map(attribute='name') | list }}"
|
||||
gitlab_repo_names: "{{ gitlab_repos.json | map(attribute='name') | list }}"
|
||||
gitea_repo_names: "{{ gitea_repos.json | map(attribute='name') | list }}"
|
||||
all_repos: "{{ github_repo_names | union(gitlab_repo_names) | union(gitea_repo_names) }}"
|
||||
repos_common_to_all: "{{ github_repo_names | intersect(gitlab_repo_names) | intersect(gitea_repo_names) }}"
|
||||
repos_incomplete: "{{ all_repos | difference(repos_common_to_all) }}"
|
||||
gitea_repo_owner: "{{ gitea_sync_owner }}"
|
||||
loop: "{{ repos_incomplete }}"
|
||||
loop_control:
|
||||
loop_var: gitea_repo_name
|
||||
|
||||
@@ -2,4 +2,8 @@ github_api_token: '{{ hostvars[groups.gitea[0]].gitea_vault.github_api_token }}'
|
||||
gitlab_api_token: '{{ hostvars[groups.gitea[0]].gitea_vault.gitlab_api_token }}'
|
||||
|
||||
github_organization: '{{ gitea_organization }}'
|
||||
gitlab_root_group: '{{ gitea_organization }}'
|
||||
gitlab_root_group: '{{ gitea_organization }}'
|
||||
|
||||
# Les propriétaires en face suivent celui qu'on balaie côté Gitea.
|
||||
github_owner: '{{ gitea_sync_owner }}'
|
||||
gitlab_owner: '{{ gitea_sync_owner }}'
|
||||
|
||||
@@ -91,7 +91,7 @@ Le compte de service est **par application** : ajouter un bucket ne crée aucune
|
||||
- **Le dépôt `tools` n'est plus modifié** quand une application change ses buckets. C'était l'objet de la décision.
|
||||
- **Ordre de déploiement contraint** : le module doit exister sur `main` de `tools` avant qu'une application l'appelle (`?ref=main`), et le provisionneur doit exister avant le premier plan d'application.
|
||||
- **Le provisionneur est un secret partagé** entre les rôles CI. Sa compromission permet de créer des buckets et des comptes, pas de lire des objets. Si ce risque devient inacceptable, la suite est une identité de provisionnement **par application**, bornée par préfixe de bucket — MinIO ne le permet pas simplement aujourd'hui.
|
||||
- **Non vérifié à la rédaction** : les noms d'actions d'administration MinIO de la politique du provisionneur viennent de la documentation, pas d'un essai. Le premier `apply` les confirmera ou les corrigera.
|
||||
- **Vérifié par le premier `apply`** (kadans, 2026-07-26) : les actions d'administration passent telles quelles — compte de service, politique et attachement ont été créés. Une seule correction a été nécessaire côté S3, `s3:ListBucket` : le provider interroge l'existence du bucket (HeadBucket) avant de le créer, et MinIO répond `Access Denied` sans cette action. Elle donne au provisionneur la vue des **clés** d'un bucket, jamais leur **contenu** — la garantie « ne peut pas lire les vidéos d'une autre app » tient toujours.
|
||||
|
||||
## Alternatives Considered
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
[vibe](../../../README.md) > [Guidebooks](../../README.md) > [Factory provisioning](../README.md) > [Ansible](README.md) > **07 · Mirrors**
|
||||
|
||||
# 07 · Mirrors — Gitea → GitHub / GitLab
|
||||
|
||||
> [!NOTE]
|
||||
> **Status:** ✅ active · **Last Updated:** 2026-07-27
|
||||
> **Upstream:** [Ansible sub-hub](README.md) · [Factory provisioning hub](../README.md)
|
||||
> **Downstream:** [Roles reference](roles.md) — `gitea_repo`, `gitea_sync`, `gitea_token`
|
||||
> **Related:** [Inventory & variables](inventory.md) · [03 · CI/CD](03-cicd.md)
|
||||
|
||||
Gitea is the **source of truth**; GitHub and GitLab hold a pushed copy. [`playbooks/07_mirrors.yml`](../../../../ansible/arcodange/factory/playbooks/07_mirrors.yml) walks the repos declared in `gitea_mirrored_repos` ([`inventory/group_vars/all/gitea.yml`](../../../../ansible/arcodange/factory/inventory/group_vars/all/gitea.yml)) and, for each, calls [`gitea_repo`](../../../../ansible/arcodange/factory/roles/gitea_repo): create the counterpart repo **private** if it is missing, then attach a push mirror refreshed every **8 h** *and* on **every commit**.
|
||||
|
||||
Nothing is pulled back. A mirror only ever pushes Gitea → forge, so a change made on GitHub is overwritten at the next sync.
|
||||
|
||||
```sh
|
||||
uv run ansible-playbook -i ansible/arcodange/factory/inventory \
|
||||
ansible/arcodange/factory/playbooks/07_mirrors.yml
|
||||
|
||||
# GitHub only — while the personal GitLab namespace is still unset:
|
||||
… -e gitea_mirror_gitlab=false
|
||||
```
|
||||
|
||||
The Gitea token is minted for the run by `gitea_token` and **revoked in `post_tasks`**. Everything is tagged `gitea_mirrors`.
|
||||
|
||||
---
|
||||
|
||||
## Two ways to pick repos, and when each fits
|
||||
|
||||
| | [`gitea_sync`](../../../../ansible/arcodange/factory/roles/gitea_sync) | `gitea_mirrored_repos` + `07_mirrors.yml` |
|
||||
| --- | --- | --- |
|
||||
| Selection | Automatic: diffs the three forges for **one owner**, reconciles whatever is missing somewhere | Explicit list, reviewed in the inventory |
|
||||
| Fits | The organisation, where every repo is meant to exist everywhere | The personal account, where each repo leaving the homelab is a deliberate call |
|
||||
| Blind spot | `repos_incomplete = all − common` says nothing about *why* a repo is missing — a repo deleted on purpose from GitHub is recreated | Anything absent from the list is silently never mirrored |
|
||||
|
||||
Both drive the same `gitea_repo` role, so the mirror they produce is identical.
|
||||
|
||||
---
|
||||
|
||||
## Owner mapping
|
||||
|
||||
A Gitea repo owned by the **user** `arcodange` does not belong on the GitHub **organisation** — and GitHub does not even create it the same way (`POST /user/repos` instead of `POST /orgs/<org>/repos`). Hence three knobs, all defaulting to the previous org-only behaviour:
|
||||
|
||||
| Var | Default | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `gitea_repo_owner` | `gitea_organization` | Owner **on Gitea** |
|
||||
| `github_owner` / `gitlab_owner` | `github_organization` / `gitlab_root_group` | Owner **on the far forge** |
|
||||
| `github_owner_is_org` | `true` | `false` routes creation to `POST /user/repos` |
|
||||
| `gitea_mirror_github` / `gitea_mirror_gitlab` | `true` | Turn a forge off entirely |
|
||||
|
||||
> [!IMPORTANT]
|
||||
> GitLab was **not optional** before. Its create call expected `201` with no `ignore_errors`, so a GitLab failure aborted the iteration — including the GitHub half that had nothing to do with it. `gitea_mirror_gitlab: false` is the way out.
|
||||
|
||||
> [!WARNING]
|
||||
> A GitHub repo created **empty** adopts as its default branch the *first branch the mirror pushes*, which is routinely a work branch rather than `main`. The role realigns it against Gitea's default branch, but the mirror is asynchronous: the alignment fails on the run that creates the repo and succeeds on the next one. Run the playbook twice, or fix the branch by hand.
|
||||
|
||||
---
|
||||
|
||||
## Current state (2026-07-27)
|
||||
|
||||
| Owner | Repos mirrored | Target |
|
||||
| --- | --- | --- |
|
||||
| `arcodange-org` | 10 (`factory`, `tools`, `erp`, `cms`, `webapp`, `url-shortener`, `docker.tofu`, `docker-build-workflow`, `super-linter-workflow`, `vault-action`) | `github.com/arcodange-org/*` + GitLab |
|
||||
| `arcodange` (user) | 5 (`kadans`, `kadans-api`, `kadans-dossier`, `kadans-jobs`, `video_analysis`) — all **private** | `github.com/arcodange/*` |
|
||||
|
||||
Not mirrored, deliberately left out of `gitea_mirrored_repos`: `documents`, `studio`, `prospection`, `kissmetrics_contract_proposal` (org) and `.profile`, `DanceVideos`, `SecondBrain`, `dance-lessons-coach`, `frame-sdk`, `telegram-gateway` (user).
|
||||
|
||||
> [!NOTE]
|
||||
> The personal repos have **no GitLab mirror yet**: `gitlab_personal_namespace_id` is still `~`. Fill it with the numeric namespace ID of the `arcodange` account on gitlab.com, otherwise creation would land the project in the `arcodange-org` group.
|
||||
|
||||
---
|
||||
|
||||
## Reading the truth from Gitea
|
||||
|
||||
The push mirrors live in Gitea, not in this repo — the playbook is idempotent precisely because it asks first:
|
||||
|
||||
```sh
|
||||
curl -s -H "Authorization: token $GITEA_TOKEN" \
|
||||
https://gitea.arcodange.lab/api/v1/repos/arcodange/kadans/push_mirrors
|
||||
```
|
||||
|
||||
`last_update` tells you when the mirror last pushed. A repo with no entry has no mirror, whatever this page claims.
|
||||
@@ -5,7 +5,7 @@
|
||||
> [!NOTE]
|
||||
> **Status:** ✅ active · **Last Updated:** 2026-06-23
|
||||
> **Upstream:** [Factory provisioning hub](../README.md) · [Lab ecosystem · 01 factory](../../lab-ecosystem/01-factory.md)
|
||||
> **Downstream:** [01 · System](01-system.md) · [02 · Setup](02-setup.md) · [03 · CI/CD](03-cicd.md) · [04 · Tools](04-tools.md) · [05 · Backup](05-backup.md) · [06 · Recover](06-recover.md) · [Inventory & variables](inventory.md) · [Roles reference](roles.md)
|
||||
> **Downstream:** [01 · System](01-system.md) · [02 · Setup](02-setup.md) · [03 · CI/CD](03-cicd.md) · [04 · Tools](04-tools.md) · [05 · Backup](05-backup.md) · [06 · Recover](06-recover.md) · [07 · Mirrors](07-mirrors.md) · [Inventory & variables](inventory.md) · [Roles reference](roles.md)
|
||||
> **Related:** [Secrets & Vault](../../lab-ecosystem/secrets-and-vault.md) · [Storage & recovery](../../lab-ecosystem/storage-and-recovery.md) · [Naming conventions](../../lab-ecosystem/naming-conventions.md) · [ADR-0001 safe prod-like environment](../../../ADR/0001-safe-prod-like-environment.md)
|
||||
|
||||
Ansible is the **imperative half** of the factory: it takes three bare Raspberry Pis (`pi1`, `pi2`, `pi3`) and turns them into a running K3s cluster with Docker, Longhorn storage, Gitea CI runners, CrowdSec, and Vault. OpenTofu (the declarative half) then provisions everything that lives *outside* the cluster — see the [OpenTofu sub-hub](../opentofu/README.md).
|
||||
@@ -22,7 +22,7 @@ Everything ships as a single Ansible **collection** committed under [`ansible/ar
|
||||
| `requirements.yml` | [`ansible/requirements.yml`](../../../../ansible/requirements.yml) | External dependencies pulled at install time (see table below). |
|
||||
| `ansible.cfg` | [`ansible/arcodange/factory/ansible.cfg`](../../../../ansible/arcodange/factory/ansible.cfg) | `collections_path = ~/.ansible/collections` and `scp_if_ssh = True` for the SSH connection plugin. |
|
||||
| `inventory/` | [`ansible/arcodange/factory/inventory/`](../../../../ansible/arcodange/factory/inventory) | `hosts.yml` + `group_vars/`. Detailed in [Inventory & variables](inventory.md). |
|
||||
| `playbooks/` | [`ansible/arcodange/factory/playbooks/`](../../../../ansible/arcodange/factory/playbooks) | The numbered pipeline `01..05` plus the `recover/` branch. |
|
||||
| `playbooks/` | [`ansible/arcodange/factory/playbooks/`](../../../../ansible/arcodange/factory/playbooks) | The numbered pipeline `01..05`, the `recover/` branch, and the on-demand [`07_mirrors.yml`](../../../../ansible/arcodange/factory/playbooks/07_mirrors.yml). |
|
||||
| `roles/` | [`ansible/arcodange/factory/roles/`](../../../../ansible/arcodange/factory/roles) | Seven reusable roles. Detailed in [Roles reference](roles.md). |
|
||||
|
||||
### External dependencies (`requirements.yml`)
|
||||
|
||||
@@ -126,10 +126,10 @@ Smaller roles, mostly Gitea/forge plumbing and one-shot helpers. Shared roles li
|
||||
|
||||
| Role | Purpose | Key vars / notes | Secrets |
|
||||
| --- | --- | --- | --- |
|
||||
| [`gitea_repo`](../../../../ansible/arcodange/factory/roles/gitea_repo) | Ensure a repo exists across Gitea + GitHub + GitLab and add **8h push mirrors** (`sync_on_commit: true`) to GitHub/GitLab. | Creates missing repos on each forge; mirror URLs + namespace IDs in [`vars/main.yml`](../../../../ansible/arcodange/factory/roles/gitea_repo/vars/main.yml). | `github_api_token`, `gitlab_api_token` (from `gitea_vault`). |
|
||||
| [`gitea_repo`](../../../../ansible/arcodange/factory/roles/gitea_repo) | Ensure a repo exists across Gitea + GitHub + GitLab and add **8h push mirrors** (`sync_on_commit: true`) to GitHub/GitLab. | Creates missing repos on each forge (**private** by default). Owner is per-forge — `gitea_repo_owner` / `github_owner` / `gitlab_owner`, with `github_owner_is_org: false` for a personal account. Each forge can be switched off (`gitea_mirror_github` / `gitea_mirror_gitlab`). See [07 · Mirrors](07-mirrors.md). | `github_api_token`, `gitlab_api_token` (from `gitea_vault`). |
|
||||
| [`gitea_token`](../../../../ansible/arcodange/factory/roles/gitea_token) | Generate / replace / delete a Gitea access token via `docker exec … gitea admin user generate-access-token`. | Stores the raw token in the fact named by `gitea_token_fact_name`; `gitea_token_replace` / `gitea_token_delete` toggles; scopes default to `write:admin,organization,package,repository,user`. | The minted token itself (a fact, not persisted). |
|
||||
| [`gitea_secret`](../../../../ansible/arcodange/factory/roles/gitea_secret) | `PUT` a Gitea **Actions secret** at user or org scope. | `gitea_secret_name` / `_value`; `gitea_owner_type` (`user`\|`org`) selects the API path. | `gitea_api_token` (Authorization). |
|
||||
| [`gitea_sync`](../../../../ansible/arcodange/factory/roles/gitea_sync) | List repos on all **three forges**, diff them, and call `gitea_repo` for the repos missing somewhere. | Computes `repos_incomplete = all − common`; loops `gitea_repo` over the gaps. | GitHub/GitLab/Gitea API tokens. |
|
||||
| [`gitea_sync`](../../../../ansible/arcodange/factory/roles/gitea_sync) | List repos on all **three forges** for **one owner**, diff them, and call `gitea_repo` for the repos missing somewhere. | Computes `repos_incomplete = all − common`; loops `gitea_repo` over the gaps. `gitea_sync_owner` + `gitea_sync_owner_is_org` pick the owner (a user is not served at the same API paths). **Not currently invoked by any playbook** — the explicit list in [07 · Mirrors](07-mirrors.md) is what runs. | GitHub/GitLab/Gitea API tokens. |
|
||||
| [`traefik_certs`](../../../../ansible/arcodange/factory/roles/traefik_certs) | Extract the live **`*.arcodange.lab`** cert from Traefik's `acme.json`. | `kubectl exec` into Traefik → `jq` the LetsEncrypt wildcard cert → `traefik_cert_pem` fact; no-op if already set. | — (reads in-cluster acme.json). |
|
||||
| [`playwright`](../../../../ansible/arcodange/factory/roles/playwright) | Run a Playwright browser-automation script in Docker. | Builds `playwright:<version>` (default `1.47.0`) from `files/`, runs the script with `playwright_env` injected as `-e`; default script `loginGitea.js`. Used by `hashicorp_vault` for the OIDC app setup. | Script-specific env (e.g. Gitea admin creds). |
|
||||
| [`deploy_gitea`](../../../../ansible/arcodange/factory/playbooks/setup/roles/deploy_gitea) | Deploy Gitea: template [`app.ini.j2`](../../../../ansible/arcodange/factory/playbooks/setup/roles/deploy_gitea/tasks/main.yml), `docker compose up`, then **health-check `:3000`** until ready. | Compose source is `/home/pi/arcodange/docker_composes/gitea`; admin user `arcodange`. | (consumes the vaulted Gitea compose env). |
|
||||
|
||||
Reference in New Issue
Block a user