ci: align deployment on Kuby (Docker + rsync + SSH key + healthcheck + rollback)
All checks were successful
ci / ci (push) Successful in 44s
ci / ci (pull_request) Successful in 43s

- Add HTTP /health endpoint (aiohttp) for Docker healthcheck + post-deploy check
- Dockerfile HEALTHCHECK hits /health; docker-compose exposes 1890, container vision-bot
- scripts/deploy_runner.sh runs on .117 (docker compose build/restart)
- .forgejo/workflows/deploy.yml: single job on omhk-deploy runner (clone, syntax test,
  rsync, deploy, health check, auto-rollback) like Kuby
- .forgejo/workflows/ci.yml: single job (ruff + pytest) on dev/PR
This commit is contained in:
Mathis 2026-09-19 16:04:59 +02:00
parent e32ca9c8fb
commit e22a98a4a3
8 changed files with 308 additions and 48 deletions

View file

@ -1,70 +1,46 @@
name: CI/CD name: ci
on: on:
push: push:
branches: [main, dev] branches: [dev]
pull_request: pull_request:
branches: [main] branches: [main]
jobs: jobs:
lint: ci:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Recuperation du depot
run: | run: |
git clone https://omegakubeserv.tail951d2f.ts.net/Omega_Kube/VisionApp.git . set -euo pipefail
rm -rf repo
git clone https://omegakubeserv.tail951d2f.ts.net/Omega_Kube/VisionApp.git repo
cd repo
git checkout "${{ github.sha }}" git checkout "${{ github.sha }}"
shell: bash
- name: Setup Python - name: Installation de Python
run: | run: |
set -euo pipefail
apt-get update && apt-get install -y python3 python3-pip python3-venv apt-get update && apt-get install -y python3 python3-pip python3-venv
python3 -m venv .venv python3 -m venv .venv
. .venv/bin/activate . .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt pip install -r repo/requirements.txt -r repo/requirements-dev.txt
shell: bash
- name: Ruff lint - name: Lint (ruff)
run: | run: |
set -euo pipefail
. .venv/bin/activate . .venv/bin/activate
cd repo
ruff check . ruff check .
- name: Ruff format check
run: |
. .venv/bin/activate
ruff format --check . ruff format --check .
shell: bash
test: - name: Tests (pytest)
runs-on: ubuntu-latest
steps:
- name: Checkout
run: |
git clone https://omegakubeserv.tail951d2f.ts.net/Omega_Kube/VisionApp.git .
git checkout "${{ github.sha }}"
- name: Setup Python
run: |
apt-get update && apt-get install -y python3 python3-pip python3-venv
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt -r requirements-dev.txt
- name: Syntax check
run: |
. .venv/bin/activate
python3 -m py_compile bot.py
python3 -m py_compile vision.py
python3 -m py_compile config.py
- name: Run tests
run: | run: |
set -euo pipefail
. .venv/bin/activate . .venv/bin/activate
cd repo
pytest tests/ -v || true pytest tests/ -v || true
shell: bash
deploy:
needs: [lint, test]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: |
apt-get update && apt-get install -y sshpass openssh-client
sshpass -p "${{ secrets.PROD_PASSWORD }}" ssh -o StrictHostKeyChecking=no ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} "cd /home/discord/Bots/VisionApp && git pull origin main --rebase && source venv/bin/activate && pip install -r requirements.txt && echo '${{ secrets.PROD_PASSWORD }}' | sudo -S systemctl restart vision-studio && echo 'Deploy OK'"

View file

@ -0,0 +1,166 @@
# ═══════════════════════════════════════════════════════════════════════════
# Vision Studio — Deploiement automatique via Forgejo Actions
#
# Declencheur : push sur `main`.
# Runner : forgejo-runner self-hosted (label omhk-deploy, sur .22).
# Cible : 192.168.1.117 -> conteneur Docker `vision-bot`
# (/home/discord/Bots/VisionApp, bind-mount .:/app).
#
# Pipeline : syntaxe -> rsync -> deploy -> health check -> rollback auto.
# Calque du deploy.yml de Kuby.
# ═══════════════════════════════════════════════════════════════════════════
name: deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: omhk-deploy
steps:
# ═══ CLONE ═════════════════════════════════════════════════════════
- name: Recuperation du depot (clone interne Forgejo)
run: |
set -euo pipefail
rm -rf repo
git clone --depth 20 "http://192.168.1.22:3000/Omega_Kube/VisionApp.git" repo
shell: bash
- name: Verification des outils de livraison
run: |
set -euo pipefail
command -v rsync && command -v ssh && command -v curl
shell: bash
- name: Configuration de la cle SSH (secret VISION_SSH_KEY)
env:
VISION_SSH_KEY: ${{ secrets.VISION_SSH_KEY }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
chmod 700 ~/.ssh
printf '%s\n' "$VISION_SSH_KEY" > ~/.ssh/id_vision
chmod 600 ~/.ssh/id_vision
ssh-keyscan -H 192.168.1.117 >> ~/.ssh/known_hosts 2>/dev/null
shell: bash
# ═══ TEST SYNTAXE (sur .22, rapide) ══════════════════════════════
- name: Test syntaxe Python
run: |
set -euo pipefail
cd repo
NPROC=$(nproc 2>/dev/null || echo 2)
ERR_FILE=$(mktemp)
find . -name "*.py" \
-not -path "*/backups/*" \
-not -path "*/__pycache__/*" \
-not -path "*/.git/*" \
-not -path "*/venv/*" \
-not -path "*/.venv/*" \
-print0 | xargs -0 -P "$NPROC" -I {} bash -c 'python3 -m py_compile "$1" 2>>"$2" || echo "ERREUR: $1" >>"$2"' _ {} "$ERR_FILE"
if [ -s "$ERR_FILE" ]; then
cat "$ERR_FILE"
rm -f "$ERR_FILE"
echo "DEPLOY ANNULE: erreurs de syntaxe"
exit 1
fi
rm -f "$ERR_FILE"
echo "Syntaxe Python: OK"
shell: bash
- name: Sauvegarde commit precedent (pour rollback)
run: |
set -euo pipefail
cd repo
PREV=$(git rev-parse HEAD~1 2>/dev/null || echo "")
echo "PREV_COMMIT=$PREV" >> "$GITHUB_ENV"
echo "Commit precedent: $PREV"
shell: bash
# ═══ LIVRAISON ════════════════════════════════════════════════════
- name: Livraison du code (rsync)
run: |
set -euo pipefail
cd repo
rsync -az --no-o --no-g \
--exclude '.git/' --exclude '.env' --exclude '.env.*' \
--exclude 'data/' --exclude 'logs/' --exclude '*.log' --exclude 'nohup.out' \
--exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \
--exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \
--exclude '.vscode/' --exclude '.idea/' \
-e "ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts" \
. \
discord@192.168.1.117:/home/discord/Bots/VisionApp/
shell: bash
# ═══ DEPLOY ═══════════════════════════════════════════════════════
- name: Deploiement du conteneur (docker compose, cote .117)
run: |
set -euo pipefail
cd repo
COMMIT="$(git rev-parse --short HEAD)"
CHANGED="$(git diff --name-only HEAD~20 HEAD 2>/dev/null || true)"
REBUILD="false"
if echo "$CHANGED" | grep -qE '(^|/)(requirements\.txt|Dockerfile)$'; then
REBUILD="true"
fi
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \
discord@192.168.1.117 \
"bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${COMMIT}' '${REBUILD}'"
shell: bash
# ═══ HEALTH CHECK ═════════════════════════════════════════════════
- name: Health check post-deploy
id: health_check
run: |
set -euo pipefail
echo "Attente du demarrage du bot..."
for i in $(seq 1 10); do
sleep 3
STATUS=$(curl -s -m 5 http://192.168.1.117:1890/health 2>/dev/null || echo "")
if echo "$STATUS" | grep -q '"status":"ok"'; then
echo "Health check OK (tentative $i)"
exit 0
fi
echo "Tentative $i/10..."
done
echo "HEALTH CHECK FAILED apres 10 tentatives"
exit 1
shell: bash
# ═══ ROLLBACK ═════════════════════════════════════════════════════
- name: Rollback vers commit precedent
if: failure() && steps.health_check.outcome == 'failure'
run: |
set -euo pipefail
if [ -z "${PREV_COMMIT:-}" ]; then
echo "Pas de commit precedent — rollback impossible"
exit 1
fi
echo "═══ ROLLBACK vers ${PREV_COMMIT} ═══"
cd repo
git checkout "$PREV_COMMIT"
rsync -az --no-o --no-g \
--exclude '.git/' --exclude '.env' --exclude '.env.*' \
--exclude 'data/' --exclude 'logs/' --exclude '*.log' --exclude 'nohup.out' \
--exclude '__pycache__/' --exclude 'venv/' --exclude '.venv/' \
--exclude '*.db' --exclude '*.db-journal' --exclude '*.db-wal' --exclude '*.db-shm' \
--exclude '.vscode/' --exclude '.idea/' \
-e "ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts" \
. \
discord@192.168.1.117:/home/discord/Bots/VisionApp/
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \
discord@192.168.1.117 \
"bash /home/discord/Bots/VisionApp/scripts/deploy_runner.sh main '${PREV_COMMIT}' false"
echo "ROLLBACK termine"
exit 1
shell: bash
- name: Verification finale
run: |
set -euo pipefail
ssh -i ~/.ssh/id_vision -o StrictHostKeyChecking=no -o UserKnownHostsFile=~/.ssh/known_hosts \
discord@192.168.1.117 \
"docker ps --filter name=vision-bot --format '{{.Names}} {{.Status}}' && curl -s -m 5 http://localhost:1890/health && echo"
shell: bash

View file

@ -18,6 +18,6 @@ RUN pip install --no-cache-dir -r requirements.txt
COPY . . COPY . .
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD python -c "import discord; print('ok')" || exit 1 CMD curl -fsS http://localhost:1890/health || exit 1
CMD ["python", "vision.py"] CMD ["python", "vision.py"]

View file

@ -15,6 +15,7 @@ LOGS_DIR.mkdir(exist_ok=True)
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "") DISCORD_TOKEN = os.getenv("DISCORD_TOKEN", "")
APPLICATION_ID = os.getenv("APPLICATION_ID", "") APPLICATION_ID = os.getenv("APPLICATION_ID", "")
DEV = os.getenv("DEV", "").strip() in ("1", "true", "yes") DEV = os.getenv("DEV", "").strip() in ("1", "true", "yes")
HEALTH_PORT = int(os.getenv("HEALTH_PORT", "1890"))
FORGEJO_URL = os.getenv("FORGEJO_URL", "") FORGEJO_URL = os.getenv("FORGEJO_URL", "")
FORGEJO_TOKEN = os.getenv("FORGEJO_TOKEN", "") FORGEJO_TOKEN = os.getenv("FORGEJO_TOKEN", "")

View file

@ -3,15 +3,20 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: vision-studio image: vision-vision
container_name: vision-bot
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
environment: environment:
- VISION_INSTANCE=prod - VISION_INSTANCE=prod
- HEALTH_PORT=1890
dns: dns:
- 1.1.1.1 - 1.1.1.1
- 8.8.8.8 - 8.8.8.8
ports:
- "1890:1890"
volumes: volumes:
- .:/app
- vision-data:/app/data - vision-data:/app/data
- vision-logs:/app/logs - vision-logs:/app/logs
deploy: deploy:

27
health.py Normal file
View file

@ -0,0 +1,27 @@
import logging
from aiohttp import web
logger = logging.getLogger("vision")
async def start_health_server(port: int, host: str = "0.0.0.0"):
"""Demarre un mini serveur HTTP expose par le conteneur Docker.
Le runner de deploiement interroge /health pour valider le demarrage
(health check post-deploy, comme Kuby).
"""
async def health(_request: web.Request) -> web.Response:
return web.Response(text='{"status":"ok"}', content_type="application/json")
app = web.Application()
app.router.add_get("/health", health)
app.router.add_get("/api/health", health)
runner = web.AppRunner(app)
await runner.setup()
site = web.TCPSite(runner, host, port)
await site.start()
logger.info("Health server demarre sur %s:%s", host, port)
return runner

82
scripts/deploy_runner.sh Executable file
View file

@ -0,0 +1,82 @@
#!/bin/bash
# ══════════════════════════════════════════════════════════════════════════════
# Vision Studio — Script de deploiement via Forgejo Actions (hote .117)
#
# Calque du deploy_runner.sh de Kuby.
# Tourne sur l'HOTE .117 (utilisateur discord), livre par rsync depuis
# .forgejo/workflows/deploy.yml puis execute a distance en SSH.
#
# Architecture prod .117 :
# - Vision Studio tourne dans Docker : service `vision` -> conteneur `vision-bot`
# (image `vision-vision`, restart: unless-stopped, code bind-mount .:/app).
# - Le code est livre par rsync sur /home/discord/Bots/VisionApp :
# un `docker compose restart` suffit pour recharger du code pur.
# - Si requirements.txt ou Dockerfile changent -> `docker compose build`
# est necessaire (pip install ne persiste pas). Le flag `rebuild` est
# calcule par le workflow (diff git) et passe en argument.
#
# Usage : deploy_runner.sh <branch> <commit> <rebuild>
# ══════════════════════════════════════════════════════════════════════════════
set -uo pipefail
ROOT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")/.." && pwd)"
BRANCH="${1:-main}"
COMMIT="${2:-inconnu}"
REBUILD="${3:-false}"
LOCK_FILE="/tmp/vision_deploy_runner.lock"
DEPLOY_LOG="${ROOT_DIR}/data/deploy.log"
CONTAINER="vision-bot"
log() { echo "[deploy] $(date '+%F %T') $*"; }
acquire_lock() {
if [ -f "$LOCK_FILE" ]; then
local pid
pid=$(cat "$LOCK_FILE" 2>/dev/null)
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
log "ERROR: deploy deja en cours (PID $pid)"
exit 1
fi
log "WARN: lock orphelin, nettoyage"
rm -f "$LOCK_FILE"
fi
echo $$ > "$LOCK_FILE"
}
release_lock() { rm -f "$LOCK_FILE"; }
trap release_lock EXIT
cd "$ROOT_DIR"
acquire_lock
log "Deploy demarre — branch=$BRANCH commit=$COMMIT rebuild=$REBUILD"
if ! command -v docker &>/dev/null; then
log "ERROR: docker introuvable sur l'hote"
exit 1
fi
if [ "$REBUILD" = "true" ]; then
log "requirements/Dockerfile modifies -> rebuild image Docker..."
if ! docker compose build --no-cache vision; then
log "ERROR: docker compose build echoue"
exit 1
fi
docker compose up -d --force-recreate vision
else
log "Rechargement du code (bind-mount) -> redemarrage conteneur..."
docker compose up -d vision
docker compose restart vision
fi
sleep 5
if ! docker ps --filter "name=${CONTAINER}" --filter "status=running" --format '{{.Names}}' | grep -q "${CONTAINER}"; then
log "ERROR: conteneur ${CONTAINER} non demarre"
docker logs --tail 40 "${CONTAINER}" 2>&1 || true
exit 1
fi
mkdir -p "$(dirname "$DEPLOY_LOG")"
echo "$(date '+%F %T') | $BRANCH | $COMMIT | rebuild=$REBUILD | auto" >> "$DEPLOY_LOG"
log "Deploy termine — conteneur ${CONTAINER} en marche"

View file

@ -4,6 +4,7 @@ import sys
import config import config
from bot import VisionBot from bot import VisionBot
from health import start_health_server
logging.basicConfig( logging.basicConfig(
level=logging.INFO, level=logging.INFO,
@ -24,8 +25,10 @@ async def main():
instance = "DEV" if config.DEV else "PROD" instance = "DEV" if config.DEV else "PROD"
logger.info(f"Lancement de Vision Studio [{instance}]...") logger.info(f"Lancement de Vision Studio [{instance}]...")
bot = VisionBot()
await start_health_server(config.HEALTH_PORT)
bot = VisionBot()
try: try:
await bot.start(config.DISCORD_TOKEN) await bot.start(config.DISCORD_TOKEN)
except KeyboardInterrupt: except KeyboardInterrupt: