MYNDKOR Kit ZIP
Client Windows · GitHub Actions

Procédure : workflows, fichiers YAML, runner

Réponse courte : le client n’installe pas les runners hébergés GitHub. Il crée .github\workflows, y met le YAML, configure Secrets/Environments, et n’installe un Self-Hosted Runner Windows que pour déployer sur sa machine / son serveur.

Thomasdsf/myndkor-chat-demo test = GitHub-hosted deploy local = Self-hosted
À retenir pour le client

runs-on: ubuntu-latest → cloud GitHub, zéro install.
runs-on: self-hosted → installer le runner sur le Windows cible.

Faut-il installer le « hosted runner » GitHub ?

FAQ
GitHub-hostedSelf-hosted Windows
Où ça tourne ?Cloud GitHubPC / serveur Windows du client
Installation chez le client ?NONOUI (si deploy on-prem/local)
Dans nos YAMLJob testJob deploy-prod (cas local)
Cas Azure / AWS OIDCSuffit souvent tout seulPas nécessaire
Comment l’avoir ?Automatique dès Actions activéSettings → Actions → Runners → New → Windows
Phrase client : « GitHub fournit déjà des machines pour les tests. On installe un runner seulement sur la machine qui doit recevoir la production. »

Procédure Windows client (complète)

À remettre
Fichier kit : examples/cicd/PROCEDURE-WINDOWS-CLIENT.md — aussi dans le ZIP.

Installer Git + Python (PATH)

Pas de runner hosted à installer. Juste Git et Python 3.12.

Créer le repo GitHub

Public (Free) ou Pro si privé. Sans README si push du kit.

Créer les dossiers locaux

%USERPROFILE%\demo_dev (travail) et demo_prod (cible deploy).

Créer .github\workflows et coller 1 YAML

Ex. ci-cd-local-runner.yml pour la démo Windows.

Push + branches main / recette / prod

GitHub détecte automatiquement les workflows.

UI : Environment production + Ruleset + check test

Required reviewers = gate d’approbation.

Installer Self-Hosted Runner Windows + run.cmd

Statut Idle. Job test reste en cloud.

Valider Fail Fast → Merge → Approve → :8001

Preuve bout-en-bout.

examples/cicd/PROCEDURE-WINDOWS-CLIENT.md
# Procédure client Windows — GitHub Actions de A à Z

Public : client **Windows 10/11** (ou Windows Server)  
Repo modèle : https://github.com/Thomasdsf/myndkor-chat-demo  
À animer / remettre tel quel.

---

## 1. Ce qu’il faut comprendre avant d’installer quoi que ce soit

### Deux types de « runners »

| Type | C’est quoi ? | Faut-il l’installer chez le client ? |
|---|---|---|
| **GitHub-hosted** (`ubuntu-latest`, `windows-latest`) | Machines **fournies par GitHub** dans le cloud | **NON** — rien à installer. Dès qu’Actions est actif, ça tourne. |
| **Self-hosted** (`runs-on: self-hosted`) | Logiciel runner installé **sur le PC/serveur Windows du client** | **OUI** — seulement si on déploie **chez eux** (dossier local, IIS, serveur on-prem…). |

### Dans nos YAML Myndkor

```text
Job "test"     → runs-on: ubuntu-latest     → GitHub-hosted (aucun install)
Job "deploy"   → runs-on: self-hosted       → Runner Windows à installer sur le poste/serveur cible
```

**Donc :**
- Pour **tester** (pytest) → pas besoin d’installer de runner.
- Pour **déployer en prod locale / on-prem Windows** → il faut installer le **Self-Hosted Runner** sur la machine Windows de prod (ou le poste de démo).
- Pour **Azure Web App / AWS App Runner** (exemples cloud) → en général **pas** de self-hosted : tout passe en GitHub-hosted + login cloud (OIDC).

> « Hosted runner GitHub » = déjà inclus. On n’installe **pas** un produit « GitHub Hosted Runner » chez le client.

---

## 2. Prérequis logiciels (poste Windows client)

Installer **une fois** (PowerShell pour vérifier) :

| Logiciel | Pourquoi | Vérif |
|---|---|---|
| [Git for Windows](https://git-scm.com/download/win) | clone / commit / push | `git --version` |
| [Python 3.12](https://www.python.org/downloads/windows/) cocher **Add to PATH** | app + venv | `python --version` |
| Compte GitHub + droits admin sur le repo | Settings, Actions, Environments | — |
| (Démo chat) [Ollama](https://ollama.com/download/windows) + modèle | runtime IA | `ollama list` |
| Navigateur | UI GitHub | — |

```powershell
git --version
python --version
pip --version
```

Si `python` inconnu : réinstaller Python avec « Add to PATH », puis **fermer/rouvrir** PowerShell.

---

## 3. Créer le dépôt et l’arborescence (workflows inclus)

### 3.1 Créer le repo GitHub

1. https://github.com/new  
2. Nom : ex. `myndkor-chat-demo`  
3. Visibilité : **Public** si compte Free (sinon Environments/Rulesets incomplets) — ou Pro/Team si Privé  
4. **Ne pas** cocher « Add README » si vous allez pousser le kit ensuite  

### 3.2 Créer les dossiers locaux Windows

```powershell
# Dossier de travail (DEV)
$Dev = "$env:USERPROFILE\demo_dev"
New-Item -ItemType Directory -Force -Path $Dev | Out-Null
cd $Dev

# Dossier cible PROD locale (utilisé par le runner self-hosted)
$Prod = "$env:USERPROFILE\demo_prod"
New-Item -ItemType Directory -Force -Path $Prod | Out-Null
```

### 3.3 Mettre les fichiers du projet + créer `.github\workflows`

Depuis le **kit ZIP** décompressé (ex. `Downloads\kit`) :

```powershell
$Kit = "$env:USERPROFILE\Downloads\kit"   # <-- adapter le chemin

# Fichiers applicatifs
Copy-Item "$Kit\app.py","$Kit\test_app.py","$Kit\requirements.txt","$Kit\.env.example","$Kit\.gitignore" -Destination $Dev -Force
Copy-Item "$Kit\Dockerfile","$Kit\.dockerignore" -Destination $Dev -Force -ErrorAction SilentlyContinue

# === CRÉER LE DOSSIER WORKFLOWS (obligatoire) ===
New-Item -ItemType Directory -Force -Path "$Dev\.github\workflows" | Out-Null

# === METTRE LE YAML ACTIF (1 seul pour commencer) ===
# Démo / prod locale Windows :
Copy-Item "$Kit\.github\workflows\ci-cd-local-runner.yml" `
  -Destination "$Dev\.github\workflows\ci-cd-local-runner.yml" -Force
```

Arborescence attendue :

```text
C:\Users\<user>\demo_dev\
├── .github\
│   └── workflows\
│       └── ci-cd-local-runner.yml     ← pipeline actif
├── app.py
├── test_app.py
├── requirements.txt
├── .env.example
├── .gitignore
└── Dockerfile                         (optionnel)
```

### 3.4 (Plus tard) Ajouter un autre exemple client

Les autres YAML sont dans `kit\examples\cicd\` — **ne les copiez pas tous** d’un coup.

```powershell
# Exemple : basculer vers Azure (désactive le local en le renommant)
Rename-Item "$Dev\.github\workflows\ci-cd-local-runner.yml" "ci-cd-local-runner.yml.disabled"
Copy-Item "$Kit\examples\cicd\ci-cd-azure-webapp.yml" `
  -Destination "$Dev\.github\workflows\ci-cd-azure-webapp.yml"
```

> 1 YAML de deploy actif = 1 stratégie. Plusieurs YAML qui écoutent `push` sur `prod` = plusieurs deploys en parallèle.

---

## 4. Premier commit / push + branches

```powershell
cd $Dev
git init -b main
git add .
git commit -m "chore: bootstrap app + github actions workflow"
git remote add origin https://github.com/<ORG>/<REPO>.git
# ex: https://github.com/Thomasdsf/myndkor-chat-demo.git
git push -u origin main

git branch recette
git push -u origin recette
git branch prod
git push -u origin prod
```

Vérifier dans GitHub : onglet **Actions** — le workflow apparaît.

---

## 5. Configurer GitHub (UI) — avant le runner

### 5.1 Activer Actions (si besoin)
Repo → **Settings → Actions → General**  
- « Allow all actions and reusable workflows »  
- Save

### 5.2 Environment `production` (gate d’approbation)
**Settings → Environments → New environment → `production`**
- ✅ **Required reviewers** : le DevOps / vous
- **Environment secrets** : `OLLAMA_API_KEY` (valeur factice OK en lab)
- **Environment variables** : `OLLAMA_HOST`, `APP_TITLE`

### 5.3 Ruleset sur `prod`
**Settings → Rules → Rulesets → New**
- Target : branche `prod`
- Require pull request before merging
- Require status checks to pass → ajouter le check nommé **`test`**
- Block force pushes

### 5.4 (Public Free) Secret Scanning + Push Protection
**Settings → Code security**

---

## 6. Installer le Self-Hosted Runner Windows (pour le job deploy)

### Quand l’installer ?
- ✅ Déploiement vers `%USERPROFILE%\demo_prod`, serveur Windows, IIS, dossier local  
- ❌ Uniquement Azure/AWS cloud via OIDC → **pas nécessaire**

### 6.1 Télécharger depuis GitHub
1. Repo → **Settings → Actions → Runners → New self-hosted runner**  
2. Choisir **Windows** / **x64**  
3. Laisser la page ouverte : elle affiche les commandes + un **token** temporaire

Lien type : `https://github.com/<ORG>/<REPO>/settings/actions/runners/new`

### 6.2 Commandes PowerShell (souvent en Administrateur)

```powershell
# Dossier runner (séparé de demo_dev)
mkdir $env:USERPROFILE\actions-runner
cd $env:USERPROFILE\actions-runner

# Coller ICI les 3 commandes affichées par GitHub :
# 1) Invoke-WebRequest ... actions-runner-win-x64-....zip
# 2) Extract archive
# 3) .\config.cmd --url https://github.com/<ORG>/<REPO> --token <TOKEN_UI>
```

Pendant `config.cmd` :
- Runner group : défaut  
- Runner name : ex. `win-demo-01`  
- Labels : laisser défaut (`self-hosted`, `Windows`, `X64`) — **important** car le YAML dit `runs-on: self-hosted`  
- Work folder : défaut `_work`

### 6.3 Démarrer le runner

**Pendant un atelier / test :**
```powershell
cd $env:USERPROFILE\actions-runner
.\run.cmd
```
Laisser la fenêtre ouverte. Statut GitHub : **Idle** (vert).

**En production Windows (service permanent) :**
```powershell
# PowerShell Administrateur, dans actions-runner
.\svc.cmd install
.\svc.cmd start
.\svc.cmd status
```

### 6.4 Ce que GitHub-hosted fait pendant ce temps
Rien à faire : le job `test` sur `ubuntu-latest` démarre tout seul dans le cloud GitHub (minutes Actions du compte).

---

## 7. Lancer l’app DEV (optionnel mais utile pour la démo)

```powershell
cd $env:USERPROFILE\demo_dev
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# Si erreur scripts : Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

pip install -r requirements.txt
Copy-Item .env.example .env
notepad .env
# APP_ENV=DEV
# APP_TITLE=Chat DEV
# OLLAMA_HOST=http://127.0.0.1:11434

uvicorn app:app --host 0.0.0.0 --port 8000
# Navigateur : http://127.0.0.1:8000
```

La PROD (`:8001` dans `demo_prod`) sera démarrée **par le runner** après Approve.

---

## 8. Scénario de validation (Fail Fast → Deploy)

```powershell
cd $env:USERPROFILE\demo_dev
git checkout recette

# Casser le test volontairement
(Get-Content app.py) -replace 'REQUIRED_MODEL = "gemma4:32"','REQUIRED_MODEL = "WRONG"' |
  Set-Content app.py -Encoding utf8
git add -A
git commit -m "feat: break model for fail-fast"
git push origin recette
```

1. Ouvrir PR `recette` → `prod`  
2. Job **test** (GitHub-hosted) → **rouge** → Merge bloqué  
3. Corriger :
```powershell
(Get-Content app.py) -replace 'REQUIRED_MODEL = "WRONG"','REQUIRED_MODEL = "gemma4:32"' |
  Set-Content app.py -Encoding utf8
git add app.py
git commit -m "fix: restore REQUIRED_MODEL"
git push origin recette
```
4. Test **vert** → **Merge**  
5. Job deploy → **Waiting** (environment production)  
6. **Review deployments → Approve and deploy**  
7. Le **self-hosted Windows** copie vers `demo_prod` et démarre `:8001`  
8. http://127.0.0.1:8001 → bandeau PROD

---

## 9. Checklist « client Windows prêt »

### Logiciels
- [ ] Git + Python 3.12 (PATH)
- [ ] (si besoin) Ollama

### Repo GitHub
- [ ] Fichiers app poussés
- [ ] Dossier `.github\workflows\` créé
- [ ] **Un** YAML actif collé dedans
- [ ] Branches `main` / `recette` / `prod`
- [ ] Environment `production` + reviewers + secrets
- [ ] Ruleset `prod` + check `test`

### Runners
- [ ] Compris : **test = GitHub-hosted** (rien à installer)
- [ ] **Self-hosted Windows** installé + Idle (si deploy local/on-prem)
- [ ] Labels OK (`self-hosted`)

### Preuve
- [ ] PR Fail Fast rouge puis vert
- [ ] Approve environment
- [ ] App PROD répond

---

## 10. FAQ rapide (à dire au client)

**Q : Je dois installer GitHub Actions Hosted Runner ?**  
R : Non. Les runners hébergés GitHub sont dans le cloud. Vous ne les installez pas.

**Q : Alors qu’est-ce qu’on installe ?**  
R : Uniquement le **Self-Hosted Runner** sur la machine Windows qui doit recevoir le déploiement (ou pour une démo locale).

**Q : Où créer les fichiers YAML ?**  
R : Toujours  
`\.github\workflows\mon-pipeline.yml`  
GitHub les détecte automatiquement après push.

**Q : Docker / Drone ?**  
R : Autre procédure (`examples/drone/PROCEDURE-DRONE.md`). Pas de dossier `.github/workflows` : fichier `.drone.yml` à la racine + runner Drone Docker.

**Q : Compte Free + repo privé ?**  
R : Environments / Rulesets limités → passer **Public** ou GitHub Pro.

Les commandes essentielles (antisèche)

PowerShell
1) Dossiers + workflows
$Dev = "$env:USERPROFILE\demo_dev"
$Prod = "$env:USERPROFILE\demo_prod"
$Kit = "$env:USERPROFILE\Downloads\kit"   # adapter

New-Item -ItemType Directory -Force -Path $Dev,$Prod | Out-Null
cd $Dev

Copy-Item "$Kit\app.py","$Kit\test_app.py","$Kit\requirements.txt","$Kit\.env.example","$Kit\.gitignore" -Destination . -Force

# Créer le dossier workflows GitHub Actions
New-Item -ItemType Directory -Force -Path ".\github\workflows" | Out-Null

# Mettre LE yaml actif (un seul)
Copy-Item "$Kit\.github\workflows\ci-cd-local-runner.yml" `
  -Destination ".\github\workflows\ci-cd-local-runner.yml" -Force

# (Plus tard) autre cas client depuis examples\cicd\*.yml — un seul à la fois
2) Git push + branches
cd $env:USERPROFILE\demo_dev
git init -b main
git add .
git commit -m "chore: bootstrap app + workflow"
git remote add origin https://github.com/Thomasdsf/myndkor-chat-demo.git
git push -u origin main
git branch recette; git push -u origin recette
git branch prod; git push -u origin prod
3) Self-Hosted Runner Windows
# UI : Repo → Settings → Actions → Runners → New self-hosted runner → Windows x64
# Coller les commandes GitHub (download + unzip + config.cmd)

mkdir $env:USERPROFILE\actions-runner
cd $env:USERPROFILE\actions-runner
# ... commandes affichées par GitHub (URL + TOKEN) ...

.\run.cmd
# OU service permanent (Admin) :
# .\svc.cmd install
# .\svc.cmd start

# Vérifier statut Idle sur la page Runners
# Rappel : le job test utilise ubuntu-latest (cloud) — RIEN à installer pour ça
4) UI GitHub (clics)
1. https://github.com/Thomasdsf/myndkor-chat-demo/settings/actions → Allow actions
2. https://github.com/Thomasdsf/myndkor-chat-demo/settings/environments → New "production"
   - Required reviewers
   - Secret OLLAMA_API_KEY
   - Vars OLLAMA_HOST, APP_TITLE
3. https://github.com/Thomasdsf/myndkor-chat-demo/settings/rules → Ruleset branche prod
   - Require PR
   - Require status check "test"
4. https://github.com/Thomasdsf/myndkor-chat-demo/settings/actions/runners → runner Idle

YAML actif démo (Self-hosted Windows)

.github/workflows
.github/workflows/ci-cd-local-runner.yml
# =============================================================================
# CAS 1 — CI/CD Local via GitHub Self-Hosted Runner (DÉMO LIVE — Windows)
# =============================================================================
# Job test  : PR + push → pytest (fail-fast) sur ubuntu-latest (cloud GitHub)
# Job deploy: merge sur branche prod → environment: production (approbation
#             manuelle) → copie vers %USERPROFILE%\demo_prod + port 8001
#
# Poste formateur (LIVE) : Windows 10/11 + runner self-hosted Windows
# Dossiers :
#   %USERPROFILE%\demo_dev   → travail / port 8000
#   %USERPROFILE%\demo_prod  → cible déploiement / port 8001
#
# Prérequis GitHub :
#   - Environnement "production" + secret OLLAMA_API_KEY
#   - Variables : OLLAMA_HOST, APP_TITLE
# =============================================================================
name: CI/CD Local Self-Hosted Runner

on:
  pull_request:
    branches: [prod, recette, main]
  push:
    branches: [prod, recette, main]

permissions:
  contents: read
  checks: write

concurrency:
  group: local-cicd-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    name: test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: pip

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Run pytest (Fail Fast)
        env:
          MODEL_NAME: gemma4:32
          APP_ENV: RECETTE
          APP_TITLE: Myndkor Chat Demo
          OLLAMA_HOST: http://127.0.0.1:11434
        run: pytest -q

  deploy-prod:
    name: deploy-prod
    needs: test
    if: github.ref == 'refs/heads/prod' && github.event_name == 'push'
    runs-on: self-hosted
    environment:
      name: production
      url: http://127.0.0.1:8001
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      # ---------- LIVE DÉMO : Windows PowerShell ----------
      - name: Déployer vers demo_prod (Windows — LIVE)
        if: runner.os == 'Windows'
        shell: powershell
        env:
          OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }}
          OLLAMA_HOST: ${{ vars.OLLAMA_HOST }}
          APP_TITLE: ${{ vars.APP_TITLE }}
        run: |
          $ErrorActionPreference = "Stop"
          $Target = Join-Path $env:USERPROFILE "demo_prod"
          New-Item -ItemType Directory -Force -Path $Target | Out-Null
          Write-Host "Cible PROD = $Target"

          $exclude = @('.git', '.venv', '__pycache__', '.env', 'app.pid', 'app.log', 'app.err')
          Get-ChildItem -Force | Where-Object { $exclude -notcontains $_.Name } | ForEach-Object {
            Copy-Item $_.FullName -Destination $Target -Recurse -Force
          }

          $hostVal = if ($env:OLLAMA_HOST) { $env:OLLAMA_HOST } else { "http://127.0.0.1:11434" }
          $titleVal = if ($env:APP_TITLE) { $env:APP_TITLE } else { "Myndkor Chat PROD" }
          @(
            "OLLAMA_HOST=$hostVal"
            "OLLAMA_API_KEY=$($env:OLLAMA_API_KEY)"
            "MODEL_NAME=gemma4:32"
            "APP_ENV=PROD"
            "APP_TITLE=$titleVal"
          ) | Set-Content -Path (Join-Path $Target ".env") -Encoding ascii

          Set-Location $Target
          python -m venv .venv
          & .\.venv\Scripts\python.exe -m pip install -q --upgrade pip
          & .\.venv\Scripts\python.exe -m pip install -q -r requirements.txt

          # Libère le port 8001 si une ancienne instance tourne
          Get-NetTCPConnection -LocalPort 8001 -ErrorAction SilentlyContinue |
            ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }
          Start-Sleep -Seconds 1

          Get-Content .env | Where-Object { $_ -and $_ -notmatch '^\s*#' } | ForEach-Object {
            $k, $v = $_ -split '=', 2
            Set-Item -Path "Env:$k" -Value $v
          }

          $uvicorn = Join-Path $Target ".venv\Scripts\uvicorn.exe"
          if (Test-Path "app.log") { Remove-Item "app.log" -Force -ErrorAction SilentlyContinue }
          if (Test-Path "app.err") { Remove-Item "app.err" -Force -ErrorAction SilentlyContinue }
          Start-Process -FilePath $uvicorn -ArgumentList "app:app --host 0.0.0.0 --port 8001" `
            -WorkingDirectory $Target `
            -RedirectStandardOutput "app.log" -RedirectStandardError "app.err" -WindowStyle Hidden
          Start-Sleep -Seconds 3
          Invoke-RestMethod http://127.0.0.1:8001/health
          Write-Host "PROD locale OK → http://127.0.0.1:8001"

      # ---------- Variante Linux/macOS/WSL (hors live Windows) ----------
      - name: Déployer vers demo_prod (Linux/macOS/WSL)
        if: runner.os != 'Windows'
        env:
          OLLAMA_API_KEY: ${{ secrets.OLLAMA_API_KEY }}
          OLLAMA_HOST: ${{ vars.OLLAMA_HOST }}
          APP_TITLE: ${{ vars.APP_TITLE }}
        run: |
          set -euo pipefail
          TARGET="${HOME}/demo_prod"
          mkdir -p "$TARGET"
          rsync -a --delete \
            --exclude '.git' --exclude '.venv' --exclude '__pycache__' \
            --exclude '.env' --exclude '*.pid' --exclude '*.log' \
            ./ "$TARGET/"
          umask 077
          {
            echo "OLLAMA_HOST=${OLLAMA_HOST:-http://127.0.0.1:11434}"
            echo "OLLAMA_API_KEY=${OLLAMA_API_KEY}"
            echo "MODEL_NAME=gemma4:32"
            echo "APP_ENV=PROD"
            echo "APP_TITLE=${APP_TITLE:-Myndkor Chat PROD}"
          } > "$TARGET/.env"
          chmod 600 "$TARGET/.env"
          cd "$TARGET"
          python3 -m venv .venv
          . .venv/bin/activate
          pip install -q -r requirements.txt
          if [ -f app.pid ] && kill -0 "$(cat app.pid)" 2>/dev/null; then
            kill "$(cat app.pid)" || true
            sleep 1
          fi
          if command -v fuser >/dev/null 2>&1; then fuser -k 8001/tcp 2>/dev/null || true; fi
          set -a; . ./.env; set +a
          nohup uvicorn app:app --host 0.0.0.0 --port 8001 > app.log 2>&1 &
          echo $! > app.pid
          sleep 2
          curl -sf "http://127.0.0.1:8001/health"
          echo
          echo "PROD locale OK → http://127.0.0.1:8001"
examples/cicd/README.md
# Exemples CI/CD clé en main (à donner au client)

> **Procédure Windows client (recommandée) :**  
> [`PROCEDURE-WINDOWS-CLIENT.md`](./PROCEDURE-WINDOWS-CLIENT.md)  
> → créer `.github\workflows`, coller le YAML, GitHub-hosted vs Self-hosted, installer le runner.

Ces YAML sont **volontairement hors** de `.github/workflows/` pour ne pas
se déclencher tous en même temps sur le repo de démo.

## Utilisation chez le client

1. Choisir **un** fichier selon l’infra cible (voir tableau ci-dessous).
2. Le copier dans `.github/workflows/` du projet client.
3. Renseigner les Secrets / Variables documentés en tête du YAML.
4. Créer l’Environment GitHub `production` (et `recette` si besoin).
5. Désactiver / ne pas copier les autres exemples.

Le workflow **live formateur** reste :
`../../.github/workflows/ci-cd-local-runner.yml` (Self-Hosted Windows).

## Catalogue

| Fichier | Cas | Stack typique |
|---|---|---|
| `ci-cd-azure-webapp.yml` | PaaS Azure | Web App + OIDC |
| `ci-cd-windows-server.yml` | On-prem Windows | Service / NSSM |
| `ci-cd-docker-ghcr.yml` | Conteneurs | Docker → GHCR |
| `ci-cd-linux-ssh.yml` | VPS / VM Linux | SSH + rsync + systemd |
| `ci-cd-aws-ecr-apprunner.yml` | PaaS AWS | ECR + App Runner OIDC |
| `ci-cd-kubernetes.yml` | Orchestration | GHCR + kubectl |
| `ci-cd-recette-puis-prod.yml` | Gouvernance 2 env | recette → prod |

## Hors GitHub Actions

| Dossier | Cas |
|---|---|
| [`../drone/`](../drone/) | **Docker + Drone CI** (`.drone.yml`, compose serveur/runner, compose app) |

Fichiers support dans le kit racine :
- `Dockerfile`
- `deploy/myndkor-chat.service` (systemd pour l’exemple SSH)
- `.dockerignore`

Si le client est plutôt Docker/Drone

Autre procédure
Pas de dossier .github/workflows : fichier .drone.yml à la racine + runner Drone (Docker). Voir procédure dédiée.
examples/drone/PROCEDURE-DRONE.md
# Procédure client — Docker + Drone CI (clé en main)

Repo de référence : https://github.com/Thomasdsf/myndkor-chat-demo  
Pack source : `examples/drone/` du kit formation.

Cette procédure est **indépendante** de la démo GitHub Actions Windows.
À remettre au client tel quel, ou à animer en atelier secondaire.

---

## Objectif final

```
Code → Drone test (pytest)
     → Build/Push image Docker (branche prod)
     → Promote manuel "production"
     → Conteneur démarré sur le serveur cible
```

---

## Phase 0 — Prérequis machine

À installer avant toute chose :

| Outil | Vérification |
|---|---|
| Git | `git --version` |
| Docker Engine + Compose v2 | `docker version` / `docker compose version` |
| Accès au dépôt Git (GitHub/Gitea/GitLab) | clone OK |
| (Optionnel lab) Ports libres `8080` (Drone UI) et `8000` (app) | |

Windows : Docker Desktop. Linux : Docker Engine officiel.

---

## Phase 1 — Créer l’arborescence projet

Dans un PowerShell / bash, depuis le dossier où vous voulez le projet client :

### Option A — Partir du kit Myndkor (recommandé)

```powershell
# Windows PowerShell
$Root = "$env:USERPROFILE\projets\myndkor-drone-demo"
New-Item -ItemType Directory -Force -Path $Root | Out-Null
cd $Root

# Copier depuis le kit (adapter le chemin si ZIP décompressé ailleurs)
$Kit = "$env:USERPROFILE\Downloads\kit"   # <-- à ajuster
Copy-Item "$Kit\app.py","$Kit\test_app.py","$Kit\requirements.txt","$Kit\.env.example","$Kit\.gitignore","$Kit\Dockerfile","$Kit\.dockerignore" -Destination . -Force
New-Item -ItemType Directory -Force -Path "deploy","docs" | Out-Null
Copy-Item "$Kit\deploy\myndkor-chat.service" -Destination "deploy\" -Force

# Fichiers Drone → racine + docs
Copy-Item "$Kit\examples\drone\.drone.yml" -Destination ".\.drone.yml" -Force
Copy-Item "$Kit\examples\drone\docker-compose.app.yml","$Kit\examples\drone\docker-compose.drone.yml","$Kit\examples\drone\drone-secrets.example.env","$Kit\examples\drone\README.md","$Kit\examples\drone\PROCEDURE-DRONE.md" -Destination "docs\" -Force
```

```bash
# Linux / macOS / WSL
ROOT="$HOME/projets/myndkor-drone-demo"
KIT="$HOME/Downloads/kit"   # <-- à ajuster
mkdir -p "$ROOT"/{deploy,docs} && cd "$ROOT"
cp "$KIT"/{app.py,test_app.py,requirements.txt,.env.example,.gitignore,Dockerfile,.dockerignore} .
cp "$KIT/deploy/myndkor-chat.service" deploy/
cp "$KIT/examples/drone/.drone.yml" ./.drone.yml
cp "$KIT/examples/drone/"{{docker-compose.app.yml,docker-compose.drone.yml,drone-secrets.example.env,README.md,PROCEDURE-DRONE.md}} docs/
```

### Option B — Projet déjà existant

```text
mon-projet/
├── app.py
├── test_app.py
├── requirements.txt
├── Dockerfile
├── .dockerignore
├── .drone.yml          ← À CRÉER ICI (racine, obligatoire)
├── .gitignore
├── .env.example
└── docs/
    ├── docker-compose.app.yml
    ├── docker-compose.drone.yml
    └── drone-secrets.example.env
```

**Point critique :** le fichier s’appelle `.drone.yml` et est à la **racine** du dépôt (pas dans `.github/workflows/`).

---

## Phase 2 — Initialiser Git + branches

```powershell
cd $Root   # ou cd mon-projet
git init -b main
git add .
git commit -m "chore: bootstrap app + drone pipeline"
# Créer le remote (GitHub public/privé selon le contrat client)
git remote add origin https://github.com/<ORG>/<REPO>.git
git push -u origin main

git branch recette
git push -u origin recette
git branch prod
git push -u origin prod
```

Convention Myndkor : `main` → `recette` → `prod`.  
Le build Docker Drone se déclenche sur **`prod`** (voir `.drone.yml`).

---

## Phase 3 — Choisir le mode Drone

### Mode 3A — Drone déjà fourni par le client (prod réelle)

1. Admin Drone : **Sync** / activer le dépôt.
2. Repository Settings → **Trusted** si besoin de Docker socket (selon politique).
3. Passer à la **Phase 4** (secrets).

### Mode 3B — Lab formateur / PoC (compose inclus)

```powershell
# Secrets OAuth GitHub (créer une OAuth App)
# Homepage : http://localhost:8080
# Callback : http://localhost:8080/login

$env:DRONE_GITHUB_CLIENT_ID = "xxxxxxxx"
$env:DRONE_GITHUB_CLIENT_SECRET = "yyyyyyyy"
$env:DRONE_RPC_SECRET = -join ((1..32) | ForEach-Object { "{0:x}" -f (Get-Random -Max 16) })
$env:DRONE_SERVER_HOST = "localhost"

cd docs   # là où se trouve docker-compose.drone.yml
docker compose -f docker-compose.drone.yml up -d
```

```bash
export DRONE_GITHUB_CLIENT_ID=xxxxxxxx
export DRONE_GITHUB_CLIENT_SECRET=yyyyyyyy
export DRONE_RPC_SECRET=$(openssl rand -hex 16)
export DRONE_SERVER_HOST=localhost
cd docs
docker compose -f docker-compose.drone.yml up -d
```

UI : http://localhost:8080 → login GitHub → activer le repo.

Vérifier :
```powershell
docker ps --filter name=drone
```

---

## Phase 4 — Créer les secrets Drone

Dans Drone → Repository → **Settings → Secrets**  
(ou Org secrets si mutualisés).

| Nom secret | Exemple / usage |
|---|---|
| `docker_username` | user registry (GHCR / Harbor / Docker Hub) |
| `docker_password` | token `write:packages` ou mot de passe registry |
| `ssh_host` | IP/hostname serveur de prod |
| `ssh_user` | `deploy` |
| `ssh_key` | clé privée OpenSSH (contenu complet) |
| `ollama_api_key` | clé API (factice OK en lab) |
| `ollama_host` | `http://127.0.0.1:11434` ou URL cloud |

Modèle : `docs/drone-secrets.example.env` (ne jamais committer les vraies valeurs).

Adapter aussi dans `.drone.yml` la ligne :
```yaml
repo: ghcr.io/thomasdsf/myndkor-chat-demo
```
→ `ghcr.io/<org>/<image>` du client.

---

## Phase 5 — Valider le Dockerfile en local (avant CI)

```powershell
cd $Root
Copy-Item .env.example .env
# Éditer APP_ENV=DEV, OLLAMA_* si besoin

docker build -t myndkor-chat:local .
docker run --rm -p 8000:8000 --env-file .env myndkor-chat:local
# Autre terminal :
curl http://127.0.0.1:8000/health
```

Ou via Compose app :
```powershell
cd docs
docker compose -f docker-compose.app.yml up -d --build
curl http://127.0.0.1:8000/health
```

---

## Phase 6 — Premier pipeline CI (Fail Fast)

1. Créer une branche / PR ou push sur `recette` :
```powershell
git checkout recette
# Casser volontairement pour montrer l’échec :
# (Get-Content app.py) -replace 'REQUIRED_MODEL = "gemma4:32"','REQUIRED_MODEL = "WRONG"' | Set-Content app.py
git commit --allow-empty -m "ci: trigger drone test"
git push origin recette
```
2. Dans Drone UI : build **test** vert (ou rouge si Fail Fast).
3. Corriger si besoin, re-push.

> Sur `pull_request` / `push`, seul le pipeline `test-and-build` tourne.  
> L’étape `docker` (push image) ne part que sur **branche `prod`**.

---

## Phase 7 — Build & push image (branche prod)

```powershell
git checkout prod
git merge recette   # ou PR recette → prod selon gouvernance GitHub/Gitea
git push origin prod
```

Dans Drone :
1. Step `test` OK  
2. Step `docker` build + push `ghcr.io/...:<sha>` et `:latest`

Vérifier l’image :
```powershell
docker pull ghcr.io/<org>/<image>:latest
```

---

## Phase 8 — Mise en production (Promote)

Équivalent GitHub « Review deployments → Approve » :

1. Ouvrir le build `prod` réussi dans Drone.
2. Bouton **Promote** (ou Deploy).
3. Target : **`production`** (doit matcher `trigger.target` du 2ᵉ pipeline).
4. Confirmer.

Le pipeline `deploy-production` démarre :
- SSH sur le serveur
- `docker login` + `docker pull`
- stop/rm ancien conteneur
- `docker run` avec `APP_ENV=PROD` + secrets
- `curl /health`

Navigateur / smoke test :
```text
http://<serveur>:8000
Bandeau vert PROD + titre injecté
```

---

## Phase 9 — Checklist de recette client

- [ ] `.drone.yml` à la racine (pas dans `.github/`)
- [ ] Dockerfile + `.dockerignore` présents
- [ ] Repo activé dans Drone + runner online
- [ ] Secrets registry + SSH + Ollama créés
- [ ] Test rouge volontaire puis vert (Fail Fast démontré)
- [ ] Image visible dans le registry après push `prod`
- [ ] Promote `production` → conteneur UP + `/health` OK
- [ ] Aucun secret dans Git (`git log -p` clean)

---

## Phase 10 — Désactiver / cohabiter avec GitHub Actions

Si le même repo a aussi des workflows Actions :

| Stratégie | Action |
|---|---|
| Démo Actions seule | Ne pas activer le repo dans Drone |
| Démo Drone seule | Renommer `.github/workflows/*.yml` → `*.yml.disabled` |
| Les deux en PoC | Branches séparées ou repos séparés |

---

## Dépannage rapide

| Symptôme | Cause fréquente | Correctif |
|---|---|---|
| Drone ignore le repo | Non activé / mauvais remote | Activate + Sync |
| Step docker fail auth | Mauvais `docker_password` | Token packages:write |
| Promote ne lance rien | Target ≠ `production` | Exact match du YAML |
| App down après deploy | Port / Ollama host | Vérifier `-e OLLAMA_HOST` et firewall |
| pytest fail dans Drone | Dépendance manquante | Aligner `requirements.txt` |

---

## Récap commandes « anti-sèche » (Windows)

```powershell
# 1) Arborescence + fichiers (Phase 1)
# 2) git init / push branches (Phase 2)
# 3) Lab Drone (optionnel)
cd docs
$env:DRONE_GITHUB_CLIENT_ID="..."
$env:DRONE_GITHUB_CLIENT_SECRET="..."
$env:DRONE_RPC_SECRET="..."
$env:DRONE_SERVER_HOST="localhost"
docker compose -f docker-compose.drone.yml up -d
# 4) Secrets dans UI Drone
# 5) git push origin prod  → test + docker
# 6) UI → Promote → production
# 7) curl http://<host>:8000/health
```
examples/drone/.drone.yml
# =============================================================================
# EXEMPLE CLIENT — Drone CI + Docker (clé en main)
# =============================================================================
# Emplacement : à copier à la RACINE du dépôt client sous le nom `.drone.yml`
#
# Flux :
#   1) test      — toujours (PR + push) : pytest dans une image Python
#   2) docker    — push branche prod : build Dockerfile + push registry
#   3) deploy    — uniquement après PROMOTION manuelle target=production
#                  (équivalent GitHub Environment "Required reviewers")
#
# Secrets Drone (repository ou org) :
#   docker_username / docker_password
#   ssh_host / ssh_user / ssh_key          (si deploy SSH)
#   ollama_api_key / ollama_host
#
# Docs Drone : https://docs.drone.io/
# =============================================================================

---
kind: pipeline
type: docker
name: test-and-build

concurrency:
  limit: 1

# Déclencheurs : PR + push (ignore tags)
trigger:
  event:
    - push
    - pull_request

steps:
  # ---------------------------------------------------------------------------
  # 1. Fail Fast — mêmes garanties que le job GitHub Actions "test"
  # ---------------------------------------------------------------------------
  - name: test
    image: python:3.12-slim
    environment:
      MODEL_NAME: gemma4:32
      APP_ENV: RECETTE
      APP_TITLE: Myndkor Chat Demo
      OLLAMA_HOST: http://127.0.0.1:11434
    commands:
      - pip install --no-cache-dir -r requirements.txt
      - pytest -q

  # ---------------------------------------------------------------------------
  # 2. Build & push image (uniquement branche prod, après tests OK)
  #    Adaptez "repo" à votre registry (ghcr.io/org/app, harbor, ECR…)
  # ---------------------------------------------------------------------------
  - name: docker
    image: plugins/docker
    settings:
      repo: ghcr.io/thomasdsf/myndkor-chat-demo
      registry: ghcr.io
      username:
        from_secret: docker_username
      password:
        from_secret: docker_password
      tags:
        - ${DRONE_COMMIT_SHA}
        - latest
      dockerfile: Dockerfile
      # purge: true   # optionnel : nettoyer le cache builder
    when:
      branch:
        - prod
      event:
        - push

---
# =============================================================================
# Pipeline de DÉPLOIEMENT — déclenché manuellement via Promote
# UI Drone → build prod réussi → Promote → target: production
# =============================================================================
kind: pipeline
type: docker
name: deploy-production

trigger:
  event:
    - promote
  target:
    - production

steps:
  # Exemple A — déploiement SSH + docker compose sur un VPS
  - name: deploy-ssh
    image: appleboy/drone-ssh
    settings:
      host:
        from_secret: ssh_host
      username:
        from_secret: ssh_user
      key:
        from_secret: ssh_key
      port: 22
      script:
        - set -euo pipefail
        - export IMAGE=ghcr.io/thomasdsf/myndkor-chat-demo:${DRONE_COMMIT_SHA}
        - echo "$DOCKER_PASSWORD" | docker login ghcr.io -u "$DOCKER_USERNAME" --password-stdin
        - docker pull "$IMAGE"
        - docker stop myndkor-chat || true
        - docker rm myndkor-chat || true
        - |
          docker run -d --name myndkor-chat --restart unless-stopped \
            -p 8000:8000 \
            -e APP_ENV=PROD \
            -e MODEL_NAME=gemma4:32 \
            -e APP_TITLE="Myndkor Chat PROD" \
            -e OLLAMA_HOST="$OLLAMA_HOST" \
            -e OLLAMA_API_KEY="$OLLAMA_API_KEY" \
            "$IMAGE"
        - docker ps --filter name=myndkor-chat
        - curl -sf http://127.0.0.1:8000/health
    environment:
      DOCKER_USERNAME:
        from_secret: docker_username
      DOCKER_PASSWORD:
        from_secret: docker_password
      OLLAMA_API_KEY:
        from_secret: ollama_api_key
      OLLAMA_HOST:
        from_secret: ollama_host

  # Exemple B (désactivé) — kubectl set image
  # Décommentez et commentez deploy-ssh si le client est full K8s.
  # - name: deploy-k8s
  #   image: bitnami/kubectl:latest
  #   environment:
  #     KUBE_CONFIG:
  #       from_secret: kube_config
  #   commands:
  #     - mkdir -p ~/.kube
  #     - echo "$KUBE_CONFIG" | base64 -d > ~/.kube/config
  #     - kubectl -n myndkor set image deploy/myndkor-chat app=ghcr.io/thomasdsf/myndkor-chat-demo:${DRONE_COMMIT_SHA}
  #     - kubectl -n myndkor rollout status deploy/myndkor-chat --timeout=180s

Guide formateur (complément)

PREPA-WINDOWS
PREPA-WINDOWS.md
# Prérequis poste Windows — Démo live myndkor-chat-demo

Compte : [Thomasdsf](https://github.com/Thomasdsf)  
Repo : https://github.com/Thomasdsf/myndkor-chat-demo (**public**)  
Formation : https://myndkor.com/formations/github-enterprise-cicd/

Poste formateur = **Windows 10/11**. Tous les blocs ci-dessous sont en **PowerShell**.

---

## 1. Logiciels à installer (une seule fois)

### 1.1 Git for Windows
1. Télécharger : https://git-scm.com/download/win  
2. Installer avec les options par défaut.  
3. Cocher **Git from the command line and also from 3rd-party software**.

Vérification :
```powershell
git --version
```

### 1.2 Python 3.12
1. Télécharger : https://www.python.org/downloads/windows/  
2. **Cocher « Add python.exe to PATH »** pendant l’install.  
3. Fermer / rouvrir PowerShell.

Vérification :
```powershell
python --version
pip --version
```

### 1.3 Ollama (Windows) + modèle `gemma4:32`
1. Télécharger : https://ollama.com/download/windows  
2. Installer et laisser Ollama démarrer (icône barre des tâches).  
3. Dans PowerShell :
```powershell
ollama pull gemma4:32
ollama list
# API locale attendue :
Invoke-RestMethod http://127.0.0.1:11434/api/tags
```

> Si tu utilises Ollama Cloud / un autre host : note l’URL pour la variable GitHub `OLLAMA_HOST`.

### 1.4 (Optionnel) GitHub CLI
https://cli.github.com/ — utile pour `gh repo` / auth, pas obligatoire pour la démo.

---

## 2. Dossiers DEV / PROD

```powershell
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\demo_dev" | Out-Null
New-Item -ItemType Directory -Force -Path "$env:USERPROFILE\demo_prod" | Out-Null
```

| Dossier | Rôle | Port |
|---|---|---|
| `%USERPROFILE%\demo_dev` | Travail formateur (git, live coding) | **8000** |
| `%USERPROFILE%\demo_prod` | Cible du runner après Approve | **8001** |

---

## 3. Cloner / pousser le kit dans le repo

Si le repo GitHub est encore vide, depuis le ZIP formation ou une copie locale du kit :

```powershell
cd $env:USERPROFILE\demo_dev
# Copier ici app.py, test_app.py, requirements.txt, .gitignore, .env.example, .github\...

git init -b main
git add .
git commit -m "chore: bootstrap myndkor chat demo"
git remote add origin https://github.com/Thomasdsf/myndkor-chat-demo.git
git push -u origin main

git branch recette
git push -u origin recette
git branch prod
git push -u origin prod
```

Sinon, simple clone :
```powershell
git clone https://github.com/Thomasdsf/myndkor-chat-demo.git $env:USERPROFILE\demo_dev
cd $env:USERPROFILE\demo_dev
```

---

## 4. Environnement Python DEV (port 8000)

```powershell
cd $env:USERPROFILE\demo_dev
python -m venv .venv
.\.venv\Scripts\Activate.ps1
# Si erreur d'exécution de scripts :
# Set-ExecutionPolicy -Scope CurrentUser RemoteSigned

pip install -r requirements.txt
Copy-Item .env.example .env
notepad .env
# APP_ENV=DEV
# APP_TITLE=Chat DEV Myndkor
# OLLAMA_HOST=http://127.0.0.1:11434
# OLLAMA_API_KEY=   (vide en local, ou factice)
# MODEL_NAME=gemma4:32

uvicorn app:app --host 0.0.0.0 --port 8000
```

Navigateur : http://127.0.0.1:8000 — bandeau **bleu DEV**.

Test unitaire :
```powershell
pytest -q
```

---

## 5. Self-Hosted Runner Windows (obligatoire pour le deploy live)

1. Ouvre : https://github.com/Thomasdsf/myndkor-chat-demo/settings/actions/runners  
2. **New self-hosted runner** → **Windows** → **x64**.  
3. Dans PowerShell **en Administrateur** (recommandé pour le service) :

```powershell
mkdir $env:USERPROFILE\actions-runner ; cd $env:USERPROFILE\actions-runner
# Colle les 3 commandes affichées par GitHub (download + extract + config)
# Exemple de forme :
# Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/vX.Y.Z/actions-runner-win-x64-X.Y.Z.zip -OutFile actions-runner-win-x64.zip
# Add-Type -AssemblyName System.IO.Compression.FileSystem
# [System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\actions-runner-win-x64.zip", "$PWD")
# .\config.cmd --url https://github.com/Thomasdsf/myndkor-chat-demo --token <TOKEN_UI>
```

Lancer le runner pendant toute la séance :
```powershell
.\run.cmd
```

Ou l’installer comme service Windows (reste online après reboot) :
```powershell
# PowerShell Administrateur, dans le dossier actions-runner
.\svc.cmd install
.\svc.cmd start
```

Vérifie le statut **Idle** sur la page Runners.

> Le job `test` tourne sur `ubuntu-latest` (cloud). Seul `deploy-prod` utilise ton runner Windows.

---

## 6. Réglages GitHub (UI) — à faire avant la séance

1. **Code security** → Secret Scanning + Push Protection  
   https://github.com/Thomasdsf/myndkor-chat-demo/settings/security_analysis  

2. **Environments → `production`**  
   https://github.com/Thomasdsf/myndkor-chat-demo/settings/environments  
   - Required reviewers : ton compte (ou un 2ᵉ compte DevOps)  
   - Secret : `OLLAMA_API_KEY` (valeur factice OK, ex. `demo_key_myndkor`)  
   - Variables : `OLLAMA_HOST=http://127.0.0.1:11434`, `APP_TITLE=Chat PROD Myndkor`

3. **Ruleset sur `prod`**  
   https://github.com/Thomasdsf/myndkor-chat-demo/settings/rules  
   - Require pull request  
   - Require status checks → `test`  
   - Block force pushes  

---

## 7. Checklist « J-0 / 5 min avant »

- [ ] Repo **public**  
- [ ] Runner Windows **Idle**  
- [ ] `demo_dev` ouvert dans VS Code / terminal, branche `recette`  
- [ ] App DEV sur http://127.0.0.1:8000  
- [ ] Ollama up + `gemma4:32`  
- [ ] Rien d’autre n’occupe le port **8001**  
- [ ] Onglets : repo, Actions, PR, `:8000`, `:8001`  
- [ ] Environment `production` + Ruleset OK  

---

## 8. Commandes Fail Fast (PowerShell) pendant le live

```powershell
cd $env:USERPROFILE\demo_dev
git checkout recette

# Casser volontairement le test
(Get-Content app.py) -replace 'REQUIRED_MODEL = "gemma4:32"','REQUIRED_MODEL = "WRONG"' | Set-Content app.py -Encoding utf8

git add -A
git commit -m "feat: break model for fail-fast demo"
git push origin recette
# → Ouvrir PR recette → prod : check test ROUGE, Merge bloqué

# Corriger
(Get-Content app.py) -replace 'REQUIRED_MODEL = "WRONG"','REQUIRED_MODEL = "gemma4:32"' | Set-Content app.py -Encoding utf8
git add app.py
git commit -m "fix: restore REQUIRED_MODEL gemma4:32"
git push origin recette
# → test VERT → Merge → Review deployments → Approve → refresh :8001
```