Skip to content

fix(upgrade): create missing install dir and skip stale stored path#1125

Merged
BYK merged 4 commits into
mainfrom
byk/fix/upgrade-missing-install-dir
Jun 23, 2026
Merged

fix(upgrade): create missing install dir and skip stale stored path#1125
BYK merged 4 commits into
mainfrom
byk/fix/upgrade-missing-install-dir

Conversation

@BYK

@BYK BYK commented Jun 23, 2026

Copy link
Copy Markdown
Member

Problem

sentry cli upgrade crashes with Unexpected error: ENOENT ... open '.../sentry.lock' when the install directory does not exist at the moment the lock file is written. Reported by sergical in #discuss-cli (2026-06-22):

➜ sentry-mcp git:(main) sentry cli upgrade
Unexpected error: Error: ENOENT: no such file or directory, open '/tmp/sentry-test-install/sentry.lock'
    at writeFileSync ...
    at acquireLock (src/lib/binary.ts)
    at downloadBinaryToTemp (src/lib/upgrade.ts)

Root cause

The upgrade pipeline derives the lock / .download paths from an install location it does not validate, and writes the lock before that directory is guaranteed to exist. Two triggers:

  1. Stale stored path (sergical): a SENTRY_INSTALL_DIR=/tmp/sentry-test-install test install recorded install.path there; the directory was later purged but the DB row survived. getCurlInstallPaths() trusted it blindly, so the upgrade tried to lock into a dead directory.
  2. Fallback dir never created: the ~/.sentry/bin fallback (and the npm/brew→nightly migration that funnels through downloadBinaryToTemp) is never mkdir-ed before acquireLock.

Fix

  • acquireLock() now mkdir -ps the lock file's parent directory before the atomic writeFileSync, so fresh installs, migrations, and purged-dir cases no longer crash.
  • getCurlInstallPaths() only trusts a stored curl path when its directory still exists, otherwise falling through to the running-binary / ~/.sentry/bin resolution.

The 2026-06-01 copyfile symlink variant is already handled on main by the realpath self-copy guard (#1046), so it is out of scope here.

Tests

Red-first regression tests added (both confirmed to reproduce the exact ENOENT before the fix):

  • acquireLock creates the parent directory when it does not exist.
  • getCurlInstallPaths ignores a stale stored path whose directory no longer exists (and still uses a stored path whose directory exists).

pnpm run typecheck and Biome lint pass; full unit suite green (one unrelated sourcemap/zip timeout flake that passes in isolation).

Related Sentry issues

Fixes CLI-1E1 (/home/developer/.sentry/bin/sentry.lock, 9 events / 2 users) and CLI-1RV (/root/.sentry/bin/sentry.lock).

Out of scope

sentry cli --version support (sergical also asked) is deferred to a separate change. sentry upgrade is intentionally not aliased (reserved for SDK upgrade flows).

`sentry cli upgrade` crashed with `ENOENT ... open '.../sentry.lock'`
when the install directory did not exist at lock time. Two triggers:

- A stale stored install path: a `SENTRY_INSTALL_DIR` test install (e.g.
  `/tmp/sentry-test-install`) recorded `install.path` there, the directory
  was later purged, but the DB row survived. `getCurlInstallPaths()` trusted
  it blindly and locked into a dead location.
- The `~/.sentry/bin` fallback (and npm→nightly migration) is never created
  before `acquireLock` writes the lock file.

Fixes:
- `acquireLock` now `mkdir -p`s the lock file's parent directory before
  writing, so the fresh/migration/purged cases no longer crash.
- `getCurlInstallPaths` only trusts a stored curl path when its directory
  still exists, falling through to the running-binary / `~/.sentry/bin`
  resolution otherwise.

Adds red-first regression tests for both paths.

Fixes CLI-1E1, CLI-1RV.
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://cli.sentry.dev/_preview/pr-1125/

Built to branch gh-pages at 2026-06-23 13:21 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

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.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 247666c. Configure here.

Comment thread src/lib/binary.ts
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Codecov Results 📊

✅ Patch coverage is 100.00%. Project has 5047 uncovered lines.
✅ Project coverage is 81.28%. Comparing base (base) to head (head).

Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    81.27%    81.28%    +0.01%
==========================================
  Files          390       390         —
  Lines        26963     26964        +1
  Branches     17520     17521        +1
==========================================
+ Hits         21913     21917        +4
- Misses        5050      5047        -3
- Partials      1823      1823         —

Generated by Codecov Action

BYK added 2 commits June 23, 2026 12:28
…rsion

Cursor Bugbot: when the lock's parent path is an existing regular file,
mkdirSync throws EEXIST, which the writeFileSync catch misrouted into
handleExistingLock — it then read ENOENT and recursed into acquireLock
until stack overflow. Move mkdirSync before the try so mkdir failures
(EEXIST/ENOTDIR/EACCES) propagate as genuine errors. Adds a regression
test asserting no RangeError when the parent path is a file.
Address adversarial review follow-ups:
- The file-as-parent test now asserts the propagated error code is EEXIST.
  Previously it only checked `toThrow()`/not-RangeError, which passed whether
  mkdir was inside or outside the lock try/catch (vacuous). EEXIST propagates
  only when mkdir is outside the try; moving it inside yields ENOTDIR (via
  handleExistingLock), so the assertion now genuinely guards the placement.
- Correct the acquireLock comment: a misplaced mkdir surfaces a misleading
  ENOTDIR (not infinite recursion) for the file-as-parent case.
- Document that existsSync() in getCurlInstallPaths also treats EACCES /
  transient-unmount as "skip stored hint", which is acceptable because the
  execPath fallback still finds a genuine running install.
Comment thread test/lib/binary.test.ts
Comment on lines +508 to +518
const lockPath = join(fileAsDir, "sentry.lock");

let caught: NodeJS.ErrnoException | undefined;
try {
acquireLock(lockPath);
} catch (error) {
caught = error as NodeJS.ErrnoException;
}
expect(caught).toBeDefined();
expect(caught?.code).toBe("EEXIST");
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The test for acquireLock incorrectly asserts the error code EEXIST. On POSIX systems like Linux, the error code will be ENOTDIR, causing the test to fail in CI.
Severity: LOW

Suggested Fix

Modify the test assertion to account for platform differences. The test should check if the error code is either ENOTDIR (for POSIX) or EEXIST (for Windows), potentially using a conditional based on process.platform.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: test/lib/binary.test.ts#L499-L518

Potential issue: The test `"propagates the mkdir EEXIST when the parent path is a
regular file"` attempts to create a lock file in a path where a parent segment is a
file, not a directory. The test asserts that the resulting error code is `EEXIST`.
However, this behavior is specific to Windows. On POSIX systems like Linux (which is
used in CI), `mkdirSync` throws an `ENOTDIR` error in this scenario. Because the test
hardcodes the expectation to `EEXIST`, it will fail on the Linux-based CI environment,
blocking the PR. The production code is unaffected as it correctly propagates the error
regardless of the code.

Also affects:

  • src/lib/binary.ts:406~406

Did we get this right? 👍 / 👎 to inform future reviews.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks, but this is a false positive — EEXIST is correct here on Linux.

The distinction is what mkdirSync targets:

  • dirname(lockPath) is testDir/not-a-dir, which is the regular file. mkdirSync(testDir/not-a-dir, { recursive: true }) therefore targets a path that already exists as a non-directory → POSIX mkdir(2) returns EEXIST.
  • ENOTDIR only occurs when an intermediate segment is a non-directory (e.g. mkdirSync(testDir/not-a-dir/sub)), which this test does not do.

Verified empirically on Linux (mkdirSync(file, {recursive:true}) -> EEXIST, readFileSync under a non-dir -> ENOTDIR), and the Unit Tests CI job passed with this exact assertion on 4b5d009.

Asserting EEXIST specifically is intentional: it guards mkdir's placement. If mkdir ran inside the lock try/catch, the EEXIST would be routed into handleExistingLock, which then reads the lock under a non-dir and surfaces ENOTDIR. Broadening the assertion to also accept ENOTDIR would make the test pass for the buggy placement too (i.e. vacuous), which is exactly the gap this test closes.

@BYK BYK merged commit ee76845 into main Jun 23, 2026
29 checks passed
@BYK BYK deleted the byk/fix/upgrade-missing-install-dir branch June 23, 2026 13:32
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