1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
//! `dora hub outdated` (spec §9): compare a dataflow's locked hub pins against
//! the latest non-yanked version available in the index.
use std::path::PathBuf;
use dora_hub_client::{reference::PackageRef, semver::VersionReq};
use super::common::{HubContext, sanitize};
use crate::command::{Executable, build::lockfile::BuildLockfile};
/// Show hub packages whose lockfile pin is behind the index.
#[derive(Debug, clap::Args)]
pub struct Outdated {
/// Dataflow whose lockfile to check.
#[clap(value_name = "DATAFLOW")]
dataflow: PathBuf,
/// Do not refresh the index over the network; use only the cached copy.
#[clap(long, action)]
offline: bool,
}
impl Executable for Outdated {
fn execute(self) -> eyre::Result<()> {
let lockfile_path = BuildLockfile::path_for_dataflow(&self.dataflow, None);
if !lockfile_path.exists() {
eyre::bail!(
"no lockfile at `{}` — run `dora build --write-lockfile {}` first",
lockfile_path.display(),
self.dataflow.display()
);
}
let lockfile = BuildLockfile::read_from(&lockfile_path)?;
let mut ctx = HubContext::load(self.offline)?;
let mut checked = 0usize;
let mut outdated = 0usize;
let mut errored = 0usize;
// both git-sourced and binary-sourced hub nodes carry the same hub
// provenance (name + version), so the upgrade check is identical.
let entries = lockfile
.git_sources
.iter()
.filter_map(|(id, s)| s.hub.as_ref().map(|h| (id, h)))
.chain(lockfile.binary_sources.iter().map(|(id, p)| (id, &p.hub)));
for (node_id, hub) in entries {
checked += 1;
// The node id, hub name, and version all come from the lockfile,
// and resolve errors embed the lockfile-derived package key — none
// are charset-validated, so sanitize every one before echoing (a
// hostile entry could otherwise inject terminal escapes). Treat a
// malformed pin as "can't check".
let node = sanitize(node_id.as_ref());
let Some((namespace, name)) = hub.name.split_once('/') else {
println!("{node}: malformed hub name `{}`", sanitize(&hub.name));
errored += 1;
continue;
};
let pinned = match hub.version.parse::<dora_hub_client::semver::Version>() {
Ok(v) => v,
Err(_) => {
println!(
"{node}: malformed pinned version `{}`",
sanitize(&hub.version)
);
errored += 1;
continue;
}
};
let catalog = match ctx.catalog_for_namespace(namespace) {
Ok(catalog) => catalog,
Err(err) => {
println!(
"{node}: {}/{} — could not read index ({})",
sanitize(namespace),
sanitize(name),
sanitize(&format!("{err:#}"))
);
errored += 1;
continue;
}
};
// the highest *non-yanked* version (ignoring the dataflow's range,
// so a major bump still shows as available to upgrade to).
let reference = PackageRef {
namespace: namespace.to_string(),
name: name.to_string(),
requirement: VersionReq::STAR,
};
match catalog.resolve(&reference) {
Ok(latest) if latest.version > pinned => {
outdated += 1;
println!(
"{node}: {}/{} {pinned} -> {} (newer available)",
sanitize(namespace),
sanitize(name),
latest.version
);
}
Ok(_) => {} // up to date
Err(err) => {
println!(
"{node}: {}/{} {pinned} — {}",
sanitize(namespace),
sanitize(name),
sanitize(&format!("{err:#}"))
);
errored += 1;
}
}
}
ctx.drain_warnings();
if checked == 0 {
println!("No hub packages pinned in {}.", lockfile_path.display());
} else if outdated > 0 {
println!(
"{outdated} of {checked} hub package(s) are outdated — \
update the `hub:` range and rebuild to upgrade."
);
} else if errored == 0 {
println!("All {checked} hub package(s) are up to date.");
}
// A check that couldn't complete must not read as "up to date": report
// it and exit non-zero so a human or CI can tell the report is partial.
if errored > 0 {
eyre::bail!("{errored} of {checked} hub package(s) could not be checked (see above)");
}
Ok(())
}
}