feat(forge): add forgejo
This commit is contained in:
@@ -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"
|
||||
}
|
||||
|
||||
144
stacks/forge.opytex.org/README.md
Normal file
144
stacks/forge.opytex.org/README.md
Normal file
@@ -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 <CLIENT_ID_AUTHENTIK> \
|
||||
--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 <JETON> \
|
||||
--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/
|
||||
96
stacks/forge.opytex.org/docker-compose.yml
Normal file
96
stacks/forge.opytex.org/docker-compose.yml
Normal file
@@ -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
|
||||
80
stacks/forge.opytex.org/env_file
Normal file
80
stacks/forge.opytex.org/env_file
Normal 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
|
||||
47
stacks/forge.opytex.org/runner/config.yaml
Normal file
47
stacks/forge.opytex.org/runner/config.yaml
Normal file
@@ -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:
|
||||
26
stacks/forge.opytex.org/secrets.env.yaml
Normal file
26
stacks/forge.opytex.org/secrets.env.yaml
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user