Skip to content

fix(upgrade): replace curl pipe with direct binary download#208

Merged
BYK merged 7 commits intomainfrom
fix/curl-upgrade-bun-compat
Feb 6, 2026
Merged

fix(upgrade): replace curl pipe with direct binary download#208
BYK merged 7 commits intomainfrom
fix/curl-upgrade-bun-compat

Conversation

@BYK
Copy link
Member

@BYK BYK commented Feb 6, 2026

Summary

Fixes sentry cli upgrade --method curl failing with Error: TODO: stream.Readable stdio @ 0.

The curl upgrade method used node:child_process.spawn to pipe curl's stdout to bash's stdin, but Bun's Node.js compatibility layer doesn't support stream.Readable in stdio options.

Fixes CLI-39.

Changes

Core Fix

  • Replace shell-based curl | bash with direct binary download via fetch()
  • Download binary to temp file (.download), then atomically rename to target
  • Handle Windows "file in use" by renaming running exe to .old first
  • Clean up .old and .download files on next CLI startup
  • Extend Bun.write() polyfill to handle Response, ArrayBuffer, Uint8Array

Concurrent Upgrade Protection

  • Added PID-based lock file mechanism to prevent concurrent upgrades
  • acquireUpgradeLock() checks if lock file exists, reads PID, verifies if process is still running
  • Stale locks from dead processes are automatically cleaned up
  • Lock is always released in finally block, even on failure

Test Coverage

  • Exported lock functions (isProcessRunning, acquireUpgradeLock, releaseUpgradeLock) for testing
  • Exported helper functions (getBinaryDownloadUrl, getCurlInstallPaths) for testing
  • Added comprehensive tests for:
    • Lock acquisition/release mechanics
    • Stale lock detection and cleanup
    • executeUpgrade("curl", ...) integration tests
    • Binary download success/failure scenarios
  • Function coverage improved from ~65% to ~85%

Testing

bun test test/lib/upgrade.test.ts
# 54 pass, 0 fail

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.
@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


Bug Fixes 🐛

  • (upgrade) Replace curl pipe with direct binary download by BYK in #208

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Feb 6, 2026

Codecov Results 📊

✅ Patch coverage is 84.91%. Project has 1959 uncovered lines.
✅ Project coverage is 76.72%. Comparing base (base) to head (head).

Files with missing lines (40)
File Patch % Lines
human.ts 55.31% ⚠️ 391 Missing
resolve-target.ts 15.28% ⚠️ 366 Missing
oauth.ts 25.10% ⚠️ 194 Missing
api-client.ts 70.62% ⚠️ 176 Missing
upgrade.ts 66.99% ⚠️ 102 Missing
view.ts 47.70% ⚠️ 91 Missing
migration.ts 47.44% ⚠️ 82 Missing
browser.ts 4.11% ⚠️ 70 Missing
span-tree.ts 5.00% ⚠️ 57 Missing
telemetry.ts 77.51% ⚠️ 56 Missing
api.ts 89.80% ⚠️ 47 Missing
seer.ts 75.54% ⚠️ 45 Missing
schema.ts 89.56% ⚠️ 40 Missing
seer.ts 79.87% ⚠️ 30 Missing
preload.ts 53.23% ⚠️ 29 Missing
utils.ts 88.94% ⚠️ 25 Missing
detector.ts 90.10% ⚠️ 20 Missing
output.ts 15.00% ⚠️ 17 Missing
code-scanner.ts 95.00% ⚠️ 16 Missing
arg-parsing.ts 90.00% ⚠️ 12 Missing
dsn-cache.ts 94.62% ⚠️ 12 Missing
fix.ts 83.61% ⚠️ 10 Missing
qrcode.ts 33.33% ⚠️ 10 Missing
fs-utils.ts 57.14% ⚠️ 9 Missing
project-root.ts 97.73% ⚠️ 7 Missing
version-check.ts 91.76% ⚠️ 7 Missing
feedback.ts 84.21% ⚠️ 6 Missing
auth.ts 95.52% ⚠️ 6 Missing
upgrade.ts 93.83% ⚠️ 5 Missing
resolver.ts 94.57% ⚠️ 5 Missing
index.ts 95.96% ⚠️ 4 Missing
project-aliases.ts 97.40% ⚠️ 2 Missing
project-root-cache.ts 96.92% ⚠️ 2 Missing
json.ts 33.33% ⚠️ 2 Missing
alias.ts 99.42% ⚠️ 1 Missing
env-file.ts 99.19% ⚠️ 1 Missing
parser.ts 98.63% ⚠️ 1 Missing
colors.ts 97.96% ⚠️ 1 Missing
helpers.ts 94.74% ⚠️ 1 Missing
helpers.ts 94.74% ⚠️ 1 Missing
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

Copy link
Member Author

@BYK BYK left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@BYK
Copy link
Member Author

BYK commented Feb 6, 2026

Addressed all review feedback:

Inline comment: Use async unlink()

✅ Changed to fire-and-forget unlink().catch() from fs/promises for non-blocking cleanup.

Main review: Concurrent upgrades & interrupted downloads

Concurrent upgrades:

  • Added PID-based lock file mechanism
  • acquireUpgradeLock() checks if lock file exists, reads PID, verifies if that process is still running
  • If process is dead → stale lock, clean up and proceed
  • If process is alive → throw error: "Another upgrade is already in progress"
  • Lock is released in finally block

Interrupted downloads:

  • cleanupOldBinary() now also cleans up .download files on startup
  • executeUpgradeCurl() cleans up any leftover temp file before starting download

Lock file safety:

  • Lock file contains PID of owning process
  • On next run, we check if that PID is still alive using process.kill(pid, 0)
  • Dead process → stale lock → safe to remove and proceed

BYK added 2 commits February 6, 2026 11:27
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.
@BYK BYK requested a review from betegon February 6, 2026 11:34
@BYK BYK marked this pull request as ready for review February 6, 2026 11:47
@getsentry getsentry deleted a comment from linear bot Feb 6, 2026
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.
@BYK
Copy link
Member Author

BYK commented Feb 6, 2026

Re: Sentry Bot's Windows Rename Concern

The Sentry bot flagged the Windows upgrade logic at lines 483-502 as a bug, claiming renameSync will fail because Windows locks running executables.

This is a false positive. Windows file locking behavior is:

  • Cannot delete a running executable
  • Can rename a running executable

This is a well-documented and widely-used self-update pattern. From Super User:

"There really is no such thing as renaming a file... Renaming is an operation on the directory entry, which is not affected by the fact that the file is locked for execution."

This pattern is used by:

  • Chrome - renames running binary during update
  • VS Code - same approach for self-updates
  • Electron apps - documented pattern in Electron's auto-updater

The code is correct:

  1. Rename running sentry.exesentry.exe.old (allowed)
  2. Write new binary to sentry.exe (target path is now free)
  3. Clean up .old on next startup (can now delete since not running)

No changes needed.

@BYK
Copy link
Member Author

BYK commented Feb 6, 2026

Updated Analysis: Windows Rename of Running Executables

After thorough research, here's a comprehensive analysis of the Sentry bot's concern about renameSync failing on Windows.

TL;DR

The implementation is correct. Windows allows renaming running executables but blocks deletion. This is a well-established self-update pattern.


Evidence

1. Super User - Technical Explanation

superuser.com/questions/488127

David Schwartz (62.5k reputation):

"There really is no such thing as renaming a file... Renaming is an operation on the directory entry, which is not affected by the fact that the file is locked for execution."

2. Windows File Locking Mechanics

unix.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/Squirrel

electronjs.org/docs/latest/tutorial/updates

The Squirrel framework uses this exact pattern for self-updates. Apps using it:

  • VS Code
  • Slack Desktop
  • Discord
  • GitHub Desktop

If renaming running executables failed on Windows, these apps couldn't self-update.


The Bot's Error

The Sentry bot conflates two different operations:

  • Deletion - Windows blocks this on running executables
  • Rename - Windows allows this (operates on directory entry, not file data)

Conclusion

The 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.
@BYK BYK merged commit 37a8841 into main Feb 6, 2026
24 checks passed
@BYK BYK deleted the fix/curl-upgrade-bun-compat branch February 6, 2026 12:52
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant