# Déploiement FTP

Pipeline de mise en production sur un serveur FTP/FTPS. À chaque exécution, seul
le **delta** est poussé : les fichiers modifiés ou nouveaux sont envoyés, les
fichiers supprimés localement sont effacés du serveur.

## Configuration

1. Copier le modèle :

   ```bash
   cp deploy.config.example.php deploy.config.php
   ```

2. Renseigner les identifiants dans `deploy.config.php` (ignoré par git, il
   contient le mot de passe FTP).

| Clé | Défaut | Rôle |
| --- | --- | --- |
| `host` | — (requis) | Hôte FTP |
| `port` | `21` | Port |
| `username` | — (requis) | Utilisateur |
| `password` | `''` | Mot de passe |
| `secure` | `false` | FTPS (TLS explicite) |
| `verifyPeer` | `true` | Vérifie le certificat en FTPS |
| `passive` | `true` | Mode passif |
| `timeout` | `30` | Timeout connexion/transfert (s) |
| `remoteRoot` | `/` | Dossier serveur cible |
| `protectedRemotePaths` | `[]` | Chemins jamais supprimés par `--mirror` |
| `manifestPath` | `.deploy-manifest.json` | Fichier d'état local (hash) |
| `backupEnabled` | `true` | Sauvegarde avant écrasement (active le rollback) |
| `backupRetention` | `5` | Nombre de sauvegardes conservées |
| `backupPath` | `var/deploy-backups` | Dossier des sauvegardes (local) |

## Exclusions

Les fichiers exclus sont définis dans `.deployignore` (syntaxe type gitignore :
`#` commentaire, `/` en tête = ancré à la racine, `/` en fin = dossiers
uniquement, globs `*`/`?`).

Quoi qu'il arrive, le déployeur **n'enverra jamais** les secrets et l'état local :
`.env`, `.env.*`, `deploy.config.php`, `.deploy-manifest*.json`, `.deployignore`,
`.git/`. Cette liste est codée en dur et ne dépend pas de `.deployignore`.

## Utilisation

```bash
php bin/deploy.php --dry-run   # aperçu du plan, ne contacte pas le serveur
php bin/deploy.php             # pousse les changements, supprime les retirés
php bin/deploy.php --mirror    # + élague les fichiers distants absents en local
```

Options :

| Option | Effet |
| --- | --- |
| `--config=PATH` | Chemin du config (défaut `./deploy.config.php`) |
| `--dry-run` | Affiche le plan sans rien envoyer |
| `--mirror` | Supprime aussi les fichiers distants orphelins (hors `protectedRemotePaths`) |
| `--rollback` | Annule le dernier déploiement (ou `--backup=ID`) |
| `--backup=ID` | Sauvegarde précise à restaurer (défaut : la plus récente) |
| `--help` | Aide |

Codes de sortie : `0` succès, `1` erreur (config/transfert), `2` arguments invalides.

## Rollback

Tant que `backupEnabled` est vrai, chaque déploiement réel **sauvegarde d'abord
les octets distants** qu'il va écraser ou supprimer, sous
`var/deploy-backups/<timestamp>/` (local, jamais uploadé, git-ignoré) :

```
<timestamp>/
  files/<chemin>         anciens octets des fichiers modifiés/supprimés
  manifest.before.json   manifeste tel qu'avant le déploiement
  backup.json            { created:[…], restored:[…] }
```

Pour revenir en arrière après un déploiement fautif :

```bash
php bin/deploy.php --rollback --dry-run   # aperçu de la restauration
php bin/deploy.php --rollback              # restaure la dernière sauvegarde
php bin/deploy.php --rollback --backup=20260627-143012   # une sauvegarde précise
```

Le rollback ré-uploade les fichiers sauvegardés (`restored`), supprime ceux que
le déploiement avait créés (`created`), puis restaure `manifest.before.json`
comme manifeste courant — de sorte que le delta suivant repart de l'état restauré.

Limites (FTP pur, sans copie serveur ni symlink) :
- Pas atomique : la restauration se fait fichier par fichier (court intervalle incohérent possible).
- Une seule sauvegarde par déploiement ; `backupRetention` borne l'historique.
- Un fichier introuvable au moment de la sauvegarde (dérive du manifeste) est reclassé en « créé » → supprimé au rollback.

## Fonctionnement

1. **Scan** : `LocalScanner` parcourt le projet et calcule un hash par fichier
   (`xxh128`, repli `md5`), en élaguant les dossiers ignorés pendant le parcours.
2. **Delta** : comparaison avec le manifeste du dernier déploiement réussi
   (`relativePath => hash`). FTP n'expose pas de hash, d'où ce manifeste local.
   - hash différent ou fichier nouveau → **upload**
   - présent au manifeste mais absent du disque → **delete**
3. **Mirror** (optionnel) : liste récursive du serveur ; tout fichier distant
   absent en local et hors `protectedRemotePaths` est supprimé.
4. **Manifeste** : réécrit après un déploiement réussi (non-dry-run) pour
   refléter l'état désormais en ligne.

Transport : tout passe par **curl** (l'extension `ext-ftp` n'est pas disponible),
qui gère FTP et FTPS. Connexion réutilisée entre les transferts.

## Migrations SQL (delta par base)

Il n'y a pas de runner de migrations automatisé : les changements de schéma sont
appliqués à la main sur le serveur. `bin/migrations-bundle.php` regroupe les
migrations **non encore déployées** en **un fichier consolidé par base**, sous
`database/deploy/<clé>.sql`, à exécuter puis supprimer après chaque déploiement.

### Quelle base cible une migration ?

Une directive d'en-tête, sur sa propre ligne, déclare la base :

```sql
-- @database: bo
```

Clés acceptées : `main` (défaut si absente → `DB_NAME`), `bo` (`BO_DB_NAME`,
`hxa_bo`), `work` (`WORK_DB_NAME`). La base `geo` est en lecture seule (pas de
migrations). Le fichier généré **n'émet aucun `USE`** : tu choisis toi-même la
base au moment de l'exécution.

### Qu'est-ce qui est « non déployé » ?

Toute migration dont le préfixe `YYYY_MM_DD_HHMMSS` est postérieur au watermark
stocké dans `database/migrations/.bundle-state.json`
(`{"deployedThrough": "..."}`). Le watermark est seedé sur la dernière migration
existante : seules les nouvelles sont regroupées.

### Utilisation

```bash
php bin/migrations-bundle.php             # génère database/deploy/<base>.sql
php bin/migrations-bundle.php --dry-run   # liste le delta sans rien écrire
php bin/migrations-bundle.php --mark      # marque le delta comme déployé
```

Flux type :

1. `php bin/migrations-bundle.php` → écrit `database/deploy/*.sql`
2. exécuter chaque fichier sur la base correspondante
   (`mysql hxa_bo < database/deploy/bo.sql`, ou via phpMyAdmin)
3. `php bin/migrations-bundle.php --mark` → avance le watermark
4. supprimer `database/deploy/*.sql` (déjà git-ignoré)

La génération est **sans effet de bord** (relançable) : seul `--mark` avance le
watermark, à lancer une fois le déploiement réussi.
