Merge pull request #1232 from ludovic111/feat/alpha-channel

🤖 Agent: merged. The fast channel is now "Alpha" (Ludovic, 2026-10-22); old "nightly" / beta_builds settings still read as Alpha. verify passes; launcher tests 96+14+1, release tools' Python tests 27, website build OK.
This commit is contained in:
Ludovic Marie authored and GitHub committed 2026-10-22 17:25:47 +02:00
commit 34c3c1cffe
18 files changed
+88 -80

No files matched your search

+1 -1
View File
@@ -1,7 +1,7 @@
name: promote
# Beta game releases become stable once a day (docs/release-flow.md, "Channels"): at 20:00 UTC (21:00 in Paris in
# winter time, 22:00 in summer) tools/release_channels.py promotes the newest beta that had been out 6 hours by then,
# passed its release smoke test and isn't covered by an issue labelled release-blocker. Launchers on the Nightly
# passed its release smoke test and isn't covered by an issue labelled release-blocker. Launchers on the Alpha
# channel install every beta when it is published; Stable gets the day's promotion. The later runs of the evening are
# retries for a run GitHub starts late or skips: they promote the same beta, so stable still moves once.
# By hand, from GitHub's app too (Actions > promote > Run workflow):
+1 -1
View File
@@ -349,7 +349,7 @@ jobs:
run: |
signature=(); [[ -f dist/SHA256SUMS.sig ]] && signature=(dist/SHA256SUMS.sig)
smoke=(); [[ -f dist/smoke-test.json ]] && smoke=(dist/smoke-test.json)
# A beta (pre-release), the Nightly channel's: promote.yml makes it stable and latest in its daily promotion
# A beta (pre-release), the Alpha channel's: promote.yml makes it stable and latest in its daily promotion
# (docs/release-flow.md).
gh release create "$TAG" --target "${{ needs.decide.outputs.head }}" --title "$(cat notes/title.txt)" \
--notes-file notes/body.md --prerelease --latest=false dist/horizon-linux-x86_64.tar.gz \
+16 -13
View File
@@ -2,9 +2,9 @@
How a change gets from a pull request to players: the checks a pull request passes, how it merges, and the
automatic releases that follow. Releases are automatic and frequent so the latest Horizon is always playable.
Each one goes out as a **beta**, which launchers on the **Nightly** channel install at once, and becomes **stable** in
Each one goes out as an **alpha** build, which launchers on the **Alpha** channel install at once, and becomes **stable** in
the next daily promotion unless something holds it back ([Channels](#channels)): launchers and servers install
stable builds unless their owner picks Nightly.
stable builds unless their owner picks Alpha.
## Pull request checks
@@ -101,8 +101,8 @@ flowchart LR
windows["Windows x64<br/>GitHub windows-latest"]
mac["macOS arm64<br/>Mac mini runner<br/>+ offscreen game run"]
end
build --> publish["publish vX.Y.Z as a beta<br/>notes from the merged PRs"]
publish --> beta(["launchers on Nightly install it"])
build --> publish["publish vX.Y.Z as an alpha<br/>notes from the merged PRs"]
publish --> alpha(["launchers on Alpha install it"])
publish --> promote{"promote.yml, daily at 20:00 UTC:<br/>6 h old by then, smoke test, no release-blocker"}
promote --> stable["stable and latest<br/>notes since the last stable"]
stable --> launcher(["every launcher and server installs it"])
@@ -121,7 +121,7 @@ commits) since the last **published** game release:
- It releases when at least one pull request counts and the newest of them merged at least 5 minutes ago, so a wave
of merges becomes one release; if merges never pause, it releases once the oldest has waited 20 minutes. There is
no minimum time between releases: every settled player-facing change ships, limited only by the release already
building. That is the Nightly channel, many releases a day.
building. That is the Alpha channel, many releases a day.
- A pull request labelled `release-now` (a crash or a blocker) releases at once.
- A pull request labelled `skip-release` (work players don't see) doesn't count and isn't listed in the notes.
- One release at a time (concurrency group `game-release`); a run that finds nothing new does nothing.
@@ -180,23 +180,26 @@ runs the game: GitHub's runners have neither a GPU worth the name nor the game f
- `### Known issues`: the bullets of [docs/known-issues.md](known-issues.md);
- thanks to outside contributors, how to install, the disclaimer.
Kira posts every release in Discord's #releases. A Nightly (a game pre-release) is posted silently: no ping and
Kira posts every release in Discord's #releases. An Alpha build (a game pre-release) is posted silently: no ping and
Discord's @silent flag, so nobody gets a notification. When the same release becomes Stable it is posted again, and
that post pings the releases role when the notes have a `### New` section (at most once every 6 hours). Launcher
releases post without a ping.
### Channels
Every game release is published as a **beta**, a GitHub pre-release, so GitHub's *latest* release stays the last
Every game release is published as an **alpha** build, a GitHub pre-release (the workflows, tools and labels
call it a *beta*: `release-blocker` holds every beta), so GitHub's *latest* release stays the last
**stable** one. The launcher's Settings has an **Updates** choice of two channels (`update_channel` in its
`settings.json`, `horizon-launcher update-channel`):
- **Stable**, the default: a tested build about once a day, the betas promoted below.
- **Nightly**: every beta as soon as it is built, many times a day; it can break. "Nightly" is a name, not a
schedule. An open launcher looks for releases every 30 minutes on Nightly (4 hours on Stable). Launchers from before 0.2 had a "Beta builds" switch instead; on, it reads as Nightly.
- **Stable**, the default: a tested build about once a day, the alpha builds promoted below.
- **Alpha**: every alpha build as soon as it is built, many times a day; it can break. "Alpha" is the project's
word for builds that are still being tested (the whole of Horizon is in beta); Stable is the tested ones. An open
launcher looks for releases every 30 minutes on Alpha (4 hours on Stable). Launchers from before 0.2 had a "Beta
builds" switch instead; on, it reads as Alpha.
`horizon-server update` installs stable builds (`--version <beta>` takes one by name). In Discord's #releases every
Nightly is posted silently and only Stable posts notify (above).
`horizon-server update` installs stable builds (`--version <tag>` takes an alpha build by name). In Discord's #releases every
Alpha is posted silently and only Stable posts notify (above).
`.github/workflows/promote.yml` runs `tools/release_channels.py promote` once a day at 20:00 UTC (21:00 in Paris in
winter time, 22:00 in summer), with retries until 21:37 for a run GitHub starts late or skips. It promotes the newest
@@ -231,7 +234,7 @@ gh workflow run promote -f tag=v0.2.3 -f force=true # even blocked or without
gh workflow run promote -f dry_run=true # what would be promoted now
```
A beta's notes cover only what changed since the release before it (beta or stable), and say it is a nightly build.
An alpha build's notes cover only what changed since the release before it (alpha or stable), and say it is an alpha build.
`release_train.py decide` counts pull requests from the newest release of either channel, so betas don't
release the same changes again.
+3 -3
View File
@@ -38,7 +38,7 @@ and some are waiting for Ludovic:
| Private vulnerability reporting | **Live** (see the end of this page). |
| Signed checksums on releases | **Live** for new releases (#1011): the `release` environment holds the key, and its public half is in `release-keys.txt` (#1023). Releases from before then are unsigned. |
| Launchers and servers checking signatures | **Not yet**: #1014 waits as a draft until a signed stable game release and a signed launcher release exist. Until then they trust GitHub's checksum, as before. |
| Beta, then stable | **Live** (#1012): every release is first a beta. The launcher's Updates setting picks Stable (the default) or Nightly (the betas). |
| Alpha, then stable | **Live** (#1012): every release is first an alpha build. The launcher's Updates setting picks Stable (the default) or Alpha (the pre-releases). |
| Reports marked suspicious | **Partly**: the scan of Discord for agents does it. Kira's issues do it once its pull request merges. |
| Code owners' review of the pipeline | **Not yet**: `.github/CODEOWNERS` and its protection (#1015) wait for the App, below. Until then pipeline changes merge like any other once `verify` passes. |
| Agents on their own GitHub App | **Not yet**: prepared, waiting for Ludovic to create it. Agents still use his account. |
@@ -53,11 +53,11 @@ and some are waiting for Ludovic:
- **`verify` always runs.** Labels no longer start the checks, and the required `verify` job has no condition, so
the only `verify` a change can have is one that ran. Main requires it, from GitHub Actions only, for everyone,
admins included.
- **Beta, then stable.** Every release is first a **beta**. It becomes **stable**, the version launchers and
- **Alpha, then stable.** Every release is first an **alpha** build (a GitHub pre-release). It becomes **stable**, the version launchers and
servers install by themselves, in the daily promotion (20:00 UTC), only after it has been out 6 hours and passed its
smoke test (the game started and drew a world on the Mac mini), and only while no issue labelled `release-blocker`
is open. If a beta misses a promotion it was due for, an issue says so. Players who want the newest builds at once
pick **Nightly** under Updates in the launcher's Settings. Discord announces stable releases only.
pick **Alpha** under Updates in the launcher's Settings. Discord announces stable releases only.
- **Signed updates.** Each release carries a list of its files' checksums, signed with Horizon's release key. The
launcher (for the game and for itself) and `horizon-server update` check that signature against the public key
built into them, then check the download against the list. Anything unsigned, signed with another key, signed
+3 -3
View File
@@ -22,8 +22,8 @@ game and the launcher of the 0.2 line.
0.2. A `feature` label no longer bumps anything: it only lists the change under **New**.
- **Patch releases** are every release the train makes: the patch after the newest release tagged on the line, game
and launcher counted separately (the game releases more often, so `v0.2.14` can go with `[email protected]`).
- **Betas** are not separate tags. As before, every game release is published as a GitHub pre-release (a beta, what
the launcher's Nightly channel installs) under its final tag and promoted in place to stable in the daily
- **Alpha builds** are not separate tags. As before, every game release is published as a GitHub pre-release (an alpha build, what
the launcher's Alpha channel installs) under its final tag and promoted in place to stable in the daily
promotion at 20:00 UTC, once it has been out 6 hours ([Channels](release-flow.md#channels)). `v0.2.4` is the same
build whether it is still a beta or already stable.
- **Pre-release tags** (`v0.3.0-rc.1`, `v0.3.0-beta.2`) are semver's and are reserved for a hand-made preview of a
@@ -126,7 +126,7 @@ Run by the orchestrator, in this order. Until step 3, nothing is published.
makes it *latest*. Do not force it (`promote` with `force=true`): it carries about 43 player-facing pull requests
from the freeze and needs its beta like any release. Check it is *latest*:
`gh api repos/ludovic111/horizon/releases/latest --jq .tag_name`.
5. Only then merge the update channels pull request (the launcher's Stable and Nightly, the daily promotion), then
5. Only then merge the update channels pull request (the launcher's Stable and Alpha, the daily promotion), then
mark the switch pull request (#1132) ready and merge it (`.github/release-line.txt` = `0.2`, and the
docs that talk about 0.2). On the next run, about 10 minutes, the train publishes `v0.2.0` as a beta and
`launcher@0.2.0`, both "the first release of 0.2", built from the same code as the bridges plus that pull request.
+5 -5
View File
@@ -83,11 +83,11 @@ page `servers.json` (the list), `servers/<name>/` (the servers created there) an
the game's menu style, the game's extra command-line arguments, and the launcher's own version
and update state.
- **Updates**: **Stable** (the default) installs a tested build about once a day, the betas that
`promote.yml` makes stable; **Nightly** installs every release as soon as it is built, several
`promote.yml` makes stable; **Alpha** installs every release as soon as it is built, several
times a day, and can break (docs/release-flow.md, "Channels"). While it is open, the launcher
looks for new releases every 4 hours on Stable and every 30 minutes on Nightly. Kept as `update_channel` in
`settings.json`; a `beta_builds: true` from launchers before 0.2 reads as Nightly.
`horizon-launcher update-channel [stable|nightly]` shows or sets it. The **Versions** page
looks for new releases every 4 hours on Stable and every 30 minutes on Alpha. Kept as `update_channel` in
`settings.json`; a `beta_builds: true` from launchers before 0.2 reads as Alpha (so does a `nightly` from the channel's first name).
`horizon-launcher update-channel [stable|alpha]` shows or sets it. The **Versions** page
shows each release's channel. The launcher's own updates don't depend on it.
- **Game arguments**: extra arguments added to the game's command line (for example
`--server-thread`), one line where quotes group a word with spaces. They are appended after the
@@ -140,7 +140,7 @@ The same binary is a command-line tool; without a subcommand it opens the window
horizon-launcher notes [<tag>] patch notes as plain text
horizon-launcher links the Discord, sponsor and website links
horizon-launcher menu-style [horizon|classic] show or set the game's menu style
horizon-launcher update-channel [stable|nightly] show or set which releases install by themselves
horizon-launcher update-channel [stable|alpha] show or set which releases install by themselves
horizon-launcher game-args [<line>] show or set the extra arguments the game starts with
horizon-launcher update [--check] Horizon and the launcher
horizon-launcher logs [--lines N] [--crash] the end of the game's log, or its newest crash report
+36 -31
View File
@@ -346,7 +346,7 @@ pub fn latest(releases: &[Release]) -> Option<&Release> {
releases.iter().find(|r| r.asset.is_some())
}
/// The releases a launcher installs by itself: stable ones, and betas too on the Nightly channel.
/// The releases a launcher installs by itself: stable ones, and betas too on the Alpha channel.
pub fn channel(releases: &[Release], channel: UpdateChannel) -> Vec<Release> {
releases
.iter()
@@ -362,15 +362,17 @@ pub enum UpdateChannel {
/// A tested build about once a day: the betas promote.yml makes stable.
#[default]
Stable,
/// Every release as soon as it is built, betas included.
Nightly,
/// Every release as soon as it is built, pre-releases included. It was called "nightly"
/// while the channels were new: that name is still read.
#[serde(alias = "nightly")]
Alpha,
}
impl UpdateChannel {
pub fn name(self) -> &'static str {
match self {
UpdateChannel::Stable => "stable",
UpdateChannel::Nightly => "nightly",
UpdateChannel::Alpha => "alpha",
}
}
@@ -378,36 +380,36 @@ impl UpdateChannel {
pub fn label(self) -> &'static str {
match self {
UpdateChannel::Stable => "Stable",
UpdateChannel::Nightly => "Nightly",
UpdateChannel::Alpha => "Alpha",
}
}
pub fn parse(name: &str) -> Option<Self> {
match name {
"stable" => Some(UpdateChannel::Stable),
"nightly" => Some(UpdateChannel::Nightly),
"alpha" | "nightly" => Some(UpdateChannel::Alpha),
_ => None,
}
}
/// Whether this channel installs `release`: betas only reach Nightly.
/// Whether this channel installs `release`: betas only reach Alpha.
pub fn takes(self, release: &Release) -> bool {
self == UpdateChannel::Nightly || !release.beta
self == UpdateChannel::Alpha || !release.beta
}
/// How often a launcher left open looks for new releases: Nightly's come many times a day,
/// How often a launcher left open looks for new releases: Alpha's come many times a day,
/// Stable's once.
pub fn check_every(self) -> std::time::Duration {
std::time::Duration::from_secs(match self {
UpdateChannel::Stable => 4 * 3600,
UpdateChannel::Nightly => 30 * 60,
UpdateChannel::Alpha => 30 * 60,
})
}
/// The channel a release is on when it is listed: betas are Nightly's only.
/// The channel a release is on when it is listed: betas are Alpha's only.
pub fn of(release: &Release) -> Self {
if release.beta {
UpdateChannel::Nightly
UpdateChannel::Alpha
} else {
UpdateChannel::Stable
}
@@ -2214,7 +2216,7 @@ pub struct Settings {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub game_args: String,
/// Which releases install by themselves: Stable by default. Launchers before 0.2 had a "Beta
/// builds" switch instead; [`Settings::parse`] reads its `beta_builds: true` as Nightly.
/// builds" switch instead; [`Settings::parse`] reads its `beta_builds: true` as Alpha.
#[serde(default)]
pub update_channel: UpdateChannel,
/// The player has read what the "I'm playing" ping holds (`telemetry::PING`): on the
@@ -2232,11 +2234,11 @@ impl Settings {
return Self::default();
};
if let Some(fields) = value.as_object_mut() {
// The switch from before 0.2: on meant every release at once, the Nightly channel.
// The switch from before 0.2: on meant every release at once, the Alpha channel.
if fields.remove("beta_builds") == Some(serde_json::Value::Bool(true)) {
fields
.entry("update_channel")
.or_insert_with(|| UpdateChannel::Nightly.name().into());
.or_insert_with(|| UpdateChannel::Alpha.name().into());
}
// A channel this launcher doesn't know is Stable, not a reason to lose every setting.
if fields
@@ -2458,10 +2460,10 @@ mod tests {
assert_eq!(offered(&["v2026.10.15.1", "v2026.10.14"]), None);
}
/// Betas (GitHub pre-releases) install by themselves only on the Nightly channel; Stable waits
/// Betas (GitHub pre-releases) install by themselves only on the Alpha channel; Stable waits
/// for promote.yml's daily promotion (docs/release-flow.md, "Channels").
#[test]
fn updates_follow_the_stable_channel_unless_nightly_is_picked() {
fn updates_follow_the_stable_channel_unless_alpha_is_picked() {
let asset = ASSET.unwrap_or("horizon-linux-x86_64.tar.gz");
let list = serde_json::json!([
{"tag_name": "v2026.10.19.02", "prerelease": true, "assets": [{"name": asset, "browser_download_url": "b", "size": 1}]},
@@ -2472,10 +2474,10 @@ mod tests {
releases.iter().map(|r| r.beta).collect::<Vec<_>>(),
[true, false]
);
let offered = |nightly: bool, installed: &[&str]| {
let offered = |alpha: bool, installed: &[&str]| {
let installed: Vec<String> = installed.iter().map(|t| t.to_string()).collect();
let on = if nightly {
UpdateChannel::Nightly
let on = if alpha {
UpdateChannel::Alpha
} else {
UpdateChannel::Stable
};
@@ -2491,12 +2493,12 @@ mod tests {
assert_eq!(offered(false, &["v2026.10.19.02"]), None);
assert_eq!(
releases.iter().map(UpdateChannel::of).collect::<Vec<_>>(),
[UpdateChannel::Nightly, UpdateChannel::Stable]
[UpdateChannel::Alpha, UpdateChannel::Stable]
);
}
#[test]
fn the_update_channel_is_stored_by_name_and_the_old_beta_switch_reads_as_nightly() {
fn the_update_channel_is_stored_by_name_and_the_old_beta_switch_reads_as_alpha() {
let read = |text: &str| Settings::parse(text).update_channel;
assert_eq!(read(""), UpdateChannel::Stable);
assert_eq!(read("{}"), UpdateChannel::Stable);
@@ -2504,12 +2506,15 @@ mod tests {
read(r#"{"update_channel": "stable"}"#),
UpdateChannel::Stable
);
assert_eq!(read(r#"{"update_channel": "alpha"}"#), UpdateChannel::Alpha);
// The channel's first name, from launchers built while it was called Nightly.
assert_eq!(
read(r#"{"update_channel": "nightly"}"#),
UpdateChannel::Nightly
UpdateChannel::Alpha
);
// Launchers before 0.2: "Beta builds" on is Nightly, off is Stable.
assert_eq!(read(r#"{"beta_builds": true}"#), UpdateChannel::Nightly);
assert_eq!(UpdateChannel::parse("nightly"), Some(UpdateChannel::Alpha));
// Launchers before 0.2: "Beta builds" on is Alpha, off is Stable.
assert_eq!(read(r#"{"beta_builds": true}"#), UpdateChannel::Alpha);
assert_eq!(read(r#"{"beta_builds": false}"#), UpdateChannel::Stable);
// Once a channel is picked, it wins over a switch left behind.
let both = r#"{"beta_builds": true, "update_channel": "stable"}"#;
@@ -2522,19 +2527,19 @@ mod tests {
// Saved as a name, without the old switch.
let mut settings = Settings::parse(r#"{"selected": "v0.2.0", "beta_builds": true}"#);
let saved = serde_json::to_value(&settings).unwrap();
assert_eq!(saved["update_channel"], "nightly");
assert_eq!(saved["update_channel"], "alpha");
assert!(saved.get("beta_builds").is_none());
assert_eq!(saved["selected"], "v0.2.0");
settings.update_channel = UpdateChannel::Stable;
let text = serde_json::to_string(&settings).unwrap();
assert_eq!(read(&text), UpdateChannel::Stable);
for c in [UpdateChannel::Stable, UpdateChannel::Nightly] {
for c in [UpdateChannel::Stable, UpdateChannel::Alpha] {
assert_eq!(UpdateChannel::parse(c.name()), Some(c));
}
// An open launcher looks again every 4 hours on Stable, every 30 minutes on Nightly.
// An open launcher looks again every 4 hours on Stable, every 30 minutes on Alpha.
let minutes = |c: UpdateChannel| c.check_every().as_secs() / 60;
assert_eq!(minutes(UpdateChannel::Stable), 240);
assert_eq!(minutes(UpdateChannel::Nightly), 30);
assert_eq!(minutes(UpdateChannel::Alpha), 30);
}
#[test]
@@ -2665,8 +2670,8 @@ mod tests {
let releases = parse_releases(&list, Some(asset), is_game_tag);
let installed = ["v2026.10.19.15".to_string(), "v2026.10.12".to_string()];
let offered = |on| horizon_update(&channel(&releases, on), &installed).map(|r| r.tag);
// A beta 0.2.0 reaches launchers on Nightly; Stable ones wait for its promotion.
assert_eq!(offered(UpdateChannel::Nightly), Some("v0.2.0".into()));
// A beta 0.2.0 reaches launchers on Alpha; Stable ones wait for its promotion.
assert_eq!(offered(UpdateChannel::Alpha), Some("v0.2.0".into()));
assert_eq!(offered(UpdateChannel::Stable), None);
let mut sorted = vec![
"v2026.10.19.15".to_string(),
+2 -2
View File
@@ -278,10 +278,10 @@ enum Cmd {
style: Option<String>,
},
/// Show or set which Horizon releases install by themselves: stable (a tested build about once
/// a day) or nightly (every release as soon as it is built; can break)
/// a day) or alpha (every release as soon as it is built; can break)
UpdateChannel {
/// Omit to show the current channel
#[arg(value_parser = ["stable", "nightly"])]
#[arg(value_parser = ["stable", "alpha"])]
channel: Option<String>,
},
/// Show or set the extra arguments the game starts with (for example --server-thread);
+3 -3
View File
@@ -1830,11 +1830,11 @@ impl Launcher {
cx.notify();
}),
);
use backend::UpdateChannel::{Nightly, Stable};
use backend::UpdateChannel::{Alpha, Stable};
let this = cx.entity().downgrade();
let update_channel = segments(
"update-channel",
&[(Stable, Stable.label()), (Nightly, Nightly.label())],
&[(Stable, Stable.label()), (Alpha, Alpha.label())],
self.settings.update_channel,
&t,
move |picked, _, cx| {
@@ -2011,7 +2011,7 @@ impl Launcher {
.flex_col()
.child("Stable: a tested build about once a day.")
.child(
"Nightly: every change as soon as it is built, several times a day; can break.",
"Alpha: every change as soon as it is built, several times a day; can break.",
)
.into_any_element(),
),
+3 -3
View File
@@ -51,8 +51,8 @@ fn tools() -> Value {
{ "name": "links", "description": "The Horizon links: the Discord invite, the GitHub Sponsors page and the website (an empty one is not offered).", "inputSchema": none },
{ "name": "menu_style", "description": "Show or set the game's menu style. \"horizon\" is Horizon's own menus, \"classic\" is vanilla's screens, which have no way back inside the game. The game reads it when it starts.",
"inputSchema": { "type": "object", "properties": { "style": { "type": "string", "enum": ["horizon", "classic"], "description": "Omit to show the current style." } } } },
{ "name": "update_channel", "description": "Show or set which Horizon releases install by themselves. \"stable\" (the default) is a tested build about once a day; \"nightly\" is every release as soon as it is built, several times a day, and can break. Only when the player asks to change it.",
"inputSchema": { "type": "object", "properties": { "channel": { "type": "string", "enum": ["stable", "nightly"], "description": "Omit to show the current channel." } } } },
{ "name": "update_channel", "description": "Show or set which Horizon releases install by themselves. \"stable\" (the default) is a tested build about once a day; \"alpha\" is every release as soon as it is built, several times a day, and can break. Only when the player asks to change it.",
"inputSchema": { "type": "object", "properties": { "channel": { "type": "string", "enum": ["stable", "alpha"], "description": "Omit to show the current channel." } } } },
{ "name": "logs", "description": "Read the game's log: the last lines of logs/latest.log in the game folder, or with crash the newest crash report (what the game wrote when it last crashed). Read-only.",
"inputSchema": { "type": "object", "properties": { "lines": { "type": "integer", "minimum": 1, "maximum": 5000, "description": "How many lines of latest.log (default 100)." }, "crash": { "type": "boolean", "description": "Return the newest crash report instead." } } } },
{ "name": "accounts", "description": "The Microsoft accounts signed in to the launcher (names and UUIDs, never tokens), which one the game plays as (none = offline), and how a sign-in started with sign_in is going.", "inputSchema": none },
@@ -291,7 +291,7 @@ impl Session {
let mut settings = backend::Settings::load();
if let Some(name) = arg_str(args, "channel") {
settings.update_channel = backend::UpdateChannel::parse(name)
.ok_or("channel is \"stable\" or \"nightly\"")?;
.ok_or("channel is \"stable\" or \"alpha\"")?;
settings.save();
}
Ok(format!(
+3 -3
View File
@@ -137,10 +137,10 @@ class ReleaseChannelTests(unittest.TestCase):
pr = {"number": 1, "title": "Fix a thing", "labels": [], "paths": ["crates/x.rs"], "issues": [], "author": "ludovic111"}
_, beta = train.notes("game", "v2026.10.19", [pr], [], beta=True)
_, stable = train.notes("game", "v2026.10.19", [pr], [], beta=False)
self.assertIn("This is a nightly build: launchers on the Nightly channel", beta)
self.assertNotIn("nightly", stable.lower())
self.assertIn("This is an alpha build: launchers on the Alpha channel", beta)
self.assertNotIn("alpha", stable.lower())
_, launcher = train.notes("launcher", "launcher-v0.9.0", [pr], [], beta=True)
self.assertNotIn("nightly", launcher.lower())
self.assertNotIn("alpha", launcher.lower())
if __name__ == "__main__":
+1 -1
View File
@@ -6,7 +6,7 @@
python3 tools/release_channels.py promote --tag v2026.10.19.03 --force # even if blocked or untested
python3 tools/release_channels.py promote --dry-run # say what would happen, change nothing
Every game release goes out as a beta (a GitHub pre-release): launchers on the Nightly channel install it at once.
Every game release goes out as a beta (a GitHub pre-release): launchers on the Alpha channel install it at once.
Stable moves once a day, at PROMOTE_AT (20:00 UTC). A beta is due for stable when:
- it is newer than the stable release (GitHub's "latest", which launchers and servers install by default),
- it had been out PROMOTE_AFTER (6 hours) at the day's promotion time, so every run until the next one picks the
+2 -2
View File
@@ -37,7 +37,7 @@ import sys
ROOT = Path(__file__).resolve().parents[1]
REPO = os.environ.get("GITHUB_REPOSITORY", "ludovic111/horizon")
# Nightly builds ship constantly: a merge starts a release within minutes, once its wave of merges has settled.
# Alpha builds ship constantly: a merge starts a release within minutes, once its wave of merges has settled.
DEBOUNCE = dt.timedelta(minutes=5)
MAX_WAIT = dt.timedelta(minutes=20)
# Kept as typed in release titles, where the clauses after the first are lowercased.
@@ -72,7 +72,7 @@ LAUNCHER_IGNORED = ["launcher/links.json", "launcher/docs/", "launcher/tests/"]
FOOTER = {
"game": "Install or update it from the Horizon launcher: it installs new versions by itself. "
"New here? Get the launcher at https://playhorizon.dev. You need your own copy of Minecraft: Java Edition.",
"beta": "This is a nightly build: launchers on the Nightly channel (Settings, Updates) install it now; Stable gets it "
"beta": "This is an alpha build: launchers on the Alpha channel (Settings, Updates) install it now; Stable gets it "
"in the next daily update if it holds up.",
"launcher": "Your launcher updates itself. New here? Download the archive for your system below, extract it, "
"and run `horizon-launcher`. Website: https://playhorizon.dev",
+1 -1
View File
@@ -60,7 +60,7 @@ Only if you say yes, and each kind has its own switch. Everything is off by defa
## How often does it update?
Often. Every change players can see is released automatically after it is merged, so new versions can come out several times a day. On the **Nightly** channel the launcher installs each one as soon as it is built; on **Stable**, the default, it installs a tested build about once a day. See [Install](/docs/install#updating).
Often. Every change players can see is released automatically after it is merged, so new versions can come out several times a day. On the **Alpha** channel the launcher installs each one as soon as it is built; on **Stable**, the default, it installs a tested build about once a day. See [Install](/docs/install#updating).
## How do I report a bug?
+1 -1
View File
@@ -7,7 +7,7 @@ Horizon is Minecraft: Java Edition 26.3 rewritten in Rust. You install it with a
- **A new game program, not a mod.** Horizon is written from scratch in Rust. It reads the same worlds, plays by the same rules and generates the same terrain as the Java game.
- **Checked against vanilla.** {{HARNESSES}} automated tests ("harnesses") feed the same inputs to Horizon and to the real game, then compare the results. Today every subsystem matches in every harness that compares it. That is {{PERCENT}} of what the harnesses compare, which is not proof that nothing else differs. The [status table](/#parity) lists what is covered.
- **Light and fast.** It starts quickly, uses little memory and draws efficiently. See [Performance](/docs/performance) for what is measured and what is not.
- **A few things vanilla doesn't have.** Its own menus (vanilla's are one setting away), graphics presets, distant terrain, controller support, optional extras such as zoom, a minimap and dynamic lights (dynamic lights are in Nightly builds for now), an invite code to play your world with friends, and a server you can run without Java.
- **A few things vanilla doesn't have.** Its own menus (vanilla's are one setting away), graphics presets, distant terrain, controller support, optional extras such as zoom, a minimap and dynamic lights (dynamic lights are in Alpha builds for now), an invite code to play your world with friends, and a server you can run without Java.
## What Horizon is not
+4 -4
View File
@@ -37,16 +37,16 @@ The game opens on Horizon's own title screen. Your worlds, options and logs live
- **Horizon.** The launcher looks for a new release when it starts and every four hours while it is open. With **Install new versions automatically** on (the default), it installs one and moves you to it if you were on the newest. New releases come out many times a day, and old versions stay installed until you remove them on the **Versions** page.
- **The launcher.** It updates itself from its own releases, then shows **Restart** in its footer.
## Stable and Nightly
## Stable and Alpha
> **Next launcher release.** The Updates switch below is merged but not in a launcher release yet. Launcher 0.17 has a **Beta builds** switch in Settings instead, which installs every new build at once, as Nightly does.
> **Next launcher release.** The Updates switch below is merged but not in a launcher release yet. Launcher 0.17 has a **Beta builds** switch in Settings instead, which installs every new build at once, as Alpha does.
In **Settings**, **Updates** picks which builds install by themselves:
- **Stable** (the default): a tested build about once a day.
- **Nightly**: every change as soon as it is built, several times a day. It can break. A launcher left open looks for new builds every 30 minutes, instead of every four hours.
- **Alpha**: every change as soon as it is built, several times a day. It can break. A launcher left open looks for new builds every 30 minutes, instead of every four hours.
Every new release is a Nightly build first. Once a day (20:00 UTC, 21:00 in Paris in winter), the newest one that has been out for six hours, passed its smoke test and has no known blocker becomes Stable. "Nightly" is just the name: it updates many times a day, not once a night. The **Versions** page shows each version's channel, and you can install any of them by hand.
Every new release is an Alpha build first. Once a day (20:00 UTC, 21:00 in Paris in winter), the newest one that has been out for six hours, passed its smoke test and has no known blocker becomes Stable. Alpha builds come out many times a day, and Horizon as a whole is still in beta; Stable is the tested ones. The **Versions** page shows each version's channel, and you can install any of them by hand.
## The Versions page
+1 -1
View File
@@ -20,7 +20,7 @@ There is no Horizon server in the middle. The code holds the host's addresses an
Guests join with the name their game uses. There is no account check, so only share the code with people you trust.
In Nightly builds, guests can also use the host's workstations: the crafter, loom, stonecutter, grindstone, anvil, smithing table, enchanting table, beacon, lectern and cartography table. Stable gets this once it has passed.
In Alpha builds, guests can also use the host's workstations: the crafter, loom, stonecutter, grindstone, anvil, smithing table, enchanting table, beacon, lectern and cartography table. Stable gets this once it has passed.
## Open to LAN
+2 -2
View File
@@ -48,7 +48,7 @@ Shader packs are **experimental**. Put a pack's folder in the `shaderpacks` fold
Horizon can run the full-screen effects of Java shader packs (OptiFine and Iris packs: colour grading, bloom, sky and fog effects). It does not run their geometry and shadow passes yet, so packs do not look as they do in Iris. Horizon also has its own shader pack format, in WGSL, with a `pack.toml` manifest, options and hot reload. It has full-screen passes too; stages that draw the world's terrain and shadows are planned. The details are in the [shader pack notes](https://github.com/ludovic111/horizon/blob/main/docs/shader-packs.md).
In Nightly builds, the **Shader Packs** page shows the full path of the `shaderpacks` folder with an **Open Folder** button, then one switch per pack. Turning a pack on picks it, and turning it off gives the vanilla picture. Packs tested so far: BSL, Complementary Reimagined, Steadfast and Sildur's Vibrant Extreme draw their full-screen passes; Bliss, Bloop, Noble, Photon and Rethinking Voxels do not load yet. To find packs, the launcher's **Mods** page installs shader packs from Modrinth (see [Mods and data packs](/docs/mods#finding-resource-packs-shader-packs-and-modpacks)).
In Alpha builds, the **Shader Packs** page shows the full path of the `shaderpacks` folder with an **Open Folder** button, then one switch per pack. Turning a pack on picks it, and turning it off gives the vanilla picture. Packs tested so far: BSL, Complementary Reimagined, Steadfast and Sildur's Vibrant Extreme draw their full-screen passes; Bliss, Bloop, Noble, Photon and Rethinking Voxels do not load yet. To find packs, the launcher's **Mods** page installs shader packs from Modrinth (see [Mods and data packs](/docs/mods#finding-resource-packs-shader-packs-and-modpacks)).
## Resource packs
@@ -69,7 +69,7 @@ Horizon plays with PlayStation (DualShock 4 and DualSense), Xbox (360, One and S
## Extras
**Options, Extras** holds small, optional conveniences. Every one is off by default, so a fresh install behaves like vanilla: fullbright, zoom, a minimap, an FPS counter, a keystroke display, auto-reconnect, Mouse Tweaks-style inventory controls and inventory sorting. In Nightly builds there is one more, **dynamic lights**: a torch, lantern or other light source that you hold, that lies dropped as an item, or a burning mob, lights the blocks around it. It is client-side only, so other players do not see it.
**Options, Extras** holds small, optional conveniences. Every one is off by default, so a fresh install behaves like vanilla: fullbright, zoom, a minimap, an FPS counter, a keystroke display, auto-reconnect, Mouse Tweaks-style inventory controls and inventory sorting. In Alpha builds there is one more, **dynamic lights**: a torch, lantern or other light source that you hold, that lies dropped as an item, or a burning mob, lights the blocks around it. It is client-side only, so other players do not see it.
## Where your settings are stored