v2 replaces pnpm with bun and moves the standalone build to apps/nextjs/.output/standalone, which already carries static assets, public and next.config.ts. Next still writes an incomplete .next/standalone, so the assembler now refuses to run without the v2 output rather than silently shipping that one. The notes now list v2's registration points, the tests that already fail on the upstream tag, and the one-way v1 to v2 database migration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
176 lines
7.7 KiB
Bash
Executable File
176 lines
7.7 KiB
Bash
Executable File
#!/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)"
|
|
|
|
# Le dépôt exige un Node récent (champ engines.node). Le Node par défaut du poste
|
|
# est plus ancien que ça : sans ce contrôle, l'échec surviendrait au milieu du
|
|
# build, avec un message de bun qui ne dit pas quoi faire.
|
|
required_node="$(grep -oP '^node = "\K[^"]+' "$REPO/mise.toml" 2>/dev/null || echo '24.18.0')"
|
|
if ! node -e "process.exit(process.versions.node.localeCompare('$required_node', undefined, { numeric: true }) < 0 ? 1 : 0)"; then
|
|
echo "Node $(node -v) est trop ancien — il en faut au moins v$required_node." >&2
|
|
echo "Par exemple : export PATH=\"\$HOME/.local/share/nvm/v24.21.0/bin:\$PATH\"" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Depuis la v2, le dépôt se construit avec bun (version fixée dans mise.toml).
|
|
required_bun="$(grep -oP '^bun = "\K[^"]+' "$REPO/mise.toml" 2>/dev/null || echo '1.4.2')"
|
|
if [ "$(bun --version 2>/dev/null)" != "$required_bun" ]; then
|
|
echo "bun $required_bun requis (trouvé : $(bun --version 2>/dev/null || echo aucun))." >&2
|
|
echo "Par exemple : npm i -g bun@$required_bun, avec le Node ci-dessus dans le PATH." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# 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.
|
|
#
|
|
# Comparaison par dialecte : la prod embarque les migrations de tous les moteurs,
|
|
# et prendre le maximum toutes dialectes confondues masquerait une divergence
|
|
# propre à celui qui tourne réellement.
|
|
DIALECT="${HOMARR_DB_DIALECT:-sqlite}"
|
|
local_migration="$(find "packages/db/migrations/$DIALECT" -name '*.sql' -printf '%f\n' | sort | tail -1)"
|
|
remote_migration="$(ssh "$HOST" "find $REMOTE_DIR/db/migrations/$DIALECT -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"
|
|
|
|
# Les trois variables reproduisent les ARG du Dockerfile. Sans la première, le
|
|
# build valide les variables d'exécution — dont URL, qui n'a aucun sens à la
|
|
# compilation — et échoue partout où il ne traîne pas un .env à la racine.
|
|
SKIP_ENV_VALIDATION=true CI=true DISABLE_REDIS_LOGS=true \
|
|
bun run turbo run build --filter=@homarr/nextjs... --filter=@homarr/cli
|
|
|
|
# ---------------------------------------------------------------------------
|
|
say "Assemblage de l'arborescence à envoyer"
|
|
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
|
|
# Même assemblage que pour une installation construite sur place : un seul
|
|
# endroit décrit la disposition d'exécution, et les deux chemins ne peuvent pas
|
|
# diverger. `run.sh` en fait partie, mais il est exclu du rsync plus bas —
|
|
# celui de l'installation est adapté à son port.
|
|
"$REPO/ops/assemble-standalone.sh" "$STAGE"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
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"
|
|
# -i sans quoi rsync ne dit rien du tout en mode simulation.
|
|
rsync -ain --delete --rsync-path="sudo rsync" \
|
|
--exclude 'run.sh' --exclude 'entrypoint.sh' \
|
|
--exclude 'nginx.conf' --exclude 'redis.conf' \
|
|
"$STAGE/" "$HOST:$REMOTE_DIR/" > /tmp/homarr-deploy-plan.txt
|
|
# Écrit d'abord, tronqué ensuite : un `| head` ferme le tuyau et rsync s'arrête
|
|
# sur SIGPIPE au bout de trente lignes, ce qui donnerait un plan mensonger.
|
|
head -30 /tmp/homarr-deploy-plan.txt
|
|
echo
|
|
echo "$(grep -c '^\*deleting' /tmp/homarr-deploy-plan.txt) suppressions, \
|
|
$(grep -vc '^\*deleting' /tmp/homarr-deploy-plan.txt) écritures. Plan complet : /tmp/homarr-deploy-plan.txt"
|
|
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 <<EOF
|
|
|
|
Déployé. Vérifie dans le navigateur que les intégrations PRÉEXISTANTES
|
|
fonctionnent encore — c'est ce qui prouve que SECRET_ENCRYPTION_KEY est intact.
|
|
|
|
Retour arrière :
|
|
ssh $HOST 'sudo systemctl stop $SERVICE \\
|
|
&& sudo rm -rf $REMOTE_DIR \\
|
|
&& sudo tar xzf /root/homarr-backup-$stamp.tar.gz -C / \\
|
|
&& sudo systemctl start $SERVICE'
|
|
|
|
La base n'a pas été remplacée ; sa copie est dans /root/homarr-db-$stamp.sqlite.
|
|
EOF
|