1use anyhow::{Context as _, Result};
2use client::Client;
3use db::kvp::KeyValueStore;
4use futures_lite::StreamExt;
5use gpui::{
6 App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, Global, Task, Window,
7 actions,
8};
9use http_client::{HttpClient, HttpClientWithUrl};
10use paths::remote_servers_dir;
11use release_channel::{AppCommitSha, ReleaseChannel};
12use semver::Version;
13use serde::{Deserialize, Serialize};
14use settings::{RegisterSetting, Settings, SettingsStore};
15use smol::fs::File;
16use smol::{fs, io::AsyncReadExt};
17use std::mem;
18use std::{
19 env::{
20 self,
21 consts::{ARCH, OS},
22 },
23 ffi::OsStr,
24 ffi::OsString,
25 path::{Path, PathBuf},
26 sync::Arc,
27 time::{Duration, SystemTime},
28};
29use util::command::new_command;
30use workspace::Workspace;
31
32const SHOULD_SHOW_UPDATE_NOTIFICATION_KEY: &str = "auto-updater-should-show-updated-notification";
33const POLL_INTERVAL: Duration = Duration::from_secs(60 * 60);
34const REMOTE_SERVER_CACHE_LIMIT: usize = 5;
35
36actions!(
37 auto_update,
38 [
39 /// Checks for available updates.
40 Check,
41 /// Dismisses the update error message.
42 DismissMessage,
43 /// Opens the release notes for the current version in a browser.
44 ViewReleaseNotes,
45 ]
46);
47
48#[derive(Clone, Debug, PartialEq, Eq)]
49pub enum VersionCheckType {
50 Sha(AppCommitSha),
51 Semantic(Version),
52}
53
54#[derive(Serialize, Debug)]
55pub struct AssetQuery<'a> {
56 asset: &'a str,
57 os: &'a str,
58 arch: &'a str,
59 metrics_id: Option<&'a str>,
60 system_id: Option<&'a str>,
61 is_staff: Option<bool>,
62}
63
64#[derive(Clone, Debug)]
65pub enum AutoUpdateStatus {
66 Idle,
67 Checking,
68 Downloading { version: VersionCheckType },
69 Installing { version: VersionCheckType },
70 Updated { version: VersionCheckType },
71 Errored { error: Arc<anyhow::Error> },
72}
73
74impl PartialEq for AutoUpdateStatus {
75 fn eq(&self, other: &Self) -> bool {
76 match (self, other) {
77 (AutoUpdateStatus::Idle, AutoUpdateStatus::Idle) => true,
78 (AutoUpdateStatus::Checking, AutoUpdateStatus::Checking) => true,
79 (
80 AutoUpdateStatus::Downloading { version: v1 },
81 AutoUpdateStatus::Downloading { version: v2 },
82 ) => v1 == v2,
83 (
84 AutoUpdateStatus::Installing { version: v1 },
85 AutoUpdateStatus::Installing { version: v2 },
86 ) => v1 == v2,
87 (
88 AutoUpdateStatus::Updated { version: v1 },
89 AutoUpdateStatus::Updated { version: v2 },
90 ) => v1 == v2,
91 (AutoUpdateStatus::Errored { error: e1 }, AutoUpdateStatus::Errored { error: e2 }) => {
92 e1.to_string() == e2.to_string()
93 }
94 _ => false,
95 }
96 }
97}
98
99impl AutoUpdateStatus {
100 pub fn is_updated(&self) -> bool {
101 matches!(self, Self::Updated { .. })
102 }
103}
104
105pub struct AutoUpdater {
106 status: AutoUpdateStatus,
107 current_version: Version,
108 client: Arc<Client>,
109 pending_poll: Option<Task<Option<()>>>,
110 quit_subscription: Option<gpui::Subscription>,
111 update_check_type: UpdateCheckType,
112}
113
114#[derive(Deserialize, Serialize, Clone, Debug)]
115pub struct ReleaseAsset {
116 pub version: String,
117 pub url: String,
118}
119
120struct MacOsUnmounter<'a> {
121 mount_path: PathBuf,
122 background_executor: &'a BackgroundExecutor,
123}
124
125impl Drop for MacOsUnmounter<'_> {
126 fn drop(&mut self) {
127 let mount_path = mem::take(&mut self.mount_path);
128 self.background_executor
129 .spawn(async move {
130 let unmount_output = new_command("hdiutil")
131 .args(["detach", "-force"])
132 .arg(&mount_path)
133 .output()
134 .await;
135 match unmount_output {
136 Ok(output) if output.status.success() => {
137 log::info!("Successfully unmounted the disk image");
138 }
139 Ok(output) => {
140 log::error!(
141 "Failed to unmount disk image: {:?}",
142 String::from_utf8_lossy(&output.stderr)
143 );
144 }
145 Err(error) => {
146 log::error!("Error while trying to unmount disk image: {:?}", error);
147 }
148 }
149 })
150 .detach();
151 }
152}
153
154#[derive(Clone, Copy, Debug, RegisterSetting)]
155struct AutoUpdateSetting(bool);
156
157/// Whether or not to automatically check for updates.
158///
159/// Default: true
160impl Settings for AutoUpdateSetting {
161 fn from_settings(content: &settings::SettingsContent) -> Self {
162 Self(content.auto_update.unwrap())
163 }
164}
165
166#[derive(Default)]
167struct GlobalAutoUpdate(Option<Entity<AutoUpdater>>);
168
169impl Global for GlobalAutoUpdate {}
170
171pub fn init(client: Arc<Client>, cx: &mut App) {
172 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
173 workspace.register_action(|_, action, window, cx| check(action, window, cx));
174
175 workspace.register_action(|_, action, _, cx| {
176 view_release_notes(action, cx);
177 });
178 })
179 .detach();
180
181 let version = release_channel::AppVersion::global(cx);
182 let auto_updater = cx.new(|cx| {
183 let updater = AutoUpdater::new(version, client, cx);
184
185 let poll_for_updates = ReleaseChannel::try_global(cx)
186 .map(|channel| channel.poll_for_updates())
187 .unwrap_or(false);
188
189 if option_env!("ZED_UPDATE_EXPLANATION").is_none()
190 && env::var("ZED_UPDATE_EXPLANATION").is_err()
191 && poll_for_updates
192 {
193 let mut update_subscription = AutoUpdateSetting::get_global(cx)
194 .0
195 .then(|| updater.start_polling(cx));
196
197 cx.observe_global::<SettingsStore>(move |updater: &mut AutoUpdater, cx| {
198 if AutoUpdateSetting::get_global(cx).0 {
199 if update_subscription.is_none() {
200 update_subscription = Some(updater.start_polling(cx))
201 }
202 } else {
203 update_subscription.take();
204 }
205 })
206 .detach();
207 }
208
209 updater
210 });
211 cx.set_global(GlobalAutoUpdate(Some(auto_updater)));
212}
213
214pub fn check(_: &Check, window: &mut Window, cx: &mut App) {
215 if let Some(message) = option_env!("ZED_UPDATE_EXPLANATION")
216 .map(ToOwned::to_owned)
217 .or_else(|| env::var("ZED_UPDATE_EXPLANATION").ok())
218 {
219 drop(window.prompt(
220 gpui::PromptLevel::Info,
221 "Zed was installed via a package manager.",
222 Some(&message),
223 &["Ok"],
224 cx,
225 ));
226 return;
227 }
228
229 if !ReleaseChannel::try_global(cx)
230 .map(|channel| channel.poll_for_updates())
231 .unwrap_or(false)
232 {
233 return;
234 }
235
236 if let Some(updater) = AutoUpdater::get(cx) {
237 updater.update(cx, |updater, cx| updater.poll(UpdateCheckType::Manual, cx));
238 } else {
239 drop(window.prompt(
240 gpui::PromptLevel::Info,
241 "Could not check for updates",
242 Some("Auto-updates disabled for non-bundled app."),
243 &["Ok"],
244 cx,
245 ));
246 }
247}
248
249pub fn release_notes_url(cx: &mut App) -> Option<String> {
250 let release_channel = ReleaseChannel::try_global(cx)?;
251 let url = match release_channel {
252 ReleaseChannel::Stable | ReleaseChannel::Preview => {
253 let auto_updater = AutoUpdater::get(cx)?;
254 let auto_updater = auto_updater.read(cx);
255 let mut current_version = auto_updater.current_version.clone();
256 current_version.pre = semver::Prerelease::EMPTY;
257 current_version.build = semver::BuildMetadata::EMPTY;
258 let release_channel = release_channel.dev_name();
259 let path = format!("/releases/{release_channel}/{current_version}");
260 auto_updater.client.http_client().build_url(&path)
261 }
262 ReleaseChannel::Nightly => {
263 "https://github.com/zed-industries/zed/commits/nightly/".to_string()
264 }
265 ReleaseChannel::Dev => "https://github.com/zed-industries/zed/commits/main/".to_string(),
266 };
267 Some(url)
268}
269
270pub fn view_release_notes(_: &ViewReleaseNotes, cx: &mut App) -> Option<()> {
271 let url = release_notes_url(cx)?;
272 cx.open_url(&url);
273 None
274}
275
276#[cfg(not(target_os = "windows"))]
277struct InstallerDir(tempfile::TempDir);
278
279#[cfg(not(target_os = "windows"))]
280impl InstallerDir {
281 async fn new() -> Result<Self> {
282 Ok(Self(
283 tempfile::Builder::new()
284 .prefix("zed-auto-update")
285 .tempdir()?,
286 ))
287 }
288
289 fn path(&self) -> &Path {
290 self.0.path()
291 }
292}
293
294#[cfg(target_os = "windows")]
295struct InstallerDir(PathBuf);
296
297#[cfg(target_os = "windows")]
298impl InstallerDir {
299 async fn new() -> Result<Self> {
300 let installer_dir = std::env::current_exe()?
301 .parent()
302 .context("No parent dir for Zed.exe")?
303 .join("updates");
304 if smol::fs::metadata(&installer_dir).await.is_ok() {
305 smol::fs::remove_dir_all(&installer_dir).await?;
306 }
307 smol::fs::create_dir(&installer_dir).await?;
308 Ok(Self(installer_dir))
309 }
310
311 fn path(&self) -> &Path {
312 self.0.as_path()
313 }
314}
315
316#[derive(Clone, Copy, Debug, PartialEq)]
317pub enum UpdateCheckType {
318 Automatic,
319 Manual,
320}
321
322impl UpdateCheckType {
323 pub fn is_manual(self) -> bool {
324 self == Self::Manual
325 }
326}
327
328impl AutoUpdater {
329 pub fn get(cx: &mut App) -> Option<Entity<Self>> {
330 cx.default_global::<GlobalAutoUpdate>().0.clone()
331 }
332
333 fn new(current_version: Version, client: Arc<Client>, cx: &mut Context<Self>) -> Self {
334 // On windows, executable files cannot be overwritten while they are
335 // running, so we must wait to overwrite the application until quitting
336 // or restarting. When quitting the app, we spawn the auto update helper
337 // to finish the auto update process after Zed exits. When restarting
338 // the app after an update, we use `set_restart_path` to run the auto
339 // update helper instead of the app, so that it can overwrite the app
340 // and then spawn the new binary.
341 #[cfg(target_os = "windows")]
342 let quit_subscription = Some(cx.on_app_quit(|_, _| finalize_auto_update_on_quit()));
343 #[cfg(not(target_os = "windows"))]
344 let quit_subscription = None;
345
346 cx.on_app_restart(|this, _| {
347 this.quit_subscription.take();
348 })
349 .detach();
350
351 Self {
352 status: AutoUpdateStatus::Idle,
353 current_version,
354 client,
355 pending_poll: None,
356 quit_subscription,
357 update_check_type: UpdateCheckType::Automatic,
358 }
359 }
360
361 pub fn start_polling(&self, cx: &mut Context<Self>) -> Task<Result<()>> {
362 cx.spawn(async move |this, cx| {
363 if cfg!(target_os = "windows") {
364 use util::ResultExt;
365
366 cleanup_windows()
367 .await
368 .context("failed to cleanup old directories")
369 .log_err();
370 }
371
372 loop {
373 this.update(cx, |this, cx| this.poll(UpdateCheckType::Automatic, cx))?;
374 cx.background_executor().timer(POLL_INTERVAL).await;
375 }
376 })
377 }
378
379 pub fn update_check_type(&self) -> UpdateCheckType {
380 self.update_check_type
381 }
382
383 pub fn poll(&mut self, check_type: UpdateCheckType, cx: &mut Context<Self>) {
384 if self.pending_poll.is_some() {
385 if self.update_check_type == UpdateCheckType::Automatic {
386 self.update_check_type = check_type;
387 cx.notify();
388 }
389 return;
390 }
391 self.update_check_type = check_type;
392
393 cx.notify();
394
395 self.pending_poll = Some(cx.spawn(async move |this, cx| {
396 let result = Self::update(this.upgrade()?, cx).await;
397 this.update(cx, |this, cx| {
398 this.pending_poll = None;
399 if let Err(error) = result {
400 this.status = match check_type {
401 // Be quiet if the check was automated (e.g. when offline)
402 UpdateCheckType::Automatic => {
403 log::info!("auto-update check failed: error:{:?}", error);
404 AutoUpdateStatus::Idle
405 }
406 UpdateCheckType::Manual => {
407 log::error!("auto-update failed: error:{:?}", error);
408 AutoUpdateStatus::Errored {
409 error: Arc::new(error),
410 }
411 }
412 };
413
414 cx.notify();
415 }
416 })
417 .ok()
418 }));
419 }
420
421 pub fn current_version(&self) -> Version {
422 self.current_version.clone()
423 }
424
425 pub fn status(&self) -> AutoUpdateStatus {
426 self.status.clone()
427 }
428
429 pub fn dismiss(&mut self, cx: &mut Context<Self>) -> bool {
430 if let AutoUpdateStatus::Idle = self.status {
431 return false;
432 }
433 self.status = AutoUpdateStatus::Idle;
434 cx.notify();
435 true
436 }
437
438 // If you are packaging Zed and need to override the place it downloads SSH remotes from,
439 // you can override this function. You should also update get_remote_server_release_url to return
440 // Ok(None).
441 pub async fn download_remote_server_release(
442 release_channel: ReleaseChannel,
443 version: Option<Version>,
444 os: &str,
445 arch: &str,
446 set_status: impl Fn(&str, &mut AsyncApp) + Send + 'static,
447 cx: &mut AsyncApp,
448 ) -> Result<PathBuf> {
449 let this = cx.update(|cx| {
450 cx.default_global::<GlobalAutoUpdate>()
451 .0
452 .clone()
453 .context("auto-update not initialized")
454 })?;
455
456 set_status("Fetching remote server release", cx);
457 let release = Self::get_release_asset(
458 &this,
459 release_channel,
460 version,
461 "zed-remote-server",
462 os,
463 arch,
464 cx,
465 )
466 .await?;
467
468 let servers_dir = paths::remote_servers_dir();
469 let channel_dir = servers_dir.join(release_channel.dev_name());
470 let platform_dir = channel_dir.join(format!("{}-{}", os, arch));
471 let version_path = platform_dir.join(format!("{}.gz", release.version));
472 smol::fs::create_dir_all(&platform_dir).await.ok();
473
474 let client = this.read_with(cx, |this, _| this.client.http_client());
475
476 if smol::fs::metadata(&version_path).await.is_err() {
477 log::info!(
478 "downloading zed-remote-server {os} {arch} version {}",
479 release.version
480 );
481 set_status("Downloading remote server", cx);
482 download_remote_server_binary(&version_path, release, client).await?;
483 }
484
485 if let Err(error) =
486 cleanup_remote_server_cache(&platform_dir, &version_path, REMOTE_SERVER_CACHE_LIMIT)
487 .await
488 {
489 log::warn!(
490 "Failed to clean up remote server cache in {:?}: {error:#}",
491 platform_dir
492 );
493 }
494
495 Ok(version_path)
496 }
497
498 pub async fn get_remote_server_release_url(
499 channel: ReleaseChannel,
500 version: Option<Version>,
501 os: &str,
502 arch: &str,
503 cx: &mut AsyncApp,
504 ) -> Result<Option<String>> {
505 let this = cx.update(|cx| {
506 cx.default_global::<GlobalAutoUpdate>()
507 .0
508 .clone()
509 .context("auto-update not initialized")
510 })?;
511
512 let release =
513 Self::get_release_asset(&this, channel, version, "zed-remote-server", os, arch, cx)
514 .await?;
515
516 Ok(Some(release.url))
517 }
518
519 async fn get_release_asset(
520 this: &Entity<Self>,
521 release_channel: ReleaseChannel,
522 version: Option<Version>,
523 asset: &str,
524 os: &str,
525 arch: &str,
526 cx: &mut AsyncApp,
527 ) -> Result<ReleaseAsset> {
528 let client = this.read_with(cx, |this, _| this.client.clone());
529
530 let (system_id, metrics_id, is_staff) = if client.telemetry().metrics_enabled() {
531 (
532 client.telemetry().system_id(),
533 client.telemetry().metrics_id(),
534 client.telemetry().is_staff(),
535 )
536 } else {
537 (None, None, None)
538 };
539
540 let version = if let Some(mut version) = version {
541 version.pre = semver::Prerelease::EMPTY;
542 version.build = semver::BuildMetadata::EMPTY;
543 version.to_string()
544 } else {
545 "latest".to_string()
546 };
547 let http_client = client.http_client();
548
549 let path = format!("/releases/{}/{}/asset", release_channel.dev_name(), version,);
550 let url = http_client.build_zed_cloud_url_with_query(
551 &path,
552 AssetQuery {
553 os,
554 arch,
555 asset,
556 metrics_id: metrics_id.as_deref(),
557 system_id: system_id.as_deref(),
558 is_staff,
559 },
560 )?;
561
562 let mut response = http_client
563 .get(url.as_str(), Default::default(), true)
564 .await?;
565 let mut body = Vec::new();
566 response.body_mut().read_to_end(&mut body).await?;
567
568 anyhow::ensure!(
569 response.status().is_success(),
570 "failed to fetch release: {:?}",
571 String::from_utf8_lossy(&body),
572 );
573
574 serde_json::from_slice(body.as_slice()).with_context(|| {
575 format!(
576 "error deserializing release {:?}",
577 String::from_utf8_lossy(&body),
578 )
579 })
580 }
581
582 async fn update(this: Entity<Self>, cx: &mut AsyncApp) -> Result<()> {
583 let (client, installed_version, previous_status, release_channel) =
584 this.read_with(cx, |this, cx| {
585 (
586 this.client.http_client(),
587 this.current_version.clone(),
588 this.status.clone(),
589 ReleaseChannel::try_global(cx).unwrap_or(ReleaseChannel::Stable),
590 )
591 });
592
593 Self::check_dependencies()?;
594
595 this.update(cx, |this, cx| {
596 this.status = AutoUpdateStatus::Checking;
597 log::info!("Auto Update: checking for updates");
598 cx.notify();
599 });
600
601 let fetched_release_data =
602 Self::get_release_asset(&this, release_channel, None, "zed", OS, ARCH, cx).await?;
603 let fetched_version = fetched_release_data.clone().version;
604 let app_commit_sha = Ok(cx.update(|cx| AppCommitSha::try_global(cx).map(|sha| sha.full())));
605 let newer_version = Self::check_if_fetched_version_is_newer(
606 release_channel,
607 app_commit_sha,
608 installed_version,
609 fetched_version,
610 previous_status.clone(),
611 )?;
612
613 let Some(newer_version) = newer_version else {
614 this.update(cx, |this, cx| {
615 let status = match previous_status {
616 AutoUpdateStatus::Updated { .. } => previous_status,
617 _ => AutoUpdateStatus::Idle,
618 };
619 this.status = status;
620 cx.notify();
621 });
622 return Ok(());
623 };
624
625 this.update(cx, |this, cx| {
626 this.status = AutoUpdateStatus::Downloading {
627 version: newer_version.clone(),
628 };
629 cx.notify();
630 });
631
632 let installer_dir = InstallerDir::new()
633 .await
634 .context("Failed to create installer dir")?;
635 let target_path = Self::target_path(&installer_dir).await?;
636 download_release(&target_path, fetched_release_data, client)
637 .await
638 .with_context(|| format!("Failed to download update to {}", target_path.display()))?;
639
640 this.update(cx, |this, cx| {
641 this.status = AutoUpdateStatus::Installing {
642 version: newer_version.clone(),
643 };
644 cx.notify();
645 });
646
647 let new_binary_path = Self::install_release(installer_dir, &target_path, cx)
648 .await
649 .with_context(|| format!("Failed to install update at: {}", target_path.display()))?;
650 if let Some(new_binary_path) = new_binary_path {
651 cx.update(|cx| cx.set_restart_path(new_binary_path));
652 }
653
654 this.update(cx, |this, cx| {
655 this.set_should_show_update_notification(true, cx)
656 .detach_and_log_err(cx);
657 this.status = AutoUpdateStatus::Updated {
658 version: newer_version,
659 };
660 cx.notify();
661 });
662 Ok(())
663 }
664
665 fn check_if_fetched_version_is_newer(
666 release_channel: ReleaseChannel,
667 app_commit_sha: Result<Option<String>>,
668 installed_version: Version,
669 fetched_version: String,
670 status: AutoUpdateStatus,
671 ) -> Result<Option<VersionCheckType>> {
672 let parsed_fetched_version = fetched_version.parse::<Version>();
673
674 if let AutoUpdateStatus::Updated { version, .. } = status {
675 match version {
676 VersionCheckType::Sha(cached_version) => {
677 let should_download =
678 parsed_fetched_version.as_ref().ok().is_none_or(|version| {
679 version.build.as_str().rsplit('.').next()
680 != Some(&cached_version.full())
681 });
682 let newer_version = should_download
683 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
684 return Ok(newer_version);
685 }
686 VersionCheckType::Semantic(cached_version) => {
687 return Self::check_if_fetched_version_is_newer_non_nightly(
688 cached_version,
689 parsed_fetched_version?,
690 );
691 }
692 }
693 }
694
695 match release_channel {
696 ReleaseChannel::Nightly => {
697 let should_download = app_commit_sha
698 .ok()
699 .flatten()
700 .map(|sha| {
701 parsed_fetched_version.as_ref().ok().is_none_or(|version| {
702 version.build.as_str().rsplit('.').next() != Some(&sha)
703 })
704 })
705 .unwrap_or(true);
706 let newer_version = should_download
707 .then(|| VersionCheckType::Sha(AppCommitSha::new(fetched_version)));
708 Ok(newer_version)
709 }
710 _ => Self::check_if_fetched_version_is_newer_non_nightly(
711 installed_version,
712 parsed_fetched_version?,
713 ),
714 }
715 }
716
717 fn check_dependencies() -> Result<()> {
718 #[cfg(not(target_os = "windows"))]
719 anyhow::ensure!(
720 which::which("rsync").is_ok(),
721 "Could not auto-update because the required rsync utility was not found."
722 );
723 Ok(())
724 }
725
726 async fn target_path(installer_dir: &InstallerDir) -> Result<PathBuf> {
727 let filename = match OS {
728 "macos" => anyhow::Ok("Zed.dmg"),
729 "linux" => Ok("zed.tar.gz"),
730 "windows" => Ok("Zed.exe"),
731 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
732 }?;
733
734 Ok(installer_dir.path().join(filename))
735 }
736
737 async fn install_release(
738 installer_dir: InstallerDir,
739 target_path: &Path,
740 cx: &AsyncApp,
741 ) -> Result<Option<PathBuf>> {
742 #[cfg(test)]
743 if let Some(test_install) =
744 cx.try_read_global::<tests::InstallOverride, _>(|g, _| g.0.clone())
745 {
746 return test_install(target_path, cx);
747 }
748 match OS {
749 "macos" => install_release_macos(&installer_dir, target_path, cx).await,
750 "linux" => install_release_linux(&installer_dir, target_path, cx).await,
751 "windows" => install_release_windows(target_path).await,
752 unsupported_os => anyhow::bail!("not supported: {unsupported_os}"),
753 }
754 }
755
756 fn check_if_fetched_version_is_newer_non_nightly(
757 mut installed_version: Version,
758 fetched_version: Version,
759 ) -> Result<Option<VersionCheckType>> {
760 // For non-nightly releases, ignore build and pre-release fields as they're not provided by our endpoints right now.
761 installed_version.pre = semver::Prerelease::EMPTY;
762 installed_version.build = semver::BuildMetadata::EMPTY;
763 let should_download = fetched_version > installed_version;
764 let newer_version = should_download.then(|| VersionCheckType::Semantic(fetched_version));
765 Ok(newer_version)
766 }
767
768 pub fn set_should_show_update_notification(
769 &self,
770 should_show: bool,
771 cx: &App,
772 ) -> Task<Result<()>> {
773 let kvp = KeyValueStore::global(cx);
774 cx.background_spawn(async move {
775 if should_show {
776 kvp.write_kvp(
777 SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string(),
778 "".to_string(),
779 )
780 .await?;
781 } else {
782 kvp.delete_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY.to_string())
783 .await?;
784 }
785 Ok(())
786 })
787 }
788
789 pub fn should_show_update_notification(&self, cx: &App) -> Task<Result<bool>> {
790 let kvp = KeyValueStore::global(cx);
791 cx.background_spawn(async move {
792 Ok(kvp.read_kvp(SHOULD_SHOW_UPDATE_NOTIFICATION_KEY)?.is_some())
793 })
794 }
795}
796
797async fn download_remote_server_binary(
798 target_path: &PathBuf,
799 release: ReleaseAsset,
800 client: Arc<HttpClientWithUrl>,
801) -> Result<()> {
802 let temp = tempfile::Builder::new().tempfile_in(remote_servers_dir())?;
803 let mut temp_file = File::create(&temp).await?;
804
805 let mut response = client.get(&release.url, Default::default(), true).await?;
806 anyhow::ensure!(
807 response.status().is_success(),
808 "failed to download remote server release: {:?}",
809 response.status()
810 );
811 smol::io::copy(response.body_mut(), &mut temp_file).await?;
812 smol::fs::rename(&temp, &target_path).await?;
813
814 Ok(())
815}
816
817async fn cleanup_remote_server_cache(
818 platform_dir: &Path,
819 keep_path: &Path,
820 limit: usize,
821) -> Result<()> {
822 if limit == 0 {
823 return Ok(());
824 }
825
826 let mut entries = smol::fs::read_dir(platform_dir).await?;
827 let now = SystemTime::now();
828 let mut candidates = Vec::new();
829
830 while let Some(entry) = entries.next().await {
831 let entry = entry?;
832 let path = entry.path();
833 if path.extension() != Some(OsStr::new("gz")) {
834 continue;
835 }
836
837 let mtime = if path == keep_path {
838 now
839 } else {
840 smol::fs::metadata(&path)
841 .await
842 .and_then(|metadata| metadata.modified())
843 .unwrap_or(SystemTime::UNIX_EPOCH)
844 };
845
846 candidates.push((path, mtime));
847 }
848
849 if candidates.len() <= limit {
850 return Ok(());
851 }
852
853 candidates.sort_by(|(path_a, time_a), (path_b, time_b)| {
854 time_b.cmp(time_a).then_with(|| path_a.cmp(path_b))
855 });
856
857 for (index, (path, _)) in candidates.into_iter().enumerate() {
858 if index < limit || path == keep_path {
859 continue;
860 }
861
862 if let Err(error) = smol::fs::remove_file(&path).await {
863 log::warn!(
864 "Failed to remove old remote server archive {:?}: {}",
865 path,
866 error
867 );
868 }
869 }
870
871 Ok(())
872}
873
874async fn download_release(
875 target_path: &Path,
876 release: ReleaseAsset,
877 client: Arc<HttpClientWithUrl>,
878) -> Result<()> {
879 let mut target_file = File::create(&target_path).await?;
880
881 let mut response = client.get(&release.url, Default::default(), true).await?;
882 anyhow::ensure!(
883 response.status().is_success(),
884 "failed to download update: {:?}",
885 response.status()
886 );
887 smol::io::copy(response.body_mut(), &mut target_file).await?;
888 log::info!("downloaded update. path:{:?}", target_path);
889
890 Ok(())
891}
892
893async fn install_release_linux(
894 temp_dir: &InstallerDir,
895 downloaded_tar_gz: &Path,
896 cx: &AsyncApp,
897) -> Result<Option<PathBuf>> {
898 let channel = cx.update(|cx| ReleaseChannel::global(cx).dev_name());
899 let home_dir = PathBuf::from(env::var("HOME").context("no HOME env var set")?);
900 let running_app_path = cx.update(|cx| cx.app_path())?;
901
902 let extracted = temp_dir.path().join("zed");
903 fs::create_dir_all(&extracted)
904 .await
905 .context("failed to create directory into which to extract update")?;
906
907 let mut cmd = new_command("tar");
908 cmd.arg("-xzf")
909 .arg(&downloaded_tar_gz)
910 .arg("-C")
911 .arg(&extracted);
912 let output = cmd
913 .output()
914 .await
915 .with_context(|| "failed to extract: {cmd}")?;
916
917 anyhow::ensure!(
918 output.status.success(),
919 "failed to extract {:?} to {:?}: {:?}",
920 downloaded_tar_gz,
921 extracted,
922 String::from_utf8_lossy(&output.stderr)
923 );
924
925 let suffix = if channel != "stable" {
926 format!("-{}", channel)
927 } else {
928 String::default()
929 };
930 let app_folder_name = format!("zed{}.app", suffix);
931
932 let from = extracted.join(&app_folder_name);
933 let mut to = home_dir.join(".local");
934
935 let expected_suffix = format!("{}/libexec/zed-editor", app_folder_name);
936
937 if let Some(prefix) = running_app_path
938 .to_str()
939 .and_then(|str| str.strip_suffix(&expected_suffix))
940 {
941 to = PathBuf::from(prefix);
942 }
943
944 let mut cmd = new_command("rsync");
945 cmd.args(["-av", "--delete"]).arg(&from).arg(&to);
946 let output = cmd
947 .output()
948 .await
949 .with_context(|| "failed to rsync: {cmd}")?;
950
951 anyhow::ensure!(
952 output.status.success(),
953 "failed to copy Zed update from {:?} to {:?}: {:?}",
954 from,
955 to,
956 String::from_utf8_lossy(&output.stderr)
957 );
958
959 Ok(Some(to.join(expected_suffix)))
960}
961
962async fn install_release_macos(
963 temp_dir: &InstallerDir,
964 downloaded_dmg: &Path,
965 cx: &AsyncApp,
966) -> Result<Option<PathBuf>> {
967 let running_app_path = cx.update(|cx| cx.app_path())?;
968 let running_app_filename = running_app_path
969 .file_name()
970 .with_context(|| format!("invalid running app path {running_app_path:?}"))?;
971
972 let mount_path = temp_dir.path().join("Zed");
973 let mut mounted_app_path: OsString = mount_path.join(running_app_filename).into();
974
975 mounted_app_path.push("/");
976 let mut cmd = new_command("hdiutil");
977 cmd.args(["attach", "-nobrowse"])
978 .arg(&downloaded_dmg)
979 .arg("-mountroot")
980 .arg(temp_dir.path());
981 let output = cmd
982 .output()
983 .await
984 .with_context(|| "failed to mount: {cmd}")?;
985
986 anyhow::ensure!(
987 output.status.success(),
988 "failed to mount: {:?}",
989 String::from_utf8_lossy(&output.stderr)
990 );
991
992 // Create an MacOsUnmounter that will be dropped (and thus unmount the disk) when this function exits
993 let _unmounter = MacOsUnmounter {
994 mount_path: mount_path.clone(),
995 background_executor: cx.background_executor(),
996 };
997
998 let mut cmd = new_command("rsync");
999 cmd.args(["-av", "--delete", "--exclude", "Icon?"])
1000 .arg(&mounted_app_path)
1001 .arg(&running_app_path);
1002 let output = cmd
1003 .output()
1004 .await
1005 .with_context(|| "failed to rsync: {cmd}")?;
1006
1007 anyhow::ensure!(
1008 output.status.success(),
1009 "failed to copy app: {:?}",
1010 String::from_utf8_lossy(&output.stderr)
1011 );
1012
1013 Ok(None)
1014}
1015
1016async fn cleanup_windows() -> Result<()> {
1017 let parent = std::env::current_exe()?
1018 .parent()
1019 .context("No parent dir for Zed.exe")?
1020 .to_owned();
1021
1022 // keep in sync with crates/auto_update_helper/src/updater.rs
1023 _ = smol::fs::remove_dir(parent.join("updates")).await;
1024 _ = smol::fs::remove_dir(parent.join("install")).await;
1025 _ = smol::fs::remove_dir(parent.join("old")).await;
1026
1027 Ok(())
1028}
1029
1030async fn install_release_windows(downloaded_installer: &Path) -> Result<Option<PathBuf>> {
1031 let mut cmd = new_command(downloaded_installer);
1032 cmd.arg("/verysilent")
1033 .arg("/update=true")
1034 .arg("!desktopicon")
1035 .arg("!quicklaunchicon");
1036 let output = cmd.output().await?;
1037 anyhow::ensure!(
1038 output.status.success(),
1039 "failed to start installer: {:?}",
1040 String::from_utf8_lossy(&output.stderr)
1041 );
1042 // We return the path to the update helper program, because it will
1043 // perform the final steps of the update process, copying the new binary,
1044 // deleting the old one, and launching the new binary.
1045 let helper_path = std::env::current_exe()?
1046 .parent()
1047 .context("No parent dir for Zed.exe")?
1048 .join("tools")
1049 .join("auto_update_helper.exe");
1050 Ok(Some(helper_path))
1051}
1052
1053pub async fn finalize_auto_update_on_quit() {
1054 let Some(installer_path) = std::env::current_exe()
1055 .ok()
1056 .and_then(|p| p.parent().map(|p| p.join("updates")))
1057 else {
1058 return;
1059 };
1060
1061 // The installer will create a flag file after it finishes updating
1062 let flag_file = installer_path.join("versions.txt");
1063 if flag_file.exists()
1064 && let Some(helper) = installer_path
1065 .parent()
1066 .map(|p| p.join("tools").join("auto_update_helper.exe"))
1067 {
1068 let mut command = util::command::new_command(helper);
1069 command.arg("--launch");
1070 command.arg("false");
1071 if let Ok(mut cmd) = command.spawn() {
1072 _ = cmd.status().await;
1073 }
1074 }
1075}
1076
1077#[cfg(test)]
1078mod tests {
1079 use client::Client;
1080 use clock::FakeSystemClock;
1081 use futures::channel::oneshot;
1082 use gpui::TestAppContext;
1083 use http_client::{FakeHttpClient, Response};
1084 use settings::default_settings;
1085 use std::{
1086 rc::Rc,
1087 sync::{
1088 Arc,
1089 atomic::{self, AtomicBool},
1090 },
1091 };
1092 use tempfile::tempdir;
1093
1094 #[ctor::ctor]
1095 fn init_logger() {
1096 zlog::init_test();
1097 }
1098
1099 use super::*;
1100
1101 pub(super) struct InstallOverride(pub Rc<dyn Fn(&Path, &AsyncApp) -> Result<Option<PathBuf>>>);
1102 impl Global for InstallOverride {}
1103
1104 #[gpui::test]
1105 fn test_auto_update_defaults_to_true(cx: &mut TestAppContext) {
1106 cx.update(|cx| {
1107 let mut store = SettingsStore::new(cx, &settings::default_settings());
1108 store
1109 .set_default_settings(&default_settings(), cx)
1110 .expect("Unable to set default settings");
1111 store
1112 .set_user_settings("{}", cx)
1113 .expect("Unable to set user settings");
1114 cx.set_global(store);
1115 assert!(AutoUpdateSetting::get_global(cx).0);
1116 });
1117 }
1118
1119 #[gpui::test]
1120 async fn test_auto_update_downloads(cx: &mut TestAppContext) {
1121 cx.background_executor.allow_parking();
1122 zlog::init_test();
1123 let release_available = Arc::new(AtomicBool::new(false));
1124
1125 let (dmg_tx, dmg_rx) = oneshot::channel::<String>();
1126
1127 cx.update(|cx| {
1128 settings::init(cx);
1129
1130 let current_version = semver::Version::new(0, 100, 0);
1131 release_channel::init_test(current_version, ReleaseChannel::Stable, cx);
1132
1133 let clock = Arc::new(FakeSystemClock::new());
1134 let release_available = Arc::clone(&release_available);
1135 let dmg_rx = Arc::new(parking_lot::Mutex::new(Some(dmg_rx)));
1136 let fake_client_http = FakeHttpClient::create(move |req| {
1137 let release_available = release_available.load(atomic::Ordering::Relaxed);
1138 let dmg_rx = dmg_rx.clone();
1139 async move {
1140 if req.uri().path() == "/releases/stable/latest/asset" {
1141 if release_available {
1142 return Ok(Response::builder().status(200).body(
1143 r#"{"version":"0.100.1","url":"https://test.example/new-download"}"#.into()
1144 ).unwrap());
1145 } else {
1146 return Ok(Response::builder().status(200).body(
1147 r#"{"version":"0.100.0","url":"https://test.example/old-download"}"#.into()
1148 ).unwrap());
1149 }
1150 } else if req.uri().path() == "/new-download" {
1151 return Ok(Response::builder().status(200).body({
1152 let dmg_rx = dmg_rx.lock().take().unwrap();
1153 dmg_rx.await.unwrap().into()
1154 }).unwrap());
1155 }
1156 Ok(Response::builder().status(404).body("".into()).unwrap())
1157 }
1158 });
1159 let client = Client::new(clock, fake_client_http, cx);
1160 crate::init(client, cx);
1161 });
1162
1163 let auto_updater = cx.update(|cx| AutoUpdater::get(cx).expect("auto updater should exist"));
1164
1165 cx.background_executor.run_until_parked();
1166
1167 auto_updater.read_with(cx, |updater, _| {
1168 assert_eq!(updater.status(), AutoUpdateStatus::Idle);
1169 assert_eq!(updater.current_version(), semver::Version::new(0, 100, 0));
1170 });
1171
1172 release_available.store(true, atomic::Ordering::SeqCst);
1173 cx.background_executor.advance_clock(POLL_INTERVAL);
1174 cx.background_executor.run_until_parked();
1175
1176 loop {
1177 cx.background_executor.timer(Duration::from_millis(0)).await;
1178 cx.run_until_parked();
1179 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1180 if !matches!(status, AutoUpdateStatus::Idle) {
1181 break;
1182 }
1183 }
1184 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1185 assert_eq!(
1186 status,
1187 AutoUpdateStatus::Downloading {
1188 version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
1189 }
1190 );
1191
1192 dmg_tx.send("<fake-zed-update>".to_owned()).unwrap();
1193
1194 let tmp_dir = Arc::new(tempdir().unwrap());
1195
1196 cx.update(|cx| {
1197 let tmp_dir = tmp_dir.clone();
1198 cx.set_global(InstallOverride(Rc::new(move |target_path, _cx| {
1199 let tmp_dir = tmp_dir.clone();
1200 let dest_path = tmp_dir.path().join("zed");
1201 std::fs::copy(&target_path, &dest_path)?;
1202 Ok(Some(dest_path))
1203 })));
1204 });
1205
1206 loop {
1207 cx.background_executor.timer(Duration::from_millis(0)).await;
1208 cx.run_until_parked();
1209 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1210 if !matches!(status, AutoUpdateStatus::Downloading { .. }) {
1211 break;
1212 }
1213 }
1214 let status = auto_updater.read_with(cx, |updater, _| updater.status());
1215 assert_eq!(
1216 status,
1217 AutoUpdateStatus::Updated {
1218 version: VersionCheckType::Semantic(semver::Version::new(0, 100, 1))
1219 }
1220 );
1221 let will_restart = cx.expect_restart();
1222 cx.update(|cx| cx.restart());
1223 let path = will_restart.await.unwrap().unwrap();
1224 assert_eq!(path, tmp_dir.path().join("zed"));
1225 assert_eq!(std::fs::read_to_string(path).unwrap(), "<fake-zed-update>");
1226 }
1227
1228 #[test]
1229 fn test_stable_does_not_update_when_fetched_version_is_not_higher() {
1230 let release_channel = ReleaseChannel::Stable;
1231 let app_commit_sha = Ok(Some("a".to_string()));
1232 let installed_version = semver::Version::new(1, 0, 0);
1233 let status = AutoUpdateStatus::Idle;
1234 let fetched_version = semver::Version::new(1, 0, 0);
1235
1236 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1237 release_channel,
1238 app_commit_sha,
1239 installed_version,
1240 fetched_version.to_string(),
1241 status,
1242 );
1243
1244 assert_eq!(newer_version.unwrap(), None);
1245 }
1246
1247 #[test]
1248 fn test_stable_does_update_when_fetched_version_is_higher() {
1249 let release_channel = ReleaseChannel::Stable;
1250 let app_commit_sha = Ok(Some("a".to_string()));
1251 let installed_version = semver::Version::new(1, 0, 0);
1252 let status = AutoUpdateStatus::Idle;
1253 let fetched_version = semver::Version::new(1, 0, 1);
1254
1255 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1256 release_channel,
1257 app_commit_sha,
1258 installed_version,
1259 fetched_version.to_string(),
1260 status,
1261 );
1262
1263 assert_eq!(
1264 newer_version.unwrap(),
1265 Some(VersionCheckType::Semantic(fetched_version))
1266 );
1267 }
1268
1269 #[test]
1270 fn test_stable_does_not_update_when_fetched_version_is_not_higher_than_cached() {
1271 let release_channel = ReleaseChannel::Stable;
1272 let app_commit_sha = Ok(Some("a".to_string()));
1273 let installed_version = semver::Version::new(1, 0, 0);
1274 let status = AutoUpdateStatus::Updated {
1275 version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
1276 };
1277 let fetched_version = semver::Version::new(1, 0, 1);
1278
1279 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1280 release_channel,
1281 app_commit_sha,
1282 installed_version,
1283 fetched_version.to_string(),
1284 status,
1285 );
1286
1287 assert_eq!(newer_version.unwrap(), None);
1288 }
1289
1290 #[test]
1291 fn test_stable_does_update_when_fetched_version_is_higher_than_cached() {
1292 let release_channel = ReleaseChannel::Stable;
1293 let app_commit_sha = Ok(Some("a".to_string()));
1294 let installed_version = semver::Version::new(1, 0, 0);
1295 let status = AutoUpdateStatus::Updated {
1296 version: VersionCheckType::Semantic(semver::Version::new(1, 0, 1)),
1297 };
1298 let fetched_version = semver::Version::new(1, 0, 2);
1299
1300 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1301 release_channel,
1302 app_commit_sha,
1303 installed_version,
1304 fetched_version.to_string(),
1305 status,
1306 );
1307
1308 assert_eq!(
1309 newer_version.unwrap(),
1310 Some(VersionCheckType::Semantic(fetched_version))
1311 );
1312 }
1313
1314 #[test]
1315 fn test_nightly_does_not_update_when_fetched_sha_is_same() {
1316 let release_channel = ReleaseChannel::Nightly;
1317 let app_commit_sha = Ok(Some("a".to_string()));
1318 let mut installed_version = semver::Version::new(1, 0, 0);
1319 installed_version.build = semver::BuildMetadata::new("a").unwrap();
1320 let status = AutoUpdateStatus::Idle;
1321 let fetched_sha = "1.0.0+a".to_string();
1322
1323 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1324 release_channel,
1325 app_commit_sha,
1326 installed_version,
1327 fetched_sha,
1328 status,
1329 );
1330
1331 assert_eq!(newer_version.unwrap(), None);
1332 }
1333
1334 #[test]
1335 fn test_nightly_does_update_when_fetched_sha_is_not_same() {
1336 let release_channel = ReleaseChannel::Nightly;
1337 let app_commit_sha = Ok(Some("a".to_string()));
1338 let installed_version = semver::Version::new(1, 0, 0);
1339 let status = AutoUpdateStatus::Idle;
1340 let fetched_sha = "b".to_string();
1341
1342 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1343 release_channel,
1344 app_commit_sha,
1345 installed_version,
1346 fetched_sha.clone(),
1347 status,
1348 );
1349
1350 assert_eq!(
1351 newer_version.unwrap(),
1352 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1353 );
1354 }
1355
1356 #[test]
1357 fn test_nightly_does_not_update_when_fetched_version_is_same_as_cached() {
1358 let release_channel = ReleaseChannel::Nightly;
1359 let app_commit_sha = Ok(Some("a".to_string()));
1360 let mut installed_version = semver::Version::new(1, 0, 0);
1361 installed_version.build = semver::BuildMetadata::new("a").unwrap();
1362 let status = AutoUpdateStatus::Updated {
1363 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1364 };
1365 let fetched_sha = "1.0.0+b".to_string();
1366
1367 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1368 release_channel,
1369 app_commit_sha,
1370 installed_version,
1371 fetched_sha,
1372 status,
1373 );
1374
1375 assert_eq!(newer_version.unwrap(), None);
1376 }
1377
1378 #[test]
1379 fn test_nightly_does_update_when_fetched_sha_is_not_same_as_cached() {
1380 let release_channel = ReleaseChannel::Nightly;
1381 let app_commit_sha = Ok(Some("a".to_string()));
1382 let mut installed_version = semver::Version::new(1, 0, 0);
1383 installed_version.build = semver::BuildMetadata::new("a").unwrap();
1384 let status = AutoUpdateStatus::Updated {
1385 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1386 };
1387 let fetched_sha = "1.0.0+c".to_string();
1388
1389 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1390 release_channel,
1391 app_commit_sha,
1392 installed_version,
1393 fetched_sha.clone(),
1394 status,
1395 );
1396
1397 assert_eq!(
1398 newer_version.unwrap(),
1399 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1400 );
1401 }
1402
1403 #[test]
1404 fn test_nightly_does_update_when_installed_versions_sha_cannot_be_retrieved() {
1405 let release_channel = ReleaseChannel::Nightly;
1406 let app_commit_sha = Ok(None);
1407 let installed_version = semver::Version::new(1, 0, 0);
1408 let status = AutoUpdateStatus::Idle;
1409 let fetched_sha = "a".to_string();
1410
1411 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1412 release_channel,
1413 app_commit_sha,
1414 installed_version,
1415 fetched_sha.clone(),
1416 status,
1417 );
1418
1419 assert_eq!(
1420 newer_version.unwrap(),
1421 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1422 );
1423 }
1424
1425 #[test]
1426 fn test_nightly_does_not_update_when_cached_update_is_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1427 {
1428 let release_channel = ReleaseChannel::Nightly;
1429 let app_commit_sha = Ok(None);
1430 let installed_version = semver::Version::new(1, 0, 0);
1431 let status = AutoUpdateStatus::Updated {
1432 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1433 };
1434 let fetched_sha = "1.0.0+b".to_string();
1435
1436 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1437 release_channel,
1438 app_commit_sha,
1439 installed_version,
1440 fetched_sha,
1441 status,
1442 );
1443
1444 assert_eq!(newer_version.unwrap(), None);
1445 }
1446
1447 #[test]
1448 fn test_nightly_does_update_when_cached_update_is_not_same_as_fetched_and_installed_versions_sha_cannot_be_retrieved()
1449 {
1450 let release_channel = ReleaseChannel::Nightly;
1451 let app_commit_sha = Ok(None);
1452 let installed_version = semver::Version::new(1, 0, 0);
1453 let status = AutoUpdateStatus::Updated {
1454 version: VersionCheckType::Sha(AppCommitSha::new("b".to_string())),
1455 };
1456 let fetched_sha = "c".to_string();
1457
1458 let newer_version = AutoUpdater::check_if_fetched_version_is_newer(
1459 release_channel,
1460 app_commit_sha,
1461 installed_version,
1462 fetched_sha.clone(),
1463 status,
1464 );
1465
1466 assert_eq!(
1467 newer_version.unwrap(),
1468 Some(VersionCheckType::Sha(AppCommitSha::new(fetched_sha)))
1469 );
1470 }
1471}