On this page

← All documentation

Channels, publishing and updates

Game updates: the releases.json contract, channels, signing, what the client does.

Status: channels, publishing, and in-game update client. Author's decision (2026-10-10): the zips are uploaded by hand to the site's server (prism.am, prism-admin release add), which publishes a public releases.json list; players update from inside the game, against this site feed, never against GitHub (the repository is private: no token in the client). The signed feed, Velopack, OVH, and executable signing remain later phases (docs/etude-mises-a-jour.md, branch docs/autoupdate-study); what follows describes what exists.

Two channels, two installations

stable pbe
channel file (next to prism.exe) stable or absent pbe
Data folder %LOCALAPPDATA%\Prism\PrismNG\data (unchanged) %LOCALAPPDATA%\Prism\PrismNG-PBE\data
Player's database, replays, mods, skins, themes those in the data folder those in its own folder
WebView2 profile (localStorage: settings, skinConfig, Workshop drafts) <data>\webview2 <data>\webview2 (of the PBE channel)
Window titles Prism, Prism — Auto… Prism PBE, Prism PBE — Auto…
Badge none "PBE" in the drawer header (Esc)
Single instance (Windows mutex) Local\PrismNG-stable Local\PrismNG-pbe

The channel file is read only once, at startup (apps/desktop/src/app/install.rs, called by startup.rs). It consists of a single word (stable or pbe, case and whitespace ignored). Missing: stable, with no message. Unreadable, too long, or an unknown word: stable and a line in the console (PRISM: … is not usable). The channel is never compiled in: this is what makes it possible to promote the exact same executable bytes from PBE to stable.

Both channels run side by side. A second instance of the same channel does not start: it brings the first one's window to the foreground (by searching for visible windows whose title is the channel's, within a prism.exe), or, if no window exists yet, shows "Prism is already running" and then exits. --write-ts-packages, --check-library, --bench-gameplay, and the --smoke-* flags never take this lock.

The PRISM_MODS_DIR and PRISM_SKINS_DIR variables work as before. Settings → About shows the channel and the version (Cargo.toml, the same for the PBE and stable zips of a given build).

WebView2 profile outside the executable's folder

Previously, WebView2 wrote prism.exe.WebView2 next to the exe: a tool that replaces the exe's folder would have wiped the settings. It now lives in <data>\webview2 (app/profile.rs, WEBVIEW2_USER_DATA_FOLDER set before any WebView). On first launch, an old prism.exe.WebView2 is copied (never moved) to <data>\webview2:

  • only if the target does not exist (an existing target always wins, even if empty); idempotent;
  • copy into webview2.copying followed by a rename: an interrupted copy does not leave a bogus profile;
  • the browser's lock files (lockfile, LOCK) are ignored, any other unreadable file is logged (PRISM: profile file … not copied) and skipped; the rest is copied;
  • a WEBVIEW2_USER_DATA_FOLDER variable that is already set is respected (nothing is migrated).

Consequence: deleting the data folder now also resets the settings (previously, they survived). Diagnostics (--smoke-*, --bench-gameplay) keep their disposable profile in the temp folder.

Cutting a release with GitHub CI (set aside for now)

This path exists (.github/workflows/release.yml, never run) but is not the one chosen: see "Cutting a release and publishing it on the site (no CI: the chosen path)".

Everything goes through .github/workflows/release.yml (triggered by hand: Actions → release → Run workflow, or by a v* tag). The workflow never starts on its own on a push.

  1. Change the workspace version (Cargo.toml) if it is a new version; merge into the branch to be published.
  2. PBE: run the workflow with channel = pbe. It builds, verifies, packages Prism-<version>-pbe.<n>-windows-x64.zip (channel = pbe), and publishes the release v<version>-pbe.<n> as a prerelease with SHA256SUMS.txt (the "draft" box creates it as a draft).
  3. Test this zip (unzip it, run prism.exe, check the PBE badge and the PrismNG-PBE folder).
  4. Promote without recompiling (the normal path to stable):
gh release download v0.1.0-pbe.3 --pattern "Prism-*-pbe.3-windows-x64.zip" --dir dist
.\scripts\promote.ps1 -Zip dist\Prism-0.1.0-pbe.3-windows-x64.zip -OutDir dist\stable
gh release create v0.1.0 dist\stable\Prism-0.1.0-windows-x64.zip dist\stable\SHA256SUMS.txt --title "Prism v0.1.0" --notes-file dist\stable\NOTES.md

promote.ps1 rejects a zip that is not pbe, checks that prism.exe matches the SHA-256 in RELEASE.txt, writes channel = stable, updates RELEASE.txt (promoted-from), rebuilds the Prism-<version>-windows-x64.zip zip, checks that the SHA-256 of prism.exe is unchanged, and writes SHA256SUMS.txt. Write NOTES.md yourself (or use --generate-notes). The workflow also accepts channel = stable (full rebuild), to be reserved for the very first zip or for an emergency.

The zip contains: prism.exe, channel, RELEASE.txt (version, channel, commit, date, SHA-256 of the exe), LISEZMOI.txt, mods\, skins\, themes\. A stable zip keeps its historical name; a PBE zip carries -pbe.<n>. Locally: .\scripts\package.ps1 [-Channel pbe] [-Label pbe.3] [-SkipBuild].

What the workflow checks (deliberately little)

Locked release build (--locked, static runtime), then: bun run check and the pure TypeScript tests (src/lib, src/menu, src/theme: language parity, settings, menus, themes); cargo test --release -p themes -p skin -p modding (the shipped content: themes, skins, mods); the committed TypeScript packages must be the ones written by the exe that was just built. The tests of the desktop crate do not run there: they would recompile the entire application in test profile, almost as long as the build itself. The full suite and the smoke tests (GPU, WebView2) stay in the integrator's workflow. The workflow has never run yet: the first run needs to be watched.

Cost (estimate)

Private repository, free plan: 2,000 minutes per month, with Windows minutes counting double (GitHub's factor of 2). A cold build on windows-latest: 25 to 40 min [estimate, not measured]; with the cargo cache: 8 to 15 min [estimate]. A PBE release would therefore cost on the order of 20 to 80 billed minutes cold, 16 to 30 with cache: the free plan only offers about thirty a month cold. These are reasons not to run the workflow on every merge.

The update client (Settings → About → Updates)

Everything lives in the crates/updater crate (no new dependencies: ureq and ring are already in the tree); the host (apps/desktop/src/app/updates.rs) routes the page's commands and states (updateState). Each step runs on its own thread, never on the window thread, never during a play session, a preview, or a replay (a download in progress stops when a play session starts; the partial file is kept and resumed).

Step What happens
Check a single GET of the list (https://prism.am/releases.json by default), with no identifier, no version, no cookie (agent Prism-updater). Automatic at most once a day (20 s after the interface is ready, then the deadline is rechecked hourly; an attempt counts, so a site that is down is not polled every hour), "Check" button on demand
Download only on click. Progress, Range resume, size and SHA-256 verified against the list; a bad file is deleted
Install on clicking "Restart to update" (or at the next launch if the package is ready): files are moved with a backup, then restart

Settings (visible, Settings → About): updateCheck (enabled by default: it only reads the list), updateChannel (empty = the channel of this installation, stable or pbe), updateFeedUrl (empty = the site; PRISM_UPDATE_FEED takes precedence). None is exported (.pvsettings.json). The feed address must be https (http only for localhost/127.0.0.1), and every address followed (package, redirect) must have the same scheme, the same host, and the same port as the feed; at most 3 redirects, followed by the client itself. A site that is unreachable or not yet deployed only yields "Updates unavailable".

The releases.json feed (the site's contract)

Per version: channel, seq, build, publishedAt (or date), url (relative to the site or absolute), sha256, size, notesMd (or notes), notesEnMd, commit, semver, builtFromBuild, withdrawn / status: "withdrawn", signature (optional, see below). Unknown fields are ignored; an unusable entry is discarded on its own. A channel's order is seq, never build (free-form label, never parsed). Same seq as the installed version: nothing to do. A "latest" older than the installed version (withdrawn): never a downgrade. Another channel: its latest version is always offered, with a warning that the data folder changes (PrismNG ↔ PrismNG-PBE: the package's channel moves the installation).

Which version is installed

The seq is assigned by the site's tooling after the zip is built: the zip cannot always carry it. The game infers it, from the most reliable source to the least reliable:

  1. its own record (update-state.json, next to the exe), written when this client installed the version, with the SHA-256 of the installed exe: it only counts as long as the running exe has this hash (a zip manually extracted over it invalidates it);
  2. seq: in RELEASE.txt if the packaging step received it (package.ps1 -Seq N); ignored if the list knows this seq with a different commit;
  3. the commit in RELEASE.txt against the one in the list (prefix, 7 to 40 hexadecimal characters; several matches: the one whose build is the package's label, otherwise the smallest seq);
  4. the label (label: = pbe.3) equal to a build.

Nothing matches: "Not recognized". The latest version is still offered, but never installed without a click. For the game to always recognize itself, publish with --commit (the one from RELEASE.txt) or give -Seq to the packaging step (see the procedure below).

Ed25519 signature (key slot empty for now)

The list can carry, per version, signature: 128 hexadecimal characters, an Ed25519 signature of the bytes prism-update-v1\n<canal>\n<seq>\n<sha256 en minuscules>\n (the hash and not the file: verifiable before any download, and the channel and the order are bound to the package). crates/updater/src/signature.rs::TRUSTED_KEYS is empty: nothing is verified and the interface displays "Unsigned". As soon as a public key (64 hexadecimal characters) is put there, a version with no signature or with an invalid signature is rejected (the latest is not replaced by an older signed one). Several keys are allowed (rotation: a version that embeds the next key before the old one is retired). The private key is never in this repository.

Proposal for the site's administration tool (to be implemented by the site's worker; nothing has been changed in its repository): prism-admin keys generate --out <fichier> (Ed25519, PKCS#8 private key kept outside the repository and the database, permissions 0600, offline backup; prints the public key in hexadecimal to paste into TRUSTED_KEYS); prism-admin release add … --sign-key <fichier> computes the signature of the message above and stores it in releases.json (signature field); verify rechecks it; regenerate keeps it (never re-signed for another seq); release next-seq --channel pbe prints the next seq for package.ps1 -Seq. Test vector (Ed25519 being deterministic): seed [7; 32], public key ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c, message (channel pbe, seq 7, hash ab×32) prism-update-v1\npbe\n7\nabababababababababababababababababababababababababababababababab\n, signature 281558931de70d1d028affffbe5b7941fcd0cb1150a5d9376e3102ce6c2322f6b743cd20ba601d65ab3370a3b2c08e45a2abe61ce800c88178b3e45525bbc000 (test the_signing_vector_published_for_the_site_verifies).

Installation, backup, rollback

A running prism.exe can be renamed on Windows (it can be neither overwritten nor deleted; proven by a test with a copy of a running system program). Everything stays next to the exe, never in the data folder (except update-check.json, the time of the last check):

update-staging/download/…zip.partial   the zip while downloading (resumed)
update-staging/files/…                 the unpacked package (downloader's archive rules: no `..`, no absolute path, no link) and verified
update-staging/ready.json              written last: the package is complete
update-backup/<seq>/files/…            each replaced file, as it was (the last two backups are kept)
update-backup/<seq>/manifest.json      replaced / added
update-state.json                      installed version, launch counter, last rollback

The package must contain prism.exe (with an MZ header) and a channel file equal to the announced channel; names starting with update- are reserved. Policy for shipped folders (mods\, skins\, themes\): a file in the package replaces the file at the same path (shipped content follows the game) and the old one is kept in update-backup/<seq>/ if it differs; anything the package does not contain is never touched or deleted (folders and files added by the player); a shipped file that a version removes stays in place (removing it requires the hash manifest of the next phase). In-place edits to a shipped file are therefore overwritten but backed up.

At launch, before the window opens: the launch counter of a freshly installed version is incremented; the ready interface (ready) resets it to zero. Two launches in a row without reaching the interface: on the third, the files are restored (the faulty files go into update-backup/<seq>/failed/), the previous version restarts and Settings → About says so. An interrupted installation (power cut, killed process) is rolled back at the next launch (the manifest is written before the first move). The new process waits for the old one to end (--after-update <pid>) before taking the instance lock and the WebView2 profile. Limitation: the safeguard runs inside the new exe; an exe that fails to load at all cannot restore itself (restore update-backup/<seq>/files/prism.exe). The program assumes the exe is named prism.exe and that its folder is writable (not in Program Files: otherwise "folder not writable").

Cutting a release and publishing it on the site (no CI: the chosen path)

  1. .\scripts\package.ps1 -Channel pbe -Label pbe.4 [-Seq N] (the seq is only used for identification: release next-seq on the site, if the tool offers it). RELEASE.txt carries the version, the channel, the label, the seq if given, the commit, the date and the SHA-256 of the exe.
  2. Upload the zip to the server; prism-admin release add --channel pbe --build "PBE 0004" --zip … --commit <commit from RELEASE.txt> --notes …. Players' games see the version at the next check (once a day, or "Check now").
  3. Promote without recompiling: .\scripts\promote.ps1 -Zip <pbe zip> produces the stable zip (the PBE's seq: is removed from it: the site assigns the stable one), then publish it the same way with --channel stable. To withdraw a version: prism-admin release withdraw (it disappears from the list; a client that had already seen it no longer downloads it).

.github/workflows/release.yml remains in the repository but the author's decision is not to use GitHub CI for now.

What is not proven

Tested: the whole crate against a real local HTTP server (feed, zip, wrong hash, wrong or missing signature, withdrawn version, interrupted then resumed download, zip-slip, same seq, channel change, redirects to another host), installation, rollback, and renaming a running executable. Not proven, only a real run against the site will show it: the site's real TLS and headers (Range, Content-Range, Caddy redirects), the identification of a published version with the real fields, the actual restart (new process, mutex, WebView2) and the application at launch. No prism.exe was launched for this work.

Next steps

  1. Generate the signing key and put it in TRUSTED_KEYS (author's decision: where the private key lives, its backup, rotation).
  2. Hash manifest for shipped folders, "Duplicate to customize", tombstones (removing a shipped file).
  3. Installer (WebView2 bootstrapper), executable signing, deltas, staged rollout.

Source in the game repository: docs/mises-a-jour.md