From dea46048281f633cbd1e04e3cc0b08f73a413051 Mon Sep 17 00:00:00 2001 From: Bertrand Benjamin Date: Thu, 20 Aug 2026 15:11:59 +0200 Subject: [PATCH] feat(forge): add forgejo --- dns/ovh_opytex_records.tf | 16 +++ stacks/forge.opytex.org/README.md | 144 +++++++++++++++++++++ stacks/forge.opytex.org/docker-compose.yml | 96 ++++++++++++++ stacks/forge.opytex.org/env_file | 80 ++++++++++++ stacks/forge.opytex.org/runner/config.yaml | 47 +++++++ stacks/forge.opytex.org/secrets.env.yaml | 26 ++++ 6 files changed, 409 insertions(+) create mode 100644 stacks/forge.opytex.org/README.md create mode 100644 stacks/forge.opytex.org/docker-compose.yml create mode 100644 stacks/forge.opytex.org/env_file create mode 100644 stacks/forge.opytex.org/runner/config.yaml create mode 100644 stacks/forge.opytex.org/secrets.env.yaml diff --git a/dns/ovh_opytex_records.tf b/dns/ovh_opytex_records.tf index 68a8f00..10db063 100644 --- a/dns/ovh_opytex_records.tf +++ b/dns/ovh_opytex_records.tf @@ -285,3 +285,19 @@ resource "ovh_domain_zone_record" "maintenant_aaaa" { ttl = 3600 target = "2001:41d0:2:bbb6:0:0:0:1" } + +resource "ovh_domain_zone_record" "forge_a" { + zone = "opytex.org" + subdomain = "forge" + fieldtype = "A" + ttl = 3600 + target = "188.165.240.182" +} + +resource "ovh_domain_zone_record" "forge_aaaa" { + zone = "opytex.org" + subdomain = "forge" + fieldtype = "AAAA" + ttl = 3600 + target = "2001:41d0:2:bbb6:0:0:0:1" +} diff --git a/stacks/forge.opytex.org/README.md b/stacks/forge.opytex.org/README.md new file mode 100644 index 0000000..44a0f0f --- /dev/null +++ b/stacks/forge.opytex.org/README.md @@ -0,0 +1,144 @@ +# Forgejo — forge.opytex.org + +Forge git. Quatre conteneurs : `forgejo` (l'application), `postgres`, +`cache` (Valkey) et `runner` (Forgejo Actions). + +Le nom de ce dossier donne le nom de projet Compose `forgeopytexorg`, dont +dérivent `forgeopytexorg_forgejo_data` et le nom du dump de base. **Le renommer +ferait repartir la forge sur des volumes vides.** + +## Déploiement + +Depuis ce dossier, sur chioggia : + +```bash +sops exec-env secrets.env.yaml 'docker compose up -d' +``` + +Les secrets sont déclarés `${VAR:?}` : **toute** commande `docker compose` +échoue sans l'enveloppe `sops exec-env`, y compris `logs`, `ps` et `exec`. + +Après une rotation de secret, `up -d` répond « up to date » et garde l'ancienne +valeur — il faut `--force-recreate`. + +## Mise en service, dans l'ordre + +Les quatre étapes ci-dessous sont à faire une seule fois, en séquence. + +### 1. Remplir les secrets + +Deux valeurs, toutes deux en **placeholder visible** (`REMPLACER-…`) : + +```bash +sops edit secrets.env.yaml +``` + +| Clé | Origine | +|---|---| +| `POSTGRES_PASSWORD` | à générer (`pwgen -s 32 1`) | +| `SSO_CLIENT_SECRET` | copié depuis le provider Authentik créé à l'étape 2 | + +`SSO_CLIENT_SECRET` n'est **pas** lu par le conteneur : il est conservé ici pour +ne pas être perdu, et injecté à la main dans la commande `add-oauth` ci-dessous. + +### 2. Créer le provider OIDC dans Authentik + +Application + provider OAuth2/OpenID, avec pour URL de redirection : + +``` +https://forge.opytex.org/user/oauth2/authentik/callback +``` + +Le segment `authentik` est le nom donné à la source à l'étape 3 : les deux +doivent coïncider. Le reste de l'URL suit `FORGEJO__server__ROOT_URL` — si le +domaine de la forge change, cette redirection doit changer avec lui. + +### 3. Déclarer la source d'authentification + +Une seule fois, forge démarrée : + +```bash +sops exec-env secrets.env.yaml 'docker compose exec -u git forgejo \ + forgejo admin auth add-oauth \ + --name authentik \ + --provider openidConnect \ + --key \ + --secret "$SSO_CLIENT_SECRET" \ + --auto-discover-url https://sso.opytex.org/application/o/forgejo/.well-known/openid-configuration \ + --scopes "openid email profile"' +``` + +Le `client_id` n'est pas un secret : il peut rester en clair dans cette commande. + +**Vérifier immédiatement qu'un compte se crée bien à la première connexion.** +Le trio `DISABLE_REGISTRATION` / `ALLOW_ONLY_EXTERNAL_REGISTRATION` / +`ENABLE_AUTO_REGISTRATION` d'`env_file` est contre-intuitif : mettre +`DISABLE_REGISTRATION=true`, ce qui semble être le réglage prudent, bloque +aussi l'auto-création via Authentik et **verrouille la forge pour tout le +monde**. Un compte admin local créé tôt évite d'y rester enfermé. + +### 4. Enregistrer le runner + +Le runner **ne s'auto-enregistre pas** par variable d'environnement — le runner +v13 ignore une partie des variables `GITEA_*`, et un jeton passé par +l'environnement finit tôt ou tard dans l'historique git. + +Récupérer un jeton dans *Administration → Runners*, puis : + +```bash +docker compose run --rm runner forgejo-runner register \ + --no-interactive \ + --instance https://forge.opytex.org \ + --token \ + --name poney \ + --config /config.yaml +``` + +Le résultat est écrit dans `/data/.runner` (volume `forgejo_runner_data`) et le +jeton n'est plus jamais nécessaire. Puis `docker compose up -d runner`. + +## Dépendance MinIO + +`runner/config.yaml` déclare le label `minio-mc`. Deux choses en dépendent, à +reprendre le jour du passage de `storage.opytex.org` (MinIO) à Garage : + +1. **Le label** `minio-mc:docker://minio/mc:latest` — image `mc` fournie aux + jobs qui le demandent en `runs-on`. +2. **L'action `lafrite/minio-deploy-action`**, appelée en URL absolue dans les + workflows, qui consomme les secrets Actions `MINIO_ENDPOINT`, + `MINIO_ACCESS_KEY` et `MINIO_SECRET_KEY`. + +Le tag `latest` combiné à `force_pull: true` fait suivre `mc` en amont sans +préavis. Rien n'est changé ici, c'est un constat. + +## Volumes et sauvegarde + +| Volume | Contenu | Sauvegarde | +|---|---|---| +| `forgeopytexorg_forgejo_data` | **tous les dépôts**, config, avatars, paquets | label `backup.enable=true` | +| `forgeopytexorg_forgejo_db` | base PostgreSQL | dump SQL (`backup.type=postgres` sur le conteneur) | +| `forgeopytexorg_forgejo_runner_data` | le seul fichier `.runner` | non sauvegardé — se refait en une commande | + +Les labels de volume ne s'appliquent qu'à la création : ceux-ci sont donc +effectifs. Ne pas en retirer ni en ajouter par la suite — Compose détecte la +divergence entre le volume et sa déclaration, et propose de le **recréer**. + +`forgejo_db` n'a délibérément aucun label : sauvegarder les fichiers d'un +PostgreSQL en cours d'écriture ne donne pas une base restaurable. + +Ajouter `forgeopytexorg_forgejo_data` à `MANUAL_VOLUMES` (`backup.conf`) reste +utile en ceinture-bretelles, comme pour les autres stacks — restic déduplique. + +## Version + +Forgejo **v15.0.3 (LTS)**, supportée jusqu'en juillet 2027. La branche non-LTS +(v16) a un horizon de support de quelques mois seulement : inadapté pour la +forge qui héberge le dépôt `infra`. + +Ne pas monter de version majeure sans sauvegarde vérifiée — les migrations de +schéma ne se reviennent pas. + +Références : + +- https://forgejo.org/docs/latest/admin/ +- https://forgejo.org/releases/ diff --git a/stacks/forge.opytex.org/docker-compose.yml b/stacks/forge.opytex.org/docker-compose.yml new file mode 100644 index 0000000..c5ee20f --- /dev/null +++ b/stacks/forge.opytex.org/docker-compose.yml @@ -0,0 +1,96 @@ +services: + forgejo: + image: codeberg.org/forgejo/forgejo:15.0.3 + restart: unless-stopped + depends_on: + - postgres + - cache + env_file: + - env_file + environment: + FORGEJO__database__PASSWD: ${POSTGRES_PASSWORD:?} + volumes: + - forgejo_data:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + # Port hôte du SSH git. Doit rester cohérent avec + # FORGEJO__server__SSH_PORT dans env_file, qui est ce que la forge + # annonce dans ses URL de clone. + - "2223:22" + networks: + - default + - traefik-proxy + - smtp + labels: + - traefik.enable=true + - traefik.docker.network=traefik-proxy + - traefik.http.routers.forgejo.entrypoints=web-secure + - traefik.http.routers.forgejo.rule=Host(`forge.opytex.org`) + - traefik.http.routers.forgejo.tls.certresolver=letsencrypt + - traefik.http.routers.forgejo.middlewares=crowdsec@file + - traefik.http.services.forgejo.loadbalancer.server.port=3000 + + postgres: + image: postgres:18 + restart: unless-stopped + env_file: + - env_file + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?} + volumes: + - forgejo_db:/var/lib/postgresql + networks: + - default + labels: + - traefik.enable=false + - backup.enable=true + - backup.type=postgres + + cache: + # Pas de `container_name` : le nom serait global au démon. Ce Valkey est + # réservé à cette forge — deux forges partageant un même Redis se volent + # leurs événements, avec des symptômes qui n'y font jamais penser. + image: valkey/valkey:9.0.3-alpine + restart: unless-stopped + healthcheck: + test: ["CMD", "valkey-cli", "ping"] + interval: 15s + timeout: 3s + retries: 30 + logging: + driver: "json-file" + options: + max-size: "1m" + networks: + - default + + + runner: + image: code.forgejo.org/forgejo/runner:13.0.0 + restart: unless-stopped + depends_on: + - forgejo + command: ["forgejo-runner", "daemon", "--config", "/config.yaml"] + volumes: + - ./runner/config.yaml:/config.yaml:ro + - forgejo_runner_data:/data + - /var/run/docker.sock:/var/run/docker.sock + networks: + - default + +volumes: + # Les labels de volume ne s'appliquent qu'à la création : ceux-ci sont + # effectifs. Ne pas en ajouter ni en retirer ensuite, Compose proposerait de + # recréer le volume. + forgejo_data: + labels: + backup.enable: true + forgejo_db: + forgejo_runner_data: + +networks: + traefik-proxy: + external: true + smtp: + external: true diff --git a/stacks/forge.opytex.org/env_file b/stacks/forge.opytex.org/env_file new file mode 100644 index 0000000..5022535 --- /dev/null +++ b/stacks/forge.opytex.org/env_file @@ -0,0 +1,80 @@ +# ============================================================================= +# FORGEJO — configuration NON sensible +# ============================================================================= +# Seul POSTGRES_PASSWORD est chiffré (secrets.env.yaml, injecté par +# `sops exec-env`). Le secret client Authentik n'est pas ici : il n'est pas lu +# par le conteneur, il se saisit une fois via `forgejo admin auth add-oauth`. +# +# Ce fichier ne contient QUE ce que les conteneurs consomment. La version +# d'image et les règles Traefik sont en dur dans docker-compose.yml : +# `env_file:` n'alimente pas l'interpolation ${...}, et une version en dur est +# détectée par Renovate. +# ============================================================================= + +# --- Base de données --- +# POSTGRES_* est lu par le conteneur postgres ET par le script de sauvegarde, +# qui fait `docker exec printenv POSTGRES_DB / POSTGRES_USER`. FORGEJO__database__* +# est lu par l'application. Les deux jeux doivent porter la MÊME valeur. +POSTGRES_USER=forgejo +POSTGRES_DB=forgejo + +FORGEJO__database__DB_TYPE=postgres +FORGEJO__database__HOST=postgres:5432 +FORGEJO__database__NAME=forgejo +FORGEJO__database__USER=forgejo + +# --- Conteneur --- +USER_UID=1000 +USER_GID=1000 +TZ=Europe/Paris + +# --- URL publique --- +# ROOT_URL est recopié dans les URL de clone affichées, dans les webhooks et +# dans le redirect OIDC : le changer suppose de reprendre le provider Authentik +# et de recréer les webhooks. +FORGEJO__server__DOMAIN=forge.opytex.org +FORGEJO__server__ROOT_URL=https://forge.opytex.org/ +FORGEJO__server__SSH_DOMAIN=forge.opytex.org +FORGEJO__server__SSH_PORT=2223 +FORGEJO__server__SSH_LISTEN_PORT=22 + +# --- Cache --- +# `cache` est le service Valkey de CETTE stack, et doit lui rester réservé : +# deux forges partageant un même Redis se volent leurs événements, avec des +# symptômes trompeurs (`AesDecrypt invalid decrypted base64 string`). +FORGEJO__cache__ENABLED=true +FORGEJO__cache__ADAPTER=redis +FORGEJO__cache__HOST=redis://cache:6379/0?pool_size=100&idle_timeout=180s +FORGEJO__cache__ITEM_TTL=24h + +# --- Inscriptions & SSO (Authentik) --- +# Combinaison volontaire et contre-intuitive : DISABLE_REGISTRATION=true +# bloquerait AUSSI l'auto-création via OIDC, et plus personne ne pourrait +# entrer. C'est ALLOW_ONLY_EXTERNAL_REGISTRATION qui interdit l'inscription +# locale tout en laissant passer Authentik. +FORGEJO__service__DISABLE_REGISTRATION=false +FORGEJO__service__ALLOW_ONLY_EXTERNAL_REGISTRATION=true +FORGEJO__service__SHOW_REGISTRATION_BUTTON=false +FORGEJO__openid__ENABLE_OPENID_SIGNIN=false +FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION=true +FORGEJO__oauth2_client__ACCOUNT_LINKING=auto +FORGEJO__oauth2_client__USERNAME=nickname +FORGEJO__oauth2_client__UPDATE_AVATAR=true + +# --- Actions & registre de paquets --- +# Le registre est une dépendance vivante de l'infra : stacks/plesna.opytex.org +# tire une image depuis cette forge. Explicite pour éviter toute désactivation +# par inadvertance. +FORGEJO__actions__ENABLED=true +FORGEJO__packages__ENABLED=true + +# --- Courriel (relais interne postfix, réseau `smtp`) --- +FORGEJO__mailer__ENABLED=true +FORGEJO__mailer__PROTOCOL=smtp +FORGEJO__mailer__SMTP_ADDR=postfix +FORGEJO__mailer__SMTP_PORT=25 +FORGEJO__mailer__FROM=noreply@opytex.org + +# --- Divers --- +FORGEJO__repository__DEFAULT_BRANCH=main +FORGEJO__security__INSTALL_LOCK=true diff --git a/stacks/forge.opytex.org/runner/config.yaml b/stacks/forge.opytex.org/runner/config.yaml new file mode 100644 index 0000000..fef40e2 --- /dev/null +++ b/stacks/forge.opytex.org/runner/config.yaml @@ -0,0 +1,47 @@ +# Configuration du Forgejo Runner. +# L'enregistrement n'est PAS fait par variable d'environnement : il se fait une +# fois, à la main, et le résultat vit dans /data/.runner (volume +# forgejo_runner_data). Voir README.md. + +log: + level: info + +runner: + file: /data/.runner + capacity: 4 + timeout: 3h + shutdown_timeout: 0s + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + labels: + - "ubuntu-latest:docker://docker.gitea.com/runner-images:ubuntu-latest" + - "ubuntu-22.04:docker://docker.gitea.com/runner-images:ubuntu-22.04" + - "ubuntu-20.04:docker://docker.gitea.com/runner-images:ubuntu-20.04" + # Consommé par les workflows qui déposent sur storage.opytex.org (MinIO). + # À revoir le jour du passage à Garage — voir README.md, section + # « Dépendance MinIO ». + - "minio-mc:docker://minio/mc:latest" + +cache: + enabled: true + dir: "" + host: "" + port: 0 + external_server: "" + +container: + network: "" + privileged: false + options: + workdir_parent: + valid_volumes: [] + # Vide = le socket Docker de l'hôte est monté dans les conteneurs de job, + # ce qu'exige docker/build-push-action (notytex). Conséquence assumée : tout + # job Actions s'exécute en root sur la machine hôte. + docker_host: "" + force_pull: true + force_rebuild: false + +host: + workdir_parent: diff --git a/stacks/forge.opytex.org/secrets.env.yaml b/stacks/forge.opytex.org/secrets.env.yaml new file mode 100644 index 0000000..c5367b1 --- /dev/null +++ b/stacks/forge.opytex.org/secrets.env.yaml @@ -0,0 +1,26 @@ +POSTGRES_PASSWORD: ENC[AES256_GCM,data:BucVeYibiQFQWzp7ncTNm/wfCT4x0eTCMCj62CA59xsp1p4XfbVq4phdpkP+cwp+lB/M9m04UiN2l5ievF5Uzg==,iv:I9vk3+/JvF4k3mbpvPqVeC6k2oF2ym9zPvBxN9p1kOA=,tag:m46/+gcDLuALftSJJmQNtQ==,type:str] +SSO_CLIENT_SECRET: ENC[AES256_GCM,data:Df+Nppy6tqv5ersu81SKleDQUWq33sIVGsfOn+oiq1jvmmLD7+TFZyTElsPLT/9wOoiHdjGOSqrEKRWW/ZnMrtlxoUmpRXbMntVcLhQIwRZNYa7YptwrLzmkdNqtYsIKYag4MYb2fnu7yqRrOpEwIInrnKhSPwgAlFbYG7yKqfo=,iv:jqDiBGOetJOwlfnhfljjN5Bbga68oaHqcK7O4Pw7fgI=,tag:H0iMVYGqx2IzlWFCM8ETRw==,type:str] +sops: + age: + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAvMGFWTVZwbVBDZ3NIdGlr + MVlGaW1QTGJsUnNHbEpVM2NITXFZTW5aaFZVCklkc2t6S1JlTmhteGNQcjdGOCtT + TS8yazMyRjdmdlpGaWVjUVpmLzlYTmsKLS0tICsvM3lkWTVqa1VyTTJINWtPRmp2 + b2VCY09ITEhCMlhXSzc5VkhtWXZHaWMK15PQvEbBK7fRNfLKMbNhXpcfFWKA/CQE + LhE+kymk1XFzB/Ce/+YgP1/ydyZOJK+uN/6+hwBMO82G//Aiep3abQ== + -----END AGE ENCRYPTED FILE----- + recipient: age18m2zl3pwgw97djshzcf4wmhrqxksl4erqse6edkpxpkg5rcz0ujs8u4c7s + - enc: | + -----BEGIN AGE ENCRYPTED FILE----- + YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpRUcwbm5rT00zV3J0TG5K + Zm1nNVRSUmtMZU03QTB3ZGJtazQ4SnlnVEVnCjAwNlNIbmtpTVZVNlE5dlhYdHBk + VnAveC9KSGx2WUJhb1pGbWJjMTNXeVEKLS0tIHUvcUlmczhrODRwVXhSUzhFN09D + ZFNiTlNwYkI1b1c5aGNtZitreGlzeXMKBrYxAUOhDW5o1oQUahpdyb6JkPZ5o/MT + w7v9A4Qwe8puT1jgIByZRxAEQfMChmGbrdy1PHoeq7DjKYayDRu8fA== + -----END AGE ENCRYPTED FILE----- + recipient: age1fh70nny5hzz8a8g9077kgfca0lhlh3wsjf4fjrv4klx5ucdtpdmsazuqdj + lastmodified: "2026-08-20T13:09:10Z" + mac: ENC[AES256_GCM,data:VDaU4oyWyIF6SeL8soPV46Aqc6aPaU3q9dZtSQDNV8hHqb7H8SLqS15Q3WacPXdO8Ee+jSydXcTf5wMENsOd2XqNoz+ClKHGZpbyglHFKOA8pNBpGnzBZ1MYgJgEB2BHal/UC17Z5J3uxhGKHD5HDyYTkQLwXkMnvoRJB74k4io=,iv:6hdeZcMpw3Pd8ZPT5eoZPOe3+QLHTJJWUMZRELuQ9iI=,tag:jhEc0vKGd7MQYq9dUNyjiA==,type:str] + unencrypted_suffix: _unencrypted + version: 3.13.3