CI/CD — Gitea Actions¶
Pipeline d'intégration/déploiement continu pour les repos hébergés sur gitea.carai.be, via Gitea Actions + act_runner self-hosted. Compatible avec la syntaxe GitHub Actions (réutilise actions/checkout@v4, actions/setup-python@v5, actions/setup-node@v4, etc.).
Architecture¶
flowchart LR
Dev[Dev push] --> Gitea
Gitea -- webhook --> Discord[Discord #dev-ci]
Gitea -- long-poll --> Runner[act-runner]
Runner -- docker spawn --> Job1[Job container<br/>catthehacker/ubuntu]
Runner -- docker spawn --> Job2[Job container]
Job1 --> Tests((Tests / Build))
Job2 -- POST webhook --> Portainer
Portainer -- pull image + redeploy --> AppStack[Stack applicatif]
Job1 -- curl --> Discord - Gitea héberge le code + les workflows + le registry container
- act_runner (stack
act-runner, Id Portainer 30) tourne sur le Kimsufi et execute les workflows dans des conteneurs Docker éphémères - Discord reçoit les notifs (webhook natif Gitea + custom dans les workflows)
- Portainer webhook déclenche le redeploy d'une stack à la fin du CI
act-runner — configuration¶
- Repo :
gitea.carai.be/phantoms/act-runner - Image :
gitea/act_runner:latest - Network :
core_internal_net(pour communiquer avec gitea container) - Instance URL :
https://gitea.carai.be(URL publique — voir gotcha plus bas) - Labels = images Docker proposées aux workflows via
runs-on:: ubuntu-latest→catthehacker/ubuntu:act-latest(Node + Python + Go + build-essential + git + docker-cli + gh CLI)ubuntu-22.04→catthehacker/ubuntu:act-latestubuntu-20.04→catthehacker/ubuntu:act-20.04
Le runner mount /var/run/docker.sock (pour spawn les conteneurs de jobs).
Pourquoi catthehacker plutôt que node:22-bookworm ou python:3.12-slim¶
Les actions GitHub Actions standards (actions/checkout, actions/setup-*, etc.) sont écrites en JavaScript et nécessitent node dans le PATH du conteneur de job. Une image langage-spécifique (genre python:slim) plante avec exec: "node": executable file not found in $PATH.
catthehacker/ubuntu:act-latest (~2.3 Go) reproduit l'environnement GitHub Actions self-hosted standard : Node + Python + Go + Java + gh CLI + docker-cli + build-essential. C'est le choix par défaut pour Gitea Actions self-hosted.
Secrets disponibles dans les workflows¶
Définis par repo dans Settings → Actions → Secrets (chiffrés en DB Gitea, exposés aux workflows via ${{ secrets.NOM }}).
Pour phantoms/Topfrag :
| Secret | Usage |
|---|---|
DISCORD_WEBHOOK | URL webhook canal Discord #dev-ci — pour notifs CI/deploy custom |
PORTAINER_WEBHOOK_URL | URL webhook stack Portainer topfrag (Id 6) — déclenche git pull + docker compose up -d |
Pour ajouter un secret via API Gitea :
curl -X PUT \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
https://gitea.carai.be/api/v1/repos/<owner>/<repo>/actions/secrets/<NAME> \
-d '{"data": "valeur-du-secret"}'
Notifications Discord¶
Deux canaux Discord en place :
#kimsufi-alerts— alertes infra (backups, uptime, monitoring)#dev-ci— activité dev (Gitea push/PR/issue + résultats CI)
Sur les repos : webhook natif Gitea (events PR/issue/release)¶
Configuré via Settings → Webhooks → type Discord. Sur phantoms/Topfrag le webhook (id=3) couvre pull_request, issues, issue_comment, release... Le push event a été retiré pour éviter la duplication avec les notifs des workflows CI qui apportent l'info utile (status pass/fail des tests/build).
Dans les workflows : ping custom via curl¶
Step type à mettre à la fin d'un job CI :
- name: Notify Discord
if: always()
run: |
STATUS="${{ job.status }}"
EMOJI=$([ "$STATUS" = "success" ] && echo "✅" || echo "❌")
curl -H "Content-Type: application/json" \
-d "{\"username\":\"Topfrag CI\",\"content\":\"$EMOJI \`${{ gitea.repository }}\` (${{ gitea.ref_name }}) — **$STATUS**\\nCommit: ${{ gitea.sha }}\"}" \
"${{ secrets.DISCORD_WEBHOOK }}"
NB : gitea.* au lieu de github.* (le contexte est renommé chez Gitea, le reste est identique à la doc GitHub Actions).
Workflow type pour Topfrag¶
.gitea/workflows/ci.yml à la racine du repo :
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
jobs:
backend:
name: Backend (tests)
runs-on: ubuntu-latest # → catthehacker/ubuntu:act-latest (Python dispo)
defaults: { run: { working-directory: backend } }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip' # ← cache pip via Gitea Act
cache-dependency-path: backend/pyproject.toml
- run: pip install -e ".[dev,ebp]"
- run: pytest
frontend:
name: Frontend (typecheck + build)
runs-on: ubuntu-latest
defaults: { run: { working-directory: frontend } }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- run: npm ci
- run: npm run build
notify-ci:
name: Notify Discord (CI)
needs: [backend, frontend]
if: always()
runs-on: ubuntu-latest
steps:
- name: Send notification
run: |
if [ "${{ needs.backend.result }}" = "success" ] && [ "${{ needs.frontend.result }}" = "success" ]; then
STATUS="success"; EMOJI="✅"
else
STATUS="failure"; EMOJI="❌"
fi
curl -H "Content-Type: application/json" \
-d "{\"username\":\"Topfrag CI\",\"content\":\"$EMOJI \`${{ gitea.repository }}\` (${{ gitea.ref_name }}) — **$STATUS**\nBackend: ${{ needs.backend.result }} · Frontend: ${{ needs.frontend.result }}\nCommit: ${{ gitea.sha }}\"}" \
"${{ secrets.DISCORD_WEBHOOK }}"
deploy:
name: Deploy (Portainer redeploy)
needs: [backend, frontend]
if: github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.backend.result == 'success' && needs.frontend.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Trigger Portainer webhook
run: curl -fSL -X POST "${{ secrets.PORTAINER_WEBHOOK_URL }}"
- name: Notify Discord (deploy)
if: always()
run: |
STATUS="${{ job.status }}"
EMOJI=$([ "$STATUS" = "success" ] && echo "🚀" || echo "💥")
curl -H "Content-Type: application/json" \
-d "{\"username\":\"Topfrag CI\",\"content\":\"$EMOJI Deploy $STATUS — \`${{ gitea.sha }}\`\"}" \
"${{ secrets.DISCORD_WEBHOOK }}"
Setup un nouveau repo pour Gitea Actions¶
- Activer Actions sur le repo :
Settings → Repository → Actions → cocher Enable(activé par défaut en Gitea 1.21+). - Ajouter les secrets (Settings → Actions → Secrets) selon ce dont le workflow a besoin (typiquement
DISCORD_WEBHOOK, parfoisPORTAINER_WEBHOOK_URL). - Configurer webhook Discord natif (optionnel, Settings → Webhooks → Discord) en filtrant les events utiles (PR + issues + release, pas push si CI déjà notifie).
- Créer
.gitea/workflows/<name>.ymlà la racine du repo. - Push → le runner pick le job automatiquement (visible dans
https://gitea.carai.be/<owner>/<repo>/actions).
Récupérer les logs d'un job (debug)¶
Si l'API /api/v1/repos/.../actions/tasks/<id>/logs retourne 404 (Gitea < 1.27), les logs sont stockés sur disque dans le conteneur Gitea, compressés en zstd :
docker run --rm -v gitea_gitea_data:/g alpine sh -c '
apk add zstd >/dev/null
zstd -dc /g/actions_log/<owner>/<repo>/<NN>/<task_id>.log.zst | tail -50
'
Le préfixe <NN> (2 chars) correspond aux 2 derniers chars de l'ID en hex.
Gotchas appris en route¶
1. node: executable file not found¶
Cause : container: minimal (genre python:3.12-slim) sans node. Fix : utiliser catthehacker/ubuntu:act-latest ou ne pas mettre container: (le runner mappe ubuntu-latest sur catthehacker automatiquement).
2. Could not resolve host: gitea dans le job¶
Cause : GITEA_INSTANCE_URL=http://gitea:3000 (interne) mais les jobs spawn sur un réseau Docker temporaire qui n'a PAS le DNS interne. Fix : GITEA_INSTANCE_URL=https://gitea.carai.be (URL publique). Le clone passe par Caddy depuis l'IP du Kimsufi → whitelistée CrowdSec.
3. CrowdSec bannit l'IP du Kimsufi¶
Cause : burst de 401/403 lors des essais docker login, ou des call CI répétés. Symptôme : docker login ou git clone échoue avec 403 Forbidden. Fix : ~/cs-whitelist.sh add 54.37.255.22 (déjà fait — voir BACKUP.md).
4. pip install lent (5-10 min) sur deps ML lourdes¶
Cause : pas de cache entre runs, re-télécharge ~1 Go de wheels à chaque fois. Fix : actions/setup-python@v5 with: cache: 'pip' — Gitea Act Runner a un cache server intégré qui stocke ~/.cache/pip par hash de pyproject.toml. Premier run ~10 min, suivants ~30 s.
5. Labels du runner figés après registration¶
Cause : les labels sont sauvegardés dans /data/.runner au moment de l'enregistrement initial. Changer l'env GITEA_RUNNER_LABELS au redeploy ne suffit PAS. Fix : éditer /data/.runner à la main (sed via alpine helper container) ou wiper le volume et re-register avec un nouveau token.
Liens utiles¶
- UI runners : https://gitea.carai.be/-/admin/actions/runners
- UI tâches du repo : https://gitea.carai.be/phantoms/Topfrag/actions
- Doc Gitea Actions : https://docs.gitea.com/usage/actions/overview
- Doc act_runner : https://gitea.com/gitea/act_runner
- Image catthehacker : https://github.com/catthehacker/docker_images