The first install on Windows now checks SHA256SUMS.sig (a Nightly's
SHA256SUMS.v2.sig, for the version its title names) against the release
key, then the archive against SHA256SUMS, as install.sh does. OpenSSL is
looked for on the PATH and in Git for Windows. Without OpenSSL 3 it says
so and keeps the GitHub SHA-256 check.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Every Nightly was signed for the constant "horizon release nightly", and
the version a launcher installed it as came from the unsigned GitHub
title, so anyone able to edit the release could serve an older signed
Nightly as tonight's.
- nightly-build.yml signs SHA256SUMS twice with the same key(s):
SHA256SUMS.sig for the channel, as before, for installed launchers;
SHA256SUMS.v2.sig for nightly-<date>-<commit>, the version the title
names (sign_release.sh ALSO_FOR). Both are verified before publishing.
- horizon-release-sign: nightly_title_version() parses the title
strictly (no fallback to published_at); nightly_digest() takes that
version and checks SHA256SUMS.v2.sig for exactly it; is_nightly_build();
SIGNATURE_V2. `verify --signature <file>` checks the v2 file.
- Every installer that checks a Nightly moves in the same step: the
launcher, horizon-server update and packaging/server/install.sh refuse
a Nightly whose title doesn't parse and install it only with a v2
signature for that version. install.ps1 checks no signature (only
GitHub's SHA-256), so it has nothing to move.
- An older Nightly still installs on request as a rollback with a
warning; the launcher's update never moves the selection back to it,
and horizon-server prints a rollback notice for --version.
- Nightlies from before this change have no v2 signature: the refusal
now says so and names `horizon-server rollback`, and docs/security.md
"Nightly signatures" describes the changeover (martipops's review of
#9129). The launcher's new messages go through trf(), in every
language table in English until translated.
- tests/install_sh_signature.sh: a v2 Nightly installs as its version; a
Nightly with only the channel signature, an older Nightly retitled as
a newer one, and an unreadable title are refused (all three installed
before this change).
Split from #9129, as martipops's review suggested.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01SJ3stK8iXVDXqoi7midcqd
Checks SHA256SUMS.sig against the release key, then the archive against
SHA256SUMS; older OpenSSL, LibreSSL or none says so and keeps the GitHub
SHA-256 check. Closes#8241.
Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
f2a99d623 split verify into python, clippy and tests jobs plus a gathering
verify: four GitHub-hosted runners per checks run instead of one. With every
pull request push and every merge to main starting checks, that filled the
organisation's shared runner pool, and every workflow's jobs (labels, overlap
notes, other pull requests' checks) queued behind it for tens of minutes.
verify runs the public-tree check, the Python tests, rustfmt, clippy and the
Rust tests one after another again, on one runner. A pull request's
public-tree check still reads only its own commits (--range base..head).
Closes#5302
Co-Authored-By: Claude Opus 5.5 <[email protected]>
verify ran the public-tree check, the Python tests, clippy and the Rust tests one
after another (~8 min). They now run as the python, clippy and tests jobs at once,
and verify gathers them (always(), failing unless all three passed), so the
required check keeps its name.
A pull request's public-tree check reads only its own commits
(check_public_tree.py --range base..head) instead of every ref's whole history;
main's pushes still run --history.
Closes#4978
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Ludovic's decision (2026-10-23): no more Alpha and Stable. release.yml
publishes every game release as the release and GitHub's latest instead of
a pre-release that promote.yml made stable once a day. The checks that
gated a promotion run in the release itself: `decide` holds the train while
an issue labelled release-blocker is open (forced runs too), and the publish
job runs `release_train.py gate` right before publishing: the Mac mini's
smoke-test.json must be there and show a world was drawn, and no blocker may
be open. Otherwise nothing is published.
Notes cover everything since the last release that is not a pre-release, so
the first single-channel release lists all of it since v0.2.0. promote.yml,
tools/release_channels.py and its tests are deleted. The cadence is
unchanged (5 minutes of settling, 20 at most, release-now at once).
Docs and site: one channel in release-flow, versioning, security, the FAQ,
install, the home page and the Releases page (Game / Launcher filter); no
player-facing Alpha wording left.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
- A table of what is live, waiting to merge, or waiting for Ludovic, so the page doesn't read as if it all were.
- Forks: their checks need a maintainer's approval, and run-parity runs them on the Mac mini; the Mac mini
section no longer says forks don't run there, and says what a clean release checkout does and doesn't fix.
- Blockers hold every beta while open; releases from before signing stay installable with pinned files.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
What an attacker could do before (instructions hidden in reports, a skipped check that counted as
passed, releases straight to every player, downloads trusting GitHub alone, pipeline files changed
like any other), what stops it now (untrusted text, verify always running, beta then stable,
signed updates, code owners, the agents' own GitHub App), what still needs Ludovic, and what it
doesn't solve. Written for the website's Contributing page; CONTRIBUTING.md links it.
Co-Authored-By: Claude Opus 5.5 <[email protected]>