Add yek --update command for self-updating functionality#213
Conversation
Co-authored-by: mohsen1 <543633+mohsen1@users.noreply.github.com>
Co-authored-by: mohsen1 <543633+mohsen1@users.noreply.github.com>
yek --update command for self-updating functionality
There was a problem hiding this comment.
Greptile Overview
Summary
This PR implements a self-update feature allowing users to update yek using yek --update. The implementation provides cross-platform support for Linux, macOS, and Windows with automatic platform detection and safe binary replacement.
Key Changes
- New CLI flag: Added
--updateflag to trigger self-update functionality - Cross-platform support: Handles x86_64/aarch64 architectures for Linux (musl), macOS (darwin), and Windows (msvc)
- GitHub integration: Downloads releases from the official GitHub repository using curl
- Safety mechanisms: Creates backups before replacement and rolls back on failure
- Custom JSON parsing: Implements lightweight JSON parsing to avoid adding serde_json dependency
Issues Found
- JSON parsing logic has an ordering dependency that could fail if GitHub API response structure changes
- Version comparison uses string equality which doesn't handle semantic versioning properly
- No SSL certificate verification when making HTTP requests
- Some error paths silently ignore failures (backup cleanup)
Testing
The PR includes comprehensive unit tests for platform detection, version extraction, and URL parsing, plus integration tests to verify CLI help text inclusion. The implementation follows existing code patterns and maintains backward compatibility.
Confidence Score: 3/5
- This PR has moderate risk due to JSON parsing and version comparison logic issues, but includes good safety mechanisms
- Score reflects identified logical issues in JSON parsing ordering dependency and version comparison that could cause update failures, balanced against comprehensive testing and safety mechanisms like backup/restore functionality
- Pay close attention to src/config.rs JSON parsing and version comparison logic
Important Files Changed
File Analysis
| Filename | Score | Overview |
|---|---|---|
| src/config.rs | 3/5 | Adds self-update functionality with JSON parsing and cross-platform support, but has potential security and robustness issues |
| tests/config_test.rs | 5/5 | Comprehensive unit tests for update functionality including target detection, version extraction, and URL parsing |
| tests/main_test.rs | 5/5 | Integration test to verify --update flag appears in help output |
Sequence Diagram
sequenceDiagram
participant User
participant Yek as yek CLI
participant FS as File System
participant GitHub as GitHub API
participant Curl as curl command
participant Tar as tar command
User->>Yek: yek --update
Yek->>FS: Get current executable path
FS-->>Yek: Return executable path
Yek->>FS: Check if executable is writable
FS-->>Yek: Return permissions status
Yek->>Yek: Determine target triple
Yek->>Curl: curl GitHub releases API
Curl->>GitHub: GET /repos/bodo-run/yek/releases/latest
GitHub-->>Curl: Return release JSON
Curl-->>Yek: Release JSON response
Yek->>Yek: Parse JSON for version & download URL
alt Version is same
Yek-->>User: Already latest version
else Version is different
Yek->>Curl: Download release archive
Curl->>GitHub: GET release archive
GitHub-->>Curl: Return tar.gz file
Curl-->>Yek: Downloaded archive
Yek->>FS: Create temp directory
Yek->>Tar: Extract archive to temp
Tar->>FS: Extract files
Tar-->>Yek: Extraction complete
Yek->>FS: Create backup of current binary
Yek->>FS: Replace current binary with new one
alt Unix system
Yek->>FS: Set executable permissions
end
Yek->>FS: Remove backup file
Yek->>FS: Cleanup temp directory
Yek-->>User: Update successful
end
3 files reviewed, 5 comments
| // Simple JSON parsing to find the browser_download_url for our asset | ||
| let lines: Vec<&str> = json.lines().collect(); | ||
| let mut found_asset = false; | ||
|
|
||
| for line in lines.iter() { | ||
| // Look for the asset name | ||
| if line.contains(&format!("\"name\": \"{}\"", asset_name)) { | ||
| found_asset = true; | ||
| continue; | ||
| } | ||
|
|
||
| // If we found our asset, look for the download URL in nearby lines | ||
| if found_asset && line.contains("browser_download_url") { | ||
| if let Some(url_start) = line.find("https://") { | ||
| if let Some(url_end) = line[url_start..].find('"') { | ||
| let url = &line[url_start..url_start + url_end]; | ||
| return Ok(url.to_string()); | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
logic: JSON parsing logic may fail if browser_download_url appears before the matching asset name in the response. The parser assumes a strict order where the asset name comes first.
| // Simple JSON parsing to find the browser_download_url for our asset | |
| let lines: Vec<&str> = json.lines().collect(); | |
| let mut found_asset = false; | |
| for line in lines.iter() { | |
| // Look for the asset name | |
| if line.contains(&format!("\"name\": \"{}\"", asset_name)) { | |
| found_asset = true; | |
| continue; | |
| } | |
| // If we found our asset, look for the download URL in nearby lines | |
| if found_asset && line.contains("browser_download_url") { | |
| if let Some(url_start) = line.find("https://") { | |
| if let Some(url_end) = line[url_start..].find('"') { | |
| let url = &line[url_start..url_start + url_end]; | |
| return Ok(url.to_string()); | |
| } | |
| } | |
| } | |
| } | |
| /// Extract download URL from GitHub releases API JSON response | |
| pub fn extract_download_url(json: &str, asset_name: &str) -> Result<String> { | |
| // Parse assets array and find matching name with its download URL | |
| let mut current_asset = None; | |
| let mut current_url = None; | |
| for line in json.lines() { | |
| // Look for asset name | |
| if let Some(name_start) = line.find("\"name\": \"") { | |
| let name_start = name_start + 9; | |
| if let Some(name_end) = line[name_start..].find('"') { | |
| current_asset = Some(&line[name_start..name_start + name_end]); | |
| } | |
| } | |
| // Look for download URL | |
| if line.contains("browser_download_url") { | |
| if let Some(url_start) = line.find("https://") { | |
| if let Some(url_end) = line[url_start..].find('"') { | |
| current_url = Some(&line[url_start..url_start + url_end]); | |
| } | |
| } | |
| } | |
| // If we have both and they match, return the URL | |
| if let (Some(asset), Some(url)) = (current_asset, current_url) { | |
| if asset == asset_name { | |
| return Ok(url.to_string()); | |
| } | |
| } | |
| // Reset when we hit a new asset object | |
| if line.trim() == "}" && current_asset.is_some() { | |
| current_asset = None; | |
| current_url = None; | |
| } | |
| } | |
| Err(anyhow!( | |
| "Could not find download URL for asset: {}", | |
| asset_name | |
| )) | |
| } |
| if new_version == current_version { | ||
| println!("You are already running the latest version!"); | ||
| return Ok(()); | ||
| } |
There was a problem hiding this comment.
logic: String comparison for version checking may fail with semantic versioning. "1.10.0" < "1.2.0" lexicographically but is actually newer.
| if new_version == current_version { | |
| println!("You are already running the latest version!"); | |
| return Ok(()); | |
| } | |
| if new_version == current_version { | |
| println!("You are already running the latest version!"); | |
| return Ok(()); | |
| } | |
| // Check if we're trying to downgrade (simple comparison) | |
| // Note: This is a basic check - for production use, consider using a semver crate | |
| if Self::is_version_newer(current_version, &new_version)? { | |
| println!("You are already running a newer version ({}) than available ({})", | |
| current_version, new_version); | |
| return Ok(()); | |
| } |
| let releases_output = Command::new("curl") | ||
| .args(["-s", &releases_url]) | ||
| .output() | ||
| .map_err(|e| anyhow!("Failed to execute curl command: {}. Is curl installed?", e))?; | ||
|
|
||
| if !releases_output.status.success() { | ||
| let stderr = String::from_utf8_lossy(&releases_output.stderr); | ||
| return Err(anyhow!("Failed to fetch release info: {}", stderr)); | ||
| } |
There was a problem hiding this comment.
style: No SSL certificate verification when using curl. Add certificate verification for security.
| let extracted_dir = temp_dir.join(format!("yek-{}", target)); | ||
| let new_binary = extracted_dir.join("yek"); |
There was a problem hiding this comment.
style: Hardcoded binary name "yek" may not work if the extracted archive has a different structure or binary name on different platforms.
| // Remove backup on success | ||
| let _ = fs::remove_file(&backup_path); |
There was a problem hiding this comment.
style: Silently ignoring backup removal failure could leave backup files behind. Consider logging the failure.
This PR implements a self-update feature that allows users to update
yekto the latest version directly from the command line usingyek --update.Features
The implementation provides:
Usage
Implementation Details
The feature uses a minimal approach that:
curlandtar) to avoid additional Rust dependenciesExample Output
Testing
Added comprehensive tests including:
The implementation follows existing project patterns and maintains code quality standards with full lint and format compliance.
Fixes #[issue-number]
Warning
Firewall rules blocked me from connecting to one or more addresses (expand for details)
I tried to connect to the following addresses, but was blocked by firewall rules:
https://api.github.com/repos/bodo-run/yek/releases/latestcurl -s REDACTED(http block)If you need me to access, download, or install something from one of these locations, you can either:
Original prompt
💬 Share your feedback on Copilot coding agent for the chance to win a $200 gift card! Click here to start the survey.