Commit Graph

124 Commits

Author SHA1 Message Date
Ludovic Marie b3e9d8ebd1 Merge pull request #1276 from ludovic111/feat/telemetry-live
🤖 Agent: merged after verify passed. telemetry.playhorizon.dev is live on Railway (service telemetry in horizon-site, volume /data): /healthz, /v1/live (a test ping counted) and /v1/benchmarks/summary (CORS for playhorizon.dev) answer. The PR's local end-to-end runs are in the description.
2026-10-23 01:14:18 +02:00
Ludovic Marie be16401f90 Telemetry: send to telemetry.playhorizon.dev, a benchmarks summary, the Railway config
- DEFAULT_ENDPOINT is https://telemetry.playhorizon.dev. The heartbeat, the launcher's live count and the
  outbox upload all go there; HORIZON_TELEMETRY_URL still names another server, and HORIZON_TELEMETRY_URL=off
  sends nothing at all. CI workflows and the harnesses that start the game or the launcher set off. Consent
  is unchanged: the four switches stay opt-in, only the anonymous ping is always on.
- GET /v1/benchmarks/summary: players' benchmark uploads grouped by GPU, CPU, render distance and game, groups
  of 3+ installs only, medians per install then across installs, no install ID, day, OS version, driver,
  memory or screen. Cached 10 minutes, CORS for the website. Same rules as website/scripts/benchmarks.mjs,
  tested on the same fixtures; a deleted install leaves its groups at once.
- /v1/aggregate no longer lists the newest 50 raw benchmark reports (hardware, driver, day, one install each):
  uploads are published only through the summary.
- Behind Railway's proxy the rate limit counts X-Real-IP, else the last X-Forwarded-For entry (the one the
  proxy adds; a client could choose the first). Still in memory only, never stored or logged.
- crates/horizon-telemetry-server/railway.json: Dockerfile build from the repository root, health check
  /healthz, one replica in europe-west4 (one SQLite file on the /data volume).
- Docs: docs/telemetry.md, the server README (the summary's shape, Railway setup), the privacy page (the ping
  is on, where the data lives, how benchmark uploads are shown).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:46:32 +02:00
Ludovic Marie 042adc674a One menu style, vanilla's: Horizon's settings join vanilla's Options
Drop the Horizon menu style (options_horizon.rs, front.rs, the live title
backdrop and its post grade, the launcher's menu-style setting, CLI and MCP
tool). Every screen is vanilla's. Horizon's own settings hang off vanilla's
Options the way mods add theirs: Horizon Settings... and Mods... take the
Telemetry/Credits slots, and Horizon Settings opens Graphics, Advanced
Graphics, Shader Packs, Controller Settings, Extras and Mods, each a vanilla
options list with tooltips. Play with friends moves behind vanilla's Friends
buttons: the pause menu's opens Play with Friends (share, copy, stop), the
title screen's opens Join a Friend, and Direct Connection takes a full code.

Old files load as before: horizon.txt's menuStyle line and
horizon-graphics.txt's level line are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 21:06:51 +02:00
Ludovic Marie 5eb224a909 Rename the Nightly update channel to Alpha
The whole of Horizon is in beta, so the channel that installs every build as
soon as it is built is called Alpha; Stable keeps its name. The launcher's
Settings, the update-channel command, the MCP tool, the release notes footer
and the docs say Alpha. update_channel is saved as "alpha"; "nightly" and the
old beta_builds switch still read as Alpha.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 17:17:33 +02:00
Ludovic Marie aa65c74e0f Update channels: stable at 20:00 UTC, Nightly checks every 30 minutes
The daily promotion moves to 20:00 UTC (21:00 in Paris in winter time):
PROMOTE_AT and promote.yml's cron (20:07, retries until 21:37), with the docs
that give the time. A launcher left open on Nightly looks for new releases
every 30 minutes; Stable keeps 4 hours. The launcher README's Settings
screenshot shows the Updates choice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:57:50 +02:00
Ludovic Marie ea5994a58a Update channels: Stable (daily) and Nightly (every build) in the launcher
The launcher's "Beta builds" switch becomes an Updates choice of two channels,
stored as "update_channel": "stable" | "nightly" (an old "beta_builds": true
reads as nightly). The CLI (update-channel) and MCP (update_channel) show and
set it, and list each release's channel as stable or nightly.

Nightly ships constantly: the release train waits 5 minutes for a merge wave to
settle (20 at most) instead of 15 (an hour). Stable moves once a day:
promote.yml runs at 19:00 UTC (retries until 20:37) and promotes the newest beta
that had been out 6 hours by 19:00, so late runs and retries pick the same one.
The stalled-promotion issue waits 30 hours. Release notes, docs, the website's
install and FAQ pages say Stable and Nightly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 14:30:19 +02:00
Ludovic Marie aa7f71a79b Merge pull request #1131 from ludovic111/feat/version-sources
🤖 Agent: merged after integration wave 1 on the desktop (main e730dc14 + #1108 #1114 #1131 #1157 #1158 #1162 = ae482a72): fmt, clippy -D warnings, workspace tests (one port-race flake in horizon-server panel, passes on rerun), release build, tools/parity.py --changed origin/main 299/302 then the 3 rerun: save_roundtrip pass, world_reopen pass x2 (one early-count flake), dedicated_server = known flake also failing on main (fixed by #1183/#1184).
2026-10-22 12:51:24 +02:00
Ludovic Marie 784e8f0648 Mod browser (1/3): Modrinth and CurseForge sources, checksum-checked downloads
The launcher's core gains `browser`: search Modrinth for what Horizon loads
(resource packs, Iris/OptiFine shader packs, modpacks; never Java mod jars),
say how well each result runs, pick the newest version that runs, and
download a file only from Modrinth's or CurseForge's file servers, with a
plain file name, kept only when its size and SHA-512 (SHA-1 on CurseForge)
match. CurseForge is a pluggable source that stays off until an API key
exists (HORIZON_CURSEFORGE_API_KEY at run time or build time; the launcher
workflow passes the repository secret). docs/mod-browser.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 03:17:25 +02:00
Ludovic Marie 30cdd6298a Version sources: a release build says its version (0.2.3), not 0.1.0 or a folder name
release.yml builds every game release with HORIZON_VERSION, the tag without its v
(docs/versioning.md), as launcher.yml already does for the launcher: no version-bump commits.

- horizon_core::release::{RELEASE, VERSION}: the built-in version, or Cargo.toml's for a build from
  source. (horizon_core::version stays Minecraft's, generated by gen_version_consts.py.)
- horizon-server and horizon-prepare --version, the server's /api/v1/status, horizon-ctl's MCP
  serverInfo and the "Horizon/<version>" user agents use it; they all said 0.1.0.
- The client's crash reports and telemetry say v0.2.3 from the built-in version wherever the game
  is installed; releases from before 0.2 (no version built in) still read the launcher's folder name.
- The launcher's benchmark telemetry used Cargo.toml's version instead of the release's.
- release.yml checks that horizon-prepare and horizon-server --version say the release's version.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 01:20:13 +02:00
Ludovic Marie 9a3237c5ac Release train: a release line (0.2) for vX.Y.Z and launcher@X.Y.Z tags
docs/versioning.md: with .github/release-line.txt (MAJOR.MINOR) the train tags game releases
vMAJOR.MINOR.PATCH and launcher releases launcher@MAJOR.MINOR.PATCH, the patch after the newest
release on the line; the line's first release goes out at once (still not while RELEASES_PAUSED is
set). Without the file nothing changes: vYYYY.MM.DD[.N] and launcher-vX.Y.Z. The file is not added
here; the switch to 0.2 is a separate one-line pull request for the day Ludovic says go.

- tag_key: every 0.2 tag ranks above every dated or launcher-v tag; semver order within, a
  pre-release before its release. release_channels.py uses the same key and tag test, so promotion
  and "latest only moves forward" work across the switch.
- A line's first release says so in its notes; 0.2.0's explains the numbering and how a server
  that says it is up to date takes it (horizon-server update --version v0.2.0).
- launcher.yml strips either prefix for HORIZON_LAUNCHER_VERSION and passes HORIZON_LAUNCHER_TAG,
  which the launcher uses to know its own line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 00:47:35 +02:00
Ludovic Marie fd9eb84bea Merge pull request #356 from ludovic111/feat/version-consts
🤖 Agent: merged (version-port tooling, rebased on main). Ludovic asked on 10-21 to clear the PR queue.
2026-10-21 16:16:03 +02:00
Ludovic Marie 1bcf90def5 Merge pull request #1014 from ludovic111/feat/verify-signed-updates
🤖 Agent: merged. Every merge condition holds: the latest stable game release (v2026.10.19.15) and launcher release (launcher-v0.16.0) verify against the release key, legacy_releases.py --check passes, and launcher and horizon-server built from this branch install signed, pinned-legacy and beta releases and refuse an edited SHA256SUMS, an altered archive and a missing signature. verify passed; parity proof in the last comment.
2026-10-21 14:47:43 +02:00
Ludovic Marie 9f385280ef Version facts in one generated file: crates/horizon-core/src/version.rs
tools/gen_version_consts.py writes what the pinned client jar's version.json
states (name, id, protocol, data version, pack formats, Java, stable, build
time) into horizon_core::version, and every Rust site reads it from there:
PROTOCOL_VERSION, GAME_VERSION, DATA_VERSION, the save's and the datafixer's
version name, /version, the crash report, the legacy ping, the dedicated
server's start line, the title screens, the default resource folder, the
resource pack format, and the launcher's and horizon-ctl's fallbacks. CI
checks the file against the pinned jar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:22:20 +02:00
Ludovic Marie 36908858a3 Launcher and horizon-server update: install only releases whose signed checksums match
The launcher (game installs and its own updates) and `horizon-server update` fetch a release's
SHA256SUMS and SHA256SUMS.sig, check the signature against the keys built in from
crates/horizon-release-sign/release-keys.txt and the archive against the signed SHA-256, and
refuse anything unsigned, signed for another tag or by another key, or that doesn't match, with a
message saying why. Merge only once release-keys.txt lists the key and a signed stable release
exists: until then every download is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:20:36 +02:00
Ludovic Marie 3ffbb1cb9f Sign and notarize the macOS builds with Developer ID
With the repository variable MACOS_SIGN_IDENTITY set, the launcher's Horizon.app and .dmg and the
game's three programs are signed with the hardened runtime and a timestamp, notarized and (app,
dmg) stapled, from a keychain of the Mac mini's own that keychain.sh unlocks with its password from
1Password. Unset, everything is signed ad hoc as before. The launcher checks an updated bundle's
signature before and after swapping it in, and puts the old one back when it doesn't hold.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 12:20:34 +02:00
Ludovic Marie 2ff8f5277d Merge pull request #1012 from ludovic111/feat/release-channels
🤖 Agent: merged after verify passed, security-1's dry runs on the branch, and an independent review whose findings were all fixed. Every release goes out as a beta and is promoted to stable after 6 hours if its smoke test passed and no release-blocker issue is open; launcher-v0.14.0 (stable by default) is already out.
2026-10-19 16:29:16 +02:00
Ludovic Marie 622b9a3a95 Releases: the Mac mini builds them from a clean checkout
Its work folder is shared with pull requests' parity jobs; with clean: false, whatever they left (target/, an
untracked .cargo/config.toml, a build script's output) could end up in a release. release.yml and launcher.yml
now always check out clean; target/ is rebuilt from scratch, and vanilla-data.sh relinks .local/ from its
preparations outside the checkout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:43:49 +02:00
Ludovic Marie a307bd2e3d Release channels: blockers hold every beta, latest only moves forward, a stalled promotion opens an issue
- A release-blocker issue's text is never read: its author can edit it after a maintainer labels it, so an
  open blocker holds every beta, and one closed as completed still holds the betas published before it closed.
- Even --force never promotes a beta that isn't newer than the stable release: horizon-server update and
  install.sh download GitHub's latest.
- When the oldest beta newer than the stable release has waited 24 hours, promote.yml opens a "Promotion to
  stable has stalled" issue with the last reason; the next promotion closes it.
- promote.yml: persist-credentials: false; issues: write for the stall issue.
- Website: the newest 100 releases, plus GitHub's latest (the stable one) when a day of betas pushes it out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:38:05 +02:00
Ludovic Marie 2a7303f76f Releases: beta and stable channels, promoted after 6 hours
Game releases go out as betas (GitHub pre-releases, never "latest") with the Mac mini's smoke
report attached. promote.yml (every 30 minutes, or by hand from a phone) runs
tools/release_channels.py, which makes the newest beta stable and latest once it has been out 6
hours, passed its smoke test and no release-blocker issue covers it, with notes rewritten to cover
everything since the last stable release. release_train.py counts merges from the newest release of
either channel, and a beta's notes say it is one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 15:34:03 +02:00
Ludovic Marie 0a1d03b4b7 Merge pull request #1011 from ludovic111/feat/signed-release-checksums
🤖 Agent: merged after verify passed, with security-1's dry runs on the branch and an independent review (no regression: parity still runs on every PR from this repo; releases go out unsigned with a warning until the signing key is set).
2026-10-19 15:32:09 +02:00
Ludovic Marie 76e3827545 release.yml sign step: a block scalar, so the line break isn't folded into an escaped space
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:47:39 +02:00
Ludovic Marie d38c07c2fa Releases: sign in a job of its own that runs only sha256sum and OpenSSL
Any repository code running earlier in the job that holds the key could reach it (a build patched
through Cargo.toml, or a step writing $GITHUB_ENV or $GITHUB_PATH for the next one). The `sign`
job checks out only tools/ci/sign_release.sh, which signs with the runner's OpenSSL; the publish
job, without the key, checks the result with horizon-release-sign as launchers will.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:18:54 +02:00
Ludovic Marie f32a91d793 Releases: signed checksums (SHA256SUMS, SHA256SUMS.sig) for game and launcher releases
crates/horizon-release-sign signs a release's checksums with an Ed25519 key that exists only as a
secret of the `release` environment, and checks them as launchers will, against the public keys
built in from release-keys.txt. Dry runs sign with a throwaway key. Until release-keys.txt lists a
key, releases keep going out with SHA256SUMS alone and a warning; once it does, a missing or
unknown key fails the release before anything is published. tools/ci/new_release_key.sh pipes a
new key straight into the secret and prints only the public key. ring and base64 are already in
the lockfile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 14:03:03 +02:00
Ludovic Marie cef652d026 checks: verify always runs, so a skipped job can never satisfy main's required check
Labels started `checks` runs whose verify job was skipped, and GitHub counts a skipped required
check as passed: auto-merge merged #823 and #837 before the real verify finished. Labels no longer
start `checks` (the fork-only run-parity label has its own workflow), and verify has no condition.
The parity job moves into a reusable workflow both call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 13:58:27 +02:00
Ludovic Marie 25f2e2396d Merge pull request #916 from ludovic111/feat/server-headless-build
🤖 Agent: reviewed; verify passes. Release dry run on this branch (run 39167763008): linux, macos, windows builds and publish all succeed; horizon-server-linux-x86_64.tar.gz (28 MB) built and import-checked (no libasound/libudev). Agent's proof: 880/879 client tests in both builds, dedicated_server/server_entities/lan on the server build, install → init → start → bot join on the ThinkPad without the libraries.
2026-10-18 23:42:28 +02:00
Ludovic Marie 41bd91f295 CI: cancel a pull request's unfinished checks when it closes
A merged or closed pull request's queued runs held the self-hosted runners
until someone cancelled them by hand. pull_request_target lets the token
cancel runs of fork pull requests too; the job checks out nothing. It
matches the branch and the head commit, so a fork's branch named like
another doesn't cancel that one's runs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 21:35:47 +02:00
Ludovic Marie 519ec1217c Server build: the game without sound or controllers, so Linux servers need neither ALSA nor udev
horizon-client's cpal output and its controllers (gilrs, the DualSense's HID output) move behind the
audio and controllers features, on by default, so the game is unchanged. Built with
--no-default-features it links neither libasound.so.2 nor libudev.so.1. release.yml packages that
build as horizon-engine in horizon-server-linux-x86_64.tar.gz (xtask package-server, checked by
check_imports.py --server); install.sh installs it on Linux (and --archive installs a local one);
horizon-server already prefers horizon-engine. The Dockerfile uses it too, and CI lints it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-18 20:17:21 +02:00
Ludovic Marie 7d50e879da Release zip: no macOS ._ metadata files
ditto without --norsrc --noextattr --noqtn stores an AppleDouble ._ entry
beside every file that has extended attributes, so the macOS release zip
held hundreds of them (horizon-macos-arm64/._horizon-ctl, tools/._*.py).
Package it as packaging/macos/package.sh already does, and fail the job if
any ._ or __MACOSX entry is left.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 15:40:07 +02:00
Ludovic Marie 1fc1b75007 Merge pull request #699 from ludovic111/ci/launcher-release-paths
Launcher releases: count every crate the launcher builds in
2026-10-17 15:32:58 +02:00
Ludovic Marie f49af8d995 Launcher releases: count every crate the launcher builds in
LAUNCHER_PATHS and launcher.yml's push paths now list horizon-bench,
horizon-friends, horizon-mods and horizon-port-pack too, the launcher's
path packages per cargo metadata. A change there (like the horizon-mods
dependency that left launcher/Cargo.lock stale) then builds and releases
the launcher instead of going unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:59:01 +02:00
Ludovic Marie 05821d4e62 Launcher: Windows tests check behaviour, not symlink paths
On Windows the launcher links another launcher's client jar with a hard link
(a copy across volumes) and its libraries and assets with a junction, since
symlinks need developer mode. canonicalize() of a hard link is its own path,
so the sources tests, which compared canonical paths, failed on Windows and
blocked every launcher release since launcher-v0.4.0. They now check that the
linked jar is the launcher's file (a byte written through one shows in the
other), which holds for a symlink and a hard link alike.

The modpack profile test compares paths as paths rather than strings ending
in "a/carry".

mklink /J's paths are always quoted: Rust only quotes arguments with spaces,
and cmd reads an unquoted & or ^ in a user folder as its own syntax.

CI runs every launcher test target, not just the library's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 14:45:52 +02:00
Ludovic Marie 0da61e660b CI parity: raise the per-harness timeout to 2400 s
world_spawn now takes about 1480 s on a loaded Mac mini runner, just
under the old 1500 s limit, so PRs failed on a timeout rather than a
mismatch (#567, #514).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 04:08:08 +02:00
Ludovic Marie ed6af00ff8 horizon-server: install scripts, a Docker image, and boot services
- packaging/server/install.sh (Linux, macOS) and install.ps1 (Windows) install the latest release (or
  --version) from GitHub, check the SHA-256 GitHub publishes for it, and keep releases side by side:
  <dir>/current points at the one in use, so an update or a rollback is one link, and the game files
  prepared from the owner's Minecraft stay in <dir>/game-files across updates.
- packaging/server/Dockerfile builds the server and the engine; the prepared game files are mounted
  read-only, and the first start writes server.toml from HORIZON_EULA, HORIZON_WORLD, HORIZON_SEED,
  HORIZON_PORT, HORIZON_WHITELIST, HORIZON_PLAYERS and HORIZON_OP (`init` reads them too).
- `horizon-server service install|uninstall|print` writes and loads a systemd unit (user, or --system),
  a launchd agent, or a Windows task at logon. It runs the installer's launcher, so updates apply on
  the next restart.
- Releases build horizon-server and ship it beside horizon-ctl and horizon-prepare; PLAY.txt says how
  to run a server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 20:20:09 +02:00
Ludovic Marie dd7282c9da Merge pull request #513 from Online5277/ci/skip-rust-for-docs-and-website
Skip Rust verify steps for website and documentation PRs
2026-10-16 18:31:28 +02:00
Ludovic Marie ece9ab2b79 Release smoke run: count frames drawn in the world
The macOS release build has failed since 05:54 UTC on 2026-10-16: on the
self-hosted Mac mini runner the game drew its 1500 frames in about 2.5 s,
before the world had started generating (chunks 0, player null), so no
release has been published since v2026.10.16.03.

--world-frames N exits after N frames drawn with the player in the world.
It is otherwise a frame-limited run (classic menus, --timeout-seconds), and
--frames keeps its meaning for the harnesses.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 17:53:47 +02:00
Cosmic b27e4b9323 Skip Rust verify steps for content-only pull requests 2026-10-16 11:46:49 -04:00
Ludovic Marie d53a366718 Opt-in data collection (2/5): the launcher asks, and Settings changes it
- First run (and when CONSENT_VERSION changes): a question over the launcher
  with four switches, all off, what each sends in plain words, what is never
  sent, "View what would be sent" (the exact JSON), and "Send nothing" /
  "Save choices".
- Settings → Data collection: the same switches any time, the install ID and
  what waits, "View what would be sent" and "Delete my data" (server deletion,
  outbox cleared, new install ID).
- The outbox is sent at start and on close (at most 3 s), only when there is a
  server (none yet) and only what the player allows.
- CLI `horizon-launcher data status|allow|deny|show|delete|send|bug|benchmark`
  and MCP tools data_collection, set_data_collection, data_preview,
  delete_my_data, report_bug. Bug reports and benchmark uploads are printed for
  the player to confirm; logs are scrubbed and lose chat lines.
- horizon-telemetry joins the launcher's paths (launcher.yml, release_train,
  pr_ready).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 18:52:45 +02:00
Ludovic Marie b3906d803d Horizon mods runtime: load sandboxed WebAssembly components with wasmtime
Manifest and .hmod reading, capability checks against the component's imports,
fuel and memory limits per call, a trapping mod stopped alone, hot reload.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 14:01:57 +02:00
Ludovic Marie f2e87721f4 CI parity: allow 120 minutes, so changes to core crates finish instead of timing out
Changes to horizon-core or horizon-world make ~280-300 harnesses rerun, which takes
60-90 min on the Mac mini runners; at 45 min those runs were cancelled and proved nothing
(#269, #280, #292 today).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 09:59:10 +02:00
Ludovic Marie 12c1af0261 Opt-in data collection (4/5): the ingest server
crates/horizon-telemetry-server, its own workspace like the launcher's (the
game's workspace never builds a web server):
- POST /v1/batch (zstd, decoded and checked by horizon-telemetry), DELETE
  /v1/installs/<id>, GET /v1/aggregate (cached 10 min, CORS for the website),
  GET /healthz.
- SQLite: machine descriptions stored once, one row of columns per session
  with 80-byte summary blobs, zstd bodies for crash/bug/benchmark, daily totals
  rebuilt when a day changes and kept after raw rows expire (180 days). About
  190 bytes per session on disk.
- Per-address token bucket (addresses in memory only), per-install daily cap,
  groups under 5 install-days hidden from the totals.
- Tests: refusals, rate limit, daily cap, deletion (rows and totals), retention,
  storage size, and end to end with the launcher's uploader.
- Dockerfile and a path-filtered CI workflow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 02:53:59 +02:00
Ludovic Marie 7a07251acf CI disk: short artifact retention, runner cleanup, sccache on the Mac runners
Releases now ship as often as changes land, so nothing they leave behind may
grow without bound. Workflow uploads keep 3 days (nightly parity logs 7); the
releases carry their own assets.

tools/ci/runner-gc.sh, run every 6 hours by launchd on the Mac mini from a copy
of main, skips a runner in a job, removes a checkout's cargo build folders past
12 GB and harness work folders under .local/ untouched for 3 days, and keeps the
2 newest shared preparations while no runner works. The runners build through
the machine's sccache, so what it removes comes back from the cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:05:59 +02:00
Ludovic Marie 86e0e122f4 Release train: ship every settled player-facing change, no 3-hour gap
Ludovic wants releases as often as changes land. A kind now releases when
player-facing pull requests merged since its last release and the newest of
them merged 15 minutes ago (a burst of merges becomes one release), or the
oldest an hour ago when merges never pause; release-now still ships at once.
There is no minimum time between releases. The scheduled checks run every 10
minutes (game at :02, launcher at :07) so a settled release goes out without
another merge.

Release titles keep proper nouns (Horizon's mark, Microsoft, LAN, ...) when
they lowercase the clauses after the first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 01:01:26 +02:00
Ludovic Marie a248e9242a Ship the launcher as Horizon.app in a .dmg, and in Linux's menus
macOS: packaging/macos/package.sh (run by launcher.yml) builds Horizon.app (Info.plist, bundle
id dev.horizon.launcher, macOS 11+, the .icns), signs it ad hoc, and makes the two release
assets: horizon-launcher-macos-arm64.dmg, the installer (the app and an Applications link over
a background, laid out by dmgbuild without a GUI), and the .zip the launcher updates from. A
launcher inside the app replaces the whole bundle: copied beside it with ditto, swapped in by
renames, restarted; a bare binary, as older launchers are, still finds its binary in the zip.
A macOS test zips, unpacks, swaps and re-verifies a signed bundle. Signing for real is a
MACOS_SIGN_IDENTITY away (packaging/macos/README.md, with what Gatekeeper asks meanwhile).

Linux: release builds keep horizon-launcher.desktop (with the binary's current path),
horizon.desktop (hidden, for the game window's icon) and the hicolor icons under
$XDG_DATA_HOME, without root; `horizon-launcher desktop-entry --remove` takes them out and
stops it, `desktop-entry` puts them back.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:08:40 +02:00
Ludovic Marie beac11eaa5 Nightly parity audit on the Mac mini, with an issue per regressed harness
nightly.yml runs every harness from scratch on main at 03:00 Paris time on the Mac mini's runner, after
preparing the resources again if the preparer changed. tools/ci/parity_issues.py compares the results with
the previous audit: a harness that passed and now fails gets one issue labelled parity-regression (an open
one is commented on), and an issue closes when its harness passes again. The bugs session picks them up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:18:31 +02:00
Ludovic Marie 76f3e7c2a8 launcher.yml: GPUI's runtime shaders on a Mac runner without the Metal toolchain (the Mac mini)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:46 +02:00
Ludovic Marie fa61f262c3 launcher: automatic releases, the version built in from the tag
launcher.yml releases the launcher by itself: after a push to main that changes launcher/,
crates/horizon-prepare or crates/horizon-theme, hourly, and by hand, with the game's rules
(tools/release_train.py decide --kind launcher: player-facing pull requests and a 3-hour cooldown,
release-now, skip-release, RELEASES_PAUSED). launcher-vX.Y.Z is the next patch, or the next minor after a
pull request labelled feature. Linux on ubuntu-22.04, Windows on GitHub, macOS on the Mac mini's runner;
published with --latest=false.

No version-bump commits: VERSION reads HORIZON_LAUNCHER_VERSION with option_env!, falling back to
Cargo.toml's, and --version, the User-Agent and the self-update comparison use it; each build checks
--version says the release's version.

With several game releases a day, the newest launcher release soon falls out of the newest 100 (launcher)
or 30 (website) releases: both now look the newest launcher-v tags up one by one when the list has none.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:46 +02:00
Ludovic Marie fb256809a7 release: the macOS smoke run draws 1500 frames; 300 end before the world has loaded
Checked on the Mac mini: 1500 offscreen frames in 12 s, 81 chunks generated, 4153 sections meshed, a player.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:20 +02:00
Ludovic Marie e994c8ab95 release: the macOS smoke run opens a world (from a scratch folder) and checks it drew one
Without --world the client stays on the title screen: 300 frames and no chunks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:20 +02:00
Ludovic Marie 8aacb48798 release: automatic game releases after main's green checks
release.yml now releases the game by itself: after every green checks run of a push to main, every hour
(a release the cooldown held goes out without another merge) and by hand. tools/release_train.py decides
from the pull requests merged since the last published game release: a release when one changed what
players run and the last release is 3 hours old, at once for a pull request labelled release-now;
skip-release pull requests don't count. RELEASES_PAUSED=1 stops it. Tags vYYYY.MM.DD, then .1, .2 the
same UTC day.

Linux (ubuntu-22.04) and Windows build on GitHub with Cargo caches, macOS on the Mac mini's runner
(MAC_RUNNER=macos-latest sends it to GitHub). Each builds horizon-client, horizon-ctl and horizon-prepare
(all three named: --bin selects only the named binaries), checks all three exist, checks their libraries,
starts each one, packages with xtask and archives; the Mac also runs the game offscreen for 300 frames on
a new world with resources prepared from its own Minecraft. The notes: New (label feature), Fixed (the
rest), Known issues (docs/known-issues.md). A hand-started run off main, or with dry_run, builds
everything and writes the notes but publishes nothing.

docs/release-flow.md describes the flow, the labels, pausing and rolling back; the launcher gets a test
that a release turned back into a draft is never offered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:20 +02:00
Ludovic Marie 874d111d60 checks: cached verify, and a parity job on the Mac mini for the harnesses a PR can affect
verify keeps its dependencies and workspace build in a cache saved by main's runs (Swatinem/rust-cache).
The new parity job runs on the Mac mini's self-hosted runner (label horizon-game), which holds the
maintainer's own Minecraft files: tools/ci/vanilla-data.sh links .local/ to the runner's copy and prepares
the resources with the pull request's own horizon-prepare, once per preparer version; then
tools/parity.py --changed <base> runs only the harnesses whose traced dependencies changed since the merge
base and that have no pass for their current inputs. A fork's pull request runs it only when a maintainer
adds run-parity, once per label, on the commit it had then.

parity.py: --changed REF, and linux_only in tools/parity_hosts.json for harnesses that differ on other
platforms for a reason that is not a Horizon bug (skipped there with the reason, unless named).

docs/release-flow.md starts with the pull request checks and merging.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 22:17:20 +02:00