Skip to content

Commit 341bbd6

Browse files
authored
Merge pull request #106 from ClickHouse/issue-103-self-update
Add self-update command and background update check
2 parents 4e7659c + efa5b84 commit 341bbd6

4 files changed

Lines changed: 363 additions & 1 deletion

File tree

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,20 @@ Supports global or project scope installation. Project scope installs Skills int
478478
- `--all` install Skills for all supported agents
479479
- `--detected-only` install Skills for supported agents that were detected on the system
480480

481+
## Self-update
482+
483+
`clickhousectl` can update itself to the latest release:
484+
485+
```bash
486+
# Update to the latest version
487+
clickhousectl update
488+
489+
# Check for updates without installing
490+
clickhousectl update --check
491+
```
492+
493+
The CLI also checks for updates in the background (at most once per 24 hours) and displays a notice when a newer version is available.
494+
481495
## Cloud integration testing
482496

483497
Cloud commands are tested against a real ClickHouse Cloud workspace. All changes to Cloud commands must pass CI testing before merge. CI tests are under [`tests/cloud_cli.rs`](tests/cloud_cli.rs).

src/cli.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,13 @@ CONTEXT FOR AGENTS:
6262
Using any flags skips interactive mode. Project scope is the default. Universal `.agents/skills`
6363
is always included.")]
6464
Skills(SkillsArgs),
65+
66+
/// Update clickhousectl to the latest version
67+
#[command(after_help = "\
68+
CONTEXT FOR AGENTS:
69+
Self-update command. Downloads the latest clickhousectl release from GitHub and replaces the
70+
current binary. Use --check to see if an update is available without installing.")]
71+
Update(UpdateArgs),
6572
}
6673

6774
#[derive(Args, Debug)]
@@ -88,6 +95,13 @@ pub struct SkillsArgs {
8895
pub global: bool,
8996
}
9097

98+
#[derive(Args, Debug)]
99+
pub struct UpdateArgs {
100+
/// Check for updates without installing
101+
#[arg(long)]
102+
pub check: bool,
103+
}
104+
91105
#[cfg(test)]
92106
mod tests {
93107
use super::*;

src/main.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ mod init;
55
mod local;
66
mod paths;
77
mod skills;
8+
mod update;
89
mod user_agent;
910
mod version_manager;
1011

1112
use clap::Parser;
1213
use cli::{
1314
ActivityCommands, AuthCommands, BackupCommands, BackupConfigCommands, Cli, CloudArgs,
1415
CloudCommands, Commands, InvitationCommands, KeyCommands, MemberCommands, OrgCommands,
15-
PrivateEndpointCommands, QueryEndpointCommands, ServiceCommands, SkillsArgs,
16+
PrivateEndpointCommands, QueryEndpointCommands, ServiceCommands, SkillsArgs, UpdateArgs,
1617
};
1718

1819
use cloud::CloudClient;
@@ -22,8 +23,25 @@ use error::{Error, Result};
2223
async fn main() {
2324
let cli = Cli::parse();
2425

26+
// For non-update commands: print a cached update notice (sync, no network)
27+
// and spawn a background task to refresh the cache.
28+
let is_update_cmd = matches!(cli.command, Commands::Update(_));
29+
let cache_refresh = if !is_update_cmd {
30+
update::print_cached_update_notice();
31+
Some(tokio::spawn(update::refresh_update_cache()))
32+
} else {
33+
None
34+
};
35+
2536
let result = run(cli.command).await;
2637

38+
// Give the cache refresh a brief window to finish so short-lived commands
39+
// don't always drop it before the write completes. The background HTTP
40+
// request itself has a 400ms timeout, so 500ms here is enough headroom.
41+
if let Some(handle) = cache_refresh {
42+
let _ = tokio::time::timeout(std::time::Duration::from_millis(500), handle).await;
43+
}
44+
2745
if let Err(e) = result {
2846
eprintln!("Error: {}", e);
2947
std::process::exit(1);
@@ -35,6 +53,27 @@ async fn run(cmd: Commands) -> Result<()> {
3553
Commands::Local(args) => local::run(args.command, args.json).await,
3654
Commands::Skills(args) => run_skills(args).await,
3755
Commands::Cloud(args) => run_cloud(*args).await,
56+
Commands::Update(args) => run_update(args).await,
57+
}
58+
}
59+
60+
async fn run_update(args: UpdateArgs) -> Result<()> {
61+
if args.check {
62+
match update::check_for_update().await? {
63+
Some((current, latest)) => {
64+
println!(
65+
"Update available: v{} → v{}",
66+
current, latest
67+
);
68+
println!("Run `clickhousectl update` to upgrade.");
69+
}
70+
None => {
71+
println!("Already up to date (v{}).", env!("CARGO_PKG_VERSION"));
72+
}
73+
}
74+
Ok(())
75+
} else {
76+
update::perform_update().await
3877
}
3978
}
4079

0 commit comments

Comments
 (0)