Aller au contenu

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-latestcatthehacker/ubuntu:act-latest (Node + Python + Go + build-essential + git + docker-cli + gh CLI)
  • ubuntu-22.04catthehacker/ubuntu:act-latest
  • ubuntu-20.04catthehacker/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

  1. Activer Actions sur le repo : Settings → Repository → Actions → cocher Enable (activé par défaut en Gitea 1.21+).
  2. Ajouter les secrets (Settings → Actions → Secrets) selon ce dont le workflow a besoin (typiquement DISCORD_WEBHOOK, parfois PORTAINER_WEBHOOK_URL).
  3. Configurer webhook Discord natif (optionnel, Settings → Webhooks → Discord) en filtrant les events utiles (PR + issues + release, pas push si CI déjà notifie).
  4. Créer .gitea/workflows/<name>.yml à la racine du repo.
  5. 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