fix(versions): stop reporting a stack sibling's version as the app's own
package.versions answered installedVersion "15.17" for btcpay-server while offering "2.4.2" — 15.17 being its postgres dependency's tag. With BTCPay's own container absent, installed_version fell back to `containers.first()`, which for a multi-container stack is an arbitrary sibling. That is the number the update decision is made from, and it is what the UI shows next to the available version, so a nonsense pair like "installed 15.17, available 2.4.2" is presented as a legitimate upgrade. The fallback now only applies when there is exactly one container, which still covers apps whose container is named differently from their id (immich_server for immich). With several containers and no identifiable backend, the honest answer is "unknown" rather than a guess at a sibling. Extracted as select_backend_container so the rule is testable directly. Tests: the BTCPay stack case, the lone differently-named container, and the archy- prefixed preference. Full suite 1157/1157. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
cbfda30579
commit
37c77f17ab
@@ -46,10 +46,18 @@ fn image_tag(image: &str) -> Option<String> {
|
||||
async fn installed_version(app_id: &str) -> Option<String> {
|
||||
let containers = get_containers_for_app(app_id).await.ok()?;
|
||||
// Prefer the backend container (exact id / `archy-<id>`) over UI companions.
|
||||
let name = containers
|
||||
.iter()
|
||||
.find(|n| n.as_str() == app_id || n.as_str() == format!("archy-{app_id}"))
|
||||
.or_else(|| containers.first())?;
|
||||
//
|
||||
// The fallback is deliberately narrow. It used to be `containers.first()`
|
||||
// unconditionally, which for a multi-container stack reported a SIBLING's
|
||||
// version as the app's own: with btcpay-server's container absent, its
|
||||
// postgres dependency was first in the list, so package.versions answered
|
||||
// installedVersion "15.17" against an available "2.4.2". That is not a
|
||||
// cosmetic mislabel — it is the number the update decision is made from.
|
||||
//
|
||||
// A single-container app is unambiguous, so the fallback still covers apps
|
||||
// whose container is named differently from their id. With several
|
||||
// containers and no identifiable backend, "unknown" is the honest answer.
|
||||
let name = select_backend_container(app_id, &containers)?;
|
||||
let out = tokio::process::Command::new("podman")
|
||||
.args(["inspect", name, "--format", "{{.ImageName}}"])
|
||||
.output()
|
||||
@@ -74,6 +82,24 @@ async fn installed_version(app_id: &str) -> Option<String> {
|
||||
Some(tag)
|
||||
}
|
||||
|
||||
/// Pick the container that represents `app_id` itself, never a stack sibling.
|
||||
///
|
||||
/// See the note at the call site: an unconditional "first container" fallback
|
||||
/// reported a dependency's image tag as the app's installed version.
|
||||
fn select_backend_container<'a>(app_id: &str, containers: &'a [String]) -> Option<&'a str> {
|
||||
if let Some(exact) = containers
|
||||
.iter()
|
||||
.find(|n| n.as_str() == app_id || n.as_str() == format!("archy-{app_id}"))
|
||||
{
|
||||
return Some(exact.as_str());
|
||||
}
|
||||
// Unambiguous only when there is nothing else it could be.
|
||||
if containers.len() == 1 {
|
||||
return Some(containers[0].as_str());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn is_floating_tag(tag: &str) -> bool {
|
||||
matches!(tag, "latest" | "stable" | "release" | "main")
|
||||
}
|
||||
@@ -295,7 +321,9 @@ impl RpcHandler {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{image_tag, is_floating_tag, parse_bitcoind_version_output};
|
||||
use super::{
|
||||
image_tag, is_floating_tag, parse_bitcoind_version_output, select_backend_container,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn floating_tag_detects_generic_channel_names() {
|
||||
@@ -332,6 +360,42 @@ mod tests {
|
||||
assert_eq!(parse_bitcoind_version_output(""), None);
|
||||
}
|
||||
|
||||
/// The BTCPay case: with btcpay-server's own container absent, its postgres
|
||||
/// dependency was first in the app's container list and its tag (15.17) was
|
||||
/// reported as BTCPay's installed version, against an available 2.4.2.
|
||||
#[test]
|
||||
fn backend_selection_never_falls_back_to_a_sibling_in_a_stack() {
|
||||
let stack = vec!["archy-btcpay-db".to_string(), "archy-nbxplorer".to_string()];
|
||||
assert_eq!(select_backend_container("btcpay-server", &stack), None);
|
||||
|
||||
let with_backend = vec![
|
||||
"archy-btcpay-db".to_string(),
|
||||
"btcpay-server".to_string(),
|
||||
];
|
||||
assert_eq!(
|
||||
select_backend_container("btcpay-server", &with_backend),
|
||||
Some("btcpay-server")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_selection_accepts_a_lone_differently_named_container() {
|
||||
let single = vec!["immich_server".to_string()];
|
||||
assert_eq!(
|
||||
select_backend_container("immich", &single),
|
||||
Some("immich_server")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backend_selection_prefers_the_archy_prefixed_name() {
|
||||
let names = vec!["something-else".to_string(), "archy-nbxplorer".to_string()];
|
||||
assert_eq!(
|
||||
select_backend_container("nbxplorer", &names),
|
||||
Some("archy-nbxplorer")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_tag_keeps_registry_port_colon() {
|
||||
assert_eq!(
|
||||
|
||||
Reference in New Issue
Block a user