backoffice: manage launcher auto-update from the Versions page

Two things change so the operator never has to SSH for a launcher
release.

1. SignManifest::run() now also re-hashes the manifest's `launcher`
   section. It looks for builds/launcher/<basename of launcher.url>;
   falls back to a tolerant glob *<version>*.exe if the exact name
   isn't found. Updates sizeBytes + sha256 in place. The Ed25519 sign
   step at the end already covered this branch — it was just blank
   data before.

2. admin/versions.php has a new "Auto-update du launcher" card above
   the manifest table. Shows the announced version + minRequired, the
   exe presence badge and the hash status, and a small form to set or
   update the launcher entry (version + minRequired only — URL is
   derived from base_url + version automatically). A "Retirer la
   section" button disables the auto-update by deleting the launcher
   key from versions.json. Lists the .exe files present in
   builds/launcher/ for visibility.

Workflow now: edit the version in the form → SFTP-upload
PSLauncher-X.Y.Z.exe to builds/launcher/ → click "🔁 Sync" once →
manifest is hashed and signed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-02 10:39:56 +02:00
parent 6bc5381562
commit 197b07966c
2 changed files with 130 additions and 0 deletions

View File

@@ -132,6 +132,34 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
saveManifest($manifestPath, $manifest); saveManifest($manifestPath, $manifest);
$message = "v{$version} retirée du manifest. Le ZIP reste dans builds/ (à supprimer en SFTP si voulu)."; $message = "v{$version} retirée du manifest. Le ZIP reste dans builds/ (à supprimer en SFTP si voulu).";
} }
elseif ($action === 'set_launcher') {
$lver = trim($_POST['launcher_version'] ?? '');
$lmin = trim($_POST['launcher_min_required'] ?? '');
if (!preg_match('/^\d+\.\d+\.\d+$/', $lver)) {
throw new Exception('Numéro de version launcher invalide (X.Y.Z attendu).');
}
if ($lmin !== '' && !preg_match('/^\d+\.\d+\.\d+$/', $lmin)) {
throw new Exception('Numéro de version "minRequired" invalide.');
}
$base = $config['base_url'] ?? 'https://asterionvr.com/PS_Launcher';
$manifest['launcher'] = [
'version' => $lver,
'minRequired' => $lmin !== '' ? $lmin : '0.5.0',
'download' => [
'url' => "{$base}/builds/launcher/PSLauncher-{$lver}.exe",
'sizeBytes' => 0,
'sha256' => 'REPLACE_AFTER_BUILD',
],
'releaseNotesUrl' => null,
];
saveManifest($manifestPath, $manifest);
$message = "Section launcher mise à jour (v{$lver}). Upload PSLauncher-{$lver}.exe dans builds/launcher/ puis clique « Sync ».";
}
elseif ($action === 'remove_launcher') {
unset($manifest['launcher']);
saveManifest($manifestPath, $manifest);
$message = "Section launcher retirée du manifest (auto-update désactivé).";
}
elseif ($action === 'sync') { elseif ($action === 'sync') {
// Appel direct à la classe — pas d'exec(), fonctionne même quand la fonction // Appel direct à la classe — pas d'exec(), fonctionne même quand la fonction
// est désactivée par le mutualisé OVH. // est désactivée par le mutualisé OVH.
@@ -205,6 +233,81 @@ Layout::header('Versions', 'versions');
</form> </form>
</div> </div>
<!-- Card LAUNCHER (auto-update du PSLauncher.exe lui-même) -->
<div class="card">
<h2>Auto-update du launcher</h2>
<?php
$launcher = $manifest['launcher'] ?? null;
$launcherFiles = is_dir("$buildsDir/launcher") ? array_map('basename', glob("$buildsDir/launcher/*.exe") ?: []) : [];
$launcherZipPresent = false;
$launcherHashed = false;
if ($launcher) {
$expectedFile = basename(parse_url($launcher['download']['url'] ?? '', PHP_URL_PATH) ?? '');
$launcherZipPresent = in_array($expectedFile, $launcherFiles, true);
$launcherHashed = !empty($launcher['download']['sha256']) && !str_starts_with($launcher['download']['sha256'], 'REPLACE');
}
?>
<?php if ($launcher): ?>
<p>
Version annoncée : <strong>v<?= htmlspecialchars($launcher['version']) ?></strong>
• minRequired : <code><?= htmlspecialchars($launcher['minRequired'] ?? '—') ?></code>
• Exe : <?= $launcherZipPresent
? "<span class='badge badge-success'>présent</span>"
: "<span class='badge badge-warning'>absent</span>" ?>
• Hash : <?= $launcherHashed
? "<span class='badge badge-success'>OK</span>"
: "<span class='badge badge-warning'>à calculer</span>" ?>
</p>
<p class="muted">URL attendue de l'exe : <code><?= htmlspecialchars($launcher['download']['url']) ?></code></p>
<?php else: ?>
<p class="muted">Section launcher absente du manifest — l'auto-update est désactivé. Configure-la ci-dessous pour l'activer.</p>
<?php endif; ?>
<h3 style="margin-top: 18px; font-size: 13px; color: var(--text-secondary); text-transform: uppercase;">Définir / mettre à jour</h3>
<form method="post">
<?= Layout::csrfField() ?>
<input type="hidden" name="action" value="set_launcher">
<div class="row">
<div class="col field">
<label>Version launcher (X.Y.Z)</label>
<input type="text" name="launcher_version" placeholder="0.6.0"
value="<?= htmlspecialchars($launcher['version'] ?? '') ?>" required pattern="\d+\.\d+\.\d+">
</div>
<div class="col field">
<label>Version minimale requise (X.Y.Z)</label>
<input type="text" name="launcher_min_required" placeholder="0.5.0"
value="<?= htmlspecialchars($launcher['minRequired'] ?? '0.5.0') ?>" pattern="\d+\.\d+\.\d+">
</div>
</div>
<div class="toolbar">
<button class="btn btn-success" type="submit">Définir</button>
<?php if ($launcher): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Retirer la section launcher du manifest ? L\'auto-update sera désactivé.')">
<?= Layout::csrfField() ?>
<input type="hidden" name="action" value="remove_launcher">
<button class="btn btn-danger" type="submit">Retirer la section</button>
</form>
<?php endif; ?>
</div>
</form>
<h3 style="margin-top: 18px; font-size: 13px; color: var(--text-secondary); text-transform: uppercase;">Workflow</h3>
<ol class="muted">
<li>Définis la version ci-dessus (ex : <code>0.6.0</code>).</li>
<li>Upload <code>PSLauncher-X.Y.Z.exe</code> via SFTP dans <code>www/PS_Launcher/builds/launcher/</code>.</li>
<li>Clique le bouton <strong>🔁 Sync (sign-manifest)</strong> du tableau du dessous — il hashera le launcher en plus des versions Proserve.</li>
</ol>
<?php if (!empty($launcherFiles)): ?>
<h3 style="margin-top: 18px; font-size: 13px; color: var(--text-secondary); text-transform: uppercase;">Exes présents dans builds/launcher/</h3>
<ul class="muted">
<?php foreach ($launcherFiles as $f): ?>
<li><code><?= htmlspecialchars($f) ?></code> — <?= Layout::formatBytes(filesize("$buildsDir/launcher/$f")) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<div class="card"> <div class="card">
<div class="toolbar"> <div class="toolbar">
<h2 style="margin: 0; flex: 1;">Manifest actuel</h2> <h2 style="margin: 0; flex: 1;">Manifest actuel</h2>

View File

@@ -82,6 +82,33 @@ final class SignManifest
} }
unset($v); unset($v);
// Section launcher : si présente, on tente de hasher le PSLauncher-{ver}.exe
// dans builds/launcher/. On ignore proprement si l'exe n'est pas là.
if (isset($manifest['launcher']) && is_array($manifest['launcher'])) {
$launcher = &$manifest['launcher'];
$lver = $launcher['version'] ?? '';
$lurl = $launcher['download']['url'] ?? '';
if ($lver !== '' && $lurl !== '') {
$lfile = basename(parse_url($lurl, PHP_URL_PATH) ?: '');
$lpath = "{$this->buildsDir}/launcher/{$lfile}";
if (!is_file($lpath)) {
// Fallback : tolérant sur le nom (PSLauncher-X.Y.Z.exe, etc.)
$candidates = glob("{$this->buildsDir}/launcher/*{$lver}*.exe", GLOB_NOSORT) ?: [];
if (count($candidates) === 1) $lpath = $candidates[0];
}
if (is_file($lpath)) {
$lsize = filesize($lpath);
$lsha = hash_file('sha256', $lpath);
$launcher['download']['sizeBytes'] = $lsize;
$launcher['download']['sha256'] = $lsha;
$this->out(" [launcher] v{$lver} : " . basename($lpath) . " ({$lsize} octets) sha256={$lsha}");
} else {
$this->out(" [launcher] v{$lver} : exe introuvable dans builds/launcher/");
}
}
unset($launcher);
}
// Bump auto de `latest` sur la plus haute version effectivement uploadée // Bump auto de `latest` sur la plus haute version effectivement uploadée
if (!empty($hashedVersions)) { if (!empty($hashedVersions)) {
usort($hashedVersions, 'version_compare'); usort($hashedVersions, 'version_compare');