mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-07 11:06:11 -10:00
Launcher: the Nightly is the only game build, for everyone
Step 3 of Nightly only (Ludovic, 2026-10-28). Every launcher installs and updates only the Nightly, with no setting: - The update check reads the `nightly` pre-release from the release list it already fetches and offers it as the update, installed as `nightly-<date>-<commit>` after its checksums signed for the Nightly channel check. Nightlies rank after every release (`ReleaseOrder::Nightly`); a second build of the same day is an update too. - A player on the newest release, on a Nightly or on nothing moves to the new Nightly at the next check; a release picked by hand and instances pinned to a version stay. Worlds live in the game's folder, so nothing touches them. Older Nightlies go once nothing plays them. - The "Nightly builds" setting, its separate install path and its `nightly` key are gone; the old "Nightly" instance stays and keeps following the Nightly. - Versions lists the current Nightly, the last week's and the releases from before; the update line names a Nightly by its title. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
a1e56a4d1b
commit
a1bdb0d22d
7 files changed
+257
-272
No files matched your search
+17
-11
@@ -12,8 +12,9 @@ The opening, then Play ([clip](docs/opening.webm)):
|
||||
|
||||
## What it does
|
||||
|
||||
- **Versions**: lists the published releases of horizon-rust/horizon, installs the build for
|
||||
this platform into its own folder, and removes it again.
|
||||
- **Versions**: lists the published builds of horizon-rust/horizon (the current Nightly, the last
|
||||
week's, then the releases from before), installs the build for this platform into its own
|
||||
folder, and removes it again.
|
||||
- **Play**: prepares resources as `PLAY.txt` describes (your launcher's 26.3 install, the
|
||||
26.3 server jar from that version's own json, and Java 25) and copies its sounds from your
|
||||
launcher's asset store (`horizon-prepare sounds`; a launcher install without them only
|
||||
@@ -212,11 +213,16 @@ page `servers.json` (the list), `servers/<name>/` (the servers created there) an
|
||||
when the game starts and brings it back to the front when the game closes; `settings
|
||||
while-playing stay|minimize|hide`), the game's extra command-line arguments, an HTTP proxy for
|
||||
downloads, and the launcher's own version and update state.
|
||||
- **Updates**: there is one channel. Every release is published as the release, GitHub's latest,
|
||||
once its smoke test passed (docs/release-flow.md), and the launcher installs it. The game
|
||||
pre-releases of the old Alpha channel (v0.2.1 on) stay on the Versions page to install
|
||||
by hand, but are never installed as an update. The `update_channel` and `beta_builds` keys
|
||||
older launchers wrote in `settings.json` are ignored and dropped on the next save.
|
||||
- **Updates**: Horizon ships only the Nightly (since 2026-10-28, docs/release-flow.md). The
|
||||
launcher installs the current one (the `nightly` pre-release, checksums signed for the Nightly
|
||||
channel) as `nightly-<date>-<commit>` and selects it when the player was on a Nightly, on the
|
||||
newest release or on nothing; a version picked by hand or pinned by an instance stays. Older
|
||||
Nightlies are removed once nothing plays them. Worlds are never touched: they live in the game's
|
||||
folder, not in a version's. The last week's Nightlies (`nightly-<date>`), the `vX.Y.Z`
|
||||
releases and the old Alpha pre-releases stay on the Versions page to install by hand, never as
|
||||
an update. The "Nightly builds" setting and its `nightly` key are gone (an old key is ignored);
|
||||
its "Nightly" instance stays and keeps following the Nightly. The `update_channel` and
|
||||
`beta_builds` keys older launchers wrote are ignored and dropped on the next save.
|
||||
- **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
|
||||
launcher's own arguments, kept as `game_args` in `settings.json`, and apply the next time the
|
||||
@@ -244,11 +250,11 @@ minimize, maximize/restore and close; macOS and Linux keep their system title ba
|
||||
|
||||
## Updates
|
||||
|
||||
The launcher looks for a newer Horizon release when it starts and every 30 minutes while it
|
||||
is open, and shows a small "available, Install" line on Play.
|
||||
The launcher looks for a newer Nightly when it starts and every 30 minutes while it is open, and
|
||||
shows a small "available, Install" line on Play.
|
||||
|
||||
It also updates itself from GitHub releases tagged `[email protected]` (Horizon's own are `vX.Y.Z`;
|
||||
neither is ever taken for the other). Before 0.2 they were `launcher-vX.Y.Z` and `vYYYY.MM.DD`:
|
||||
It also updates itself from GitHub releases tagged `[email protected]` (the game's are `nightly`, and
|
||||
`vX.Y.Z` before 2026-10-28; neither is ever taken for the other). Before 0.2 they were `launcher-vX.Y.Z` and `vYYYY.MM.DD`:
|
||||
every 0.2 tag counts as newer than all of those (docs/versioning.md). It downloads the new build, replaces its
|
||||
own binary, and offers "Restart" in the footer. Development builds and
|
||||
`HORIZON_LAUNCHER_NO_SELF_UPDATE=1` never replace themselves.
|
||||
|
||||
@@ -157,13 +157,6 @@
|
||||
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Pas préparé. Horizon a besoin des fichiers de votre propre Minecraft {version}.",
|
||||
"Prepared for Minecraft {version}": "Préparé pour Minecraft {version}",
|
||||
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Préparé pour Minecraft {version}, sans ses sons : le jeu sera muet. Préparez à nouveau pour les copier depuis votre installation de Minecraft.",
|
||||
"Nightly builds": "Versions Nightly",
|
||||
"Can include pull requests not reviewed yet, picked by trusted reviewers; may break.": "Peut inclure des pull requests pas encore relues, choisies par des relecteurs de confiance ; peut casser.",
|
||||
"Plays in its own Nightly instance: your worlds and the release stay as they are.": "Se joue dans sa propre instance Nightly : vos mondes et la version publiée restent tels quels.",
|
||||
"Installing the nightly, {percent}%": "Installation de la Nightly, {percent} %",
|
||||
"The nightly could not be installed: {error}": "La Nightly n'a pas pu être installée : {error}",
|
||||
"{version} in the Nightly instance. Choose Horizon in the sidebar to play the release.": "{version} dans l'instance Nightly. Choisissez Horizon dans la barre latérale pour jouer la version publiée.",
|
||||
"No nightly build yet.": "Pas encore de version Nightly.",
|
||||
"This build of the launcher has no Microsoft sign-in (HORIZON_MSA_CLIENT_ID), so it can't start the game.": "Cette version du launcher n'a pas de connexion Microsoft (HORIZON_MSA_CLIENT_ID) : elle ne peut pas lancer le jeu.",
|
||||
"Sign in with a Microsoft account that owns Minecraft: Java Edition to play": "Connectez-vous avec un compte Microsoft qui possède Minecraft: Java Edition pour jouer",
|
||||
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers. It also plays online servers with your name and skin.": "Horizon a besoin d'un compte Microsoft qui possède Minecraft: Java Edition, comme les autres launchers. Il permet aussi de jouer sur les serveurs en ligne avec votre nom et votre skin.",
|
||||
|
||||
@@ -157,13 +157,6 @@
|
||||
"Not prepared. Horizon needs the files of your own Minecraft {version}.": "Не подготовлено. Horizon нужны файлы вашего собственного Minecraft {version}.",
|
||||
"Prepared for Minecraft {version}": "Подготовлено для Minecraft {version}",
|
||||
"Prepared for Minecraft {version}, without its sounds: the game will be silent. Prepare again to copy them from your Minecraft install.": "Подготовлено для Minecraft {version}, но без звуков: игра будет беззвучной. Подготовьте заново, чтобы скопировать их из вашей установки Minecraft.",
|
||||
"Nightly builds": "Ночные сборки",
|
||||
"Can include pull requests not reviewed yet, picked by trusted reviewers; may break.": "Может включать ещё не проверенные pull request'ы, выбранные доверенными ревьюерами; что-то может сломаться.",
|
||||
"Plays in its own Nightly instance: your worlds and the release stay as they are.": "Запускается в отдельной сборке Nightly: ваши миры и релиз остаются как есть.",
|
||||
"Installing the nightly, {percent}%": "Установка ночной сборки, {percent}%",
|
||||
"The nightly could not be installed: {error}": "Не удалось установить ночную сборку: {error}",
|
||||
"{version} in the Nightly instance. Choose Horizon in the sidebar to play the release.": "{version} в сборке Nightly. Выберите Horizon на боковой панели, чтобы играть в релиз.",
|
||||
"No nightly build yet.": "Ночных сборок пока нет.",
|
||||
"This build of the launcher has no Microsoft sign-in (HORIZON_MSA_CLIENT_ID), so it can't start the game.": "В этой сборке лаунчера нет входа через Microsoft (HORIZON_MSA_CLIENT_ID), поэтому она не может запустить игру.",
|
||||
"Sign in with a Microsoft account that owns Minecraft: Java Edition to play": "Чтобы играть, войдите в аккаунт Microsoft, у которого есть Minecraft: Java Edition",
|
||||
"Horizon needs a Microsoft account that owns Minecraft: Java Edition, like other launchers. It also plays online servers with your name and skin.": "Как и другим лаунчерам, Horizon нужен аккаунт Microsoft, у которого есть Minecraft: Java Edition. С ним вы также играете на онлайн-серверах под своим именем и со своим скином.",
|
||||
|
||||
+149
-26
@@ -37,9 +37,13 @@ pub struct Release {
|
||||
pub body: String,
|
||||
pub date: String,
|
||||
pub asset: Option<(String, u64)>,
|
||||
/// A GitHub pre-release: the Alpha builds from before every release went out as the release
|
||||
/// (v0.2.1 on, docs/release-flow.md). Listed and installed by hand, never as an update.
|
||||
/// Listed and installed by hand, never as an update: a GitHub pre-release of the old Alpha
|
||||
/// channel (v0.2.1 on), or a Nightly older than the current one.
|
||||
pub prerelease: bool,
|
||||
/// A Nightly's GitHub tag (`nightly`, or `nightly-<date>` for the last week's): its files and
|
||||
/// signature are there, while `tag` is the version it installs as (`nightly-2026-10-28-0c02067`).
|
||||
/// None for a release.
|
||||
pub nightly: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
|
||||
@@ -154,6 +158,9 @@ pub enum ReleaseOrder {
|
||||
/// Numbers without trailing zeros, so 1.2 and 1.2.0 are equal.
|
||||
Before02(Vec<u64>),
|
||||
Semver([u64; 3], Prerelease),
|
||||
/// A Nightly (`nightly-<date>-<commit>`), by its date: newer than every release, since the
|
||||
/// Nightly is the only build Horizon ships.
|
||||
Nightly(String),
|
||||
}
|
||||
|
||||
/// (is a release, the pre-release's identifiers): a release follows every pre-release of its
|
||||
@@ -170,6 +177,9 @@ pub enum Identifier {
|
||||
const FIRST_YEAR: u64 = 1000;
|
||||
|
||||
pub fn release_order(tag: &str) -> ReleaseOrder {
|
||||
if let Some(date) = crate::nightly::date_of(tag) {
|
||||
return ReleaseOrder::Nightly(date.to_owned());
|
||||
}
|
||||
let semver = tag
|
||||
.strip_prefix(LAUNCHER_TAG_PREFIX)
|
||||
.or_else(|| tag.strip_prefix('v'))
|
||||
@@ -270,6 +280,7 @@ pub(crate) fn parse_releases(
|
||||
.collect(),
|
||||
asset: found,
|
||||
prerelease: r["prerelease"].as_bool().unwrap_or(false),
|
||||
nightly: None,
|
||||
});
|
||||
}
|
||||
out.sort_by(|a, b| compare_versions(&b.tag, &a.tag));
|
||||
@@ -280,10 +291,30 @@ fn is_launcher_tag(tag: &str) -> bool {
|
||||
launcher_version(tag).is_some()
|
||||
}
|
||||
|
||||
/// Published Horizon releases, newest first. Drafts never reach the anonymous API, and launcher
|
||||
/// releases are left out.
|
||||
/// Published Horizon builds, newest first: the current Nightly (the only build Horizon ships since
|
||||
/// 2026-10-28, docs/release-flow.md), the last week's Nightlies, then the releases from before.
|
||||
/// Drafts never reach the anonymous API, and launcher releases are left out.
|
||||
pub fn releases() -> Result<Vec<Release>, String> {
|
||||
Ok(parse_releases(&release_list()?, ASSET, is_game_tag))
|
||||
Ok(game_releases(&release_list()?))
|
||||
}
|
||||
|
||||
/// The game's builds in `list`, newest first. A Nightly is listed under the version it installs as
|
||||
/// ([`crate::nightly::version_of`]); only the current one (`nightly`) is an update.
|
||||
pub(crate) fn game_releases(list: &serde_json::Value) -> Vec<Release> {
|
||||
let mut out = parse_releases(list, ASSET, |tag| {
|
||||
is_game_tag(tag) || horizon_release_sign::is_nightly(tag)
|
||||
});
|
||||
for r in &mut out {
|
||||
if horizon_release_sign::is_nightly(&r.tag) {
|
||||
let version = crate::nightly::version_of(r);
|
||||
let tag = std::mem::replace(&mut r.tag, version);
|
||||
r.prerelease = tag != horizon_release_sign::NIGHTLY;
|
||||
r.nightly = Some(tag);
|
||||
}
|
||||
}
|
||||
// Two Nightlies of one day: the current one first (the dated one is the day's earlier build).
|
||||
out.sort_by(|a, b| compare_versions(&b.tag, &a.tag).then(a.prerelease.cmp(&b.prerelease)));
|
||||
out
|
||||
}
|
||||
|
||||
/// The launcher releases in `list` (the newest 100 releases). Game releases come out several times
|
||||
@@ -556,10 +587,17 @@ fn unpack(archive: &Path, url: &str, to: &Path) -> Result<(), String> {
|
||||
/// the fraction done.
|
||||
pub fn install(release: &Release, progress: impl Fn(f32)) -> Result<(), String> {
|
||||
let tag = release.tag.as_str();
|
||||
if !is_game_tag(tag) {
|
||||
return Err(format!("{tag} is not a Horizon release."));
|
||||
}
|
||||
let sha256 = signed_digest(tag, ASSET.unwrap_or_default())?;
|
||||
let sha256 = match &release.nightly {
|
||||
// Signed for the Nightly channel, served under its own GitHub tag.
|
||||
Some(published) => crate::signed::nightly_digest(
|
||||
published,
|
||||
ASSET.unwrap_or_default(),
|
||||
&horizon_release_sign::trusted_keys(),
|
||||
fetch_small,
|
||||
)?,
|
||||
None if is_game_tag(tag) => signed_digest(tag, ASSET.unwrap_or_default())?,
|
||||
None => return Err(format!("{tag} is not a Horizon release.")),
|
||||
};
|
||||
install_as(release, tag, &sha256, progress)
|
||||
}
|
||||
|
||||
@@ -2053,12 +2091,15 @@ pub struct Updates {
|
||||
pub launcher: Option<Release>,
|
||||
}
|
||||
|
||||
/// The newest Horizon release with a build here, if it is newer than every installed version.
|
||||
/// The newest Horizon build with a build here (the current Nightly), if it isn't installed and
|
||||
/// no installed version is newer. Two Nightlies of one day compare equal: the one on GitHub is
|
||||
/// the update.
|
||||
pub fn horizon_update(releases: &[Release], installed: &[String]) -> Option<Release> {
|
||||
let newest = latest(releases)?;
|
||||
let newer = installed
|
||||
.iter()
|
||||
.all(|t| compare_versions(&newest.tag, t).is_gt());
|
||||
let newer = !installed.contains(&newest.tag)
|
||||
&& installed
|
||||
.iter()
|
||||
.all(|t| compare_versions(&newest.tag, t).is_ge());
|
||||
newer.then(|| newest.clone())
|
||||
}
|
||||
|
||||
@@ -2098,7 +2139,7 @@ fn releases_and_updates_with(
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
let releases = parse_releases(&list, ASSET, is_game_tag);
|
||||
let releases = game_releases(&list);
|
||||
let horizon = horizon_update(&installable_updates(&releases), &installed());
|
||||
on_releases(Ok(releases));
|
||||
Ok(Updates {
|
||||
@@ -2112,12 +2153,18 @@ pub fn check_updates() -> Result<Updates, String> {
|
||||
releases_and_updates(|_| {})
|
||||
}
|
||||
|
||||
/// Whether a version installed as an update becomes the selected one: only when the player was
|
||||
/// on the newest installed version before (or had none).
|
||||
/// Whether a version installed as an update becomes the selected one: when the player had none,
|
||||
/// was on a Nightly, or on the newest installed release (so a player of the release channel moves
|
||||
/// to the Nightly at the first check, docs/release-flow.md). An older release picked by hand
|
||||
/// stays; an instance pinned to a version keeps it either way.
|
||||
pub fn select_after_update(selected: Option<&str>, installed_before: &[String]) -> bool {
|
||||
match selected {
|
||||
None => true,
|
||||
Some(s) => installed_before.first().is_none_or(|newest| newest == s),
|
||||
Some(s) if crate::nightly::date_of(s).is_some() => true,
|
||||
Some(s) => installed_before
|
||||
.iter()
|
||||
.find(|t| is_game_tag(t))
|
||||
.is_none_or(|newest| newest == s),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2146,6 +2193,9 @@ pub fn install_tag(tag: &str, as_update: bool, progress: impl Fn(f32)) -> Result
|
||||
settings.selected = Some(release.tag.clone());
|
||||
settings.save();
|
||||
}
|
||||
if as_update && release.nightly.is_some() {
|
||||
crate::nightly::after_update(&release.tag);
|
||||
}
|
||||
Ok(release.tag.clone())
|
||||
}
|
||||
|
||||
@@ -2473,7 +2523,8 @@ pub enum Msg {
|
||||
Removed(String, Result<(), String>),
|
||||
Line(Job, String),
|
||||
Exit(Job, Result<(), String>),
|
||||
Updates(Result<Updates, String>),
|
||||
/// Boxed: two releases, the largest message by far.
|
||||
Updates(Result<Box<Updates>, String>),
|
||||
LauncherProgress(f32),
|
||||
LauncherUpdated(Result<(), String>),
|
||||
/// `reuse_prepared_everywhere` prepared these versions.
|
||||
@@ -2491,10 +2542,6 @@ pub enum Msg {
|
||||
PackDone(Result<Box<crate::modpacks::Imported>, String>),
|
||||
/// How many play Horizon now, or none when the server can't be reached (`telemetry`).
|
||||
Playing(Option<u64>),
|
||||
/// The nightly being installed, done this far (`nightly`).
|
||||
NightlyProgress(f32),
|
||||
/// A nightly check ended: the version installed (None: already up to date), or why not.
|
||||
Nightly(Result<Option<String>, String>),
|
||||
/// Where the native-mods build is (`native_mods::Step::label`).
|
||||
NativeProgress(String),
|
||||
/// The native-mods build ended: the version built, or why not.
|
||||
@@ -2681,10 +2728,6 @@ pub struct Settings {
|
||||
/// The window's language. Unset follows the system's (`i18n::Language::system`).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub language: Option<crate::i18n::Language>,
|
||||
/// The player turned Nightly on (`nightly`): the nightly build installs and updates in its own
|
||||
/// instance. Off by default.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
pub nightly: bool,
|
||||
/// The player turned Native mods (experimental) on (`native_mods`): mixin mods compiled into
|
||||
/// a game of their own. Off by default.
|
||||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||||
@@ -3148,6 +3191,7 @@ mod tests {
|
||||
date: String::new(),
|
||||
asset: Some(("u".into(), 1)),
|
||||
prerelease: false,
|
||||
nightly: None,
|
||||
};
|
||||
assert!(launcher_update(&[r(own_tag())]).is_none());
|
||||
assert_eq!(launcher_version(&own_tag()), Some(VERSION));
|
||||
@@ -3188,6 +3232,7 @@ mod tests {
|
||||
date: String::new(),
|
||||
asset: Some(("u".into(), 1)),
|
||||
prerelease: false,
|
||||
nightly: None,
|
||||
};
|
||||
let mut list = vec![
|
||||
r("launcher-v0.16.0"),
|
||||
@@ -3354,6 +3399,7 @@ mod tests {
|
||||
date: String::new(),
|
||||
asset: Some(("u".into(), 1)),
|
||||
prerelease: false,
|
||||
nightly: None,
|
||||
};
|
||||
let releases = [r("v2026.10.13"), r("v2026.10.12")];
|
||||
assert_eq!(
|
||||
@@ -3370,6 +3416,83 @@ mod tests {
|
||||
assert!(select_after_update(None, &installed));
|
||||
}
|
||||
|
||||
/// The Nightly is the only build Horizon ships (docs/release-flow.md): every launcher takes the
|
||||
/// current one as its update, over every release; the last week's are listed, never offered.
|
||||
#[test]
|
||||
fn every_launcher_updates_to_the_current_nightly() {
|
||||
let asset = ASSET.unwrap_or("horizon-linux-x86_64.tar.gz");
|
||||
let build = |url: &str| serde_json::json!([{"name": asset, "browser_download_url": url, "size": 1}]);
|
||||
let list = serde_json::json!([
|
||||
{"tag_name": "nightly", "name": "Nightly 2026-10-28 · 9b772b2", "prerelease": true,
|
||||
"published_at": "2026-10-28T09:58:01Z", "assets": build("n")},
|
||||
{"tag_name": "[email protected]", "assets": []},
|
||||
{"tag_name": "v0.2.95", "assets": build("r")},
|
||||
{"tag_name": "nightly-2026-10-28", "name": "Nightly 2026-10-28 · 83f15fa", "prerelease": true,
|
||||
"published_at": "2026-10-28T07:54:57Z", "assets": build("d")},
|
||||
{"tag_name": "nightly-2026-10-27", "name": "Nightly 2026-10-27 · 7954902", "prerelease": true,
|
||||
"published_at": "2026-10-28T00:15:40Z", "assets": build("e")},
|
||||
]);
|
||||
let releases = game_releases(&list);
|
||||
let listed: Vec<_> = releases
|
||||
.iter()
|
||||
.map(|r| (r.tag.as_str(), r.nightly.as_deref(), r.prerelease))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
listed,
|
||||
[
|
||||
("nightly-2026-10-28-9b772b2", Some("nightly"), false),
|
||||
(
|
||||
"nightly-2026-10-28-83f15fa",
|
||||
Some("nightly-2026-10-28"),
|
||||
true
|
||||
),
|
||||
(
|
||||
"nightly-2026-10-27-7954902",
|
||||
Some("nightly-2026-10-27"),
|
||||
true
|
||||
),
|
||||
("v0.2.95", None, false),
|
||||
]
|
||||
);
|
||||
let offered = |installed: &[&str]| {
|
||||
let installed: Vec<String> = installed.iter().map(|t| t.to_string()).collect();
|
||||
horizon_update(&installable_updates(&releases), &installed).map(|r| r.tag)
|
||||
};
|
||||
let current = Some("nightly-2026-10-28-9b772b2".to_string());
|
||||
// A player of the release channel, a new player, one on an older Nightly of the same day.
|
||||
assert_eq!(offered(&["v0.2.95", "v0.2.94"]), current);
|
||||
assert_eq!(offered(&[]), current);
|
||||
assert_eq!(offered(&["nightly-2026-10-28-83f15fa", "v0.2.95"]), current);
|
||||
assert_eq!(offered(&["nightly-2026-10-28-9b772b2"]), None);
|
||||
// Nightlies come after every release in every list.
|
||||
assert!(compare_versions("nightly-2026-10-27-7954902", "v0.2.95").is_gt());
|
||||
assert!(
|
||||
compare_versions("nightly-2026-10-28-9b772b2", "nightly-2026-10-27-7954902").is_gt()
|
||||
);
|
||||
}
|
||||
|
||||
/// Players move to the Nightly at their next check: whoever was on the newest release, on a
|
||||
/// Nightly, or on nothing. A release picked by hand stays picked.
|
||||
#[test]
|
||||
fn players_of_the_newest_release_move_to_the_nightly() {
|
||||
let installed = [
|
||||
"nightly-2026-10-27-7954902".to_string(),
|
||||
"v0.2.95".into(),
|
||||
"v0.2.90".into(),
|
||||
];
|
||||
assert!(select_after_update(Some("v0.2.95"), &installed));
|
||||
assert!(select_after_update(
|
||||
Some("nightly-2026-10-27-7954902"),
|
||||
&installed
|
||||
));
|
||||
assert!(select_after_update(None, &installed));
|
||||
assert!(!select_after_update(Some("v0.2.90"), &installed));
|
||||
assert!(select_after_update(
|
||||
Some("v0.2.95"),
|
||||
&["v0.2.95".to_string()]
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_versions_install_automatically_unless_turned_off() {
|
||||
// No settings file, an old one that never mentioned it, and 0.1.1's default `false`.
|
||||
|
||||
+14
-126
@@ -54,7 +54,6 @@ use horizon_launcher::backend::{self, Job, Msg, Release, Running, Settings, Upda
|
||||
use horizon_launcher::i18n::{self, tr, trf};
|
||||
use horizon_launcher::instances;
|
||||
use horizon_launcher::native_mods;
|
||||
use horizon_launcher::nightly;
|
||||
use horizon_launcher::telemetry;
|
||||
use scrollbar::Scrollbar;
|
||||
use std::borrow::Cow;
|
||||
@@ -171,12 +170,6 @@ struct Launcher {
|
||||
/// nothing can launch, select or prepare it, and its row can't install or remove it again.
|
||||
removing: Option<String>,
|
||||
install_error: Option<(String, String)>,
|
||||
/// Nightly (`horizon_launcher::nightly`): how far the nightly being installed is, and why the
|
||||
/// last check or install failed.
|
||||
nightly_progress: Option<f32>,
|
||||
nightly_error: Option<String>,
|
||||
/// Play the Nightly instance once its first install ends: the player just turned Nightly on.
|
||||
nightly_select: bool,
|
||||
/// Native mods (`native_mods`): the build's step while it runs, why it failed, and the
|
||||
/// warning before a build while it is open, with the mods it names.
|
||||
native_step: Option<String>,
|
||||
@@ -392,9 +385,6 @@ impl Launcher {
|
||||
installing: None,
|
||||
removing: None,
|
||||
install_error: None,
|
||||
nightly_progress: None,
|
||||
nightly_error: None,
|
||||
nightly_select: false,
|
||||
native_step: None,
|
||||
native_error: None,
|
||||
native_confirm: None,
|
||||
@@ -522,7 +512,7 @@ impl Launcher {
|
||||
self.checked = Some(Instant::now());
|
||||
let tx = self.tx.clone();
|
||||
std::thread::spawn(move || {
|
||||
let _ = tx.send(Msg::Updates(backend::check_updates()));
|
||||
let _ = tx.send(Msg::Updates(backend::check_updates().map(Box::new)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -565,30 +555,6 @@ impl Launcher {
|
||||
self.install(&release);
|
||||
}
|
||||
|
||||
/// With Nightly on, installs the current nightly in the Nightly instance if it is new.
|
||||
fn update_nightly(&mut self) {
|
||||
if !self.settings.nightly || self.nightly_progress.is_some() {
|
||||
return;
|
||||
}
|
||||
self.nightly_progress = Some(0.);
|
||||
self.nightly_error = None;
|
||||
let (tx, installed) = (self.tx.clone(), self.installed.clone());
|
||||
std::thread::spawn(move || {
|
||||
let result = nightly::latest().and_then(|latest| {
|
||||
let Some(release) = nightly::update(latest, &installed) else {
|
||||
return Ok(None);
|
||||
};
|
||||
let progress = |p| {
|
||||
let _ = tx.send(Msg::NightlyProgress(p));
|
||||
};
|
||||
nightly::install(&release, progress).map(|_| Some(nightly::version_of(&release)))
|
||||
});
|
||||
let _ = tx.send(Msg::Nightly(result));
|
||||
});
|
||||
}
|
||||
|
||||
/// Turns Nightly on (install it, then play its instance) or off (play the release again; the
|
||||
/// Nightly instance and its worlds stay).
|
||||
/// The native mods turned on in the instance Play starts, unpacked and checked, with their
|
||||
/// bundles: what the warning names and the build compiles.
|
||||
fn native_mods_to_build(
|
||||
@@ -639,17 +605,6 @@ impl Launcher {
|
||||
});
|
||||
}
|
||||
|
||||
fn set_nightly(&mut self, on: bool) {
|
||||
self.settings.nightly = on;
|
||||
if on {
|
||||
self.nightly_select = true;
|
||||
} else if self.settings.instance.as_deref() == Some(nightly::INSTANCE) {
|
||||
self.settings.instance = None;
|
||||
}
|
||||
self.settings.save();
|
||||
self.update_nightly();
|
||||
}
|
||||
|
||||
fn fetch_releases(&mut self) {
|
||||
self.releases = Load::Loading;
|
||||
let tx = self.tx.clone();
|
||||
@@ -665,7 +620,7 @@ impl Launcher {
|
||||
let updates = backend::releases_and_updates(|releases| {
|
||||
let _ = tx.send(Msg::Releases(releases));
|
||||
});
|
||||
let _ = tx.send(Msg::Updates(updates));
|
||||
let _ = tx.send(Msg::Updates(updates.map(Box::new)));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -826,17 +781,15 @@ impl Launcher {
|
||||
}
|
||||
Msg::Updates(Ok(updates)) => {
|
||||
self.update_error = None;
|
||||
self.updates = updates;
|
||||
self.updates = *updates;
|
||||
if self.settings.auto_install() {
|
||||
self.install_update();
|
||||
}
|
||||
if let Some(release) = self.updates.launcher.clone() {
|
||||
self.update_launcher(release);
|
||||
}
|
||||
self.update_nightly();
|
||||
}
|
||||
Msg::Updates(Err(e)) => self.update_error = Some(e),
|
||||
Msg::NightlyProgress(p) => self.nightly_progress = Some(p),
|
||||
Msg::NativeProgress(step) => self.native_step = Some(step),
|
||||
Msg::Native(result) => {
|
||||
self.native_step = None;
|
||||
@@ -846,28 +799,6 @@ impl Launcher {
|
||||
self.native_error = Some(e);
|
||||
}
|
||||
}
|
||||
Msg::Nightly(result) => {
|
||||
self.nightly_progress = None;
|
||||
self.installed = backend::installed();
|
||||
self.reload_instances();
|
||||
match result {
|
||||
Ok(_)
|
||||
if self.nightly_select
|
||||
&& self.settings.nightly
|
||||
&& self
|
||||
.instances
|
||||
.list
|
||||
.iter()
|
||||
.any(|i| i.id == nightly::INSTANCE) =>
|
||||
{
|
||||
self.nightly_select = false;
|
||||
self.settings.instance = Some(nightly::INSTANCE.into());
|
||||
self.settings.save();
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(e) => self.nightly_error = Some(e),
|
||||
}
|
||||
}
|
||||
Msg::LauncherProgress(p) => {
|
||||
if let SelfUpdate::Downloading(_, f) = &mut self.self_update {
|
||||
*f = p;
|
||||
@@ -1567,6 +1498,15 @@ fn page_header(title: &str, description: &str, t: &Theme) -> Div {
|
||||
/// One row of a list, as its own panel: a name in the pixel face with an optional detail under
|
||||
/// it, then a value or a control on the right. Rows were numbered in the quiet look; `_n` keeps
|
||||
/// the call sites as they are.
|
||||
/// How an available update is named: a Nightly by its title ("Nightly 2026-10-28 · 0c02067"), a
|
||||
/// release by its tag.
|
||||
fn update_name(release: &Release) -> &str {
|
||||
match release.nightly {
|
||||
Some(_) => &release.name,
|
||||
None => &release.tag,
|
||||
}
|
||||
}
|
||||
|
||||
fn index_row(
|
||||
_n: usize,
|
||||
name: impl IntoElement,
|
||||
@@ -2782,7 +2722,7 @@ impl Launcher {
|
||||
.text_size(px(theme::CAPTION.px))
|
||||
.text_color(t.muted)
|
||||
.child(div().size(px(6.)).rounded_full().bg(t.accent))
|
||||
.child(format!("{} available", release.tag));
|
||||
.child(format!("{} available", update_name(release)));
|
||||
hero = hero.child(match installing {
|
||||
Some((_, p)) => line
|
||||
.child(
|
||||
@@ -2883,7 +2823,7 @@ impl Launcher {
|
||||
}),
|
||||
);
|
||||
let horizon = match (&self.updates.horizon, &self.update_error) {
|
||||
(Some(r), _) => trf("{version} available", &[("version", &r.tag)]),
|
||||
(Some(r), _) => trf("{version} available", &[("version", update_name(r))]),
|
||||
(None, Some(e)) => e.clone(),
|
||||
(None, None) if self.checked.is_some() => tr("Up to date").to_string(),
|
||||
_ => tr("Checking…").to_string(),
|
||||
@@ -3011,7 +2951,6 @@ impl Launcher {
|
||||
Some(toggle.into_any_element()),
|
||||
&t,
|
||||
))
|
||||
.child(self.nightly_row(n + 2, cx))
|
||||
.child(group("Launcher", "monitor"))
|
||||
.child(index_row(
|
||||
n + 3,
|
||||
@@ -3091,57 +3030,6 @@ impl Launcher {
|
||||
.children(self.agents_section(cx))
|
||||
}
|
||||
|
||||
/// Settings' Nightly row: off by default; on, the nightly build installs and updates in its
|
||||
/// own instance, and the release stays one click away (the Horizon instance).
|
||||
fn nightly_row(&self, n: usize, cx: &mut Context<Self>) -> Div {
|
||||
let t = self.theme;
|
||||
let on = self.settings.nightly;
|
||||
let toggle =
|
||||
switch("nightly", on, &t).on_click(cx.listener(|this, _: &ClickEvent, _, cx| {
|
||||
this.set_nightly(!this.settings.nightly);
|
||||
cx.notify();
|
||||
}));
|
||||
let what = tr(
|
||||
"Can include pull requests not reviewed yet, picked by trusted reviewers; may break.",
|
||||
);
|
||||
let status = match (&self.nightly_progress, &self.nightly_error) {
|
||||
_ if !on => tr("Plays in its own Nightly instance: your worlds and the release stay as they are.")
|
||||
.to_string(),
|
||||
(Some(p), _) => trf(
|
||||
"Installing the nightly, {percent}%",
|
||||
&[("percent", &format!("{:.0}", p * 100.))],
|
||||
),
|
||||
(None, Some(e)) => trf("The nightly could not be installed: {error}", &[("error", e)]),
|
||||
(None, None) => match self
|
||||
.instances
|
||||
.list
|
||||
.iter()
|
||||
.find(|i| i.id == nightly::INSTANCE)
|
||||
.and_then(|i| i.version.clone())
|
||||
{
|
||||
Some(version) => trf(
|
||||
"{version} in the Nightly instance. Choose Horizon in the sidebar to play the release.",
|
||||
&[("version", &version)],
|
||||
),
|
||||
None => tr("No nightly build yet.").to_string(),
|
||||
},
|
||||
};
|
||||
index_row(
|
||||
n,
|
||||
tr("Nightly builds"),
|
||||
Some(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.child(what.to_string())
|
||||
.child(status)
|
||||
.into_any_element(),
|
||||
),
|
||||
Some(toggle.into_any_element()),
|
||||
&t,
|
||||
)
|
||||
}
|
||||
|
||||
/// Settings' Game arguments row: the extra arguments the game starts with, kept in
|
||||
/// `settings.json` and appended to the launcher's own (quotes group a word with spaces).
|
||||
/// Settings' Native mods row (docs/native-mods.md): off by default. On, Build compiles the
|
||||
|
||||
+71
-90
@@ -1,19 +1,17 @@
|
||||
//! Nightly (docs/release-flow.md "Nightly channel"): an opt-in build of main plus pull requests
|
||||
//! picked by trusted reviewers, some not reviewed yet, published as the `nightly` pre-release each
|
||||
//! night and when a reviewer asks. Off by default (`Settings::nightly`).
|
||||
//! Nightly (docs/release-flow.md): the only build Horizon ships since 2026-10-28, main plus the pull
|
||||
//! requests the trusted reviewers picked, published as the `nightly` pre-release a few times a day.
|
||||
//! Every launcher installs it as its update (`backend::horizon_update`), as its own version
|
||||
//! `nightly-<date>-<commit>`, beside the player's worlds, which it never touches. Its checksums must
|
||||
//! be signed for the nightly channel (`signed::nightly_digest`).
|
||||
//!
|
||||
//! It never touches the release: it installs as its own version, `nightly-<date>-<commit>`, and plays
|
||||
//! in its own instance, "Nightly" (`instances/nightly/` in the game's home), so the player's worlds
|
||||
//! stay where they are and choosing the Horizon instance plays the release again. Its checksums
|
||||
//! must be signed for the nightly channel (`signed::nightly_digest`); the release channel refuses
|
||||
//! them.
|
||||
use crate::backend::{self, Release, ASSET};
|
||||
use crate::instances::{self, Instance};
|
||||
//! Before that it was opt-in and played in its own "Nightly" instance (`instances/nightly/`):
|
||||
//! that instance, and its worlds, stay, and keep following the Nightly.
|
||||
use crate::backend::{self, Release};
|
||||
use crate::instances;
|
||||
use horizon_release_sign::NIGHTLY;
|
||||
|
||||
/// The Nightly instance's id (its folder in `instances/`) and name.
|
||||
/// The id of the instance (its folder in `instances/`) the opt-in Nightly played in.
|
||||
pub const INSTANCE: &str = "nightly";
|
||||
const NAME: &str = "Nightly";
|
||||
|
||||
/// The version a nightly installs as: `nightly-2026-10-28-0c02067`, from its title
|
||||
/// ("Nightly 2026-10-28 · 0c02067"), else its publication date.
|
||||
@@ -33,6 +31,12 @@ pub fn version_of(release: &Release) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// The date of a nightly's version (`nightly-2026-10-28-0c02067`), or None for any other version.
|
||||
pub fn date_of(version: &str) -> Option<&str> {
|
||||
let date = version.strip_prefix("nightly-")?.get(..10)?;
|
||||
is_date(date).then_some(date)
|
||||
}
|
||||
|
||||
fn is_date(text: &str) -> bool {
|
||||
text.len() == 10
|
||||
&& text.chars().enumerate().all(|(i, c)| {
|
||||
@@ -44,112 +48,89 @@ fn is_date(text: &str) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
/// The current nightly build, or None while there is none.
|
||||
pub fn latest() -> Result<Option<Release>, String> {
|
||||
let url = format!(
|
||||
"https://api.github.com/repos/{}/releases/tags/{NIGHTLY}",
|
||||
backend::REPO
|
||||
);
|
||||
let release: serde_json::Value = match backend::get(&url) {
|
||||
Ok(response) => response.into_json().map_err(|e| e.to_string())?,
|
||||
Err(e) if e.ends_with("answered 404") => return Ok(None),
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
Ok(parse(&release))
|
||||
}
|
||||
|
||||
fn parse(release: &serde_json::Value) -> Option<Release> {
|
||||
let list = serde_json::Value::Array(vec![release.clone()]);
|
||||
backend::parse_releases(&list, ASSET, |tag| tag == NIGHTLY)
|
||||
/// After `version`, a Nightly, installed as an update: the old Nightly instance follows it, and the
|
||||
/// older Nightlies go unless the selection or an instance still plays them. Worlds stay.
|
||||
pub fn after_update(version: &str) {
|
||||
let list = instances::list();
|
||||
if list
|
||||
.iter()
|
||||
.any(|i| i.id == INSTANCE && i.version.as_deref().is_some_and(|v| date_of(v).is_some()))
|
||||
{
|
||||
let _ = instances::edit(INSTANCE, |meta| meta.version = Some(version.to_owned()));
|
||||
}
|
||||
let selected = backend::Settings::load().selected;
|
||||
let pinned: Vec<String> = instances::list()
|
||||
.into_iter()
|
||||
.next()
|
||||
.filter(|r| r.prerelease)
|
||||
}
|
||||
|
||||
/// The nightly to install: `latest` when it isn't installed yet.
|
||||
pub fn update(latest: Option<Release>, installed: &[String]) -> Option<Release> {
|
||||
latest.filter(|r| r.asset.is_some() && !installed.contains(&version_of(r)))
|
||||
}
|
||||
|
||||
/// Installs `release` (the current nightly) as its own version, points the Nightly instance at
|
||||
/// it (making the instance the first time) and removes older nightly versions. Returns the
|
||||
/// instance.
|
||||
pub fn install(release: &Release, progress: impl Fn(f32)) -> Result<Instance, String> {
|
||||
let sha256 = crate::signed::nightly_digest(
|
||||
ASSET.unwrap_or_default(),
|
||||
&horizon_release_sign::trusted_keys(),
|
||||
backend::fetch_small,
|
||||
)?;
|
||||
let version = version_of(release);
|
||||
backend::install_as(release, &version, &sha256, progress)?;
|
||||
let instance = instance_on(&version)?;
|
||||
for old in backend::installed() {
|
||||
if horizon_release_sign::is_nightly(&old) && old != version {
|
||||
let _ = backend::remove(&old);
|
||||
}
|
||||
.filter_map(|i| i.version)
|
||||
.collect();
|
||||
for old in stale(&backend::installed(), version, selected.as_deref(), &pinned) {
|
||||
let _ = backend::remove(&old);
|
||||
}
|
||||
Ok(instance)
|
||||
}
|
||||
|
||||
/// The Nightly instance, made empty the first time (with the player's options and offline name),
|
||||
/// playing `version`.
|
||||
fn instance_on(version: &str) -> Result<Instance, String> {
|
||||
if instances::find(INSTANCE).is_err() {
|
||||
instances::create(NAME, None)?;
|
||||
}
|
||||
instances::edit(INSTANCE, |meta| meta.version = Some(version.to_owned()))
|
||||
/// The installed Nightlies to remove once `current` is installed: every other one that is neither
|
||||
/// selected nor pinned by an instance.
|
||||
fn stale(
|
||||
installed: &[String],
|
||||
current: &str,
|
||||
selected: Option<&str>,
|
||||
pinned: &[String],
|
||||
) -> Vec<String> {
|
||||
installed
|
||||
.iter()
|
||||
.filter(|v| date_of(v).is_some() && *v != current && Some(v.as_str()) != selected)
|
||||
.filter(|v| !pinned.contains(v))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn release(name: &str, date: &str, asset: bool) -> Release {
|
||||
fn release(name: &str, date: &str) -> Release {
|
||||
Release {
|
||||
tag: NIGHTLY.into(),
|
||||
name: name.into(),
|
||||
body: String::new(),
|
||||
date: date.into(),
|
||||
asset: asset.then(|| ("https://example/horizon.zip".into(), 1)),
|
||||
asset: Some(("https://example/horizon.zip".into(), 1)),
|
||||
prerelease: true,
|
||||
nightly: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_nightly_installs_under_its_date_and_commit() {
|
||||
let r = release("Nightly 2026-10-28 · 0c02067", "2026-10-28", true);
|
||||
let r = release("Nightly 2026-10-28 · 0c02067", "2026-10-28");
|
||||
assert_eq!(version_of(&r), "nightly-2026-10-28-0c02067");
|
||||
let untitled = release("Something else", "2026-10-29", true);
|
||||
let untitled = release("Something else", "2026-10-29");
|
||||
assert_eq!(version_of(&untitled), "nightly-2026-10-29");
|
||||
// Never a release version: the release channel's checks and lists leave it out.
|
||||
assert!(!backend::is_game_tag(&version_of(&r)));
|
||||
assert_eq!(date_of(&version_of(&r)), Some("2026-10-28"));
|
||||
assert_eq!(date_of("v0.2.95"), None);
|
||||
assert_eq!(date_of("nightly"), None);
|
||||
assert!(horizon_release_sign::is_nightly(&version_of(&r)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_a_new_nightly_with_a_build_here_is_an_update() {
|
||||
let r = release("Nightly 2026-10-28 · 0c02067", "2026-10-28", true);
|
||||
let name = |r: Option<Release>| r.map(|r| r.name);
|
||||
fn only_unused_older_nightlies_are_removed() {
|
||||
let installed = [
|
||||
"nightly-2026-10-29-aaaaaaa",
|
||||
"nightly-2026-10-28-bbbbbbb",
|
||||
"nightly-2026-10-28-ccccccc",
|
||||
"nightly-2026-10-27-ddddddd",
|
||||
"v0.2.95",
|
||||
]
|
||||
.map(String::from);
|
||||
let pinned = ["nightly-2026-10-27-ddddddd".to_string()];
|
||||
assert_eq!(
|
||||
name(update(Some(r.clone()), &["v0.2.80".into()])),
|
||||
Some(r.name.clone())
|
||||
stale(
|
||||
&installed,
|
||||
"nightly-2026-10-29-aaaaaaa",
|
||||
Some("nightly-2026-10-28-ccccccc"),
|
||||
&pinned
|
||||
),
|
||||
["nightly-2026-10-28-bbbbbbb"]
|
||||
);
|
||||
let installed = ["nightly-2026-10-28-0c02067".to_string(), "v0.2.80".into()];
|
||||
assert!(update(Some(r), &installed).is_none());
|
||||
let no_build = release("Nightly 2026-10-29 · 1a2b3c4", "2026-10-29", false);
|
||||
assert!(update(Some(no_build), &installed).is_none());
|
||||
assert!(update(None, &installed).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_nightly_pre_release_is_read() {
|
||||
let json = |tag: &str, pre: bool| {
|
||||
serde_json::json!({"tag_name": tag, "name": "Nightly 2026-10-28 · 0c02067",
|
||||
"prerelease": pre, "published_at": "2026-10-28T20:40:00Z",
|
||||
"assets": [{"name": ASSET.unwrap_or("none"), "browser_download_url": "u", "size": 1}]})
|
||||
};
|
||||
assert!(parse(&json("nightly", true)).is_some());
|
||||
assert!(parse(&json("nightly", false)).is_none());
|
||||
assert!(parse(&json("v0.2.81", true)).is_none());
|
||||
}
|
||||
}
|
||||
@@ -30,14 +30,15 @@ pub fn digest(
|
||||
horizon_release_sign::signed_digest(tag, asset, sums.as_deref(), signature.as_deref(), keys)
|
||||
}
|
||||
|
||||
/// The signed SHA-256 of `asset` in the current nightly build (the `nightly` pre-release): its
|
||||
/// checksums must be signed for the nightly channel (`horizon_release_sign::nightly_digest`).
|
||||
/// The signed SHA-256 of `asset` in the Nightly published under `tag` (`nightly`, or a dated
|
||||
/// `nightly-<date>`): its checksums must be signed for the nightly channel
|
||||
/// (`horizon_release_sign::nightly_digest`), whatever tag serves them.
|
||||
pub fn nightly_digest(
|
||||
tag: &str,
|
||||
asset: &str,
|
||||
keys: &[PublicKey],
|
||||
fetch: impl Fn(&str) -> Result<Option<Vec<u8>>, String>,
|
||||
) -> Result<String, String> {
|
||||
let tag = horizon_release_sign::NIGHTLY;
|
||||
let sums = fetch(&asset_url(tag, SUMS))?;
|
||||
let signature = fetch(&asset_url(tag, SIGNATURE))?
|
||||
.map(|bytes| String::from_utf8_lossy(&bytes).into_owned());
|
||||
@@ -131,7 +132,7 @@ mod tests {
|
||||
};
|
||||
let nightly = serving(horizon_release_sign::NIGHTLY);
|
||||
assert_eq!(
|
||||
nightly_digest("horizon.zip", &keys, &nightly),
|
||||
nightly_digest("nightly", "horizon.zip", &keys, &nightly),
|
||||
Ok(sha256_hex(b"game"))
|
||||
);
|
||||
for tag in ["v0.2.81", "nightly", "nightly-2026-10-27"] {
|
||||
@@ -142,6 +143,6 @@ mod tests {
|
||||
}
|
||||
let release = serving("v0.2.81");
|
||||
assert!(digest("v0.2.81", "horizon.zip", &keys, &release).is_ok());
|
||||
assert!(nightly_digest("horizon.zip", &keys, &release).is_err());
|
||||
assert!(nightly_digest("nightly", "horizon.zip", &keys, &release).is_err());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user