fix(upgrade): replace curl pipe with direct binary download#208
Conversation
The curl upgrade method failed in Bun because node:child_process.spawn doesn't support passing stream.Readable to stdio options. Instead of piping curl output to bash, we now: - Download the binary directly via fetch() - Write to a temp file, then atomically rename - Handle Windows by renaming the running exe to .old first - Clean up .old files on next CLI startup Also extends Bun.write() polyfill to handle Response objects.
Semver Impact of This PR🟢 Patch (bug fixes) 📋 Changelog PreviewThis is how your changes will appear in the changelog. Bug Fixes 🐛
🤖 This preview updates automatically when you update the PR. |
Codecov Results 📊✅ Patch coverage is 84.91%. Project has 1959 uncovered lines. Files with missing lines (40)
Coverage diff@@ Coverage Diff @@
## main #PR +/-##
==========================================
+ Coverage 76.13% 76.72% +0.59%
==========================================
Files 65 65 —
Lines 8362 8416 +54
Branches 0 0 —
==========================================
+ Hits 6366 6457 +91
- Misses 1996 1959 -37
- Partials 0 0 —Generated by Codecov Action |
BYK
left a comment
There was a problem hiding this comment.
I think we may have weird failures in case somehow we end up having multiple sentry cli upgrade calls running at the same time. Also, if the download gets interrupted, what do we do with the left-over file? No clean up? No resume?
- Use async unlink() for non-blocking fire-and-forget cleanup - Add PID-based lock file to prevent concurrent upgrade race conditions - Clean up leftover .download files from interrupted downloads - Update cleanupOldBinary() to clean both .old and .download files - Add tests for .download file cleanup
|
Addressed all review feedback: Inline comment: Use async unlink()✅ Changed to fire-and-forget Main review: Concurrent upgrades & interrupted downloadsConcurrent upgrades:
Interrupted downloads:
Lock file safety:
|
Export lock functions and add tests for: - getBinaryDownloadUrl - getCurlInstallPaths - isProcessRunning - acquireUpgradeLock / releaseUpgradeLock - executeUpgrade curl method integration tests Function coverage improved from ~65% to ~85%
The acquireUpgradeLock tests failed on CI because the ~/.sentry/bin directory doesn't exist on the GitHub runner.
getBinaryDownloadUrl was constructing URLs without the 'v' prefix: https://github.com/.../download/1.0.0/sentry-... But GitHub release assets require the tag name with 'v' prefix: https://github.com/.../download/v1.0.0/sentry-... This would cause all curl upgrades to fail with 404 Not Found.
1. cleanupOldBinary: Remove .download cleanup to avoid deleting temp
files from active upgrades in other processes. The .download cleanup
is already handled inside executeUpgradeCurl() under the exclusive lock.
2. acquireUpgradeLock: Use atomic { flag: 'wx' } for lock creation to
prevent race conditions where two processes could both believe they
hold the lock. If file exists, check if stale and retry atomically.
Re: Sentry Bot's Windows Rename ConcernThe Sentry bot flagged the Windows upgrade logic at lines 483-502 as a bug, claiming This is a false positive. Windows file locking behavior is:
This is a well-documented and widely-used self-update pattern. From Super User:
This pattern is used by:
The code is correct:
No changes needed. |
Updated Analysis: Windows Rename of Running ExecutablesAfter thorough research, here's a comprehensive analysis of the Sentry bot's concern about TL;DRThe implementation is correct. Windows allows renaming running executables but blocks deletion. This is a well-established self-update pattern. Evidence1. Super User - Technical Explanationsuperuser.com/questions/488127 David Schwartz (62.5k reputation):
2. Windows File Locking Mechanicsunix.stackexchange.com/questions/49299 Windows memory-maps executable files during process creation. This prevents deletion (freeing data blocks) but not renaming (modifying directory entry metadata). 3. Real-World Usage: Electron/Squirrelelectronjs.org/docs/latest/tutorial/updates The Squirrel framework uses this exact pattern for self-updates. Apps using it:
If renaming running executables failed on Windows, these apps couldn't self-update. The Bot's ErrorThe Sentry bot conflates two different operations:
ConclusionThe current implementation follows a well-established Windows self-update pattern used by major applications. No changes needed. Note: I couldn't find official Microsoft documentation explicitly confirming this, but the evidence from multiple technical sources and real-world usage by major apps is strong. |
handleExistingLock was catching all errors when reading the lock file, but only ENOENT (file disappeared) should trigger a retry. Other errors like EACCES (permission denied) would cause infinite mutual recursion between acquireUpgradeLock and handleExistingLock, leading to stack overflow. Now both the read and unlink catch blocks check for specific error codes: - ENOENT: safe to retry (file disappeared or already gone) - Other errors: re-throw to avoid infinite recursion Added test that creates an unreadable lock file to verify the fix.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
Summary
Fixes
sentry cli upgrade --method curlfailing withError: TODO: stream.Readable stdio @ 0.The curl upgrade method used
node:child_process.spawnto pipe curl's stdout to bash's stdin, but Bun's Node.js compatibility layer doesn't supportstream.Readableinstdiooptions.Fixes CLI-39.
Changes
Core Fix
curl | bashwith direct binary download viafetch().download), then atomically rename to target.oldfirst.oldand.downloadfiles on next CLI startupBun.write()polyfill to handleResponse,ArrayBuffer,Uint8ArrayConcurrent Upgrade Protection
acquireUpgradeLock()checks if lock file exists, reads PID, verifies if process is still runningfinallyblock, even on failureTest Coverage
isProcessRunning,acquireUpgradeLock,releaseUpgradeLock) for testinggetBinaryDownloadUrl,getCurlInstallPaths) for testingexecuteUpgrade("curl", ...)integration testsTesting