Compare commits

..
Author SHA1 Message Date
koka 2a4fc2427e Record the direct Prism download and first-launch validation
Build Sanctuary / build (push) Waiting to run
2026-10-25 16:53:38 +01:00
koka c219d237ab Ship a ready Prism bootstrap instead of manual setup
Build Sanctuary / build (push) Waiting to run
2026-10-25 16:49:08 +01:00
koka 8b6c6c6b21 Document verified one-time Prism setup for Beta players
Build Sanctuary / build (push) Waiting to run
2026-10-25 16:38:53 +01:00
koka a6b547c4c6 Publish one verified Beta MRpack matching the dedicated CT
Build Sanctuary / build (push) Waiting to run
2026-10-25 05:25:16 +01:00
koka 235cf51d6e Document Sanctuary Web alignment with the Beta database
Build Sanctuary / build (push) Waiting to run
2026-10-25 05:09:39 +01:00
koka 17c1cd9e71 Verify permanent Beta route and retire UPnP fallback
Build Sanctuary / build (push) Waiting to run
2026-10-25 04:51:51 +01:00
koka 276c8f2144 Record Beta server deployment, web connections and remaining Bbox target
Build Sanctuary / build (push) Waiting to run
2026-10-25 04:44:08 +01:00
8 changed files with 617 additions and 44 deletions
+7 -2
View File
@@ -1,5 +1,9 @@
# 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.
## 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).
@@ -7,8 +11,9 @@ 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. Une nouvelle CT pour 20 joueurs est en préparation
sur `gpu`, séparément de l'Alpha.
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.
+6
View File
@@ -21,6 +21,12 @@ tasks.register('verifyServerUpdater', Exec) {
}
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.'
+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`.
+128 -2
View File
@@ -59,5 +59,131 @@ 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. La mise en service du
nouveau serveur et le raccordement aux services sont en cours.
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.
+27 -34
View File
@@ -1,5 +1,9 @@
# 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.
## beta.280 — JEI embarqué et profil serveur
JEI rejoint le JAR Sanctuary ; `mods/sanctuary.pw.toml` reste le seul artefact
@@ -16,6 +20,19 @@ et les commandes de synchronisation/démarrage. [Mode d'emploi](server-pack.md),
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
@@ -2127,45 +2144,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
+10 -6
View File
@@ -247,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',
@@ -261,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)
+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()
+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()