Compare commits

...
Author SHA1 Message Date
koka fb20160eff Document the complete Sanctuary update workflow
Build Sanctuary / build (push) Waiting to run
2026-10-26 00:46:35 +01:00
koka 2a4fc2427e Record the direct Prism download and first-launch validation
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 16:53:38 +01:00
koka c219d237ab Ship a ready Prism bootstrap instead of manual setup
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 16:49:08 +01:00
koka 8b6c6c6b21 Document verified one-time Prism setup for Beta players
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 16:38:53 +01:00
koka a6b547c4c6 Publish one verified Beta MRpack matching the dedicated CT
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 05:25:16 +01:00
koka 235cf51d6e Document Sanctuary Web alignment with the Beta database
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 05:09:39 +01:00
koka 17c1cd9e71 Verify permanent Beta route and retire UPnP fallback
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 04:51:51 +01:00
koka 276c8f2144 Record Beta server deployment, web connections and remaining Bbox target
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 04:44:08 +01:00
koka 23b295356b Embed JEI and add a managed dedicated server pack (beta.280)
Build Sanctuary / build (push) Canceled after 0s
2026-10-25 04:17:18 +01:00
20 changed files with 1377 additions and 85 deletions
+3
View File
@@ -2,6 +2,9 @@
Ce dépôt contient le pack Sanctuary et ses mods. Lire `README.md`, puis le ticket concerné.
La vision est dans `docs/vision.md` ; elle décrit aussi des fonctionnalités futures.
Pour développer/livrer le site, packwiz, la Beta dédiée et Prism local, lire
`docs/updates.md`. La compétence personnelle `sanctuary-updates` mémorise les
chemins et accès propres au poste ; ne pas déduire une publication d'un simple push.
- Avant une modification, lire l'état Git et préserver les changements existants.
- Un ticket correspond à un résultat jouable ou vérifiable et à une branche `codex/<sujet>`.
+18
View File
@@ -1,5 +1,23 @@
# Sanctuary
Le [téléchargement direct Sanctuary Beta](docs/beta-download.md) fournit une
instance Prism prête : importer le ZIP, puis lancer pour télécharger le pack.
Les publications du canal restent commandées par le créateur via Codex.
[Procédure de développement et de mise à jour complète](docs/updates.md) :
site, artefacts immuables, canal packwiz, serveur Beta et même instance Prism locale.
## beta.280 — JEI réintégré et packwiz serveur
[Un seul JAR Sanctuary avec le fork JEI et un profil packwiz serveur](docs/jei-server-beta280.md).
Client et serveur partagent le même artefact ; le profil serveur ne contient
que Sanctuary et Fabric API. Le [kit Linux](docs/server-pack.md) prépare les
mises à jour et la connexion aux services Web existants. Build, 13 GameTests,
sept contrôles de l'outil de mise à jour, installation packwiz et démarrage du
JAR de production vérifiés. La nouvelle CT 350 sur `gpu` accueille le serveur
de 20 places, relié à l'API, à la communauté et à la console privée du site.
[Release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280).
Les **quatre expansions validées sont disponibles dans Sanctuary normal** :
Nord glacial, Sud des canyons, Ouest océanique et Est volcanique.
Créer un monde **Sanctuary** en Petit, Moyen ou Grand, puis activer les ancres
+4 -2
View File
@@ -113,8 +113,10 @@ aucune copie des textures de mobs ni aucun ancien pouvoir n’est embarqué.
Just Enough Items, mezz, MIT : fork source de la branche 26.2, commit
`aae2dfcfb82e6b5bce787ba72bb2eda7339b274f`, adapté à Minecraft 26.3-pre-2.
Depuis beta.257, le fork Minecraft 26.3 `30.32.0-sanctuary.4` est un JAR client
séparé et facultatif, avec la licence d’origine et [la provenance du portage](mods/jei/PROVENANCE.md).
Depuis beta.280, le fork Minecraft 26.3 `30.32.0-sanctuary.4` est à nouveau
embarqué dans le JAR Sanctuary, avec la licence d’origine et
[la provenance du portage](mods/jei/PROVENANCE.md). Sa distribution séparée
facultative concernait les beta.257–278.
Les patches et la reconstruction sont fournis ; les téléchargements restent ignorés.
Baked Substring Index 0.1.0 (MIT, James Mitchell) et Suffix Tree 1.1.0
(Apache-2.0, Alessandro Bahgat Shehata) sont inclus avec leurs licences.
+19
View File
@@ -15,6 +15,18 @@ allprojects {
tasks.named('build') { dependsOn(':sanctuary:build', ':jei:build', ':sanctuary-test:build') }
tasks.named('check') { dependsOn(':sanctuary:check', ':jei:check', ':sanctuary-test:check', 'verifyPack') }
tasks.register('verifyServerUpdater', Exec) {
group = 'verification'
commandLine('python3', 'scripts/test_server_runtime.py')
}
tasks.named('check') { dependsOn('verifyServerUpdater') }
tasks.register('verifyWebDistribution', Exec) {
group = 'verification'
commandLine('python3', 'scripts/test_publish_web_pack.py')
}
tasks.named('check') { dependsOn('verifyWebDistribution') }
tasks.register('verifyPack', Exec) {
group = 'verification'
description = 'Verify pinned pack dependencies, versions and index hashes.'
@@ -28,6 +40,13 @@ tasks.register('assemblePack', Exec) {
commandLine('python3', 'scripts/pack.py', 'assemble')
}
tasks.register('assembleServerPack', Exec) {
group = 'distribution'
description = 'Build the Linux server kit and verify its packwiz profile.'
dependsOn('assemblePack')
commandLine('python3', 'scripts/server_pack.py')
}
tasks.register('verifyResourcePack', Exec) {
group = 'verification'
description = 'Verify the versioned Sanctuary texture sources and their release hashes.'
+116
View File
@@ -0,0 +1,116 @@
# DIST-WEB — téléchargement direct d'une instance Prism prête
Branche `codex/beta-download`, serveur dédié CT 350 sur `gpu`. Cette livraison
conserve les binaires immuables beta.280 et ne modifie ni monde ni format de
sauvegarde. L'Alpha reste hors cible.
## Résultat pour le joueur
Le bouton **Télécharger** de [Sanctuary Web](https://sanctuary-minecraft.net)
donne directement le fichier **Sanctuary-Beta.zip** :
```text
https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip
```
Le joueur importe ce ZIP dans Prism (**Ajouter une instance → Importer depuis
un ZIP**), puis lance **Sanctuary Beta**. Packwiz télécharge le pack complet au
premier lancement et suit le même canal aux lancements suivants. La commande
avant lancement et le bootstrap sont déjà inclus ; aucune configuration manuelle
ou téléchargement séparé d'outil n'est demandé.
Le format ZIP d'instance Prism conserve ces réglages, contrairement à un MRpack
standard. Le MRpack beta.280 reste accessible par son ancienne adresse pour
compatibilité des liens, sans devenir un deuxième bouton du site. Le parcours
manuel de configuration et son lien dans le pied de page sont retirés.
L'archive légère contient uniquement `instance.cfg`, `mmc-pack.json`, l'icône et
`minecraft/packwiz-installer-bootstrap.jar`. Elle démarre avec zéro mod local.
L'instance est nommée **Sanctuary Beta**, avec `ManagedPack=false`,
`OverrideCommands=true`, la sélection automatique de Java et le canal stable :
```text
https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
```
Le nom du bouton et le téléchargement direct sont conservés. Le pied de page et
le survol indiquent la version réellement publiée. Les réponses de téléchargement
utilisent `Cache-Control: no-store` pour garder la même adresse à chaque livraison.
## Publication demandée via Codex
Aucune tâche périodique ne décide de publier une version. À la demande du créateur :
1. Vérifier et publier les nouveaux binaires immuables, puis le canal packwiz
selon [le protocole de distribution](packwiz.md).
2. Générer l'instance prête avec `python3 scripts/pack.py prism <url-du-canal>`.
Joindre `build/Sanctuary-Prism-beta.xxx.zip` à la release, avec le MRpack et
le kit serveur. L'export Prism est reproductible et conserve ses commandes.
3. Mettre à jour la CT dédiée selon [le contrat serveur](server-pack.md), en
conservant sauvegardes, réglages et secrets, puis vérifier son redémarrage.
4. Publier le téléchargement Web après contrôle de cette CT et des deux archives :
```sh
python3 scripts/publish_web_pack.py \
--web-ct 320 --server-ct 350 --version beta.280 \
--mrpack-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack \
--prism-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-Prism-beta.280.zip \
--pack-url https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
```
Cette dernière commande s'exécute sur `gpu`, où `pct` est disponible. `--dry-run`
vérifie sans publier. Le script contrôle les index, les versions, les hashes des
dépendances et le JAR Sanctuary de la CT, du canal et du MRpack. Il refuse une
archive Prism sans commande active, visant un autre canal ou une autre plateforme,
avec un bootstrap non vérifié ou des fichiers étrangers.
Les fichiers Web restent dans
`/var/www/sanctuary/storage/app/distribution/releases/beta.xxx/prism/`.
Chaque publication est immuable. Sous verrou, `current` bascule atomiquement après
écriture complète. Les anciennes versions restent disponibles pour une reprise.
Le script ne redémarre aucun serveur et ne change pas le canal lui-même.
## Vérifications
Huit tests Python couvrent l'alignement CT/canal/MRpack, le refus d'un serveur
arrêté ou divergent, le contrat d'import Prism prêt, les fichiers interdits et la
publication atomique avec conservation de la précédente en cas de conflit.
Ils font partie de `./gradlew check`.
Les quatre tests de téléchargement et les trois tests des métriques Laravel
passent, ainsi que Pint : le bouton sert un ZIP anonyme directement, le nom est
`Sanctuary-Beta.zip`, les publications incomplètes restent indisponibles, et le
changement de version ne demande pas de reconstruire le cache de configuration.
Le MRpack reste téléchargeable par son ancien lien.
Les tests natifs utilisent une extraction de l'archive prête dans un chemin
contenant des espaces, avec les variables de sa propre commande avant lancement.
Ils exécutent packwiz sans interface ; aucun Minecraft ni monde personnel n'est
ouvert. Les fichiers témoins de réglages, favoris et sauvegardes sont comparés
avant et après synchronisation. Les journaux sont conservés sous
`build/prism-ready-validation*/`, hors de Git.
`./gradlew check build assemblePack -PsanctuaryFocusedTests=recipes,progression
-PsanctuaryAtlasOnly=true` réussit. L'archive prête mesure **92 417 octets**, SHA-256
`30dd37f3a6e1d79f8b443f6312918258276c85d5b6a5e7fbdc9c3fea7562dbf2`.
Une première tentative native reçoit des HTTP 502 de Gitea ; une nouvelle
extraction entièrement vide réussit ensuite ses deux lancements de synchronisation.
Le reçu est `build/prism-ready-validation-retry/validation.json`.
L'archive est ajoutée à la release beta.280 sans changer son tag ni ses artefacts
existants. La CT Web publie ensuite le ZIP et le MRpack vérifiés dans la même
bascule atomique. Le code Web est livré au commit
`46d784ecbb8ba3d0f20199b85bcf486831ba3750` ; les changements préexistants sont
conservés et les empreintes des comptes et permissions restent identiques.
Sauvegarde privée :
`/root/sanctuary-beta-maintenance/web-prism-ready-20261025-1792943502/`.
Le parcours navigateur **accueil → Télécharger → Sanctuary-Beta.zip reçu** est
vérifié à 1280 × 900 et 390 × 844 : HTTP 200 direct, pas de changement de page,
nom et SHA-256 identiques à l'archive testée, aucune erreur d'application ni
débordement horizontal. Le lien de guide manuel est absent. Le MRpack historique
répond toujours HTTP 200. Le navigateur utilisé est Playwright, le plugin
Browser étant absent ; reçu et captures sous `/tmp/sanctuary-prism-ready-*`.
Reçus opérationnels ignorés : `build/prism-ready-publication.json`,
`build/prism-ready-web-publication.json`, `build/prism-ready-web-deployment.log`
et `build/prism-ready-build.log`.
+189
View File
@@ -0,0 +1,189 @@
# DIST-280 — JEI embarqué et packwiz serveur
Branche `codex/jei-packwiz-server`, base commit `697e4a2` (beta.278).
Version réservée `beta.280` : beta.279 appartient au chantier parallèle
Map Island Zoom, dont les modifications ne sont pas reprises ici.
## Contrat
- Un seul JAR Sanctuary à distribuer, avec le fork JEI exact
`30.32.0-sanctuary.4` et ses notices/bibliothèques embarqués. Fabric API reste
une dépendance commune ; Sodium et Sound Physics restent facultatifs côté client.
- Même JAR sur client et serveur. JEI possède une initialisation commune
(recettes et transfert), ses écrans restent dans son entrée client.
- Un manifeste serveur `server/pack.toml` sur le même canal packwiz que le
client, avec uniquement Sanctuary et Fabric API. Même version, URL et hash
du JAR Sanctuary dans les deux profils. Publication atomique des manifestes
après vérification de l'artefact immuable.
- Un kit serveur local avec mise à jour packwiz sans interface et lancement
Fabric. Les mondes, configurations, permissions, listes de joueurs et secrets
restent hors de l'index. Une erreur de mise à jour empêche le démarrage.
- Documenter les paramètres existants de l'API Sanctuary Web et de la MariaDB
communautaire, sans inventer d'API ni migrer les données ou configurer un site
public implicitement. Les secrets restent dans l'environnement du service.
## Vérifications locales
`./gradlew check build assemblePack assembleServerPack
-PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true` réussit :
13 GameTests requis, contrôles purs du dépôt et assemblages. Le fixture ciblé
évite le harnais worldgen historique `unified`, sans modifier sa génération.
Les sept tests de l'outil de mise à jour passent séparément, y compris le
relais du signal d'arrêt et l'attente de Java avant libération du verrou.
Installation réelle avec packwiz, deux synchronisations et six fichiers
personnels témoins conservés octet par octet. Le JAR de production démarre et
s'arrête dans un monde plat de développement neuf avec le JEI imbriqué ;
celui-ci correspond au fork construit. Minecraft normalise lui-même les fichiers
`server.properties`, `ops.json` et `whitelist.json` au lancement : leurs valeurs
sont vérifiées après cet arrêt, distinctement de la préservation par packwiz.
La transition du profil client retire réellement le JEI séparé suivi par
packwiz et conserve les favoris. Les manifestes de publication client/serveur
référencent le même artefact. Aucun nouveau parcours graphique n'est revendiqué ;
les contrôles historiques Vulkan de l'interface ne sont pas rejoués ici.
Journaux et reçus ignorés : `build/jei-server-final.log`,
`build/server280-updater-final.log`, `build/server280-validation-delivery/validation.json`,
`build/client280-validation/validation.json`.
## Serveur dédié demandé le 25 octobre 2026
Le créateur demande une **nouvelle CT pour 20 joueurs** sur le nœud `gpu`.
La CT 350 `sanctuary-beta-20` est créée à cette fin : Debian 13 amd64,
6 vCPU, 8 Gio de RAM, disque 80 Gio, non privilégiée. Le budget Java retenu est
6 Gio, avec 20 places, portée 6 et simulation 4 ; ce dimensionnement n'est pas
une mesure de charge avec vingt joueurs connectés.
L'Alpha (CT 300), les CT 330 et 340 et les mondes existants restent hors cible.
Le nouveau monde utilisera `sanctuary:sanctuary` (réglages initiaux
`sanctuary:island231_medium`), graine `28020261025`, version beta.280.
Aucune expansion n'est activée par le déploiement. Le site est dans la CT 320 ;
son API et son schéma communautaire 3 sont présents.
## Livraison et mise en service
La [release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280)
est publiée depuis le commit `23b295356b499404e0056affbb5d3b3811c1b01f`.
Le canal packwiz est avancé au commit `338e8a62387048c7e703ef036fda759ef57b4130`,
avec les profils client et serveur dans le même commit. Aucun JEI séparé n'est
joint. Le MRpack contient le seul JAR Sanctuary en override, et trois dépendances
téléchargées (Fabric API, Sodium et Sound Physics).
| Artefact | SHA-256 |
| --- | --- |
| `sanctuary-beta.280.jar` | `489dade38218d34d2a4502fa4ffe71f0bc13035948dc1c52b633b528e9855c7e` |
| `Sanctuary-Server-beta.280.zip` | `54ed73e75b53305a9f0cc5505f3d64624d23598f906ac69d121030255ced0d5a` |
| `Sanctuary-beta.280.mrpack` | `b23ba2d9cf72d27da2595e3f4a32a36656f71abf2d2cb4d6cefd7f9d29e4cd01` |
Le kit téléchargé dans la nouvelle CT correspond à cet artefact immuable.
Java 25, Python 3.13 et le service `sanctuary-beta.service` sont installés ;
le service est activé au démarrage. Le premier monde Sanctuary a été créé à
la graine annoncée, avec le générateur 231, taille Medium, en Survie et
`online-mode=true`. La réponse réseau Minecraft annonce 26.3, protocole 777,
**20 places**, et le JEI embarqué est chargé côté serveur.
La synchronisation packwiz a été exécutée après arrêt propre du nouveau serveur :
**61 fichiers de monde, de configuration et de secrets gardent leurs hashes**.
Le même monde redémarre ensuite. L'outil maintient son verrou jusqu'à la fin
de la sauvegarde Java, y compris lorsque systemd transmet SIGTERM.
L'API du site accepte l'authentification et refuse un corps de vérification
vide avec HTTP 422 : aucun code d'habitant n'est consommé. L'accueil serveur
confirme que les codes sont requis. Un compte MariaDB propre à Beta a été créé,
restreint à son hôte et aux sept tables nécessaires, avec TLS obligatoire ;
Connector/J utilise `sslMode=verify-full`. Le schéma existant 3 n'est pas migré.
La ligne de statistiques du nouveau serveur est alimentée toutes les 30 secondes.
La console du site était non configurée. Seules ses trois variables RCON ont
été renseignées pour Beta, après sauvegarde privée ; le cache Laravel est
reconstruit sans redémarrer le site. La commande de lecture `list`, appelée
par le vrai service PHP, répond « 0 of a max of 20 ». Le port RCON est filtré
dans la CT Beta : accès du site et de loopback uniquement, refus depuis un autre
client LAN. Aucune redirection publique RCON n'est créée.
Les secrets restent dans `/etc/sanctuary/beta.env`, le `server.properties`
privé et l'environnement Web, jamais dans Git ou packwiz. Les unités
`sanctuary-beta-rcon-firewall.service` et le complément systemd associé
appartiennent à cette installation, hors de l'index de mods.
Le domaine demandé est `beta.sanctuary-minecraft.net`. Les deux enregistrements
OVH sont publiés : A pour Beta et SRV `_minecraft._tcp.beta` vers le port 25567,
TTL 300. Les quatorze enregistrements DNS préexistants sont conservés. Une
réservation DHCP lie l'adresse de la CT à sa MAC ; elle est activée après test
de syntaxe et redémarrage de dnsmasq, sans changer ses deux fichiers existants.
Aucun serveur Minecraft ou proxy existant n'est redémarré par cette opération.
Une redirection UPnP Beta seule a servi d'amorçage. Le 25 octobre, le créateur
réenregistre la règle Bbox permanente « Sanctuary beta 20 » : **port externe
25567 → 192.168.1.122:25565**. L'API de la Bbox déclare encore `protocol=all`
(TCP/UDP) ; l'accès Minecraft TCP est vérifié.
La redirection UPnP appartenant à Beta est ensuite supprimée. Son timer est
désactivé, le service est arrêté, puis les trois fichiers du maintien sont
retirés de la configuration active et archivés dans la CT sous
`/root/sanctuary-beta-maintenance/upnp-retired-20261025-1792900244/`.
Les sept autres règles NAT permanentes et les quatre autres redirections UPnP
sont conservées. Le processus Minecraft reste le même : aucun redémarrage ni
modification d'une sauvegarde n'est nécessaire à ce retrait.
Après suppression du secours, `beta.sanctuary-minecraft.net:25567` répond bien
Minecraft **26.3**, protocole **777**, **0/20**, avec le MOTD du nouveau Beta.
Le SRV public permet de saisir seulement `beta.sanctuary-minecraft.net`.
La vérification utilise le chemin public depuis le LAN (hairpin) ; une validation
depuis un réseau Internet extérieur reste à effectuer.
Le [MRpack client publié](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack)
est retéléchargé et vérifié : 16 589 058 octets, hash identique au fichier local,
ZIP intègre, version beta.280, Minecraft 26.3 et un seul override Sanctuary avec
JEI intégré.
Aucune charge réelle avec vingt joueurs, nouvelle connexion OAuth Discord ou
nouvelle qualification graphique n'est revendiquée.
Les sources sont isolées de Map Island Zoom. La prochaine livraison doit
reprendre ce profil serveur et utiliser un compteur supérieur à beta.280.
L'instance Prism personnelle n'a pas été synchronisée par cette mise en service.
Preuves locales supplémentaires : `build/server280-publication.log`,
`build/server280-deployment.log`, `build/server280-gpu-update-validation.log`,
`build/server280-rcon-setup.log`, `build/server280-dhcp-reservation.log`,
`build/server280-upnp-retirement.log` et
les inventaires sous `/root/beta-publication-20261025/` sur PVE. Ces fichiers
restent hors de Git.
## Raccordement Sanctuary Web corrigé le 25 octobre 2026
Le statut et l'Intendance de [Sanctuary Web](https://sanctuary-minecraft.net)
lisaient encore l'ancienne base distante par la connexion Laravel `game`, alors
que Beta écrivait déjà dans la base `sanctuary` de la CT Web 320. La vérification
initiale des écritures serveur ne couvrait donc pas toutes les lectures du site.
Hermes confirme l'identité de cette instance Web ; les anciens sites de release
ne sont pas remis en service.
La connexion `game` utilise maintenant la même base locale que Beta, en gardant
`COMMUNITY_DB_CONNECTION=game` pour les services et les ressources
d'administration. Le cache de configuration Laravel est reconstruit. Les sources
Web déployées, qui contiennent des changements préexistants, restent inchangées.
La connexion Discord fonctionne selon le créateur et n'est pas modifiée.
La nouvelle communauté ne contient aucune publication : Gazette, Tableau et
Intendance affichent désormais cet état, sans suppression dans l'ancienne base.
Les trois comptes, dont deux administrateurs, le groupe d'équipe, ses deux
adhésions et les vingt-deux permissions sont conservés. Les empreintes des
identités, des droits administrateurs et des tables d'équipe sont identiques
avant et après l'opération.
Les services publics et les requêtes d'administration Gazette/Tableau lisent
zéro publication. La page d'accueil et les deux listes publiques répondent HTTP
200. Les métriques sont fraîches, le serveur est déclaré en fonctionnement, et
son temps cumulé continue d'augmenter. Le compteur reste à zéro jour tant que
Beta n'a pas cumulé vingt-quatre heures ; les anciens décès ne sont plus affichés.
La configuration précédente est sauvegardée dans la CT Web, avec accès root
seul, sous
`/root/sanctuary-beta-maintenance/web-game-connection-20261025-1792901058/`.
Le reçu `build/sanctuary-web-beta-alignment.log` et la capture
`build/sanctuary-web-beta-after.png` restent hors de Git. Cette correction de
configuration et de documentation ne change aucun binaire beta.280 ni aucune
sauvegarde Minecraft.
+52 -39
View File
@@ -1,5 +1,42 @@
# Distribution packwiz et Prism
Le [téléchargement direct de Sanctuary Web](beta-download.md) donne une instance
Prism déjà configurée. Le premier lancement télécharge les mods ; les suivants
suivent les publications explicitement demandées via Codex.
La [procédure de reprise et de publication complète](updates.md) relie aussi
le serveur dédié, le téléchargement du site et l'instance locale. Les reçus
des anciennes versions ci-dessous restent historiques.
## beta.280 — JEI embarqué et profil serveur
JEI rejoint le JAR Sanctuary ; `mods/sanctuary.pw.toml` reste le seul artefact
Sanctuary à publier. `python3 scripts/pack.py release <sanctuary-https-url>`
prépare désormais le client et `server/pack.toml`, qui partagent le même JAR,
la même URL et le même hash. Le profil serveur contient seulement Fabric API
et Sanctuary ; Sodium et Sound Physics restent facultatifs côté client.
Les deux profils sont publiés dans le même commit du canal, après vérification
du JAR immuable. L'ancienne entrée JEI est retirée de l'index client.
`./gradlew assembleServerPack` construit un kit Linux avec les outils épinglés
et les commandes de synchronisation/démarrage. [Mode d'emploi](server-pack.md),
[contrat et vérifications](jei-server-beta280.md). La configuration Sanctuary Web,
MariaDB et les secrets restent hors de l'index. La préparation locale ne publie
pas le canal et ne met pas à jour une installation personnelle.
Livraison du 25 octobre : [release beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/tag/beta.280)
publiée, canal `338e8a62387048c7e703ef036fda759ef57b4130`, puis nouveau serveur
CT 350 sur `gpu` installé avec le kit vérifié. Le monde neuf, les paramètres
et les services Web sont vérifiés dans le [reçu](jei-server-beta280.md).
Une synchronisation du serveur conserve 61 fichiers suivis. L'Alpha et
l'instance Prism personnelle restent hors de cette mise en service.
La règle Bbox permanente est ensuite vérifiée et le secours UPnP Beta retiré.
Le domaine Minecraft `beta.sanctuary-minecraft.net` répond sans ce secours,
avec 20 places. [MRpack client beta.280](https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.280/Sanctuary-beta.280.mrpack)
vérifié et disponible ; le [reçu](jei-server-beta280.md) conserve les empreintes
et la limite de validation réseau.
## beta.257 — Mods client séparés
JEI Sanctuary, Sound Physics Remastered et Sodium sont facultatifs et séparés
@@ -2078,18 +2115,18 @@ La procédure complète, également utilisable manuellement :
1. Incrémenter ensemble les versions de `gradle.properties` et `packwiz/pack.toml`
selon la [convention `beta.xxx`](versioning.md).
2. Exécuter `./gradlew check build assemblePack` et les vérifications du ticket.
3. Préparer les métadonnées de la livraison, avec les deux URL exactes :
3. Pour préparer les métadonnées manuellement depuis beta.280, utiliser la
seule URL du JAR Sanctuary contenant JEI :
```sh
python3 scripts/pack.py release \
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.257/sanctuary-beta.257.jar \
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.257/jei-sanctuary-26.3-30.32.0-sanctuary.4.jar
https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/sanctuary-beta.xxx.jar
```
4. Pousser le commit source vérifié. Créer une release Gitea correspondant à ce
commit et joindre les JAR Sanctuary et JEI construits, sans remplacer les artefacts d'une release
commit et joindre le JAR Sanctuary construit, sans remplacer les artefacts d'une release
antérieure. Vérifier que le téléchargement public possède le SHA-256 indiqué
dans `build/packwiz-release/mods/sanctuary.pw.toml` et `jei-sanctuary.pw.toml`.
dans `build/packwiz-release/mods/sanctuary.pw.toml`, identique au profil serveur.
5. Publier le contenu de `build/packwiz-release/` à la racine de la branche
`packwiz` dans un seul commit, puis vérifier les fichiers et hashes depuis
l'URL publique. Préserver l'historique de ce canal et ne jamais y copier le
@@ -2111,45 +2148,21 @@ python3 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/bran
Il contient les versions exactes Minecraft/Fabric, la commande de lancement et
`packwiz-installer-bootstrap` v0.0.3. Le script vérifie le SHA-256 du bootstrap
officiel avant d'écrire `build/Sanctuary-Prism-auto-update.zip`. Ce ZIP est réservé
officiel avant d'écrire `build/Sanctuary-Prism-beta.xxx.zip`. Ce ZIP est réservé
aux nouvelles installations ; les instances déjà branchées sur packwiz suivent
le canal sans être réimportées.
## Raccorder l'instance Prism existante
## Importer l'instance Prism prête
Fermer Minecraft avant de modifier ses mods. Fermer également Prism avant de
modifier directement `instance.cfg`, pour éviter que son état en mémoire écrase
les changements. Dans l'instance Sanctuary Beta :
Le bouton **Télécharger** de [Sanctuary Web](https://sanctuary-minecraft.net)
télécharge directement [Sanctuary-Beta.zip](https://sanctuary-minecraft.net/downloads/sanctuary-prism.zip).
Dans Prism : **Ajouter une instance → Importer depuis un ZIP**, choisir ce fichier,
puis lancer **Sanctuary Beta**. Packwiz télécharge automatiquement les mods.
La commande avant lancement et l'outil sont déjà dans l'archive.
1. Sauvegarder `instance.cfg` et les anciens JAR Sanctuary dans un dossier de
sauvegarde hors de `mods/`. Vérifier que l'instance utilise Minecraft
`26.3-pre-2` et Fabric `0.19.5` ; ne pas convertir une instance 26.2.
2. Mettre le bootstrap issu du ZIP dans son dossier `minecraft/` (ou `.minecraft/`
selon l'installation).
3. Dans **Modifier → Paramètres → Commandes personnalisées**, activer les
commandes et renseigner la commande avant lancement ci-dessous.
4. Pour une instance issue d'un ancien `.mrpack`, déplacer seulement l'ancien
JAR Sanctuary non géré dans la sauvegarde préparée, puis faire la première
synchronisation packwiz. Conserver les autres mods et données personnelles.
Si la synchronisation échoue, remettre ce JAR et les réglages sauvegardés.
5. Vérifier qu'il reste un seul mod Sanctuary actif et que `packwiz.json` est
présent dans le dossier Minecraft. Les prochaines mises à jour sont suivies
par ce manifeste local : ne pas le supprimer entre les versions.
Commande à coller dans l'interface Prism :
```text
"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" --bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" --pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
```
Une modification directe de `instance.cfg` doit échapper les guillemets au format
Qt INI (`\"`) et activer `OverrideCommands=true`. Ces clés et
`PreLaunchCommand` doivent être dans la section **`[General]`**, jamais ajoutées
après la section `[UI]`. Le ZIP préparé le fait déjà. Rouvrir les commandes
personnalisées dans Prism pour vérifier que la commande est effectivement lue.
Une instance importée d'un `.mrpack` peut être marquée `ManagedPack=true` :
dès que packwiz en assure les mises à jour, utiliser `ManagedPack=false` pour
éviter deux mécanismes de gestion sur le même pack.
Conserver cette même instance pour les versions suivantes : une publication
explicitement demandée via Codex est suivie au lancement suivant. Une instance
déjà branchée sur le canal stable n'a pas besoin d'être réimportée.
## Données locales et mondes
+100
View File
@@ -0,0 +1,100 @@
# Sanctuary — serveur Linux avec packwiz
Prérequis : Java 25, Python 3.11 ou plus récent, Linux et accès HTTPS sortant.
Plateforme épinglée : Minecraft **26.3**, Fabric Loader **0.19.5**, Fabric API
**0.160.5+26.3**. Le même JAR Sanctuary contient le fork JEI
**30.32.0-sanctuary.4**, son réseau commun et son interface client.
## Préparer le kit
Dans les sources : `./gradlew check build assemblePack assembleServerPack`.
Le kit est `build/Sanctuary-Server-beta.280.zip` ; les manifestes locaux sont
dans `build/packwiz-server/`. Le ZIP est un amorçage : il télécharge les mods
depuis le canal au premier démarrage, il n'est pas une copie hors ligne du pack.
Il n'est utilisable sur le canal public qu'après publication de cette version.
Décompresser le kit **une seule fois**, dans un dossier serveur neuf. Pour
reprendre une installation existante, arrêter le service, sauvegarder le dossier
entier, vérifier Minecraft/Loader et poser uniquement les fichiers du kit en
conservant le monde et la configuration. Ne pas écraser un fichier déjà présent
sans comparaison. Un ancien JEI externe géré par packwiz est retiré par le nouvel
index ; un JAR ajouté manuellement doit être retiré après sauvegarde pour éviter
de charger une autre version du fork. Conserver `config/jei/`.
Canal prévu, publié avec le client dans le même commit de distribution :
```text
https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/server/pack.toml
```
`./server.sh update` synchronise les mods sans démarrer Minecraft.
`./server.sh start` synchronise puis lance Fabric en console. En cas d'échec,
le serveur ne démarre pas. Accepter l'EULA Minecraft et régler le serveur
explicitement avant ouverture aux joueurs ; le kit ne les remplit pas.
Le lanceur Fabric télécharge les fichiers Mojang/Loader nécessaires au lancement.
Les outils de démarrage sont épinglés par hash dans `.tools/platform.json`.
Un changement de Minecraft ou Loader est refusé avant mise à jour et exige un
nouveau kit et un contrat de migration. Le verrou empêche une mise à jour en
parallèle d'un serveur lancé par ce kit. Arrêter aussi tout service historique
qui utiliserait un autre lanceur.
Variables facultatives : `SANCTUARY_JAVA` (chemin du binaire Java),
`SANCTUARY_MEMORY` (`4G` par défaut), `SANCTUARY_PACK_URL` (autre canal ou
serveur HTTP de laboratoire). Les variables sont héritées de l'environnement ;
le script ne charge pas automatiquement un `.env`.
Un exemple systemd est fourni sous `examples/sanctuary-beta.service`.
Installer le kit dans `/srv/minecraft/sanctuary-beta`, créer l'utilisateur
`minecraft`, donner ce dossier à cet utilisateur et placer l'environnement
privé dans `/etc/sanctuary/beta.env` (root, mode 600). Copier ensuite l'unité
dans `/etc/systemd/system/`, recharger systemd et activer le service. Le mode
`KillMode=mixed` transmet l'arrêt au lanceur, qui attend la sauvegarde Java
avant de rendre le verrou de mise à jour.
## Relier Sanctuary Web
L'API d'accueil et la communauté sont deux services existants distincts.
Le pack ne contient ni secret ni configuration de production.
Pour l'accès/candidature, fournir dans l'environnement du service Minecraft :
```text
SANCTUARY_APP_URL=https://sanctuary-minecraft.net
SANCTUARY_API_TOKEN=<jeton API du site>
```
Le site doit fournir les POST authentifiés `/api/v1/access-codes/verify` et
`/redeem`, avec les reçus d'admission de beta.166. Aucun appel d'essai ne doit
consommer un vrai code de joueur. Sans ces deux paramètres, le mod autonome
laisse l'accueil sans code ; une configuration partielle ferme l'accueil.
Pour Gazette/Tableau et les statistiques, reprendre le **serverId existant**
dans `config/sanctuary-community.json`, configurer `mode: database`, le JDBC
MariaDB, l'utilisateur et `passwordEnv: SANCTUARY_COMMUNITY_DB_PASSWORD`.
Le mot de passe est uniquement dans l'environnement. Utiliser le même UUID dans
`SANCTUARY_COMMUNITY_SERVER_ID` côté Laravel. L'exemple sous `examples/` doit être
complété, puis copié volontairement ; il ne remplace aucune configuration.
Garder `online-mode=true` pour l'identité et la liaison des comptes.
Les migrations SQL sont celles du site et doivent être vérifiées sur la base
cible avant activation. Basculer du mode fichier vers MariaDB n'importe pas les
anciennes publications : cela demande un contrat de migration séparé.
Le kit n'effectue aucune migration SQL ni modification de sauvegarde.
## Mises à jour et retour arrière
Arrêter le service, sauvegarder, lancer `./server.sh update`, vérifier les logs,
puis redémarrer le même dossier serveur. `packwiz.json` doit rester présent.
Le pack ne possède que les mods Fabric API et Sanctuary. Il ne gère ni monde,
`server.properties`, `eula.txt`, `config/`, listes de joueurs, permissions,
captures ou secrets. Les réglages des opérateurs restent en place.
Sauvegarder aussi l'identifiant d'admission et ses reçus avec le monde, ainsi
que les configurations et l'UUID communautaire. Un retour arrière reprend le
dernier artefact immuable et la sauvegarde correspondant à son contrat de
données ; ne pas supprimer de journaux pour forcer le chargement.
Référence : [installation packwiz sur serveur](https://packwiz.infra.link/tutorials/installing/packwiz-installer/#using-a-modpack-with-a-server).
Dans le dépôt, consulter aussi `docs/inscription-web-beta166.md` et
`docs/community-contract-v1.md` pour les contrats et droits SQL complets.
+235
View File
@@ -0,0 +1,235 @@
# UPDATE-OPS — développer et publier Sanctuary
Procédure de reprise demandée par Koka le **26 octobre 2026**, branche
`codex/update-workflow`. Elle relie les sources du jeu, packwiz, le serveur Beta,
Sanctuary Web et l'instance Prism locale. Les anciennes livraisons restent dans
[packwiz.md](packwiz.md) ; le parcours de téléchargement actuel est décrit dans
[beta-download.md](beta-download.md).
## Reprendre un chantier
Lire `AGENTS.md`, `README.md`, le ticket et l'état Git des dépôts concernés.
Choisir la branche source qui contient les dernières livraisons voulues, puis
travailler sur `codex/<sujet>`. Les branches des collègues peuvent avancer : ne
pas prendre automatiquement `main`, le plus grand compteur ou un autre worktree
comme base. Comparer les commits et les changements avant toute intégration.
| Élément | Référence à retrouver |
| --- | --- |
| Sources du jeu | `https://git.botsu.net/koka/sanctuary-beta.git` |
| Sources du site | `https://git.botsu.net/chris/sanctuary-web.git` |
| Canal client | `https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml` |
| Canal serveur | Même adresse, avec `packwiz/server/pack.toml` |
| Site actif | `https://sanctuary-minecraft.net` — Sanctuary Web, pas un ancien site de release |
| Entrée Minecraft | `beta.sanctuary-minecraft.net` — SRV vers le port public 25567 |
| Téléchargement unique du site | `/downloads/sanctuary-prism.zip`, nom reçu `Sanctuary-Beta.zip` |
Développer/tester localement ne publie pas. Un simple `commit et push` pousse
les sources ; il ne redémarre pas les services. Quand Koka demande une livraison
complète ou « mets tout à jour », dérouler les cibles demandées jusqu'à leur
vérification sans redemander les choix déjà établis. Cette note mémorise la
procédure ; elle ne lance aucune publication périodique.
## Préparer une livraison du jeu
Pour chaque nouveau binaire Sanctuary, prendre le prochain numéro **libre**
`beta.xxx` après examen des branches/releases. Aligner `mod_version` et
`pack_version` dans `gradle.properties`, puis `version` dans `packwiz/pack.toml`.
Une modification documentaire ou du site seule ne change pas ce compteur.
Utiliser **Java 25** et **Python 3.11+**. Sur le Mac actuel, `python3` peut encore
être Python 3.9 : utiliser `python3.14` pour les outils opérationnels. Aucun
changement de Minecraft/Fabric ou de format de sauvegarde n'est implicite.
```sh
./gradlew check build assemblePack assembleServerPack
python3.14 scripts/pack.py prism https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml
```
Ajouter les vérifications utiles au ticket ; les essais graphiques du jeu
ciblent **Vulkan uniquement**. Le profil ciblé employé pour beta.280 était
`-PsanctuaryFocusedTests=recipes,progression -PsanctuaryAtlasOnly=true` : ce reçu
historique ne remplace pas les tests d'un futur changement touchant un autre
comportement. Consigner les limites, ne pas présenter un contrôle exclu comme
réussi.
Les sorties attendues pour la version choisie sont :
- `mods/sanctuary/build/libs/sanctuary-beta.xxx.jar` : un JAR Sanctuary contenant JEI ;
- `build/Sanctuary-beta.xxx.mrpack` : format conservé pour les liens existants ;
- `build/Sanctuary-Prism-beta.xxx.zip` : instance prête pour le bouton du site ;
- `build/Sanctuary-Server-beta.xxx.zip` : kit Linux ;
- les métadonnées client/serveur de `build/packwiz-release/` après publication.
Vérifier les versions, SHA-256, index et contenus avant publication. Ne pas
versionner les binaires, dépendances, mondes ni secrets. Le ZIP Prism contient
seulement sa configuration, son icône et le bootstrap ; le premier lancement
télécharge les mods. Ne pas lui ajouter des réglages ou sauvegardes personnels.
## Publier les artefacts et le canal
Committer et pousser la branche source vérifiée, puis exécuter depuis cette
même branche propre, en remplaçant `beta.xxx` et le chemin des notes :
```sh
python3.14 scripts/publish_pack.py \
--notes-file docs/notes-du-ticket.md \
--asset build/Sanctuary-beta.xxx.mrpack \
--asset build/Sanctuary-Prism-beta.xxx.zip \
--asset build/Sanctuary-Server-beta.xxx.zip
```
Cette commande crée/vérifie la release, joint les artefacts, publie ensemble
les profils client et serveur et vérifie leurs téléchargements publics.
Elle utilise les identifiants du gestionnaire Git en mémoire. Le tag est
exactement `beta.xxx`, sans `v`. Une release déjà publiée reste immuable : ne
pas déplacer le tag ou remplacer une pièce jointe différente. Une reprise se
fait depuis le commit source exact de la release, pas depuis un commit
documentaire ultérieur portant le même compteur.
`publish_pack.py` avance déjà le canal : ne pas faire un deuxième push manuel
du pack. Il ne met à jour ni le serveur, ni Prism, ni le téléchargement Web.
Tester deux synchronisations avec le véritable installateur dans un dossier
isolé ; vérifier l'absence de doublon Sanctuary et la stabilité du second
passage. Conserver le reçu et les hashes dans `build/`, hors de Git.
## Mettre à jour le serveur Beta dédié
La cible est la **CT 350 sur le nœud `gpu`**, dossier
`/srv/minecraft/sanctuary-beta`, service `sanctuary-beta.service`, utilisateur
`minecraft`. La CT 300 Alpha et les autres serveurs sont hors cible.
L'accès et les chemins propres au poste sont dans la compétence locale
`sanctuary-updates`, référence `references/local-environment.md`.
Quand la livraison comprend le serveur, après les builds et avant d'avancer
le canal, arrêter proprement la Beta. Attendre la fin de Java et de la
sauvegarde, puis sauvegarder son dossier entier et son environnement privé
dans un emplacement root privé hors du dossier géré par packwiz. Noter le
commit du canal et les hashes précédents. Cela évite que des clients à jour
rejoignent brièvement l'ancien serveur.
Après publication du canal, ces commandes s'exécutent **dans la CT 350** :
```sh
systemctl stop sanctuary-beta.service
runuser -u minecraft -- /srv/minecraft/sanctuary-beta/server.sh update
systemctl start sanctuary-beta.service
systemctl is-active sanctuary-beta.service
journalctl -u sanctuary-beta.service -n 100 --no-pager
```
La sauvegarde précède `update` ; le premier `stop` ci-dessus reste nécessaire
si cette étape est exécutée séparément. Ne redémarrer qu'après une mise à jour
réussie. L'outil refuse un changement implicite de Minecraft/Fabric et une
mise à jour concurrente au lancement. Il ne charge pas automatiquement
`/etc/sanctuary/beta.env` : reprendre seulement les variables non secrètes
nécessaires si le Java ou le canal diffèrent de ceux du kit. Le service charge
l'environnement privé au démarrage.
Conserver le monde, `packwiz.json`, `config/`, `server.properties`,
`eula.txt`, les listes de joueurs, opérateurs, permissions et reçus d'admission.
Garder l'UUID communautaire existant. Ne pas recréer une CT, réextraire un kit
sur tout le serveur ou réinitialiser Gazette/Tableau/Intendance pour une mise
à jour courante. [Contrat complet du serveur](server-pack.md).
Contrôler le JAR effectivement chargé, Fabric API, l'admission sans consommer
un vrai code, les logs, le ping Minecraft et la fraîcheur des métriques Web.
Un ping depuis le LAN via l'IP publique ne prouve pas un accès depuis un réseau
extérieur. Ne pas remettre l'ancien secours UPnP : la règle Bbox permanente
existe déjà.
## Actualiser le téléchargement et la version du site
Après vérification de la CT Beta, transférer le script versionné
`scripts/publish_web_pack.py` sur **`gpu`** dans un dossier temporaire privé et
vérifier que sa copie a le même SHA-256. Lancer sur `gpu`, où `pct` existe :
```sh
python3 /chemin/temporaire/publish_web_pack.py \
--web-ct 320 --server-ct 350 --version beta.xxx \
--mrpack-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/Sanctuary-beta.xxx.mrpack \
--prism-url https://git.botsu.net/koka/sanctuary-beta/releases/download/beta.xxx/Sanctuary-Prism-beta.xxx.zip \
--pack-url https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/pack.toml \
--dry-run
```
Si le contrôle passe, reprendre la même commande sans `--dry-run`. Ce script
vérifie le serveur en marche, les deux profils, les archives et leurs hashes,
puis bascule atomiquement `storage/app/distribution/current` dans la CT Web.
Il ne modifie pas le code du site et ne redémarre pas Minecraft.
Le footer et le survol du bouton lisent cette publication via
`App\Services\PackDistribution`. Ne pas remplacer leurs versions en dur ni
reconfigurer le bouton à chaque livraison. Le bouton **Télécharger** donne
directement le ZIP prêt ; le joueur l'importe une fois dans Prism puis lance
pour télécharger le pack. Aucun guide manuel ni téléchargement séparé ne
remplace ce parcours. L'ancien `/downloads/sanctuary-beta.mrpack` reste valide.
Vérifier le ZIP reçu anonymement (HTTP 200, nom `Sanctuary-Beta.zip`, SHA-256),
le footer et le survol, puis le lien MRpack historique. Une bascule de version
ne nécessite pas de reconstruire le cache Laravel.
## Livrer une modification du site
Travailler dans les sources **Sanctuary Web** et lire leur `AGENTS.md` ainsi
que les règles/compétences applicables. Tester les comportements modifiés,
formater le PHP avec `vendor/bin/pint --dirty --format agent`, construire les
assets concernés et vérifier le rendu navigateur. Les contrôles existants
pour cette chaîne sont `tests/Feature/DownloadTest.php` et
`tests/Feature/ServerMetricsTest.php`.
**La CT 320 est un checkout de production avec des changements préexistants.**
Ne pas y faire `git pull`, `reset --hard`, `clean`, ou une copie avec suppression
depuis `origin/main`. Inspecter les fichiers concernés et leurs hashes, préparer
un patch limité conservant les autres différences, sauvegarder les fichiers
et caches affectés dans `/root/sanctuary-beta-maintenance/`, puis appliquer
avec refus si les fichiers ont changé depuis l'inspection. Conserver
propriétaires/modes et écrire les assets avant les vues qui les référencent.
Ne vider/reconstruire que les caches concernés : `view:clear` pour Blade ;
`config:cache` si la configuration change ; `route:cache` si des routes changent
et étaient déjà mises en cache. Exécuter Artisan comme `www-data` depuis
`/var/www/sanctuary`. Aucun changement de base, réinitialisation de données ou
migration ne découle d'une simple livraison de présentation.
Contrôler le site public, les métriques et la distribution. Les comptes admins,
groupes staff et permissions restent identiques ; comparer leurs empreintes si
l'intervention touche leur contexte. Les scripts ponctuels dans `build/`
restent des reçus historiques, à ne pas rejouer avec leurs anciennes hypothèses.
La procédure complémentaire du dépôt Web est `docs/updates.md`.
## Synchroniser l'instance locale existante
Retrouver l'instance **Sanctuary Beta** et lire sa configuration. Son dossier
historique peut encore s'appeler `Sanctuary-0.1.0-alpha.1` ; ce nom n'indique
pas sa version courante. Ne pas créer une nouvelle instance à chaque livraison,
ne pas réimporter le ZIP et ne pas toucher aux anciennes instances 26.2.
Si la demande inclut la mise à jour locale : attendre la fermeture du jeu,
sauvegarder les fichiers gérés et les métadonnées de l'instance hors de `mods/`,
relever les hashes des données personnelles, puis exécuter sa commande packwiz
avant lancement avec ses vrais `INST_JAVA`, `INST_MC_DIR`, `INST_DIR`.
Pour une synchronisation sans lancer Minecraft, ajouter `-g` à l'installateur.
Ne jamais lancer `instance.cfg` comme du shell ni évaluer une chaîne de commande
non vérifiée. Les chemins avec espaces doivent rester des arguments séparés.
Faire deux passages et comparer : un seul JAR Sanctuary attendu, mêmes fichiers
gérés au second passage, mêmes données personnelles avant/après. Conserver
`saves/`, captures, favoris, réglages, packs graphiques et configurations tierces.
Ne pas ouvrir un monde personnel pour prouver une simple synchronisation.
## Reprise et reçu de livraison
Pour un échec, conserver l'état/les logs, revenir au dernier artefact immuable
et à la sauvegarde compatible. Un retour du canal doit former un nouveau commit
de métadonnées sans réécrire l'historique. Une reprise du téléchargement Web
rebascule son lien `current` vers une publication complète conservée ; elle ne
change pas à elle seule le serveur ou le canal. Ne pas démarrer un ancien JAR
sur des données migrées sans vérifier le contrat de sauvegarde.
Consigner pour chaque livraison : commit source, version/tag, commit du canal,
hashes et URL des artefacts, CT/service et version chargée, publication Web,
version locale effectivement synchronisée, sauvegardes, vérifications et limites.
Mettre à jour la référence d'environnement de la compétence lorsque les cibles
ou chemins changent. La mémorisation de cette procédure ne prétend pas que
toutes les installations sont déjà synchronisées.
+2 -2
View File
@@ -9,8 +9,8 @@ loom_version=1.17.20
fabric_api_version=0.160.5+26.3
# Release counter: beta.001, .002, .003, ... (see docs/versioning.md).
mod_version=beta.278
pack_version=beta.278
mod_version=beta.280
pack_version=beta.280
resource_pack_version=beta.121
maven_group=fr.koka.sanctuary
jei_version=30.32.0-sanctuary.4
+4 -2
View File
@@ -27,8 +27,10 @@ le cache ignoré manque, extrait le commit fixé et applique `patches/26.3.patch
La première reconstruction nécessite Internet, Git, patch et Python 3.
`./gradlew :jei:sourcesJar` produit aussi les sources Java effectivement compilées.
Depuis beta.257, JEI est un JAR client séparé et facultatif. Sanctuary conserve
son pont de catalogue ; le retrait de JEI ne supprime aucune connaissance serveur.
Depuis beta.280, JEI est de nouveau embarqué dans Sanctuary, avec ses métadonnées
Fabric, ses entrées communes/cliente et son pont de catalogue. Il n'est plus
distribué séparément dans packwiz. La séparation facultative beta.257–278
n'a modifié aucune connaissance serveur ; ces données restent conservées.
JEI conserve ses deux bibliothèques internes nécessaires à la recherche :
- `net.mezzdev:baked-substring-index:0.1.0`, MIT, James Mitchell.
+2
View File
@@ -686,6 +686,8 @@ dependencies {
if (providers.gradleProperty('sanctuaryJeiRuntime').map { it.toBoolean() }.getOrElse(true)) {
runtimeOnly project(':jei')
}
// One distributed Sanctuary JAR; keep the exact JEI fork and its own metadata.
include project(':jei')
minecraft "com.mojang:minecraft:${rootProject.minecraft_version}"
implementation "net.fabricmc:fabric-loader:${rootProject.loader_version}"
implementation "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}"
+1 -1
View File
@@ -1,6 +1,6 @@
name = "Sanctuary"
author = "KOKA99CAB"
version = "beta.278"
version = "beta.280"
pack-format = "packwiz:1.1.0"
[index]
+58 -34
View File
@@ -106,7 +106,7 @@ def https_url(value):
return value
def assemble(jar_url=None, jei_url=None):
def assemble(jar_url=None):
values, entries = check()
jar = ROOT / 'mods/sanctuary/build/libs' / ('sanctuary-' + values['mod_version'] + '.jar')
require(jar.is_file(), 'Build Sanctuary before assembling the pack: ' + str(jar))
@@ -122,17 +122,19 @@ def assemble(jar_url=None, jei_url=None):
require('fr/koka/sanctuarytest/QuickTestMod.class' not in archive.namelist(), 'Lab entrypoint leaked into production')
require(not {'jei', 'sound_physics_remastered', 'sodium'} & metadata['depends'].keys(),
'External client mods must not be required by Sanctuary')
embedded_jei = []
for nested_mod in metadata.get('jars', []):
with zipfile.ZipFile(io.BytesIO(archive.read(nested_mod['file']))) as candidate:
if 'fabric.mod.json' in candidate.namelist():
info = json.loads(candidate.read('fabric.mod.json'))
require(info['id'] not in ('jei', 'sound_physics_remastered', 'sodium'),
require(info['id'] not in ('sound_physics_remastered', 'sodium'),
'External client mod is still nested in Sanctuary: ' + info['id'])
if info['id'] == 'jei':
embedded_jei.append(archive.read(nested_mod['file']))
require(all('licenses/sound-physics-remastered/' + name in archive.namelist()
for name in ('LICENSE', 'PROVENANCE.md')), 'Sound Physics notices missing')
built_jei = ROOT / 'mods/jei/build/libs' / f"jei-sanctuary-{values['minecraft_version']}-{values['jei_version']}.jar"
require(built_jei.is_file(), 'Build the separate Sanctuary JEI fork before assembling the pack')
with zipfile.ZipFile(built_jei) as jei:
require(len(embedded_jei) == 1, 'Sanctuary must embed exactly one JEI fork')
with zipfile.ZipFile(io.BytesIO(embedded_jei[0])) as jei:
info = json.loads(jei.read('fabric.mod.json'))
require(info['id'] == 'jei' and info['version'] == values['jei_version'], 'JEI metadata drift')
require(info['depends']['minecraft'] == values['minecraft_version'].replace('-pre-', '-pre.'), 'JEI Minecraft version drift')
@@ -152,7 +154,6 @@ def assemble(jar_url=None, jei_url=None):
info = json.loads(archive.read('fabric.mod.json'))
require(info['id'] == 'sound_physics_remastered' and info['version'] == values['sound_physics_version'], 'Sound Physics version drift')
require(info['depends']['minecraft'] == values['minecraft_version'] + '.x', 'Sound Physics Minecraft version drift')
require(bool(jar_url) == bool(jei_url), 'Release needs immutable URLs for both Sanctuary and JEI')
destination = ROOT / ('build/packwiz-release' if jar_url else 'build/packwiz')
# Only this generated staging directory is replaced; never touch a game instance.
if destination.exists():
@@ -180,36 +181,55 @@ def assemble(jar_url=None, jei_url=None):
else:
shutil.copy2(jar, target)
staged_entries.append({'file': 'mods/' + jar.name, 'hash': sha256(target)})
target = destination / 'mods' / ('jei-sanctuary.pw.toml' if jei_url else built_jei.name)
if jei_url:
target.write_text('\n'.join([
'name = "JEI Sanctuary"', 'filename = ' + json.dumps(built_jei.name),
'side = "client"', '', '[download]',
'url = ' + json.dumps(https_url(jei_url)), 'hash-format = "sha256"',
'hash = ' + json.dumps(sha256(built_jei)), '', '[option]',
'optional = true', 'default = true',
'description = "Catalogue de recettes facultatif. / Optional recipe catalogue."', '',
]), encoding='utf-8')
staged_entries.append({'file': 'mods/jei-sanctuary.pw.toml', 'hash': sha256(target), 'metafile': True})
else:
# Local previews ship the fork as a loose JAR, removable in the launcher.
shutil.copy2(built_jei, target)
staged_entries.append({'file': 'mods/' + built_jei.name, 'hash': sha256(target)})
write_index(destination, staged_entries)
server = destination / 'server' if jar_url else ROOT / 'build/packwiz-server'
if server.exists():
shutil.rmtree(server)
server.mkdir(parents=True)
server_entries = []
for entry in staged_entries:
if entry['file'] not in ('mods/fabric-api.pw.toml', 'mods/sanctuary.pw.toml', 'mods/' + jar.name):
continue
target = server / entry['file']
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(destination / entry['file'], target)
server_entries.append(entry)
write_index(server, server_entries, name='Sanctuary Server')
verify_server(server)
print('Client pack staged at ' + str(destination))
print('Server pack staged at ' + str(server))
def write_index(destination, entries, name=None):
lines = ['hash-format = "sha256"', '']
for entry in sorted(staged_entries, key=lambda e: e['file']):
for entry in sorted(entries, key=lambda e: e['file']):
lines.append('[[files]]')
for key, value in entry.items():
lines.append(key + ' = ' + json.dumps(value))
lines.extend(key + ' = ' + json.dumps(value) for key, value in entry.items())
lines.append('')
index_path = destination / 'index.toml'
index_path.write_text('\n'.join(lines), encoding='utf-8')
manifest = (SOURCE / 'pack.toml').read_text(encoding='utf-8')
if name:
manifest = manifest.replace('name = "Sanctuary"', 'name = ' + json.dumps(name), 1)
manifest, count = re.subn(r'(?m)^hash = "[a-f0-9]+"$',
'hash = "' + sha256(index_path) + '"', manifest)
require(count == 1, 'Expected a single pack index hash')
(destination / 'pack.toml').write_text(manifest, encoding='utf-8')
check_index(destination)
print('Pack staged at ' + str(destination))
def verify_server(base):
manifest, entries = check_index(base)
values = properties()
require(manifest['version'] == values['pack_version'], 'Server pack version drift')
require(manifest['versions'] == read_toml(SOURCE / 'pack.toml')['versions'], 'Server platform drift')
files = {entry['file'] for entry in entries}
sanctuary = 'mods/sanctuary.pw.toml' if 'mods/sanctuary.pw.toml' in files else 'mods/sanctuary-' + values['mod_version'] + '.jar'
require(files == {'mods/fabric-api.pw.toml', sanctuary}, 'Server pack must contain only Fabric API and Sanctuary')
for entry in entries:
if entry.get('metafile'):
require(read_toml(base / entry['file'])['side'] == 'both', 'Client mod in server pack')
return manifest, entries
def prism(pack_url):
@@ -227,7 +247,7 @@ def prism(pack_url):
# Prism instance.cfg uses Qt INI escaping for quotation marks.
config = '\n'.join([
'[General]', 'ConfigVersion=1.3', 'InstanceType=OneSix',
'name=Sanctuary - ' + values['pack_version'], 'iconKey=sanctuary-beta', 'AutomaticJava=true',
'name=Sanctuary Beta', 'iconKey=sanctuary-beta', 'AutomaticJava=true',
'OverrideJavaLocation=false', 'IgnoreJavaCompatibility=false',
'OverrideMemory=true', 'MinMemAlloc=512', 'MaxMemAlloc=4096',
'ManagedPack=false', 'OverrideCommands=true', 'LogPrePostOutput=true',
@@ -241,13 +261,17 @@ def prism(pack_url):
{'uid': 'net.fabricmc.fabric-loader', 'version': values['loader_version']},
],
}
destination = ROOT / 'build/Sanctuary-Prism-auto-update.zip'
destination = ROOT / 'build' / ('Sanctuary-Prism-' + values['pack_version'] + '.zip')
destination.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(destination, 'w', zipfile.ZIP_DEFLATED) as archive:
archive.writestr('instance.cfg', config)
archive.write(SOURCE / 'icon.png', 'sanctuary-beta.png')
archive.writestr('mmc-pack.json', json.dumps(components, indent=2) + '\n')
archive.writestr('minecraft/packwiz-installer-bootstrap.jar', bootstrap)
files = {'instance.cfg': config.encode(), 'sanctuary-beta.png': (SOURCE / 'icon.png').read_bytes(),
'mmc-pack.json': (json.dumps(components, indent=2) + '\n').encode(),
'minecraft/packwiz-installer-bootstrap.jar': bootstrap}
for name, data in files.items():
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.compress_type = zipfile.ZIP_DEFLATED
entry.external_attr = 0o100644 << 16
archive.writestr(entry, data)
print('Prism auto-update template: ' + str(destination))
print('Pre-launch command: ' + command)
@@ -255,10 +279,10 @@ def prism(pack_url):
if __name__ == '__main__':
require((len(sys.argv) == 2 and sys.argv[1] in ('check', 'assemble'))
or (len(sys.argv) == 3 and sys.argv[1] == 'prism')
or (len(sys.argv) == 4 and sys.argv[1] == 'release'),
'Usage: python3 scripts/pack.py check|assemble|release <sanctuary-https-url> <jei-https-url>|prism <pack-https-url>')
or (len(sys.argv) == 3 and sys.argv[1] == 'release'),
'Usage: python3 scripts/pack.py check|assemble|release <sanctuary-https-url>|prism <pack-https-url>')
if sys.argv[1] == 'release':
assemble(https_url(sys.argv[2]), https_url(sys.argv[3]))
assemble(https_url(sys.argv[2]))
elif sys.argv[1] == 'prism':
prism(sys.argv[2])
elif sys.argv[1] == 'assemble':
+7 -5
View File
@@ -124,6 +124,10 @@ def channel_file(relative):
def publish_channel(origin, version, source_head):
staged = ROOT / 'build/packwiz-release'
pack.check_index(staged)
pack.verify_server(staged / 'server')
pack.require((staged / 'mods/sanctuary.pw.toml').read_bytes() ==
(staged / 'server/mods/sanctuary.pw.toml').read_bytes(),
'Client and server must publish the same Sanctuary artifact')
files = [path for path in staged.rglob('*') if path.is_file()]
pack.require(all(channel_file(path.relative_to(staged)) for path in files),
'Distribution branch must contain only generated TOML metadata and the pack icon')
@@ -183,10 +187,9 @@ def main():
mod = json.loads(archive.read('fabric.mod.json'))
pack.require(mod['id'] == 'sanctuary' and mod['version'] == values['mod_version'],
'Built JAR does not match the Sanctuary source version')
jei = ROOT / 'mods/jei/build/libs' / f"jei-sanctuary-{values['minecraft_version']}-{values['jei_version']}.jar"
# Validate every artifact before creating a public release or uploading a file.
pack.assemble()
assets = [jar, jei] + [path.resolve() for path in args.asset]
assets = [jar] + [path.resolve() for path in args.asset]
pack.require(all(path.is_file() for path in assets), 'A requested release asset is missing')
pack.require(len({path.name for path in assets}) == len(assets), 'Release asset names must be unique')
notes = args.notes_file.read_text(encoding='utf-8') if args.notes_file else (
@@ -207,10 +210,9 @@ def main():
pack.require(not release.get('draft'), 'The existing release is a draft; publish it explicitly first')
pack.require(remote_tag_commit(tag) == head, 'Release tag must resolve to the verified source commit')
jar_url = gitea.upload(release, jar)
jei_url = gitea.upload(release, jei)
for asset in assets[2:]:
for asset in assets[1:]:
gitea.upload(release, asset)
pack.assemble(jar_url, jei_url)
pack.assemble(jar_url)
publish_channel(origin, version, head)
stable_url = gitea.repo_url + '/raw/branch/' + CHANNEL + '/pack.toml'
with urlopen(stable_url, timeout=60) as response:
+204
View File
@@ -0,0 +1,204 @@
#!/usr/bin/env python3
"""Publish verified Prism/MRpack archives matching a CT, when Codex invokes it.
Run on the Proxmox node hosting both CTs. This does not restart Minecraft,
advance packwiz, change a world, or copy server configuration into the client.
"""
import argparse
import base64
import configparser
import hashlib
import io
import json
from pathlib import PurePosixPath
import re
import subprocess
import sys
import tomllib
from urllib.parse import urljoin, urlsplit
from urllib.request import urlopen
import zipfile
BOOTSTRAP_SHA256 = 'a8fbb24dc604278e97f4688e82d3d91a318b98efc08d5dbfcbcbcab6443d116c'
def require(condition, message):
if not condition:
raise SystemExit(message)
def digest(data):
return hashlib.sha256(data).hexdigest()
def fetch(url):
require(urlsplit(url).scheme == 'https', 'Distribution URLs must use HTTPS')
with urlopen(url, timeout=60) as response:
data = response.read(64 * 1024 * 1024 + 1)
require(len(data) <= 64 * 1024 * 1024, 'Distribution file exceeds 64 MiB')
return data
def channel(url):
manifest = tomllib.loads(fetch(url).decode())
index_data = fetch(urljoin(url, manifest['index']['file']))
require(manifest['index']['hash-format'] == 'sha256'
and digest(index_data) == manifest['index']['hash'], 'Pack index hash mismatch')
index = tomllib.loads(index_data.decode())
require(index['hash-format'] == 'sha256', 'Unsupported index hashes')
mods = {}
icon_hash = None
for entry in index['files']:
path = PurePosixPath(entry['file'])
require(not path.is_absolute() and '..' not in path.parts, 'Unsafe index path')
data = fetch(urljoin(url, str(path)))
require(digest(data) == entry['hash'], 'Indexed file hash mismatch: ' + str(path))
if str(path) == 'icon.png':
icon_hash = digest(data)
if entry.get('metafile'):
mod = tomllib.loads(data.decode())
require(mod['filename'] not in mods, 'Duplicate mod filename')
mods[mod['filename']] = mod
return {'manifest': manifest, 'mods': mods, 'icon_sha256': icon_hash}
def verify_prism(data, client, pack_url):
with zipfile.ZipFile(io.BytesIO(data)) as archive:
require(archive.testzip() is None, 'Corrupt Prism archive')
require(set(archive.namelist()) == {'instance.cfg', 'mmc-pack.json', 'sanctuary-beta.png',
'minecraft/packwiz-installer-bootstrap.jar'}, 'Unexpected Prism archive contents')
require(digest(archive.read('minecraft/packwiz-installer-bootstrap.jar')) == BOOTSTRAP_SHA256,
'Prism bootstrap hash mismatch')
require(digest(archive.read('sanctuary-beta.png')) == client['icon_sha256'], 'Prism icon drift')
config = configparser.ConfigParser(interpolation=None)
config.read_string(archive.read('instance.cfg').decode())
general = config['General']
require(general['ManagedPack'] == 'false' and general['OverrideCommands'] == 'true'
and general['InstanceType'] == 'OneSix' and general['name'] == 'Sanctuary Beta',
'Prism instance is not configured for automatic updates')
command = ('"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" '
'--bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" '
'--pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" ' + pack_url)
require(general['PreLaunchCommand'].replace('\\"', '"') == command, 'Prism pre-launch command drift')
components = json.loads(archive.read('mmc-pack.json'))['components']
require({c['uid']: c['version'] for c in components} ==
{'net.minecraft': client['manifest']['versions']['minecraft'],
'net.fabricmc.fabric-loader': client['manifest']['versions']['fabric']}
and len(components) == 2, 'Prism platform drift')
return {'prism_size': len(data), 'prism_sha256': digest(data)}
def verify(mrpack, client, server, installed, version):
require(re.fullmatch(r'beta\.(?!000$)[0-9]{3}', version), 'Invalid Beta version')
require(installed['running'], 'Dedicated Beta server must be running')
for profile in (client, server):
require(profile['manifest']['version'] == version, 'Channel version differs from dedicated CT')
require(client['manifest']['versions'] == server['manifest']['versions'], 'Client/server platform drift')
filename = 'sanctuary-' + version + '.jar'
require(set(server['mods']) == {filename, installed['fabric_api_filename']}, 'Unexpected server mods')
require(client['mods'][filename] == server['mods'][filename], 'Client/server Sanctuary artifact drift')
download = server['mods'][filename]['download']
require(download['hash-format'] == 'sha256' and download['hash'] == installed['sanctuary_sha256'],
'Sanctuary channel does not match the dedicated CT')
api = server['mods'][installed['fabric_api_filename']]['download']
require(installed['fabric_api_hashes'].get(api['hash-format']) == api['hash'],
'Fabric API channel does not match the dedicated CT')
with zipfile.ZipFile(io.BytesIO(mrpack)) as archive:
require(archive.testzip() is None, 'Corrupt MRpack')
index = json.loads(archive.read('modrinth.index.json'))
require(index['game'] == 'minecraft' and index['formatVersion'] == 1, 'Unsupported MRpack')
require(index['versionId'] == version, 'MRpack version differs from dedicated CT')
require(index['dependencies'] == {'minecraft': client['manifest']['versions']['minecraft'],
'fabric-loader': client['manifest']['versions']['fabric']}, 'MRpack platform drift')
jar = archive.read('overrides/mods/' + filename)
require(digest(jar) == installed['sanctuary_sha256'], 'MRpack Sanctuary differs from dedicated CT')
require({name for name in archive.namelist() if not name.endswith('/')} ==
{'modrinth.index.json', 'overrides/icon.png', 'overrides/mods/' + filename},
'Unexpected MRpack overrides; review client contents before publication')
files = {entry['path']: entry for entry in index['files']}
require(len(files) == len(index['files']), 'Duplicate MRpack entry')
require(set(files) == {'mods/' + name for name in client['mods'] if name != filename}, 'MRpack mod list drift')
for name, mod in client['mods'].items():
if name == filename:
continue
entry = files['mods/' + name]
require(entry['hashes'].get(mod['download']['hash-format']) == mod['download']['hash'], 'MRpack dependency hash drift')
require(entry['downloads'] == [mod['download']['url']], 'MRpack dependency URL drift')
return {'version': version, 'minecraft': index['dependencies']['minecraft'],
'fabric': index['dependencies']['fabric-loader'], 'mrpack_sha256': digest(mrpack),
'mrpack_size': len(mrpack), 'sanctuary_sha256': installed['sanctuary_sha256']}
INSTALL = r'''
import base64,fcntl,json,os,sys
from pathlib import Path
p=json.load(sys.stdin);root=Path(p['root'])/'storage/app/distribution';root.mkdir(parents=True,exist_ok=True)
lock=(root/'.publish.lock').open('a');fcntl.flock(lock,fcntl.LOCK_EX)
release=root/'releases'/p['metadata']['version']/'prism';release.mkdir(parents=True,exist_ok=True)
files={'Sanctuary-Beta.mrpack':base64.b64decode(p['mrpack']),
'Sanctuary-Beta.zip':base64.b64decode(p['prism']),
'distribution.json':(json.dumps(p['metadata'],indent=2)+'\n').encode()}
for name,data in files.items():
f=release/name
if f.exists() and f.read_bytes()!=data:raise SystemExit('Immutable Web release conflict: '+name)
for name,data in files.items():
f=release/name
if not f.exists():
tmp=f.with_suffix(f.suffix+'.new');tmp.write_bytes(data);tmp.chmod(0o644);os.replace(tmp,f)
current=root/'current'
if current.exists() and not current.is_symlink():raise SystemExit('Existing distribution/current is not a symlink')
previous=os.readlink(current) if current.is_symlink() else None
tmp=root/('current-'+str(os.getpid()));tmp.symlink_to('releases/'+p['metadata']['version']+'/prism');os.replace(tmp,current)
print(json.dumps({'published':p['metadata'],'previous':previous,'current':str(current)}))
'''
def ct(number, *args, input=None):
result = subprocess.run(['pct', 'exec', str(number), '--', *args], input=input,
text=True, capture_output=True, timeout=120)
require(result.returncode == 0, 'CT operation failed: ' + result.stderr[:300])
return result.stdout
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--web-ct', type=int, required=True)
parser.add_argument('--server-ct', type=int, required=True)
parser.add_argument('--version', required=True)
parser.add_argument('--mrpack-url', required=True)
parser.add_argument('--prism-url', required=True)
parser.add_argument('--pack-url', required=True)
parser.add_argument('--web-root', default='/var/www/sanctuary')
parser.add_argument('--server-root', default='/srv/minecraft/sanctuary-beta')
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
require(args.web_ct != args.server_ct, 'Web and Minecraft CT must be distinct')
inventory = r'''
import hashlib,json,subprocess,sys
from pathlib import Path
p=Path(sys.argv[1]);jars=list((p/'mods').glob('sanctuary-*.jar'));apis=list((p/'mods').glob('fabric-api-*.jar'))
assert len(jars)==len(apis)==1,'Ambiguous installed mods'
print(json.dumps({'running':subprocess.run(['systemctl','is-active','--quiet','sanctuary-beta.service']).returncode==0,
'sanctuary_sha256':hashlib.sha256(jars[0].read_bytes()).hexdigest(),
'fabric_api_filename':apis[0].name,'fabric_api_hashes':{a:hashlib.new(a,apis[0].read_bytes()).hexdigest() for a in ('sha256','sha512')}}))
'''
installed = json.loads(ct(args.server_ct, 'python3', '-c', inventory, args.server_root))
client = channel(args.pack_url)
server = channel(urljoin(args.pack_url, 'server/pack.toml'))
mrpack = fetch(args.mrpack_url)
metadata = verify(mrpack, client, server, installed, args.version)
prism = fetch(args.prism_url)
metadata.update(verify_prism(prism, client, args.pack_url))
metadata.update({'packwiz_url': args.pack_url, 'source_url': args.mrpack_url,
'prism_source_url': args.prism_url})
if args.dry_run:
print(json.dumps({'verified': metadata, 'web_ct': args.web_ct, 'server_ct': args.server_ct}, indent=2))
else:
payload = json.dumps({'root': args.web_root, 'metadata': metadata,
'mrpack': base64.b64encode(mrpack).decode(),
'prism': base64.b64encode(prism).decode()})
print(ct(args.web_ct, 'python3', '-c', INSTALL, input=payload))
if __name__ == '__main__':
main()
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Build a Linux server bootstrap kit; never install into an existing server."""
import hashlib
import json
from pathlib import Path
from urllib.request import urlopen
import zipfile
import pack
TOOLS = {
'fabric-server-launch.jar': (
'https://meta.fabricmc.net/v2/versions/loader/26.3/0.19.5/1.1.2/server/jar',
'0b56ad54d762172e8b8748e467f584f071e4dedecd93dc336cf2c68837e790be'),
'packwiz-installer.jar': (
'https://github.com/packwiz/packwiz-installer/releases/download/v0.5.14/packwiz-installer.jar',
'c9f646908d340d84773948a9a7d98bc1dae250d35e1016dc6e2b8459760b5598'),
'packwiz-installer-bootstrap.jar': (pack.BOOTSTRAP_URL, pack.BOOTSTRAP_SHA256),
}
PACK_URL = 'https://git.botsu.net/koka/sanctuary-beta/raw/branch/packwiz/server/pack.toml'
def put(archive, name, data, mode=0o100644):
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.compress_type = zipfile.ZIP_DEFLATED
entry.external_attr = mode << 16
archive.writestr(entry, data)
def assemble():
values = pack.properties()
manifest, _ = pack.verify_server(pack.ROOT / 'build/packwiz-server')
pack.require(manifest['versions'] == {'minecraft': '26.3', 'fabric': '0.19.5'},
'Re-pin the server launcher for an explicit Minecraft/Fabric migration')
cache = pack.ROOT / 'build/server-tools'
cache.mkdir(parents=True, exist_ok=True)
for name, (url, digest) in TOOLS.items():
target = cache / name
if not target.is_file():
with urlopen(url, timeout=45) as response:
data = response.read()
pack.require(hashlib.sha256(data).hexdigest() == digest, 'Server tool hash mismatch: ' + name)
target.write_bytes(data)
pack.require(pack.sha256(target) == digest, 'Cached server tool hash mismatch: ' + name)
platform = {'versions': manifest['versions'], 'pack_url': PACK_URL,
'sha256': {name: digest for name, (_, digest) in TOOLS.items()}}
output = pack.ROOT / 'build' / f'Sanctuary-Server-{values["pack_version"]}.zip'
with zipfile.ZipFile(output, 'w', zipfile.ZIP_DEFLATED) as archive:
for name in TOOLS:
put(archive, '.tools/' + name, (cache / name).read_bytes())
put(archive, '.tools/server.py', (pack.ROOT / 'scripts/server_runtime.py').read_bytes())
put(archive, '.tools/platform.json', json.dumps(platform, indent=2) + '\n')
put(archive, 'server.sh', '#!/bin/sh\nset -eu\ncd "$(dirname "$0")"\nexec python3 .tools/server.py "$@"\n', mode=0o100755)
put(archive, 'README.md', (pack.ROOT / 'docs/server-pack.md').read_bytes())
# Examples are deliberately outside config/ and outside the pack index.
put(archive, 'examples/sanctuary-community.json.example', json.dumps({
'mode': 'database', 'serverId': 'REPLACE_WITH_EXISTING_SERVER_UUID',
'jdbcUrl': 'jdbc:mariadb://db.example.net:3306/sanctuary?sslMode=verify-full',
'user': 'sanctuary_game', 'passwordEnv': 'SANCTUARY_COMMUNITY_DB_PASSWORD',
}, indent=2) + '\n')
put(archive, 'examples/sanctuary.env.example',
'SANCTUARY_APP_URL=https://sanctuary-minecraft.net\n'
'SANCTUARY_API_TOKEN=\nSANCTUARY_COMMUNITY_DB_PASSWORD=\n'
'SANCTUARY_MEMORY=4G\n')
put(archive, 'examples/sanctuary-beta.service',
'[Unit]\nDescription=Sanctuary Beta Minecraft server\n'
'Wants=network-online.target\nAfter=network-online.target\n\n'
'[Service]\nType=simple\nUser=minecraft\nGroup=minecraft\n'
'WorkingDirectory=/srv/minecraft/sanctuary-beta\n'
'EnvironmentFile=/etc/sanctuary/beta.env\n'
'ExecStart=/srv/minecraft/sanctuary-beta/server.sh start\n'
'Restart=on-failure\nRestartSec=30\nTimeoutStopSec=90\nKillMode=mixed\n'
'UMask=0077\nNoNewPrivileges=true\nPrivateTmp=true\nProtectHome=true\n'
'ProtectSystem=strict\nReadWritePaths=/srv/minecraft/sanctuary-beta\n'
'LimitNOFILE=65535\n\n[Install]\nWantedBy=multi-user.target\n')
print('Server bootstrap kit: ' + str(output))
if __name__ == '__main__':
assemble()
+69
View File
@@ -0,0 +1,69 @@
#!/usr/bin/env python3
"""Linux server kit entrypoint. Configuration and worlds are never pack-owned."""
import argparse
import fcntl
import hashlib
import json
import os
from pathlib import Path
import signal
import subprocess
import tomllib
from urllib.request import urlopen
def update(root, platform, url, java):
# packwiz updates mods, not Minecraft/Loader. Refuse an implicit platform migration.
with urlopen(url, timeout=30) as response:
manifest = tomllib.loads(response.read(65536).decode('utf-8'))
if manifest['versions'] != platform['versions']:
raise SystemExit('Minecraft/Fabric changed: prepare an explicit migration before updating.')
tools = root / '.tools'
for name, digest in platform['sha256'].items():
if hashlib.sha256((tools / name).read_bytes()).hexdigest() != digest:
raise SystemExit('Server tool hash mismatch: ' + name)
subprocess.run([java, '-jar', str(tools / 'packwiz-installer-bootstrap.jar'),
'--bootstrap-main-jar', str(tools / 'packwiz-installer.jar'),
'--bootstrap-no-update', '-g', '-s', 'server',
'--pack-folder', str(root), url], cwd=root, check=True)
def run_minecraft(root, java, memory):
process = subprocess.Popen([java, '-Xms512M', '-Xmx' + memory, '-jar',
str(root / '.tools/fabric-server-launch.jar'), 'nogui'], cwd=root)
def stop(signum, _frame):
process.send_signal(signum)
previous = {s: signal.signal(s, stop) for s in (signal.SIGTERM, signal.SIGINT)}
try:
# Keep the update lock until Java has finished saving during shutdown.
return process.wait()
finally:
for signum, handler in previous.items():
signal.signal(signum, handler)
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('command', choices=('update', 'start'), nargs='?', default='start')
args = parser.parse_args()
root = Path(__file__).resolve().parents[1]
platform = json.loads((root / '.tools/platform.json').read_text())
java = os.environ.get('SANCTUARY_JAVA', 'java')
with (root / '.sanctuary-server.lock').open('a') as lock:
try:
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
raise SystemExit('Server is running or an update is already in progress.')
update(root, platform, os.environ.get('SANCTUARY_PACK_URL', platform['pack_url']), java)
if args.command == 'start':
memory = os.environ.get('SANCTUARY_MEMORY', '4G')
# No shell evaluation of Java options or credentials.
raise SystemExit(run_minecraft(root, java, memory))
if __name__ == '__main__':
try:
main()
except (OSError, ValueError, KeyError, subprocess.CalledProcessError) as error:
# Do not print environment values or HTTP response bodies.
raise SystemExit('Server operation failed (' + type(error).__name__ + ').')
+119
View File
@@ -0,0 +1,119 @@
"""Check CT/channel alignment and atomic publication without a live PVE."""
import base64
import copy
import hashlib
import io
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
import zipfile
from unittest.mock import patch
import pack
import publish_web_pack as web
class WebDistributionTests(unittest.TestCase):
def setUp(self):
self.jar = b'sanctuary-runtime'
self.version = 'beta.280'
self.filename = 'sanctuary-beta.280.jar'
self.sanctuary = {'filename': self.filename, 'download': {'hash-format': 'sha256', 'hash': web.digest(self.jar)}}
self.api = {'filename': 'fabric-api.jar', 'download': {'url': 'https://example.test/api.jar', 'hash-format': 'sha512', 'hash': hashlib.sha512(b'api').hexdigest()}}
self.profile = {'manifest': {'version': self.version, 'versions': {'minecraft': '26.3', 'fabric': '0.19.5'}},
'mods': {self.filename: self.sanctuary, 'fabric-api.jar': self.api}}
self.installed = {'running': True, 'sanctuary_sha256': web.digest(self.jar), 'fabric_api_filename': 'fabric-api.jar',
'fabric_api_hashes': {'sha512': hashlib.sha512(b'api').hexdigest()}}
def mrpack(self, jar=None, version='beta.280', extra=None):
index = {'formatVersion': 1, 'game': 'minecraft', 'versionId': version,
'dependencies': {'minecraft': '26.3', 'fabric-loader': '0.19.5'},
'files': [{'path': 'mods/fabric-api.jar', 'hashes': {'sha512': self.api['download']['hash']}, 'downloads': [self.api['download']['url']]}]}
out = io.BytesIO()
with zipfile.ZipFile(out, 'w') as z:
z.writestr('modrinth.index.json', json.dumps(index))
z.writestr('overrides/icon.png', b'icon')
z.writestr('overrides/mods/' + self.filename, self.jar if jar is None else jar)
if extra:
z.writestr(extra, 'private-server-data')
return out.getvalue()
def test_matching_runtime_produces_verified_download_metadata(self):
data = self.mrpack()
result = web.verify(data, self.profile, self.profile, self.installed, self.version)
self.assertEqual(result['version'], 'beta.280')
self.assertEqual(result['sanctuary_sha256'], web.digest(b'sanctuary-runtime'))
self.assertEqual(result['mrpack_sha256'], web.digest(data))
self.assertEqual(result['mrpack_size'], len(data))
def test_stopped_or_different_ct_is_rejected(self):
for change in ({'running': False}, {'sanctuary_sha256': '0' * 64}, {'fabric_api_hashes': {'sha512': '0' * 128}}):
with self.subTest(change=change), self.assertRaises(SystemExit):
web.verify(self.mrpack(), self.profile, self.profile, self.installed | change, self.version)
def test_different_channel_or_mrpack_is_rejected(self):
changed = copy.deepcopy(self.profile)
changed['manifest']['version'] = 'beta.281'
with self.assertRaisesRegex(SystemExit, 'Channel version'):
web.verify(self.mrpack(), changed, self.profile, self.installed, self.version)
for data in (self.mrpack(jar=b'another-build'), self.mrpack(version='beta.281')):
with self.subTest(), self.assertRaises(SystemExit):
web.verify(data, self.profile, self.profile, self.installed, self.version)
def test_server_files_cannot_enter_the_client_download(self):
with self.assertRaisesRegex(SystemExit, 'Unexpected MRpack overrides'):
web.verify(self.mrpack(extra='overrides/server.properties'), self.profile, self.profile, self.installed, self.version)
def prism(self, override='true', url='https://pack.example/pack.toml', extra=None):
command = ('"$INST_JAVA" -jar "$INST_MC_DIR/packwiz-installer-bootstrap.jar" '
'--bootstrap-main-jar "$INST_MC_DIR/packwiz-installer.jar" '
'--pack-folder "$INST_MC_DIR" --multimc-folder "$INST_DIR" ' + url)
config = '[General]\nname=Sanctuary Beta\nInstanceType=OneSix\nManagedPack=false\nOverrideCommands='+override+'\nPreLaunchCommand='+command.replace('"', '\\"')+'\n'
out = io.BytesIO()
with zipfile.ZipFile(out, 'w') as z:
z.writestr('instance.cfg', config)
z.writestr('sanctuary-beta.png', b'icon')
z.writestr('minecraft/packwiz-installer-bootstrap.jar', b'bootstrap')
z.writestr('mmc-pack.json', json.dumps({'components': [{'uid':'net.minecraft','version':'26.3'}, {'uid':'net.fabricmc.fabric-loader','version':'0.19.5'}]}))
if extra:
z.writestr(extra, 'unwanted')
return out.getvalue()
def test_prism_is_ready_to_import_with_automatic_updates(self):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')):
result = web.verify_prism(self.prism(), self.profile | {'icon_sha256': web.digest(b'icon')}, 'https://pack.example/pack.toml')
self.assertEqual(result['prism_sha256'], web.digest(self.prism()))
def test_prism_without_prelaunch_or_with_another_channel_is_rejected(self):
with patch.object(web, 'BOOTSTRAP_SHA256', web.digest(b'bootstrap')):
for data in (self.prism(override='false'), self.prism(url='https://other.example/pack.toml')):
with self.subTest(), self.assertRaises(SystemExit):
web.verify_prism(data, self.profile | {'icon_sha256': web.digest(b'icon')}, 'https://pack.example/pack.toml')
def test_prism_cannot_include_worlds_or_unverified_tools(self):
with self.assertRaisesRegex(SystemExit, 'bootstrap hash mismatch'):
web.verify_prism(self.prism(), self.profile, 'https://pack.example/pack.toml')
with self.assertRaisesRegex(SystemExit, 'Unexpected Prism archive contents'):
web.verify_prism(self.prism(extra='minecraft/saves/world.dat'), self.profile, 'https://pack.example/pack.toml')
def test_atomic_publication_preserves_old_release_and_rejects_mutation(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
def publish(version, data):
payload = {'root': directory, 'metadata': {'version': version}, 'mrpack': base64.b64encode(data).decode(), 'prism': base64.b64encode(b'prism-ready').decode()}
return subprocess.run([sys.executable, '-c', web.INSTALL], input=json.dumps(payload), text=True, capture_output=True)
self.assertEqual(publish('beta.280', b'first').returncode, 0)
self.assertEqual(publish('beta.281', b'second').returncode, 0)
current = root / 'storage/app/distribution/current/Sanctuary-Beta.mrpack'
self.assertEqual(current.read_bytes(), b'second')
self.assertEqual((root / 'storage/app/distribution/releases/beta.280/prism/Sanctuary-Beta.mrpack').read_bytes(), b'first')
self.assertNotEqual(publish('beta.281', b'conflicting').returncode, 0)
self.assertEqual(current.read_bytes(), b'second')
self.assertEqual(publish('beta.281', b'second').returncode, 0)
if __name__ == '__main__':
unittest.main()
+95
View File
@@ -0,0 +1,95 @@
"""Guard the updater's platform boundary and failure behaviour without a live server."""
import hashlib
import io
import json
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
# Reuse pack's macOS Python 3.11+ selection before importing tomllib users.
import pack
import server_runtime
class ServerUpdateTests(unittest.TestCase):
def setUp(self):
self.temp = tempfile.TemporaryDirectory()
self.addCleanup(self.temp.cleanup)
self.root = Path(self.temp.name)
(self.root / '.tools').mkdir()
(self.root / '.tools/tool.jar').write_bytes(b'verified-tool')
self.platform = {'versions': {'minecraft': '26.3', 'fabric': '0.19.5'},
'sha256': {'tool.jar': hashlib.sha256(b'verified-tool').hexdigest()}}
self.platform['pack_url'] = 'https://pack.example/pack.toml'
(self.root / '.tools/platform.json').write_text(json.dumps(self.platform))
def manifest(self, minecraft='26.3', loader='0.19.5'):
return io.BytesIO(f'[versions]\nminecraft="{minecraft}"\nfabric="{loader}"\n'.encode())
def test_headless_server_update_uses_pinned_tools_and_instance(self):
with patch('server_runtime.urlopen', return_value=self.manifest()), patch('server_runtime.subprocess.run') as run:
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', '/java path/java')
args = run.call_args.args[0]
self.assertEqual(args[0], '/java path/java')
self.assertEqual(args[args.index('-s') + 1], 'server')
self.assertIn('-g', args)
self.assertIn('--bootstrap-no-update', args)
self.assertEqual(args[args.index('--pack-folder') + 1], str(self.root))
self.assertTrue(run.call_args.kwargs['check'])
def test_platform_changes_stop_before_packwiz(self):
for minecraft, loader in [('26.4', '0.19.5'), ('26.3', '0.20.0')]:
with self.subTest(minecraft=minecraft, loader=loader), patch('server_runtime.urlopen', return_value=self.manifest(minecraft, loader)), patch('server_runtime.subprocess.run') as run:
with self.assertRaisesRegex(SystemExit, 'explicit migration'):
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', 'java')
run.assert_not_called()
def test_tool_hash_mismatch_stops_before_packwiz(self):
(self.root / '.tools/tool.jar').write_bytes(b'changed')
with patch('server_runtime.urlopen', return_value=self.manifest()), patch('server_runtime.subprocess.run') as run:
with self.assertRaisesRegex(SystemExit, 'hash mismatch'):
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', 'java')
run.assert_not_called()
def test_update_failure_propagates_and_personal_files_are_untouched(self):
settings = self.root / 'server.properties'
settings.write_bytes(b'online-mode=true\n')
with patch('server_runtime.urlopen', return_value=self.manifest()), patch('server_runtime.subprocess.run', side_effect=subprocess.CalledProcessError(1, 'installer')):
with self.assertRaises(subprocess.CalledProcessError):
server_runtime.update(self.root, self.platform, 'https://pack.example/pack.toml', 'java')
self.assertEqual(settings.read_bytes(), b'online-mode=true\n')
def test_failed_update_prevents_minecraft_start(self):
with patch('server_runtime.__file__', str(self.root / '.tools/server.py')), patch('sys.argv', ['server.py', 'start']), patch('server_runtime.update', side_effect=OSError('unreachable')), patch('server_runtime.subprocess.run') as run:
with self.assertRaises(OSError):
server_runtime.main()
run.assert_not_called()
def test_running_server_prevents_an_update(self):
with (self.root / '.sanctuary-server.lock').open('a') as lock:
server_runtime.fcntl.flock(lock, server_runtime.fcntl.LOCK_EX)
with patch('server_runtime.__file__', str(self.root / '.tools/server.py')), patch('sys.argv', ['server.py', 'update']), patch('server_runtime.update') as update:
with self.assertRaisesRegex(SystemExit, 'already in progress'):
server_runtime.main()
update.assert_not_called()
def test_shutdown_waits_for_java_before_releasing_the_lock(self):
handlers = {}
def install(signum, handler):
handlers[signum] = handler
return 'original-handler'
with patch('server_runtime.signal.signal', side_effect=install), patch('server_runtime.subprocess.Popen') as popen:
process = popen.return_value
def wait():
handlers[server_runtime.signal.SIGTERM](server_runtime.signal.SIGTERM, None)
return 0
process.wait.side_effect = wait
self.assertEqual(server_runtime.run_minecraft(self.root, 'java', '8G'), 0)
process.send_signal.assert_called_once_with(server_runtime.signal.SIGTERM)
self.assertTrue(all(h == 'original-handler' for h in handlers.values()))
if __name__ == '__main__':
unittest.main()