Repository: denoland/deno
Pull request: feat(upgrade): add context to error messages for better troubleshooting

Users encountering "Permission denied (os error 13)" during `deno upgrade` had no way to determine which operation was failing. This is particularly problematic on systems where the temp directory may have restricted permissions (e.g., Synology DSM).

Adds context to error-prone operations in the upgrade flow:

- `tempfile::TempDir::new()` → "failed to create temporary directory"
- Archive extraction → "failed to extract archive"  
- `fs::set_permissions()` → "failed to set permissions on '{path}'"
- `fs::remove_file()` (dry-run) → "failed to remove '{path}'"
- `std::env::current_exe()` → "failed to get the path of the current executable"
- `fs::metadata()` → "failed to get metadata of the current executable at '{path}'"
- Replace executable (non-Windows) → "failed to replace the executable at '{path}'"
- `check_exe()` command execution → "failed to run '{path}'"

Before:
```
error: Permission denied (os error 13)
```

After:
```
error: failed to create temporary directory: Permission denied (os error 13)
```

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Cannot upgrade deno on Synology DSM - error: Permission denied (os error 13)</issue_title>
> <issue_description>Version: Deno 2.6.6
> 
> Quite simply, I cannot upgrade the static binary of Deno on a Synology. Despite it giving a generic "permission denied" error, it does not appear to be, as I can give it incredibly broad permissions and it still fails.
> 
> ```
> jochs@synology ~ $ ls -l /volume1/Apps/Sync/General/bin/Linux/deno
> -rwxrwxrwx 1 jochs users 116104568 Jan 22 14:57 /volume1/Apps/Sync/General/bin/Linux/deno*
> jochs@synology ~ $ whoami
> jochs
> jochs@synology ~ $ /volume1/Apps/Sync/General/bin/Linux/deno upgrade
> Current Deno version: v2.6.6
> Looking up stable version
> 
> Found latest stable version v2.6.7
> 
> Downloading https://github.com/denoland/deno/releases/download/v2.6.7/deno-x86_64-unknown-linux-gnu.zip
> Deno is upgrading to version 2.6.7
> error: Permission denied (os error 13)
> jochs@synology ~ $ /volume1/Apps/Sync/General/bin/Linux/deno upgrade --dry-run
> Current Deno version: v2.6.6
> Looking up stable version
> 
> Found latest stable version v2.6.7
> 
> Downloading https://github.com/denoland/deno/releases/download/v2.6.7/deno-x86_64-unknown-linux-gnu.zip
> Deno is upgrading to version 2.6.7
> error: Permission denied (os error 13)
> jochs@synology ~ $ /volume1/Apps/Sync/General/bin/Linux/deno upgrade --output /Volumes/Apps/bin/
> Current Deno version: v2.6.6
> Looking up stable version
> 
> Found latest stable version v2.6.7
> 
> Downloading https://github.com/denoland/deno/releases/download/v2.6.7/deno-x86_64-unknown-linux-gnu.zip
> Deno is upgrading to version 2.6.7
> error: Permission denied (os error 13)
> jochs@synology ~ $ /volume1/Apps/Sync/General/bin/Linux/deno upgrade --output ~/
> Current Deno version: v2.6.6
> Looking up stable version
> 
> Found latest stable version v2.6.7
> 
> Downloading https://github.com/denoland/deno/releases/download/v2.6.7/deno-x86_64-unknown-linux-gnu.zip
> Deno is upgrading to version 2.6.7
> error: Permission denied (os error 13)
> jochs@synology ~ $ 
> ```
> 
> Regardless of the path chosen, the permissions set on the directory and binary, I consistently get "Permission denied". However, the lack of any verbose output means I don't know what permissions are the problem, or what it's trying to do when the error occurs.
> 
> Anything I can do to further troubleshoot this would be welcome.</issue_description>
> 
> <agent_instructions>Inspector `upgrade` function in `cli/tools/upgrade.rs` and add `.context()` or `.with_context()` calls to places that use `?` to bubble up errors. Especially look for places that create temporary directories.</agent_instructions>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> <comment_new><author>@littledivy</author><body>
> This could be a permission issue with temp dir. Does it work if you set it to somewhere else?
> ```
> mkdir -p "$HOME/tmp"
> TMPDIR="$HOME/tmp" /volume1/Apps/Sync/General/bin/Linux/deno upgrade
> ```</body></comment_new>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes denoland/deno#31995

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs.

Changed files:
filename: cli/tools/upgrade.rs
patch: @@ -484,7 +484,8 @@ pub async fn upgrade(
   let factory = CliFactory::from_flags(flags);
   let http_client_provider = factory.http_client_provider();
   let client = http_client_provider.get_or_create()?;
-  let current_exe_path = std::env::current_exe()?;
+  let current_exe_path = std::env::current_exe()
+    .context("failed to get the path of the current executable")?;
   let full_path_output_flag = match &upgrade_flags.output {
     Some(output) => Some(
       std::env::current_dir()
@@ -554,19 +555,25 @@ pub async fn upgrade(
     ))
   );
 
-  let temp_dir = tempfile::TempDir::new()?;
+  let temp_dir =
+    tempfile::TempDir::new().context("failed to create temporary directory")?;
   let new_exe_path = archive::unpack_into_dir(archive::UnpackArgs {
     exe_name: "deno",
     archive_name: &ARCHIVE_NAME,
     archive_data: &archive_data,
     is_windows: cfg!(windows),
     dest_path: temp_dir.path(),
+  })
+  .context("failed to extract archive")?;
+  fs::set_permissions(&new_exe_path, permissions).with_context(|| {
+    format!("failed to set permissions on '{}'", new_exe_path.display())
   })?;
-  fs::set_permissions(&new_exe_path, permissions)?;
   check_exe(&new_exe_path)?;
 
   if upgrade_flags.dry_run {
-    fs::remove_file(&new_exe_path)?;
+    fs::remove_file(&new_exe_path).with_context(|| {
+      format!("failed to remove '{}'", new_exe_path.display())
+    })?;
     log::info!("Upgraded successfully (dry run)");
     if requested_version.release_channel() == ReleaseChannel::Stable {
       print_release_notes(
@@ -964,7 +971,12 @@ fn check_windows_access_denied_error(
   };
 
   if !cfg!(windows) {
-    return Err(err.into());
+    return Err(err).with_context(|| {
+      format!(
+        "failed to replace the executable at '{}'",
+        output_exe_path.display()
+      )
+    });
   }
 
   const WIN_ERROR_ACCESS_DENIED: i32 = 5;
@@ -1021,7 +1033,12 @@ fn set_exe_permissions(
   output_exe_path: &Path,
 ) -> Result<std::fs::Permissions, AnyError> {
   let Ok(metadata) = fs::metadata(output_exe_path) else {
-    let metadata = fs::metadata(current_exe_path)?;
+    let metadata = fs::metadata(current_exe_path).with_context(|| {
+      format!(
+        "failed to get metadata of the current executable at '{}'",
+        current_exe_path.display()
+      )
+    })?;
     return Ok(metadata.permissions());
   };
 
@@ -1052,7 +1069,8 @@ fn check_exe(exe_path: &Path) -> Result<(), AnyError> {
   let output = Command::new(exe_path)
     .arg("-V")
     .stderr(std::process::Stdio::inherit())
-    .output()?;
+    .output()
+    .with_context(|| format!("failed to run '{}'", exe_path.display()))?;
   if !output.status.success() {
     bail!(
       "Failed to validate Deno executable. This may be because your OS is unsupported or the executable is corrupted"