Unify release selection across clients
ci / test (push) Canceled after 0s
ci / fuzz-smoke (push) Canceled after 0s
ci / macos-client (macos-aarch64, macos-14) (push) Canceled after 0s
ci / macos-client (macos-x86_64, macos-13) (push) Canceled after 0s
ci / windows-client (push) Canceled after 0s
ci / package-release (linux-x86_64, ubuntu-latest, , , ) (push) Canceled after 0s
ci / package-release (macos-aarch64, macos-14, , , ) (push) Canceled after 0s
ci / package-release (macos-x86_64, macos-13, , , ) (push) Canceled after 0s
ci / package-release (windows-aarch64, windows-latest, aarch64, windows, aarch64-pc-windows-msvc) (push) Canceled after 0s
ci / package-release (windows-x86_64, windows-latest, , , ) (push) Canceled after 0s
ci / remote-bench (push) Canceled after 0s
ci / publish-gitea-release (push) Canceled after 0s

This commit is contained in:
DuProcess
2026-07-17 18:58:58 -04:00
parent 89b81d73b1
commit 7f8d5711ea
7 changed files with 641 additions and 439 deletions
+105 -162
View File
@@ -4313,27 +4313,6 @@ fn update_installer_url(raw_base: &str, os: &str) -> String {
format!("{raw_base}/raw/branch/main/{}", update_installer_name(os))
}
fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
let web = repo
.strip_suffix(".git")
.unwrap_or(repo)
.trim_end_matches('/');
if !web.starts_with("http://") && !web.starts_with("https://") {
return None;
}
Some(format!("{web}/releases/latest/download/{artifact}"))
}
fn release_tag_from_effective_url(web: &str, effective: &str) -> Option<String> {
let prefix = format!("{web}/releases/tag/");
let tag = effective.strip_prefix(&prefix)?;
if tag.is_empty() {
None
} else {
Some(tag.to_string())
}
}
fn release_version_from_tag(tag: &str) -> &str {
tag.strip_prefix('v').unwrap_or(tag)
}
@@ -4346,20 +4325,6 @@ fn update_version_status(local: &str, latest: &str) -> &'static str {
}
}
fn update_binary_version_for_installer(local: &str, latest_tag: Option<&str>) -> Option<String> {
let latest = latest_tag.map(release_version_from_tag)?;
if compare_dotted_versions(latest, local) == std::cmp::Ordering::Less {
Some(format!("v{local}"))
} else {
None
}
}
fn effective_update_artifact_tag(local: &str, latest_tag: Option<&str>) -> Option<String> {
update_binary_version_for_installer(local, latest_tag)
.or_else(|| latest_tag.map(ToOwned::to_owned))
}
fn compare_dotted_versions(left: &str, right: &str) -> std::cmp::Ordering {
let left = parse_comparable_version(left);
let right = parse_comparable_version(right);
@@ -4447,7 +4412,7 @@ fn parse_dotted_version(version: &str) -> Vec<u64> {
.collect()
}
fn latest_release_tag(repo: &str) -> Result<Option<String>> {
fn repository_release_tag(repo: &str) -> Result<Option<String>> {
let web = repo
.strip_suffix(".git")
.unwrap_or(repo)
@@ -4455,28 +4420,30 @@ fn latest_release_tag(repo: &str) -> Result<Option<String>> {
if !web.starts_with("http://") && !web.starts_with("https://") {
return Ok(None);
}
let latest_url = format!("{web}/releases/latest");
let manifest_url = format!("{web}/raw/branch/main/Cargo.toml");
let output = if cfg!(windows) {
let script = windows_effective_url_script(&latest_url);
windows_powershell_output(&script, &format!("resolve latest release for {web}"))?
let script = windows_url_contents_script(&manifest_url);
windows_powershell_output(&script, &format!("read release version from {web}"))?
} else {
Command::new("curl")
.arg("-fsSL")
.arg("-o")
.arg("/dev/null")
.arg("-w")
.arg("%{url_effective}")
.arg(latest_url)
.arg(manifest_url)
.stdin(Stdio::null())
.output()
.with_context(|| format!("resolve latest release for {web}"))?
.with_context(|| format!("read release version from {web}"))?
};
if !output.status.success() {
return Ok(None);
}
let effective = String::from_utf8_lossy(&output.stdout);
let effective = effective.trim();
Ok(release_tag_from_effective_url(web, effective))
let manifest =
String::from_utf8(output.stdout).context("repository Cargo.toml is not UTF-8")?;
let parsed: toml::Value = toml::from_str(&manifest).context("parse repository Cargo.toml")?;
let version = parsed
.get("package")
.and_then(|package| package.get("version"))
.and_then(toml::Value::as_str)
.filter(|version| !version.is_empty());
Ok(version.map(|version| format!("v{}", version.trim_start_matches('v'))))
}
fn release_tag_download_url(repo: &str, tag: &str, artifact: &str) -> Option<String> {
@@ -4524,7 +4491,7 @@ fn run_update(
let update_remote_server = options.role.updates_remote_server_from_os(os);
if options.check_only {
let local_version = env!("CARGO_PKG_VERSION");
let latest_tag = latest_release_tag(&repo)?;
let release_tag = repository_release_tag(&repo)?;
println!("dosh {local_version}");
println!("repo: {repo}");
if let Some(local_installer_role) = local_installer_role {
@@ -4538,7 +4505,7 @@ fn run_update(
);
println!("remote_server: {}", config.server);
}
match latest_tag.as_deref() {
match release_tag.as_deref() {
Some(tag) => {
let latest_version = release_version_from_tag(tag);
println!(
@@ -4553,22 +4520,14 @@ fn run_update(
update_remote_server,
&repo,
&artifact,
release_tag.as_deref(),
) {
LocalPrebuiltCheckTarget::Url(latest_url) => {
let mut status = "missing";
let mut display_url = latest_url.clone();
if let Some(tag_url) =
effective_update_artifact_tag(local_version, latest_tag.as_deref())
.as_deref()
.and_then(|tag| release_tag_download_url(&repo, tag, &artifact))
{
display_url = tag_url;
if url_reachable(&display_url)? {
status = "available";
}
} else if url_reachable(&latest_url)? {
status = "available";
}
LocalPrebuiltCheckTarget::Url(display_url) => {
let status = if url_reachable(&display_url)? {
"available"
} else {
"missing; source fallback will build the same tag"
};
println!("prebuilt: {status} ({artifact})");
println!("prebuilt_url: {display_url}");
}
@@ -4578,6 +4537,9 @@ fn run_update(
LocalPrebuiltCheckTarget::RemoteOnly => {
println!("prebuilt: skipped for remote-only update");
}
LocalPrebuiltCheckTarget::ReleaseUnknown => {
println!("prebuilt: unavailable; repository release version is unknown");
}
}
return Ok(());
}
@@ -4586,10 +4548,7 @@ fn run_update(
.join("dosh")
.join("source");
let use_prebuilt = std::env::var("DOSH_USE_PREBUILT").unwrap_or_else(|_| "1".to_string());
let binary_version = update_binary_version_for_installer(
env!("CARGO_PKG_VERSION"),
latest_release_tag(&repo)?.as_deref(),
);
let binary_version = repository_release_tag(&repo)?;
if update_remote_server {
run_remote_server_update(
config,
@@ -4619,6 +4578,7 @@ enum LocalPrebuiltCheckTarget {
Url(String),
NonHttpRepo,
RemoteOnly,
ReleaseUnknown,
}
fn local_prebuilt_check_target(
@@ -4626,6 +4586,7 @@ fn local_prebuilt_check_target(
update_remote_server: bool,
repo: &str,
artifact: &str,
release_tag: Option<&str>,
) -> LocalPrebuiltCheckTarget {
if local_installer_role.is_none() {
return if update_remote_server {
@@ -4634,7 +4595,10 @@ fn local_prebuilt_check_target(
LocalPrebuiltCheckTarget::NonHttpRepo
};
}
latest_release_download_url(repo, artifact)
let Some(release_tag) = release_tag else {
return LocalPrebuiltCheckTarget::ReleaseUnknown;
};
release_tag_download_url(repo, release_tag, artifact)
.map(LocalPrebuiltCheckTarget::Url)
.unwrap_or(LocalPrebuiltCheckTarget::NonHttpRepo)
}
@@ -4727,6 +4691,7 @@ fn unix_update_script(
);
if let Some(version) = binary_version {
env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version)));
env.push_str(" DOSH_BINARY_EXACT=1");
}
let mut script = format!(
"curl -fsSL {} | {} sh -s -- {}",
@@ -4758,6 +4723,7 @@ fn remote_unix_server_update_script(
);
if let Some(version) = binary_version {
env.push_str(&format!(" DOSH_BINARY_VERSION={}", shell_word(version)));
env.push_str(" DOSH_BINARY_EXACT=1");
}
format!(
"curl -fsSL {} | {} sh -s -- server",
@@ -4795,6 +4761,7 @@ fn windows_update_script(
"$env:DOSH_BINARY_VERSION={};",
powershell_string(version)
));
script.push_str("$env:DOSH_BINARY_EXACT='1';");
}
if config.server != "user@example.com" {
script.push_str(&format!(
@@ -4809,19 +4776,12 @@ fn windows_update_script(
script
}
fn windows_effective_url_script(url: &str) -> String {
fn windows_url_contents_script(url: &str) -> String {
let url = powershell_string(url);
format!(
"$ErrorActionPreference='Stop';\
$ProgressPreference='SilentlyContinue';\
$response=Invoke-WebRequest -UseBasicParsing -Uri {url} -MaximumRedirection 5;\
if ($response.BaseResponse.ResponseUri) {{ \
$response.BaseResponse.ResponseUri.AbsoluteUri \
}} elseif ($response.BaseResponse.RequestMessage -and $response.BaseResponse.RequestMessage.RequestUri) {{ \
$response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri \
}} else {{ \
{url} \
}}"
(Invoke-WebRequest -UseBasicParsing -Uri {url} -MaximumRedirection 5).Content"
)
}
@@ -10950,39 +10910,38 @@ mod tests {
SshPathTokenContext, StartupGateMode, StatusAction, TERMINAL_CLEANUP,
TERMINAL_SNAPSHOT_RESET, UpdateOptions, UpdateRole, auth_allows, cache_key,
cache_server_prefix, cleanup_stream_state, clear_cached_credentials,
effective_update_artifact_tag, ensure_tui_safe_status_overlay, expand_ssh_path_tokens,
first_resolved_addr, imported_host_block, input_contains_focus_in, input_matches_escape,
is_local_status_target, is_resume_response_for_client, latest_release_download_url,
load_first_native_identity_with_prompt, local_symlink_target_is_dir,
local_username_from_env, native_proxy_udp_warning, newest_client_trace_path_from,
note_authenticated_contact, note_snapshot_rendered, parse_dynamic_forward,
parse_escape_key, parse_local_forward, parse_remote_forward, parse_single_remote_path,
parse_ssh_config, parse_trace_line, parse_trace_options, parse_trace_report_options,
parse_trace_summary, parse_update_options, post_submit_hold_duration,
queue_or_send_stream_data, queue_pending_user_input, queue_stale_pending_user_input,
raw_contains_host_table, recv_response_until, refresh_live_addr, release_artifact_name_for,
release_tag_download_url, release_tag_from_effective_url, release_version_from_tag,
remote_unix_server_update_script, render_frame_bytes, render_status_overlay, requested_env,
resolve_forward_agent_endpoint, resolved_startup_command, retire_stream_state,
retransmit_stream_closes, retransmit_stream_eofs, retransmit_stream_opens,
retransmit_stream_window_adjusts, rewrite_forward_command, sanitize_trace_name,
selected_predict_mode, selected_udp_host, send_stream_eof, server_version_mismatch,
should_flush_terminal_input_after_contact, should_health_log_client_start,
should_hold_during_startup_gate, should_hold_post_submit_input,
should_reconnect_before_input_for_local_sleep, should_repaint_idle_terminal,
should_strip_unowned_terminal_reports, socket_bind_display, split_after_command_submit,
split_forward_spec, split_trace_tokens, ssh_command_target, ssh_config_uses_proxy,
ssh_config_word_for_os, ssh_destination_host, ssh_username, ssh_with_user, startup_command,
status_ssh_target, strip_stale_mouse_reports, strip_terminal_focus_reports,
strip_unowned_terminal_reports, summarize_trace_file, summarize_trace_file_with_mode,
terminal_private_mode_transition, toml_bare_key_or_quoted, top_trace_events,
trace_report_warnings, unix_update_script, update_binary_version_for_installer,
update_installer_url, update_version_status, upsert_managed_block, valid_forward_host,
vscode_command_candidates, vscode_fallback_command, vscode_safe_alias,
wake_repaint_retry_deadline, windows_command_word, windows_deferred_update_script,
windows_effective_url_script, windows_mode_from_readonly,
ensure_tui_safe_status_overlay, expand_ssh_path_tokens, first_resolved_addr,
imported_host_block, input_contains_focus_in, input_matches_escape, is_local_status_target,
is_resume_response_for_client, load_first_native_identity_with_prompt,
local_symlink_target_is_dir, local_username_from_env, native_proxy_udp_warning,
newest_client_trace_path_from, note_authenticated_contact, note_snapshot_rendered,
parse_dynamic_forward, parse_escape_key, parse_local_forward, parse_remote_forward,
parse_single_remote_path, parse_ssh_config, parse_trace_line, parse_trace_options,
parse_trace_report_options, parse_trace_summary, parse_update_options,
post_submit_hold_duration, queue_or_send_stream_data, queue_pending_user_input,
queue_stale_pending_user_input, raw_contains_host_table, recv_response_until,
refresh_live_addr, release_artifact_name_for, release_tag_download_url,
release_version_from_tag, remote_unix_server_update_script, render_frame_bytes,
render_status_overlay, requested_env, resolve_forward_agent_endpoint,
resolved_startup_command, retire_stream_state, retransmit_stream_closes,
retransmit_stream_eofs, retransmit_stream_opens, retransmit_stream_window_adjusts,
rewrite_forward_command, sanitize_trace_name, selected_predict_mode, selected_udp_host,
send_stream_eof, server_version_mismatch, should_flush_terminal_input_after_contact,
should_health_log_client_start, should_hold_during_startup_gate,
should_hold_post_submit_input, should_reconnect_before_input_for_local_sleep,
should_repaint_idle_terminal, should_strip_unowned_terminal_reports, socket_bind_display,
split_after_command_submit, split_forward_spec, split_trace_tokens, ssh_command_target,
ssh_config_uses_proxy, ssh_config_word_for_os, ssh_destination_host, ssh_username,
ssh_with_user, startup_command, status_ssh_target, strip_stale_mouse_reports,
strip_terminal_focus_reports, strip_unowned_terminal_reports, summarize_trace_file,
summarize_trace_file_with_mode, terminal_private_mode_transition, toml_bare_key_or_quoted,
top_trace_events, trace_report_warnings, unix_update_script, update_installer_url,
update_version_status, upsert_managed_block, valid_forward_host, vscode_command_candidates,
vscode_fallback_command, vscode_safe_alias, wake_repaint_retry_deadline,
windows_command_word, windows_deferred_update_script, windows_mode_from_readonly,
windows_powershell_command_candidates, windows_readonly_from_mode, windows_update_script,
windows_url_reachable_script, windows_vt_input_mode, windows_vt_output_mode,
windows_url_contents_script, windows_url_reachable_script, windows_vt_input_mode,
windows_vt_output_mode,
};
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
use dosh::native::EnvVar;
@@ -13421,9 +13380,10 @@ mod tests {
false,
"https://git.palav.dev/Palav/dosh.git",
"dosh-windows-x86_64.zip",
Some("v1.0.0-rc42"),
),
super::LocalPrebuiltCheckTarget::Url(
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-windows-x86_64.zip"
"https://git.palav.dev/Palav/dosh/releases/download/v1.0.0-rc42/dosh-windows-x86_64.zip"
.to_string()
)
);
@@ -13433,6 +13393,7 @@ mod tests {
false,
"git@git.palav.dev:Palav/dosh.git",
"dosh-windows-x86_64.zip",
Some("v1.0.0-rc42"),
),
super::LocalPrebuiltCheckTarget::NonHttpRepo
);
@@ -13442,9 +13403,20 @@ mod tests {
UpdateRole::Server.updates_remote_server_from_os("windows"),
"https://git.palav.dev/Palav/dosh.git",
"dosh-windows-x86_64.zip",
Some("v1.0.0-rc42"),
),
super::LocalPrebuiltCheckTarget::RemoteOnly
);
assert_eq!(
super::local_prebuilt_check_target(
Some("client"),
false,
"https://git.palav.dev/Palav/dosh.git",
"dosh-windows-x86_64.zip",
None,
),
super::LocalPrebuiltCheckTarget::ReleaseUnknown
);
}
#[test]
@@ -13552,12 +13524,13 @@ mod tests {
#[test]
fn windows_update_probes_are_powershell_native() {
let latest =
windows_effective_url_script("https://git.palav.dev/Palav/dosh/releases/latest");
assert!(latest.contains("Invoke-WebRequest -UseBasicParsing"));
assert!(latest.contains("-MaximumRedirection 5"));
assert!(latest.contains("$response.BaseResponse.ResponseUri.AbsoluteUri"));
assert!(!latest.contains("curl"));
let manifest = windows_url_contents_script(
"https://git.palav.dev/Palav/dosh/raw/branch/main/Cargo.toml",
);
assert!(manifest.contains("Invoke-WebRequest -UseBasicParsing"));
assert!(manifest.contains("-MaximumRedirection 5"));
assert!(manifest.contains(".Content"));
assert!(!manifest.contains("curl"));
let reachable = windows_url_reachable_script(
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-windows-x86_64.zip",
@@ -13578,36 +13551,7 @@ mod tests {
}
#[test]
fn update_uses_local_tag_when_release_latest_is_older() {
assert_eq!(
update_binary_version_for_installer("1.0.0-rc41", Some("v1.0.0-rc37")).as_deref(),
Some("v1.0.0-rc41")
);
assert_eq!(
update_binary_version_for_installer("1.0.0-rc41", Some("v1.0.0-rc41")),
None
);
assert_eq!(
update_binary_version_for_installer("1.0.0-rc41", Some("v1.0.0-rc42")),
None
);
assert_eq!(
update_binary_version_for_installer("1.0.0", Some("v1.0.0-rc42")).as_deref(),
Some("v1.0.0")
);
assert_eq!(
update_binary_version_for_installer("1.0.0-rc42", Some("v1.0.0")),
None
);
assert_eq!(
effective_update_artifact_tag("1.0.0-rc41", Some("v1.0.0-rc37")).as_deref(),
Some("v1.0.0-rc41")
);
assert_eq!(
effective_update_artifact_tag("1.0.0-rc41", Some("v1.0.0-rc42")).as_deref(),
Some("v1.0.0-rc42")
);
assert_eq!(effective_update_artifact_tag("1.0.0-rc41", None), None);
fn update_pins_the_repository_release_for_every_platform() {
let config = ClientConfig::default();
let unix = unix_update_script(
&config,
@@ -13619,6 +13563,7 @@ mod tests {
Some("v1.0.0-rc41"),
);
assert!(unix.contains("DOSH_BINARY_VERSION='v1.0.0-rc41'"));
assert!(unix.contains("DOSH_BINARY_EXACT=1"));
assert!(unix.contains("| DOSH_REPO="));
assert!(unix.contains(" sh -s -- 'both'"));
@@ -13632,6 +13577,7 @@ mod tests {
Some("v1.0.0-rc41"),
);
assert!(windows.contains("$env:DOSH_BINARY_VERSION='v1.0.0-rc41';"));
assert!(windows.contains("$env:DOSH_BINARY_EXACT='1';"));
}
#[test]
@@ -13687,17 +13633,23 @@ mod tests {
}
#[test]
fn release_download_url_uses_latest_release_asset() {
fn release_download_url_uses_explicit_repository_release() {
assert_eq!(
latest_release_download_url(
release_tag_download_url(
"https://git.palav.dev/Palav/dosh.git",
"v1.0.0-rc42",
"dosh-linux-x86_64.tar.gz"
)
.unwrap(),
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-linux-x86_64.tar.gz"
"https://git.palav.dev/Palav/dosh/releases/download/v1.0.0-rc42/dosh-linux-x86_64.tar.gz"
);
assert!(
latest_release_download_url("git@git.palav.dev:Palav/dosh.git", "artifact").is_none()
release_tag_download_url(
"git@git.palav.dev:Palav/dosh.git",
"v1.0.0-rc42",
"artifact"
)
.is_none()
);
}
@@ -13726,15 +13678,7 @@ mod tests {
}
#[test]
fn release_tag_parses_effective_latest_url() {
assert_eq!(
release_tag_from_effective_url(
"https://example.com/owner/dosh",
"https://example.com/owner/dosh/releases/tag/v1.0.0"
)
.as_deref(),
Some("v1.0.0")
);
fn release_tag_builds_exact_artifact_url() {
assert_eq!(release_version_from_tag("v1.0.0"), "1.0.0");
assert_eq!(release_version_from_tag("2026.06.28"), "2026.06.28");
assert_eq!(
@@ -13748,7 +13692,6 @@ mod tests {
"https://example.com/owner/dosh/releases/download/v1.0.0/dosh-linux-x86_64.tar.gz"
)
);
assert!(release_tag_from_effective_url("https://example.com/owner/dosh", "").is_none());
}
#[test]