Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb20160eff | ||
|
|
2a4fc2427e | ||
|
|
c219d237ab | ||
|
|
8b6c6c6b21 | ||
|
|
a6b547c4c6 | ||
|
|
235cf51d6e | ||
|
|
17c1cd9e71 | ||
|
|
276c8f2144 | ||
|
|
23b295356b |
@@ -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>`.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.'
|
||||
|
||||
@@ -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`.
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
name = "Sanctuary"
|
||||
author = "KOKA99CAB"
|
||||
version = "beta.278"
|
||||
version = "beta.280"
|
||||
pack-format = "packwiz:1.1.0"
|
||||
|
||||
[index]
|
||||
|
||||
+58
-34
@@ -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':
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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__ + ').')
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user