[ADD] script forgejo: installer une forge git en option cochable
Une case au déploiement, et une forge git auto-hébergée répond sur le port 3000, git par SSH sur 2222. Le travail vit dans un script dédié, appelable seul sur une machine existante : une seule autorité pour les deux usages. Le binaire officiel est statique, donc le même fichier sert apt, dnf, pacman et zypper — c'est ce qui rend l'option portable sans une branche par distribution. Les architectures suivent l'amont (amd64, arm64, arm-6) ; la case se grise sur s390x plutôt que de poser un binaire inexécutable. Les quatre secrets sont écrits par le script : sans oauth2.JWT_SECRET, Forgejo tente de les persister lui-même et boucle sur un app.ini qu'il n'a pas le droit d'écrire. Vérifié sur une VM : somme de contrôle validée, service actif, API qui répond, dépôt créé puis cloné par git, et relance en 1,5 s sans rien réécrire. --- EN --- One checkbox at deploy time, and a self-hosted git forge answers on port 3000, git over SSH on 2222. The work lives in a dedicated script, callable on its own for an existing machine: one authority for both uses. The official binary is static, so the same file serves apt, dnf, pacman and zypper — that is what makes the option portable without a branch per distribution. Architectures follow upstream (amd64, arm64, arm-6); the checkbox greys out on s390x rather than dropping a binary that cannot run. The script writes all four secrets itself: without oauth2.JWT_SECRET, Forgejo tries to persist them and loops on an app.ini it is not allowed to write. Verified on a VM: checksum validated, service active, API answering, a repo created then cloned over git, and a replay in 1.5 s rewriting nothing. Assisted-by: Claude Opus 5
This commit is contained in:
parent
69431e17c9
commit
952c92b52a
8 changed files with 728 additions and 17 deletions
301
script/forgejo/install_forgejo.sh
Executable file
301
script/forgejo/install_forgejo.sh
Executable file
|
|
@ -0,0 +1,301 @@
|
|||
#!/usr/bin/env bash
|
||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
#
|
||||
# Installe Forgejo — https://forgejo.org — depuis le binaire statique officiel
|
||||
# publié sur https://codeberg.org/forgejo/forgejo.
|
||||
#
|
||||
# Le binaire est statique et sans dépendance : le même fichier sert Debian,
|
||||
# Ubuntu, AlmaLinux, Rocky, openSUSE et Arch. Ce script ne touche donc à AUCUN
|
||||
# gestionnaire de paquets — c'est ce qui le rend portable sur les plateformes
|
||||
# ERPLibre sans une branche par distribution.
|
||||
#
|
||||
# Il n'appelle PAS env_var.sh, à la différence des scripts d'installation
|
||||
# ERPLibre : Forgejo ne dépend ni du dépôt ni de son venv, et le script doit
|
||||
# rester utilisable hors d'un checkout.
|
||||
#
|
||||
# Réglages, tous par variables d'environnement :
|
||||
# FORGEJO_VERSION version à poser (défaut : la dernière publiée)
|
||||
# FORGEJO_HTTP_PORT port web (défaut : 3000)
|
||||
# FORGEJO_SSH_PORT port SSH interne de Forgejo (défaut : 2222)
|
||||
# FORGEJO_ADMIN_USER compte administrateur créé (défaut : erplibre)
|
||||
# FORGEJO_ADMIN_PASSWORD son mot de passe (défaut : erplibre)
|
||||
# FORGEJO_ADMIN_EMAIL son courriel (défaut : admin@erplibre.local)
|
||||
# FORGEJO_USER compte système propriétaire (défaut : git)
|
||||
# FORGEJO_SKIP_ADMIN à 1, ne crée aucun compte (installateur web)
|
||||
set -euo pipefail
|
||||
|
||||
Red='\033[0;31m'
|
||||
Green='\033[0;32m'
|
||||
Yellow='\033[0;33m'
|
||||
Color_Off='\033[0m'
|
||||
|
||||
VERSION="${FORGEJO_VERSION:-}"
|
||||
HTTP_PORT="${FORGEJO_HTTP_PORT:-3000}"
|
||||
SSH_PORT="${FORGEJO_SSH_PORT:-2222}"
|
||||
# « admin » est REFUSÉ par Forgejo — « CreateUser: name is reserved », mesuré.
|
||||
# La liste des noms réservés couvre aussi api, assets, avatars, explore, user…
|
||||
ADMIN_USER="${FORGEJO_ADMIN_USER:-erplibre}"
|
||||
ADMIN_PASSWORD="${FORGEJO_ADMIN_PASSWORD:-erplibre}"
|
||||
ADMIN_EMAIL="${FORGEJO_ADMIN_EMAIL:-admin@erplibre.local}"
|
||||
RUN_USER="${FORGEJO_USER:-git}"
|
||||
SKIP_ADMIN="${FORGEJO_SKIP_ADMIN:-0}"
|
||||
|
||||
BIN=/usr/local/bin/forgejo
|
||||
CONF_DIR=/etc/forgejo
|
||||
CONF="$CONF_DIR/app.ini"
|
||||
DATA=/var/lib/forgejo
|
||||
UNIT=/etc/systemd/system/forgejo.service
|
||||
API=https://codeberg.org/api/v1/repos/forgejo/forgejo/releases
|
||||
DL=https://codeberg.org/forgejo/forgejo/releases/download
|
||||
|
||||
usage() {
|
||||
sed -n '5,26p' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
}
|
||||
case "${1:-}" in
|
||||
-h|--help) usage ;;
|
||||
esac
|
||||
|
||||
say() { echo -e " $*"; }
|
||||
die() { echo -e " ${Red}✗ $*${Color_Off}" >&2; exit 1; }
|
||||
|
||||
# --- 1. Architecture -------------------------------------------------------
|
||||
# Forgejo publie amd64, arm64 et arm-6. PAS de s390x : sur cette architecture
|
||||
# il faudrait le bâtir depuis les sources en Go, ce que ce script ne fait pas —
|
||||
# il le dit plutôt que de télécharger un binaire qui ne s'exécutera pas.
|
||||
case "$(uname -m)" in
|
||||
x86_64) ARCH=amd64 ;;
|
||||
aarch64|arm64) ARCH=arm64 ;;
|
||||
armv6l|armv7l) ARCH=arm-6 ;;
|
||||
*) die "Forgejo ne publie pas de binaire pour $(uname -m)" \
|
||||
"(amd64, arm64 et arm-6 seulement)." ;;
|
||||
esac
|
||||
|
||||
# --- 2. Version ------------------------------------------------------------
|
||||
if [ -z "$VERSION" ]; then
|
||||
# La liste des versions est en JSON : on la lit avec python3, présent dans
|
||||
# toutes les images cloud visées. Sans lui, on retombe sur grep — mieux
|
||||
# qu'un abandon, et le motif est celui d'un champ JSON, pas d'une page web.
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
VERSION=$(curl -fsSL --max-time 30 "$API?limit=1" 2>/dev/null \
|
||||
| python3 -c 'import json,sys
|
||||
try:
|
||||
print(json.load(sys.stdin)[0]["tag_name"].lstrip("v"))
|
||||
except Exception:
|
||||
pass' || true)
|
||||
else
|
||||
VERSION=$(curl -fsSL --max-time 30 "$API?limit=1" 2>/dev/null \
|
||||
| grep -o '"tag_name":"v[^"]*"' | head -1 \
|
||||
| sed 's/.*"v//;s/"//' || true)
|
||||
fi
|
||||
fi
|
||||
[ -n "$VERSION" ] || die "Version de Forgejo introuvable (réseau ? $API)"
|
||||
say "Forgejo $VERSION pour $ARCH"
|
||||
|
||||
# --- 3. Déjà posé ? --------------------------------------------------------
|
||||
# Rejouer une installation est le cas normal. Comparer la version évite de
|
||||
# retélécharger 34 Mo pour rien, et de redémarrer un service qui va bien.
|
||||
if [ -x "$BIN" ] && "$BIN" --version 2>/dev/null | grep -q "version $VERSION"; then
|
||||
say "${Green}binaire déjà en version $VERSION, téléchargement évité${Color_Off}"
|
||||
else
|
||||
# L'archive .xz pèse 34 Mo contre 114 Mo pour le binaire nu. On la prend
|
||||
# quand xz est là, sans jamais l'installer : le binaire nu est le repli.
|
||||
tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
if command -v xz >/dev/null 2>&1; then
|
||||
asset="forgejo-$VERSION-linux-$ARCH.xz"
|
||||
else
|
||||
asset="forgejo-$VERSION-linux-$ARCH"
|
||||
fi
|
||||
say "téléchargement de $asset"
|
||||
curl -fsSL --retry 3 --max-time 900 "$DL/v$VERSION/$asset" \
|
||||
-o "$tmp/$asset" || die "téléchargement impossible : $DL/v$VERSION/$asset"
|
||||
# Somme de contrôle publiée à côté du fichier : une archive tronquée par une
|
||||
# coupure réseau donne un binaire qui ne s'exécute pas, et l'erreur arrive
|
||||
# alors dix étapes plus loin.
|
||||
if curl -fsSL --max-time 60 "$DL/v$VERSION/$asset.sha256" \
|
||||
-o "$tmp/$asset.sha256" 2>/dev/null; then
|
||||
(cd "$tmp" && sha256sum -c "$asset.sha256" >/dev/null) \
|
||||
|| die "somme de contrôle invalide pour $asset"
|
||||
say "somme de contrôle vérifiée"
|
||||
else
|
||||
say "${Yellow}⚠ somme de contrôle indisponible, non vérifiée${Color_Off}"
|
||||
fi
|
||||
case "$asset" in
|
||||
*.xz) xz -d "$tmp/$asset"; src="$tmp/${asset%.xz}" ;;
|
||||
*) src="$tmp/$asset" ;;
|
||||
esac
|
||||
chmod +x "$src"
|
||||
sudo install -m 0755 "$src" "$BIN"
|
||||
say "${Green}binaire posé : $BIN${Color_Off}"
|
||||
rm -rf "$tmp"
|
||||
trap - EXIT
|
||||
fi
|
||||
|
||||
# --- 4. Compte système et répertoires --------------------------------------
|
||||
if ! id "$RUN_USER" >/dev/null 2>&1; then
|
||||
sudo useradd --system --create-home --home-dir "/home/$RUN_USER" \
|
||||
--shell /bin/bash --comment "Forgejo" "$RUN_USER"
|
||||
say "compte système créé : $RUN_USER"
|
||||
fi
|
||||
sudo mkdir -p "$DATA"/{custom,data,log} "$CONF_DIR"
|
||||
sudo chown -R "$RUN_USER:$RUN_USER" "$DATA"
|
||||
sudo chmod 750 "$DATA"
|
||||
# Le fichier de configuration appartient à root et se LIT par le groupe : le
|
||||
# service en a besoin, et Forgejo y écrit ses secrets au premier démarrage si
|
||||
# on ne les pose pas soi-même — ce que fait l'étape suivante.
|
||||
sudo chown root:"$RUN_USER" "$CONF_DIR"
|
||||
sudo chmod 770 "$CONF_DIR"
|
||||
|
||||
# --- 5. Configuration ------------------------------------------------------
|
||||
# JAMAIS réécrite si elle existe : elle porte les secrets, et un utilisateur a
|
||||
# pu l'ajuster. C'est aussi ce qui rend ce script rejouable.
|
||||
# « sudo test », et non « [ -f ] » : /etc/forgejo appartient à root:git en 770,
|
||||
# donc l'utilisateur qui lance le script ne peut même pas y statuer un fichier.
|
||||
# Le test échouait toujours, et CHAQUE passage réécrivait la configuration avec
|
||||
# des secrets neufs — ce qui invalide les sessions et les jetons existants.
|
||||
if sudo test -f "$CONF"; then
|
||||
say "configuration conservée : $CONF"
|
||||
else
|
||||
host=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
[ -n "$host" ] || host=localhost
|
||||
# Les QUATRE secrets, et pas seulement les deux évidents. Vécu : sans
|
||||
# « oauth2.JWT_SECRET », Forgejo tente de l'écrire dans app.ini au
|
||||
# démarrage, n'y arrive pas — le fichier appartient à root — et s'arrête
|
||||
# sur « [F] save oauth2.JWT_SECRET failed ». Le service redémarrait en
|
||||
# boucle, 25 fois, sans jamais écouter le port.
|
||||
#
|
||||
# Les poser ici garde app.ini NON inscriptible par le service : c'est la
|
||||
# bonne posture, et ça évite un fichier de configuration qui se réécrit
|
||||
# tout seul.
|
||||
secret=$("$BIN" generate secret SECRET_KEY)
|
||||
token=$("$BIN" generate secret INTERNAL_TOKEN)
|
||||
jwt=$("$BIN" generate secret JWT_SECRET)
|
||||
lfs_jwt=$("$BIN" generate secret JWT_SECRET)
|
||||
# « INSTALL_LOCK = true » verrouille l'installateur web : la machine est
|
||||
# utilisable sans passer par un formulaire, ce qui est tout l'intérêt d'une
|
||||
# option cochée au déploiement. SQLite, pour ne pas disputer PostgreSQL à
|
||||
# Odoo, qui vit sur la même VM.
|
||||
sudo tee "$CONF" >/dev/null <<CONFEOF
|
||||
APP_NAME = ERPLibre Forgejo
|
||||
RUN_USER = $RUN_USER
|
||||
RUN_MODE = prod
|
||||
WORK_PATH = $DATA
|
||||
|
||||
[server]
|
||||
PROTOCOL = http
|
||||
DOMAIN = $host
|
||||
HTTP_PORT = $HTTP_PORT
|
||||
ROOT_URL = http://$host:$HTTP_PORT/
|
||||
APP_DATA_PATH = $DATA/data
|
||||
DISABLE_SSH = false
|
||||
START_SSH_SERVER = true
|
||||
SSH_DOMAIN = $host
|
||||
SSH_PORT = $SSH_PORT
|
||||
SSH_LISTEN_PORT = $SSH_PORT
|
||||
LFS_START_SERVER = true
|
||||
LFS_JWT_SECRET = $lfs_jwt
|
||||
|
||||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = $DATA/data/forgejo.db
|
||||
|
||||
[repository]
|
||||
ROOT = $DATA/data/forgejo-repositories
|
||||
|
||||
[security]
|
||||
INSTALL_LOCK = true
|
||||
PASSWORD_COMPLEXITY = off
|
||||
SECRET_KEY = $secret
|
||||
INTERNAL_TOKEN = $token
|
||||
|
||||
[oauth2]
|
||||
JWT_SECRET = $jwt
|
||||
|
||||
[service]
|
||||
DISABLE_REGISTRATION = false
|
||||
REQUIRE_SIGNIN_VIEW = false
|
||||
|
||||
[lfs]
|
||||
PATH = $DATA/data/lfs
|
||||
|
||||
[log]
|
||||
ROOT_PATH = $DATA/log
|
||||
LEVEL = info
|
||||
CONFEOF
|
||||
sudo chown root:"$RUN_USER" "$CONF"
|
||||
sudo chmod 640 "$CONF"
|
||||
say "${Green}configuration écrite : $CONF${Color_Off}"
|
||||
fi
|
||||
|
||||
# --- 6. Service ------------------------------------------------------------
|
||||
sudo tee "$UNIT" >/dev/null <<UNITEOF
|
||||
[Unit]
|
||||
Description=Forgejo (Beyond coding. We forge.)
|
||||
After=network.target network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=$RUN_USER
|
||||
Group=$RUN_USER
|
||||
WorkingDirectory=$DATA
|
||||
ExecStart=$BIN web --config $CONF
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
Environment=USER=$RUN_USER HOME=/home/$RUN_USER GITEA_WORK_DIR=$DATA
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
UNITEOF
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now forgejo.service >/dev/null 2>&1 \
|
||||
|| die "le service refuse de démarrer : sudo journalctl -u forgejo -n 40"
|
||||
|
||||
# --- 7. Attendre qu'il RÉPONDE --------------------------------------------
|
||||
# Une requête HTTP, pas un « systemctl is-active » : le service est « active »
|
||||
# bien avant d'écouter, et « activating » en boucle de redémarrage ressemble à
|
||||
# un démarrage en cours. /api/v1/version prouve que l'application SERT — la
|
||||
# création du compte administrateur qui suit a besoin de la base migrée.
|
||||
#
|
||||
# Et surtout pas « exec 3<>/dev/tcp/... » : « exec » est un builtin spécial, et
|
||||
# une redirection qui échoue termine le shell. Le script mourait donc en
|
||||
# silence, au premier tour de la boucle, code 1 sans un mot — vécu.
|
||||
ready=0
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS -o /dev/null --max-time 3 \
|
||||
"http://127.0.0.1:$HTTP_PORT/api/v1/version"; then
|
||||
ready=1
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
[ "$ready" = 1 ] || die "aucune réponse sur le port $HTTP_PORT après 120 s" \
|
||||
"(sudo journalctl -u forgejo -n 40)"
|
||||
|
||||
# --- 8. Compte administrateur ---------------------------------------------
|
||||
# Créé seulement s'il n'y a AUCUN compte : rejouer le script ne doit pas
|
||||
# échouer sur « user already exists », ni écraser un mot de passe choisi.
|
||||
if [ "$SKIP_ADMIN" = 1 ]; then
|
||||
say "aucun compte créé (FORGEJO_SKIP_ADMIN=1)"
|
||||
elif sudo -u "$RUN_USER" "$BIN" admin user list --config "$CONF" 2>/dev/null \
|
||||
| tail -n +2 | grep -q .; then
|
||||
say "comptes déjà présents, administrateur non recréé"
|
||||
else
|
||||
sudo -u "$RUN_USER" "$BIN" admin user create --admin \
|
||||
--username "$ADMIN_USER" --password "$ADMIN_PASSWORD" \
|
||||
--email "$ADMIN_EMAIL" --must-change-password=false \
|
||||
--config "$CONF" >/dev/null \
|
||||
|| die "création de l'administrateur impossible"
|
||||
say "${Green}administrateur créé : $ADMIN_USER / $ADMIN_PASSWORD${Color_Off}"
|
||||
fi
|
||||
|
||||
# --- 9. Résumé -------------------------------------------------------------
|
||||
host=$(hostname -I 2>/dev/null | awk '{print $1}')
|
||||
[ -n "$host" ] || host=localhost
|
||||
version=$("$BIN" --version 2>/dev/null | head -1)
|
||||
say "${Green}Forgejo prêt${Color_Off} : http://$host:$HTTP_PORT/"
|
||||
say " $version"
|
||||
say " git par SSH : port $SSH_PORT (serveur interne de Forgejo)"
|
||||
say " service : sudo systemctl status forgejo"
|
||||
say " journal : sudo journalctl -u forgejo -f"
|
||||
|
|
@ -337,6 +337,25 @@ Rendering is `swangle` in the AVD's own `config.ini` — `auto` would open a
|
|||
black screen, and `swiftshader_indirect` no longer exists, the emulator
|
||||
answering `Selected GPU option ... is not valid`.
|
||||
|
||||
A sixth, **Forgejo**, installs a self-hosted git forge — the software behind
|
||||
Codeberg — from the project's official static binary, and leaves it serving on
|
||||
port 3000 with git-over-SSH on 2222. Like the mobile build it needs no desktop,
|
||||
and unlike it no package family is excluded: the binary is static, so the same
|
||||
file serves apt, dnf, pacman and zypper. That is what makes it portable across
|
||||
the ERPLibre platforms without a branch per distribution. Architectures follow
|
||||
upstream, which publishes amd64, arm64 and arm-6 — the checkbox greys out on
|
||||
s390x rather than dropping a binary that cannot run.
|
||||
|
||||
The work lives in `script/forgejo/install_forgejo.sh`, callable on its own for
|
||||
an existing machine: `./script/forgejo/install_forgejo.sh`. It verifies the
|
||||
published checksum, writes all four secrets itself so the service never needs
|
||||
to rewrite its own configuration, and stores its data in SQLite so it does not
|
||||
dispute PostgreSQL with Odoo on the same VM. Replaying it is cheap and safe —
|
||||
1.5 s measured with everything in place: it skips a binary already at the right
|
||||
version, never overwrites an existing `app.ini`, and does not recreate the
|
||||
administrator. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER` and
|
||||
a few others tune it; `--help` lists them.
|
||||
|
||||
Each tool is filtered per VM — by architecture, desktop flavour and package
|
||||
family — and its disk cost is added to the plan before anything is created.
|
||||
|
||||
|
|
@ -483,6 +502,26 @@ Le rendu est « swangle » dans le `config.ini` de l'AVD — « auto » ouvrirai
|
|||
écran noir, et « swiftshader_indirect » n'existe plus, l'émulateur répondant
|
||||
`Selected GPU option ... is not valid`.
|
||||
|
||||
Un sixième, **Forgejo**, installe une forge git auto-hébergée — le logiciel
|
||||
derrière Codeberg — depuis le binaire statique officiel du projet, et la laisse
|
||||
en service sur le port 3000, avec git par SSH sur 2222. Comme la compilation
|
||||
mobile, elle n'a besoin d'aucun bureau ; contrairement à elle, aucune famille de
|
||||
paquets n'est exclue : le binaire est statique, donc le même fichier sert apt,
|
||||
dnf, pacman et zypper. C'est ce qui la rend portable sur les plateformes
|
||||
ERPLibre sans une branche par distribution. Les architectures suivent l'amont,
|
||||
qui publie amd64, arm64 et arm-6 — la case se grise sur s390x plutôt que de
|
||||
poser un binaire qui ne s'exécutera pas.
|
||||
|
||||
Le travail vit dans `script/forgejo/install_forgejo.sh`, appelable seul sur une
|
||||
machine existante : `./script/forgejo/install_forgejo.sh`. Il vérifie la somme
|
||||
de contrôle publiée, écrit lui-même les quatre secrets pour que le service n'ait
|
||||
jamais à réécrire sa propre configuration, et garde ses données en SQLite pour
|
||||
ne pas disputer PostgreSQL à Odoo sur la même VM. Le rejouer est sans risque et
|
||||
bon marché — 1,5 s mesuré, tout étant en place : il saute un binaire déjà à la
|
||||
bonne version, ne réécrit jamais un `app.ini` existant et ne recrée pas
|
||||
l'administrateur. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER`
|
||||
et quelques autres le règlent ; `--help` les énumère.
|
||||
|
||||
Chaque outil est filtré VM par VM — architecture, saveur de bureau et famille
|
||||
de paquets — et sa place disque s'ajoute au plan avant que rien ne soit créé.
|
||||
|
||||
|
|
|
|||
|
|
@ -228,6 +228,26 @@ Le rendu est « swangle » dans le `config.ini` de l'AVD — « auto » ouvrirai
|
|||
écran noir, et « swiftshader_indirect » n'existe plus, l'émulateur répondant
|
||||
`Selected GPU option ... is not valid`.
|
||||
|
||||
Un sixième, **Forgejo**, installe une forge git auto-hébergée — le logiciel
|
||||
derrière Codeberg — depuis le binaire statique officiel du projet, et la laisse
|
||||
en service sur le port 3000, avec git par SSH sur 2222. Comme la compilation
|
||||
mobile, elle n'a besoin d'aucun bureau ; contrairement à elle, aucune famille de
|
||||
paquets n'est exclue : le binaire est statique, donc le même fichier sert apt,
|
||||
dnf, pacman et zypper. C'est ce qui la rend portable sur les plateformes
|
||||
ERPLibre sans une branche par distribution. Les architectures suivent l'amont,
|
||||
qui publie amd64, arm64 et arm-6 — la case se grise sur s390x plutôt que de
|
||||
poser un binaire qui ne s'exécutera pas.
|
||||
|
||||
Le travail vit dans `script/forgejo/install_forgejo.sh`, appelable seul sur une
|
||||
machine existante : `./script/forgejo/install_forgejo.sh`. Il vérifie la somme
|
||||
de contrôle publiée, écrit lui-même les quatre secrets pour que le service n'ait
|
||||
jamais à réécrire sa propre configuration, et garde ses données en SQLite pour
|
||||
ne pas disputer PostgreSQL à Odoo sur la même VM. Le rejouer est sans risque et
|
||||
bon marché — 1,5 s mesuré, tout étant en place : il saute un binaire déjà à la
|
||||
bonne version, ne réécrit jamais un `app.ini` existant et ne recrée pas
|
||||
l'administrateur. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER`
|
||||
et quelques autres le règlent ; `--help` les énumère.
|
||||
|
||||
Chaque outil est filtré VM par VM — architecture, saveur de bureau et famille
|
||||
de paquets — et sa place disque s'ajoute au plan avant que rien ne soit créé.
|
||||
|
||||
|
|
|
|||
|
|
@ -211,6 +211,25 @@ Rendering is `swangle` in the AVD's own `config.ini` — `auto` would open a
|
|||
black screen, and `swiftshader_indirect` no longer exists, the emulator
|
||||
answering `Selected GPU option ... is not valid`.
|
||||
|
||||
A sixth, **Forgejo**, installs a self-hosted git forge — the software behind
|
||||
Codeberg — from the project's official static binary, and leaves it serving on
|
||||
port 3000 with git-over-SSH on 2222. Like the mobile build it needs no desktop,
|
||||
and unlike it no package family is excluded: the binary is static, so the same
|
||||
file serves apt, dnf, pacman and zypper. That is what makes it portable across
|
||||
the ERPLibre platforms without a branch per distribution. Architectures follow
|
||||
upstream, which publishes amd64, arm64 and arm-6 — the checkbox greys out on
|
||||
s390x rather than dropping a binary that cannot run.
|
||||
|
||||
The work lives in `script/forgejo/install_forgejo.sh`, callable on its own for
|
||||
an existing machine: `./script/forgejo/install_forgejo.sh`. It verifies the
|
||||
published checksum, writes all four secrets itself so the service never needs
|
||||
to rewrite its own configuration, and stores its data in SQLite so it does not
|
||||
dispute PostgreSQL with Odoo on the same VM. Replaying it is cheap and safe —
|
||||
1.5 s measured with everything in place: it skips a binary already at the right
|
||||
version, never overwrites an existing `app.ini`, and does not recreate the
|
||||
administrator. `FORGEJO_VERSION`, `FORGEJO_HTTP_PORT`, `FORGEJO_ADMIN_USER` and
|
||||
a few others tune it; `--help` lists them.
|
||||
|
||||
Each tool is filtered per VM — by architecture, desktop flavour and package
|
||||
family — and its disk cost is added to the plan before anything is created.
|
||||
|
||||
|
|
|
|||
|
|
@ -5422,6 +5422,32 @@ class TODO:
|
|||
# manifeste ajoute, et du venv d'outils pour le synchroniser.
|
||||
"phase": "after",
|
||||
},
|
||||
# Forgejo est un SERVICE, pas un outil de bureau : une VM serveur le
|
||||
# prend aussi bien qu'une VM graphique. Son binaire est STATIQUE — le
|
||||
# même fichier sur apt, dnf, pacman et zypper — donc aucune famille de
|
||||
# paquets n'est exclue, et c'est ce qui le rend portable sur toutes les
|
||||
# plateformes ERPLibre sans une branche par distribution.
|
||||
#
|
||||
# Les architectures, elles, sont bornées par l'amont : Forgejo publie
|
||||
# amd64, arm64 et arm-6, et RIEN pour s390x. Sur celle-là il faudrait le
|
||||
# bâtir en Go ; la case se grise plutôt que de poser un binaire qui ne
|
||||
# s'exécute pas.
|
||||
#
|
||||
# Disque : ~115 Mo de binaire (34 Mo téléchargés en .xz), la base SQLite
|
||||
# et les dépôts que l'utilisateur y poussera.
|
||||
"forgejo": {
|
||||
"label": "Forgejo (git forge)",
|
||||
"hint": "self-hosted git forge on :3000, SQLite",
|
||||
"disk_gb": 2,
|
||||
"arches": ("amd64", "arm64"),
|
||||
"desktops": (),
|
||||
"needs_desktop": False,
|
||||
"families": (),
|
||||
# APRÈS l'installation : le script vit dans le dépôt, donc après le
|
||||
# clone. Rien d'autre ne l'y oblige — Forgejo ne dépend ni du venv
|
||||
# ni d'Odoo.
|
||||
"phase": "after",
|
||||
},
|
||||
# L'émulateur n'a pas besoin de bureau DANS la VM : il s'affiche sur
|
||||
# l'écran de qui s'y connecte, par « ssh -X ». Il a besoin, lui, de KVM
|
||||
# dans la VM — donc de virtualisation imbriquée sur l'hôte, ce que le
|
||||
|
|
@ -6474,6 +6500,25 @@ class TODO:
|
|||
+ f'echo " {t("smoother, without X11:")} TODO > Execute > Deploy > QEMU/KVM > tunnel > 4"'
|
||||
)
|
||||
|
||||
def _qemu_forgejo_steps(self, el_dir):
|
||||
"""Pose Forgejo dans la VM, par le script dédié du dépôt.
|
||||
|
||||
Tout le travail est DANS le script — architecture, version, somme de
|
||||
contrôle, compte système, configuration, service, compte
|
||||
administrateur. Ce bloc ne fait que l'appeler : une seule autorité, et
|
||||
la même commande sert un déploiement de VM et une installation à la
|
||||
main sur une machine existante.
|
||||
|
||||
Pas de garde, comme la compilation mobile : une VM dont la forge
|
||||
demandée n'existe pas n'est pas la VM demandée. Le script, lui, est
|
||||
rejouable — il ne retélécharge pas un binaire déjà en place et ne
|
||||
réécrit jamais une configuration existante.
|
||||
"""
|
||||
return (
|
||||
f'echo "== {t("Forgejo (git forge)")} =="; '
|
||||
f"{el_dir}/script/forgejo/install_forgejo.sh"
|
||||
)
|
||||
|
||||
def _qemu_after_remote_cmd(self, tools, prod=False):
|
||||
"""Phase d'APRÈS l'installation : prologue commun, SDK commun, puis ce
|
||||
qui a été coché.
|
||||
|
|
@ -6486,29 +6531,39 @@ class TODO:
|
|||
émulateur créé avec succès effacerait le verdict de la compilation."""
|
||||
picked = [
|
||||
k
|
||||
for k in ("mobile", "avd")
|
||||
for k in ("forgejo", "mobile", "avd")
|
||||
if k in (tools or ()) and k in self._QEMU_VM_TOOLS
|
||||
]
|
||||
if not picked:
|
||||
return ""
|
||||
el_dir = self._qemu_install_dir(prod)
|
||||
parts = []
|
||||
# Forgejo d'abord : une minute, contre une heure pour le SDK et l'APK.
|
||||
# Un échec rapide se voit tôt plutôt qu'après le long.
|
||||
if "forgejo" in picked:
|
||||
parts.append(f"{{ {self._qemu_forgejo_steps(el_dir)}; }}")
|
||||
groups = []
|
||||
if "mobile" in picked:
|
||||
groups.append(self._qemu_mobile_build_steps(el_dir))
|
||||
if "avd" in picked:
|
||||
groups.append(self._qemu_avd_steps(el_dir))
|
||||
return (
|
||||
f'echo "== {t("ERPLibre mobile, Android SDK (long)")} =="; '
|
||||
+ self._qemu_android_prologue_cmd()
|
||||
+ self._qemu_android_sdk_steps(el_dir)
|
||||
# Chaque groupe entre ACCOLADES. Sans elles, « && » ne lie que la
|
||||
# première commande du groupe suivant : mesuré, un APK manquant
|
||||
# laissait tourner l'émulateur puis rendait 0 — la VM repassait au
|
||||
# vert alors que rien n'avait compilé. C'est le même piège que le
|
||||
# bloc de service systemd, quelques centaines de lignes plus haut.
|
||||
+ " && ".join(f"{{ {g}; }}" for g in groups)
|
||||
+ "; "
|
||||
)
|
||||
if groups:
|
||||
# UN seul prologue et un seul SDK même quand les deux options le
|
||||
# sont : deux prologues, et le second tronquerait le journal
|
||||
# détaillé du premier.
|
||||
parts.append(
|
||||
"{ "
|
||||
+ f'echo "== {t("ERPLibre mobile, Android SDK (long)")} =="; '
|
||||
+ self._qemu_android_prologue_cmd()
|
||||
+ self._qemu_android_sdk_steps(el_dir)
|
||||
# Chaque groupe entre ACCOLADES. Sans elles, « && » ne lie que
|
||||
# la première commande du groupe suivant : mesuré, un APK
|
||||
# manquant laissait tourner l'émulateur puis rendait 0 — la VM
|
||||
# repassait au vert alors que rien n'avait compilé.
|
||||
+ " && ".join(f"{{ {g}; }}" for g in groups)
|
||||
+ "; }"
|
||||
)
|
||||
return " && ".join(parts) + "; "
|
||||
|
||||
def _qemu_mobile_remote_cmd(self, prod=False):
|
||||
"""Compilation mobile seule — la forme que testent les tests."""
|
||||
|
|
|
|||
|
|
@ -2260,6 +2260,14 @@ TRANSLATIONS = {
|
|||
"line": {"fr": "ligne", "en": "line"},
|
||||
"free space": {"fr": "libre", "en": "free"},
|
||||
"silent": {"fr": "silence", "en": "silent"},
|
||||
"Forgejo (git forge)": {
|
||||
"fr": "Forgejo (forge git)",
|
||||
"en": "Forgejo (git forge)",
|
||||
},
|
||||
"self-hosted git forge on :3000, SQLite": {
|
||||
"fr": "forge git auto-hébergée sur :3000, SQLite",
|
||||
"en": "self-hosted git forge on :3000, SQLite",
|
||||
},
|
||||
"no project yet, second try on the warm caches": {
|
||||
"fr": "pas de projet encore ; seconde tentative sur les caches chauds",
|
||||
"en": "no project yet, second try on the warm caches",
|
||||
|
|
|
|||
|
|
@ -99,10 +99,17 @@ class TestToolDisk(unittest.TestCase):
|
|||
|
||||
def test_a_server_only_pays_for_what_it_gets(self):
|
||||
"""Un serveur ne porte aucun IDE, donc il n'en paie pas le disque —
|
||||
mais il paie bien ce qu'il reçoit : compilation mobile et émulateur."""
|
||||
mais il paie bien ce qu'il reçoit : compilation mobile, émulateur et
|
||||
forge. La somme est calculée depuis la table plutôt qu'écrite en
|
||||
chiffre : ajouter un outil sans écran ne doit pas casser ce test, il
|
||||
doit le suivre."""
|
||||
expected = sum(
|
||||
spec["disk_gb"]
|
||||
for key, spec in TODO._QEMU_VM_TOOLS.items()
|
||||
if not spec["needs_desktop"]
|
||||
)
|
||||
self.assertEqual(
|
||||
TODO._QEMU_VM_TOOLS["mobile"]["disk_gb"]
|
||||
+ TODO._QEMU_VM_TOOLS["avd"]["disk_gb"],
|
||||
expected,
|
||||
self.todo._qemu_tools_disk_gb(self.all, "amd64", "", "ubuntu"),
|
||||
)
|
||||
|
||||
|
|
@ -278,7 +285,9 @@ class TestMobileBuild(unittest.TestCase):
|
|||
"""Elle compile, elle n'affiche rien : un bureau serait du gaspillage.
|
||||
L'émulateur non plus n'en a pas besoin — il s'affiche par ssh -X."""
|
||||
got = self.todo._qemu_tools_for(self.all, "amd64", "", "ubuntu")
|
||||
self.assertEqual(["mobile", "avd"], got)
|
||||
self.assertEqual(["mobile", "forgejo", "avd"], got)
|
||||
# Forgejo est là pour la même raison que la compilation : un
|
||||
# service ne demande pas d'écran.
|
||||
|
||||
def test_it_is_bounded_to_apt(self):
|
||||
"""install-android.sh du dépôt mobile commence par « sudo apt install
|
||||
|
|
|
|||
260
test/test_qemu_forgejo.py
Normal file
260
test/test_qemu_forgejo.py
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
"""Option Forgejo : la case, son filtrage, et le script qui fait le travail.
|
||||
|
||||
Forgejo est un service, pas un outil de bureau : une VM serveur le prend comme
|
||||
une VM graphique. Son binaire est statique, donc le même fichier sert apt, dnf,
|
||||
pacman et zypper — c'est ce qui le rend portable sans une branche par
|
||||
distribution. Les architectures, elles, sont bornées par l'amont : Forgejo
|
||||
publie amd64, arm64 et arm-6, et rien pour s390x.
|
||||
|
||||
Le script est vérifié en l'EXÉCUTANT sur ses chemins de refus — architecture
|
||||
inconnue, version introuvable — qui précèdent toute élévation de privilège et
|
||||
ne touchent donc à rien.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pathlib
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.argv = ["todo.py"]
|
||||
from script.todo.todo import TODO # noqa: E402
|
||||
|
||||
SCRIPT = (
|
||||
pathlib.Path(__file__).resolve().parent.parent
|
||||
/ "script/forgejo/install_forgejo.sh"
|
||||
)
|
||||
|
||||
|
||||
class TestTheCheckbox(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
self.spec = TODO._QEMU_VM_TOOLS["forgejo"]
|
||||
|
||||
def test_it_is_offered_in_the_form(self):
|
||||
keys = [k for k, _l, _h in TODO._qemu_vm_tool_choices()]
|
||||
self.assertIn("forgejo", keys)
|
||||
|
||||
def test_a_plain_server_gets_it(self):
|
||||
"""Une forge n'affiche rien : elle n'a pas besoin de bureau."""
|
||||
self.assertFalse(self.spec["needs_desktop"])
|
||||
got = self.todo._qemu_tools_for(("forgejo",), "amd64", "", "ubuntu")
|
||||
self.assertIn("forgejo", got)
|
||||
|
||||
def test_every_package_family_gets_it(self):
|
||||
"""Le binaire est statique : aucune famille n'est exclue, à la
|
||||
différence de la compilation mobile que son installateur borne à apt.
|
||||
"""
|
||||
self.assertEqual(self.spec["families"], ())
|
||||
for distro in ("ubuntu", "debian", "almalinux", "opensuse", "arch"):
|
||||
self.assertIn(
|
||||
"forgejo",
|
||||
self.todo._qemu_tools_for(("forgejo",), "amd64", "", distro),
|
||||
distro,
|
||||
)
|
||||
|
||||
def test_arm64_yes_s390x_no(self):
|
||||
"""Forgejo publie amd64, arm64 et arm-6. Sur s390x il faudrait le bâtir
|
||||
en Go : la case se grise plutôt que de poser un binaire inexécutable.
|
||||
"""
|
||||
for arch in ("amd64", "arm64"):
|
||||
self.assertIn(
|
||||
"forgejo",
|
||||
self.todo._qemu_tools_for(("forgejo",), arch, "", "ubuntu"),
|
||||
arch,
|
||||
)
|
||||
self.assertNotIn(
|
||||
"forgejo",
|
||||
self.todo._qemu_tools_for(("forgejo",), "s390x", "", "ubuntu"),
|
||||
)
|
||||
|
||||
def test_its_disk_cost_is_counted_in_the_plan(self):
|
||||
self.assertGreater(
|
||||
self.todo._qemu_tools_disk_gb(("forgejo",), "amd64", "", "ubuntu"),
|
||||
0,
|
||||
)
|
||||
|
||||
|
||||
class TestTheInstallBlock(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO.__new__(TODO)
|
||||
|
||||
def test_it_calls_the_dedicated_script(self):
|
||||
"""Une seule autorité : la même commande sert le déploiement d'une VM et
|
||||
une installation à la main."""
|
||||
for prod, root in (
|
||||
(False, "$HOME/git/erplibre"),
|
||||
(True, "/opt/erplibre"),
|
||||
):
|
||||
block = self.todo._qemu_forgejo_steps(
|
||||
self.todo._qemu_install_dir(prod)
|
||||
)
|
||||
self.assertIn(f"{root}/script/forgejo/install_forgejo.sh", block)
|
||||
|
||||
def test_alone_it_does_not_drag_the_android_prologue(self):
|
||||
"""Cocher Forgejo seul ne doit pas installer un SDK Android."""
|
||||
cmd = self.todo._qemu_after_remote_cmd(("forgejo",), False)
|
||||
self.assertIn("install_forgejo.sh", cmd)
|
||||
self.assertNotIn("sdkmanager", cmd)
|
||||
self.assertNotIn("mstep", cmd)
|
||||
|
||||
def test_with_the_mobile_build_forgejo_comes_first(self):
|
||||
"""Une minute contre une heure : un échec rapide se voit tôt."""
|
||||
cmd = self.todo._qemu_after_remote_cmd(("forgejo", "mobile"), False)
|
||||
self.assertLess(cmd.index("install_forgejo.sh"), cmd.index("gradlew"))
|
||||
|
||||
def test_a_forgejo_failure_still_fails_the_vm(self):
|
||||
"""Même contrat que la compilation mobile : une VM dont la forge
|
||||
demandée n'existe pas n'est pas la VM demandée. Les groupes sont donc
|
||||
liés par « && », jamais par « ; »."""
|
||||
cmd = self.todo._qemu_after_remote_cmd(("forgejo", "mobile"), False)
|
||||
between = cmd[
|
||||
cmd.index("install_forgejo.sh") : cmd.index("ERPLibre mobile")
|
||||
]
|
||||
self.assertIn("&&", between)
|
||||
self.assertNotIn("|| true", between)
|
||||
|
||||
def test_valid_shell_in_every_combination(self):
|
||||
for tools in (
|
||||
("forgejo",),
|
||||
("forgejo", "mobile"),
|
||||
("forgejo", "avd"),
|
||||
("forgejo", "mobile", "avd"),
|
||||
):
|
||||
cmd = self.todo._qemu_after_remote_cmd(tools, False)
|
||||
res = subprocess.run(
|
||||
["bash", "-n"],
|
||||
input="mstep() { :; }; mdiag() { :; };\n" + cmd,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
self.assertEqual(0, res.returncode, f"{tools}: {res.stderr}")
|
||||
|
||||
|
||||
class TestTheScript(unittest.TestCase):
|
||||
"""Le script lui-même, exécuté sur ses chemins de refus."""
|
||||
|
||||
def test_it_is_executable_and_valid_shell(self):
|
||||
self.assertTrue(os.access(SCRIPT, os.X_OK), "pas exécutable")
|
||||
res = subprocess.run(
|
||||
["bash", "-n", str(SCRIPT)], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(0, res.returncode, res.stderr)
|
||||
|
||||
def test_help_explains_the_knobs_and_exits_clean(self):
|
||||
res = subprocess.run(
|
||||
["bash", str(SCRIPT), "--help"], capture_output=True, text=True
|
||||
)
|
||||
self.assertEqual(0, res.returncode, res.stderr)
|
||||
for knob in (
|
||||
"FORGEJO_VERSION",
|
||||
"FORGEJO_HTTP_PORT",
|
||||
"FORGEJO_ADMIN_USER",
|
||||
):
|
||||
self.assertIn(knob, res.stdout)
|
||||
|
||||
def _run_with_stubs(self, stubs, env=None):
|
||||
"""Lance le script avec un PATH bouchonné. Les chemins testés ici
|
||||
s'arrêtent AVANT tout sudo : rien n'est installé nulle part."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
bin_dir = pathlib.Path(tmp) / "bin"
|
||||
bin_dir.mkdir()
|
||||
for name, body in stubs.items():
|
||||
(bin_dir / name).write_text(f"#!/bin/bash\n{body}\n")
|
||||
(bin_dir / name).chmod(0o755)
|
||||
return subprocess.run(
|
||||
["bash", str(SCRIPT)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
env=dict(
|
||||
os.environ,
|
||||
PATH=f"{bin_dir}:/usr/bin:/bin",
|
||||
**(env or {}),
|
||||
),
|
||||
timeout=120,
|
||||
)
|
||||
|
||||
def test_an_unpublished_architecture_is_refused_by_name(self):
|
||||
"""Sur s390x, Forgejo n'a pas de binaire. Le dire vaut mieux que
|
||||
télécharger un fichier qui ne s'exécutera pas."""
|
||||
res = self._run_with_stubs({"uname": "echo s390x"})
|
||||
self.assertNotEqual(0, res.returncode)
|
||||
self.assertIn("s390x", res.stdout + res.stderr)
|
||||
|
||||
def test_an_unreachable_release_feed_is_named(self):
|
||||
res = self._run_with_stubs({"uname": "echo x86_64", "curl": "exit 7"})
|
||||
self.assertNotEqual(0, res.returncode)
|
||||
self.assertIn("Version", res.stdout + res.stderr)
|
||||
|
||||
def test_a_pinned_version_needs_no_feed(self):
|
||||
"""FORGEJO_VERSION évite l'appel réseau : utile hors ligne, et c'est ce
|
||||
qui rend ce test rapide."""
|
||||
res = self._run_with_stubs(
|
||||
{"uname": "echo x86_64", "curl": "exit 7", "sudo": "exit 0"},
|
||||
env={"FORGEJO_VERSION": "9.9.9"},
|
||||
)
|
||||
out = res.stdout + res.stderr
|
||||
self.assertIn("9.9.9", out)
|
||||
# Il échoue plus loin (le téléchargement est bouchonné), pas sur la
|
||||
# version : c'est bien le réseau du flux qui a été évité.
|
||||
self.assertNotIn("Version de Forgejo introuvable", out)
|
||||
|
||||
|
||||
class TestTheScriptGuards(unittest.TestCase):
|
||||
"""Quatre pièges rencontrés en le mettant au point, tous mesurés."""
|
||||
|
||||
def setUp(self):
|
||||
self.body = SCRIPT.read_text()
|
||||
|
||||
@property
|
||||
def code_lines(self):
|
||||
"""Les lignes de CODE : le piège est expliqué en commentaire, et un
|
||||
test qui cherche dans les commentaires trébuche sur sa propre
|
||||
documentation — vécu à l'écriture de ce fichier."""
|
||||
return [
|
||||
ln
|
||||
for ln in self.body.splitlines()
|
||||
if ln.strip() and not ln.lstrip().startswith("#")
|
||||
]
|
||||
|
||||
def test_it_never_probes_with_exec_on_dev_tcp(self):
|
||||
"""« exec » est un builtin spécial : une redirection qui échoue termine
|
||||
le shell. Le script mourait au premier tour de la boucle d'attente,
|
||||
code 1 et pas un mot."""
|
||||
guilty = [ln for ln in self.code_lines if "exec 3<>" in ln]
|
||||
self.assertEqual([], guilty)
|
||||
self.assertIn("/api/v1/version", self.body)
|
||||
|
||||
def test_the_config_test_goes_through_sudo(self):
|
||||
"""/etc/forgejo est en 770 root:git : « [ -f ] » échouait toujours, et
|
||||
chaque passage réécrivait la configuration avec des secrets neufs."""
|
||||
self.assertIn("sudo test -f", self.body)
|
||||
|
||||
def test_all_four_secrets_are_written(self):
|
||||
"""Sans oauth2.JWT_SECRET, Forgejo tente de l'écrire dans app.ini,
|
||||
n'y arrive pas, et boucle sur « [F] save oauth2.JWT_SECRET failed »."""
|
||||
for key in (
|
||||
"SECRET_KEY",
|
||||
"INTERNAL_TOKEN",
|
||||
"JWT_SECRET",
|
||||
"LFS_JWT_SECRET",
|
||||
):
|
||||
self.assertIn(key, self.body, key)
|
||||
|
||||
def test_the_default_admin_name_is_not_reserved(self):
|
||||
"""Forgejo refuse « admin » : « CreateUser: name is reserved »."""
|
||||
self.assertIn("FORGEJO_ADMIN_USER:-erplibre", self.body)
|
||||
self.assertNotIn("FORGEJO_ADMIN_USER:-admin}", self.body)
|
||||
|
||||
def test_it_touches_no_package_manager(self):
|
||||
"""C'est ce qui le rend portable : le binaire est statique."""
|
||||
for pm in ("apt-get install", "dnf install", "pacman -S", "zypper"):
|
||||
self.assertNotIn(pm, self.body, pm)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue