From 17a2e577c0478009deca59cb71cfd410b486dec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zachary=20Gu=C3=A9not?= Date: Sun, 13 Sep 2026 16:45:53 +0200 Subject: [PATCH] chore(ops): add LXC deploy script and fork maintenance notes Co-Authored-By: Claude Opus 5 --- ops/README.md | 112 +++++++++++++++++++++++++++++++++ ops/deploy-lxc.sh | 155 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) create mode 100644 ops/README.md create mode 100755 ops/deploy-lxc.sh diff --git a/ops/README.md b/ops/README.md new file mode 100644 index 000000000..ae7ddb05e --- /dev/null +++ b/ops/README.md @@ -0,0 +1,112 @@ +# Fork personnel — exploitation + +Ce dossier n'existe pas en amont. Il ne créera donc jamais de conflit lors d'un +rebase, ce qui est exactement pourquoi les fichiers d'exploitation vivent ici et +pas dans `scripts/`. + +## Déployer + +```bash +ops/deploy-lxc.sh --dry-run # montre ce qui serait écrit, sans rien toucher +ops/deploy-lxc.sh +``` + +Le script exige un arbre propre, construit, compare les migrations et l'ABI Node +avant de toucher quoi que ce soit, sauvegarde `/opt/homarr` **et** la base, arrête +le service, synchronise, redémarre, puis affiche les journaux. La commande de +retour arrière est imprimée à la fin, horodatage compris. + +Ce qu'il ne remplace jamais : `/etc/homarr/homarr.env`, `run.sh`, `entrypoint.sh`, +`nginx.conf`, `redis.conf` — ils appartiennent à l'installation du LXC, pas au +build — ni `/var/lib/homarr/db.sqlite`, qui est sauvegardée mais laissée en place. + +Après déploiement, le contrôle qui compte n'est pas que le nouveau widget +s'affiche : c'est que les intégrations **préexistantes** fonctionnent encore. +C'est la seule preuve que `SECRET_ENCRYPTION_KEY` n'a pas bougé. + +## Suivre les mises à jour d'Homarr + +Aujourd'hui `origin` pointe sur le dépôt d'Homarr lui-même et **aucune copie +distante de ce travail n'existe** : une panne de disque effacerait la branche. +Premier geste, avant tout le reste : + +```bash +git remote rename origin upstream +git remote add origin /homarr.git +git push -u origin feat/unifi-os-integration +``` + +### Le cycle de montée de version + +Homarr publie souvent. Se caler sur une **étiquette de version** plutôt que sur +`dev` évite de rebaser sur du code en cours : + +```bash +git fetch upstream --tags +git log --oneline dev..upstream/dev | head # ce qui est arrivé +git rebase v1.78.0 feat/unifi-os-integration # remplace par la version visée +``` + +Puis, dans cet ordre — chaque étape a déjà attrapé quelque chose : + +```bash +pnpm install # le lockfile bouge presque à chaque version +pnpm turbo typecheck # les 37 paquets +pnpm test packages/integrations/ packages/widgets/ +pnpm test packages/widgets/src/manifest.spec.ts # les points d'enregistrement +ops/deploy-lxc.sh --dry-run +``` + +`manifest.spec.ts` mérite sa ligne à part : il compare l'icône du catalogue à +celle de la définition **par identité**, et c'est le seul test qui détecte un +point d'enregistrement perdu dans un conflit. + +### Où les conflits vont tomber + +Le gros du travail vit dans des dossiers neufs — `packages/integrations/src/unifi-os/`, +`unifi-site-manager/`, `packages/widgets/src/unifi-*/` — qui ne peuvent pas +entrer en conflit. Les points de friction sont les fichiers partagés où ce fork +ajoute une ligne dans une liste que l'amont fait vivre aussi : + +| Fichier | Ce qu'on y ajoute | +|---|---| +| `packages/definitions/src/integration.ts` | les deux genres d'intégration | +| `packages/definitions/src/widget.ts` | les deux genres de widget | +| `packages/definitions/src/widget-integration-map.ts` | l'association widget ↔ intégration | +| `packages/definitions/src/docs/integration-doc-slugs.ts` | les slugs de documentation | +| `packages/definitions/src/docs/widget-doc-slugs.ts` | idem, côté widgets | +| `packages/integrations/src/base/creator.ts` | la construction des classes | +| `packages/integrations/src/index.ts` | les exports | +| `packages/integrations/src/types.ts` | les types réexportés **côté client** | +| `packages/api/src/router/widgets/index.ts` | les routeurs tRPC | +| `packages/widgets/src/manifest.ts` | deux cartes distinctes, module et composant | +| `packages/widgets/src/index.tsx` | l'import et l'entrée | +| `packages/widgets/src/catalog.ts` | l'icône | +| `packages/widgets/src/refetch-intervals.ts` | l'intervalle de rafraîchissement | +| `apps/nextjs/src/components/integration/integration-grid-shared.ts` | le libellé de catégorie | +| `apps/nextjs/src/app/[locale]/manage/integrations/_components/secrets/integration-secret-icons.ts` | l'icône du secret | +| `packages/translation/src/lang/en.json` | tous les libellés | + +Deux de ces lignes ne sont vérifiées par **aucun** compilateur, et les perdre +dans un conflit passerait tous les tests : + +- `integration-grid-shared.ts` est typé `Record` : une entrée + manquante affiche `UNIFISITEMANAGER` en majuscules collées au lieu du nom. +- `en.json` n'est pas typé du tout : une clé perdue affiche son chemin brut. + +Les deux se voient d'un coup d'œil sur la page « New integration » — c'est le +contrôle visuel à faire après chaque montée de version. + +### Rejouer les résolutions de conflit + +Les mêmes conflits reviendront à chaque version, au même endroit. Git sait les +rejouer tout seul : + +```bash +git config rerere.enabled true +``` + +À activer **avant** le premier rebase : Git n'enregistre que ce qu'il voit +résoudre. Une fois qu'une résolution est connue, les suivantes sont +automatiques — à relire quand même, l'amont peut avoir changé la forme de la +liste autour. diff --git a/ops/deploy-lxc.sh b/ops/deploy-lxc.sh new file mode 100755 index 000000000..ef747f64b --- /dev/null +++ b/ops/deploy-lxc.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash +# +# Déploie le build standalone de ce dépôt sur le LXC Homarr. +# +# Reproduit la disposition que le Dockerfile officiel assemble (ses COPY du +# stage `runner`), parce que l'installation du LXC en vient : /opt/homarr a la +# même forme qu'une image, sans le conteneur autour. +# +# Ce que le script NE touche PAS, volontairement : +# - /etc/homarr/homarr.env : secrets et configuration de l'instance +# - run.sh, entrypoint.sh, nginx.conf, redis.conf : modifiés par l'installateur +# du LXC, ils appartiennent à l'installation et non au build +# - /var/lib/homarr/db.sqlite : la base, sauvegardée mais jamais remplacée +# +# Usage : ops/deploy-lxc.sh [--dry-run] +# +set -euo pipefail + +HOST="${HOMARR_HOST:-zachary@10.70.1.13}" +REMOTE_DIR="${HOMARR_REMOTE_DIR:-/opt/homarr}" +REMOTE_DB="${HOMARR_REMOTE_DB:-/var/lib/homarr/db.sqlite}" +SERVICE="${HOMARR_SERVICE:-homarr}" + +REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DRY_RUN="" +[ "${1:-}" = "--dry-run" ] && DRY_RUN="--dry-run" + +say() { printf '\n\033[1m==> %s\033[0m\n' "$1"; } + +# --------------------------------------------------------------------------- +say "Contrôles préalables" + +cd "$REPO" +[ -z "$(git status --porcelain)" ] || { + echo "Arbre de travail sale. Committe ou remise avant de déployer." >&2 + exit 1 +} +echo "Commit déployé : $(git log --oneline -1)" + +# Une migration présente ici et absente en prod n'est pas bloquante — Homarr les +# applique au démarrage — mais elle rend le rollback non trivial : restaurer +# l'archive ne défait pas un schéma déjà migré. On veut le savoir AVANT. +local_migration="$(find packages/db/migrations -name '*.sql' -printf '%f\n' | sort | tail -1)" +remote_migration="$(ssh "$HOST" "find $REMOTE_DIR/db/migrations -name '*.sql' -printf '%f\n' | sort | tail -1")" +echo "Dernière migration — locale : $local_migration | prod : $remote_migration" +if [ "$local_migration" != "$remote_migration" ]; then + echo + echo "!! Les migrations divergent. Le rollback ne sera PAS une simple restauration." + echo " Sauvegarde la base avant de continuer, puis relance en connaissance de cause." + read -r -p " Continuer quand même ? [oui/NON] " answer + [ "$answer" = "oui" ] || exit 1 +fi + +# --------------------------------------------------------------------------- +say "Build" + +pnpm turbo build --filter=@homarr/nextjs... --filter=@homarr/cli + +# --------------------------------------------------------------------------- +say "Assemblage de l'arborescence à envoyer" + +STAGE="$(mktemp -d)" +trap 'rm -rf "$STAGE"' EXIT + +# L'ordre suit le Dockerfile : le standalone d'abord, le reste par-dessus. +cp -a apps/nextjs/.next/standalone/. "$STAGE/" +mkdir -p "$STAGE/apps/nextjs/.next" +cp -a apps/nextjs/.next/static "$STAGE/apps/nextjs/.next/static" +cp -a apps/nextjs/public "$STAGE/apps/nextjs/public" +cp -a apps/nextjs/next.config.ts apps/nextjs/package.json pnpm-workspace.yaml "$STAGE/" + +mkdir -p "$STAGE/apps/cli" +cp -a packages/cli/cli.cjs "$STAGE/apps/cli/cli.cjs" + +# Binaire natif : compilé ici, chargé là-bas. Même architecture et même ABI Node +# (24.x des deux côtés) sont donc des prérequis — le contrôle est plus bas. +mkdir -p "$STAGE/build" +cp -a node_modules/better-sqlite3/build/Release/better_sqlite3.node "$STAGE/build/" + +mkdir -p "$STAGE/db" +cp -a packages/db/migrations "$STAGE/db/migrations" + +echo "Arborescence prête : $(du -sh "$STAGE" | cut -f1)" + +# --------------------------------------------------------------------------- +say "Contrôle de compatibilité du binaire natif" + +local_abi="$(node -p 'process.versions.modules')" +remote_abi="$(ssh "$HOST" 'node -p "process.versions.modules"')" +local_arch="$(uname -m)" +remote_arch="$(ssh "$HOST" 'uname -m')" +echo "ABI Node — local : $local_abi | prod : $remote_abi" +echo "Arch — local : $local_arch | prod : $remote_arch" +if [ "$local_abi" != "$remote_abi" ] || [ "$local_arch" != "$remote_arch" ]; then + echo "!! better_sqlite3.node ne se chargera pas en prod. Build sur le LXC, ou aligne Node." >&2 + exit 1 +fi + +if [ -n "$DRY_RUN" ]; then + say "Mode --dry-run : arrêt avant toute modification de la prod" + rsync -an --delete \ + --exclude 'run.sh' --exclude 'entrypoint.sh' \ + --exclude 'nginx.conf' --exclude 'redis.conf' \ + "$STAGE/" "$HOST:$REMOTE_DIR/" | head -40 + exit 0 +fi + +# --------------------------------------------------------------------------- +say "Sauvegarde" + +stamp="$(date +%F-%H%M)" +ssh -t "$HOST" "sudo tar czf /root/homarr-backup-$stamp.tar.gz $REMOTE_DIR \ + && sudo cp $REMOTE_DB /root/homarr-db-$stamp.sqlite \ + && sudo ls -lh /root/homarr-backup-$stamp.tar.gz /root/homarr-db-$stamp.sqlite" + +# --------------------------------------------------------------------------- +say "Arrêt du service" + +# Jamais de rsync sur un Next.js en cours d'exécution : il lit ses chunks à la +# demande et servirait des morceaux de deux builds différents. +ssh -t "$HOST" "sudo systemctl stop $SERVICE && ! systemctl is-active --quiet $SERVICE && echo arrêté" + +# --------------------------------------------------------------------------- +say "Envoi" + +# --delete retire ce que le build ne produit plus ; les quatre fichiers exclus +# sont protégés de cette suppression par leur exclusion même. +rsync -a --delete --rsync-path="sudo rsync" \ + --exclude 'run.sh' --exclude 'entrypoint.sh' \ + --exclude 'nginx.conf' --exclude 'redis.conf' \ + "$STAGE/" "$HOST:$REMOTE_DIR/" + +# --------------------------------------------------------------------------- +say "Redémarrage" + +ssh -t "$HOST" "sudo systemctl start $SERVICE" +sleep 10 +ssh "$HOST" "systemctl is-active $SERVICE; curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://127.0.0.1/" + +say "Journaux (40 dernières lignes)" +ssh -t "$HOST" "sudo journalctl -u $SERVICE -n 40 --no-pager" + +cat <