Skip to content

fix(scripts): self-heal corrupt Mailpit install and extract atomically#259

Open
rlorenzo wants to merge 1 commit into
mainfrom
fix/mailpit-self-heal
Open

fix(scripts): self-heal corrupt Mailpit install and extract atomically#259
rlorenzo wants to merge 1 commit into
mainfrom
fix/mailpit-self-heal

Conversation

@rlorenzo

Copy link
Copy Markdown
Contributor

Problem

An interrupted Mailpit install (e.g. Ctrl+C during extraction) could leave a truncated mailpit.exe. The manage script then got permanently stuck: the version check on the broken binary returned null, which short-circuited the update/reinstall logic, and every subsequent dev/dev:build failed with Error managing Mailpit: spawn EFTYPE (Windows ERROR_BAD_EXE_FORMAT) with no way to recover short of manually deleting the install folder.

Changes

  • Self-heal: if the binary exists but cannot report its version, treat it as corrupt and reinstall instead of trying to spawn it.
  • Atomic extraction: extract each zip entry to a .partial file and rename it into place only after the write is fully flushed, so an interrupted install can never leave a truncated file at the final path.
  • Spawn error handling: async spawn failures now log a clear error and resolve gracefully instead of crashing the script with an unhandled error event.

Testing

  • Fresh install path: deleted C:\Tools\mailpit, ran npm run mailpit:start, verified download, hash check, atomic extraction, and startup.
  • Self-heal path: truncated the installed exe by ~78KB to reproduce the original broken state, reran start, verified it detected the corrupt binary, reinstalled over it, and started successfully.
  • npm run lint scripts/manage-mailpit.js clean; pre-commit lint/test/build all passed.

@codecov-commenter

codecov-commenter commented Jul 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 45.29%. Comparing base (0c5ab2b) to head (008d1d0).

Additional details and impacted files
@@           Coverage Diff           @@
##             main     #259   +/-   ##
=======================================
  Coverage   45.29%   45.29%           
=======================================
  Files         897      897           
  Lines       51928    51928           
  Branches     4878     4878           
=======================================
  Hits        23523    23523           
  Misses      27803    27803           
  Partials      602      602           
Flag Coverage Δ
backend 45.39% <ø> (ø)
frontend 43.41% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

@codecov-commenter

Copy link
Copy Markdown

Bundle Report

Bundle size has no change ✅

@rlorenzo

Copy link
Copy Markdown
Contributor Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Full review finished.

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Mailpit ZIP extraction now finalizes files atomically, startup handles process errors, and management reinstalls binaries whose installed version cannot be determined.

Changes

Mailpit management reliability

Layer / File(s) Summary
Safe ZIP extraction finalization
scripts/manage-mailpit.js
ZIP entries are written to .partial files and renamed after stream closure; stream or rename failures reject extraction.
Startup process error handling
scripts/manage-mailpit.js
startMailpit cancels the startup timer, logs process errors, and resolves false.
Unknown-version reinstall recovery
scripts/manage-mailpit.js
manageMailpit reinstalls an installed binary when its version cannot be read and returns false if reinstalling fails.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: recovering from corrupt Mailpit installs and making extraction atomic.
Description check ✅ Passed The description matches the changeset and explains the self-heal, atomic extraction, and spawn error handling fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/mailpit-self-heal

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
scripts/manage-mailpit.js (2)

476-484: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Clear the timeout on process spawn failure.

The setTimeout is not cleared if the spawned process immediately errors out. This leaves a dangling timer that will execute seconds after the promise has already rejected, leading to redundant checks and conflicting success/failure logs.

Store the timer ID and clear it in the error handler.

🛠️ Proposed fix
+        let startupTimer;
+
         process.on("error", (err) => {
+            clearTimeout(startupTimer);
             logError(`Failed to start Mailpit: ${err.message}`)
             resolve(false)
         })

         process.unref()

         // Wait a moment and check if it started successfully
-        setTimeout(async () => {
+        startupTimer = setTimeout(async () => {
             const isRunning = await checkMailpitWeb()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/manage-mailpit.js` around lines 476 - 484, Update the process startup
flow around the error handler and delayed startup check to store the setTimeout
identifier, then clear that timer inside the process "error" handler before
resolving failure. Preserve the existing delayed success check when no spawn
error occurs.

249-273: 🗄️ Data Integrity & Integration | 🔴 Critical | ⚡ Quick win

Prevent finalizing corrupt files and background leaks on stream errors.

The close event fires even if the write stream encounters an error or is destroyed. When an error occurs, the promise correctly rejects, but the close handler still fires, renaming the broken .partial file into place and calling zipfile.readEntry(). This defeats the atomicity fix and continues extracting files in the background after returning a failure.

Track the error state to skip finalization and ensure writeStream is destroyed on read errors.

🛠️ Proposed fix
                         const partialPath = `${filePath}.partial`
                         const writeStream = fs.createWriteStream(partialPath)
+                        let hasErrored = false;

                         writeStream.on("close", () => {
+                            if (hasErrored) return;
+
                             try {
                                 fs.renameSync(partialPath, filePath)
                             } catch (renameErr) {
                                 logError(`Error finalizing ${filePath}: ${renameErr.message}`)
                                 reject(renameErr)
                                 return
                             }
                             extractedFiles.push(entry.fileName)
                             logInfo(`Extracted: ${entry.fileName}`)
                             zipfile.readEntry()
                         })

                         readStream.on("error", (readErr) => {
+                            hasErrored = true;
+                            writeStream.destroy();
                             logError(`Error reading ${entry.fileName}: ${readErr.message}`)
                             reject(readErr)
                         })

                         writeStream.on("error", (writeErr) => {
+                            hasErrored = true;
                             logError(`Error writing ${filePath}: ${writeErr.message}`)
                             reject(writeErr)
                         })
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/manage-mailpit.js` around lines 249 - 273, Update the extraction
stream handling around the writeStream close/error listeners to track whether a
read or write error occurred; have the close handler return without renaming,
logging success, or calling zipfile.readEntry() when errored. In the readStream
error handler, mark the stream as failed, reject the promise, and destroy
writeStream to prevent background writes; apply the same failed-state guard in
the writeStream error handler.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@scripts/manage-mailpit.js`:
- Around line 476-484: Update the process startup flow around the error handler
and delayed startup check to store the setTimeout identifier, then clear that
timer inside the process "error" handler before resolving failure. Preserve the
existing delayed success check when no spawn error occurs.
- Around line 249-273: Update the extraction stream handling around the
writeStream close/error listeners to track whether a read or write error
occurred; have the close handler return without renaming, logging success, or
calling zipfile.readEntry() when errored. In the readStream error handler, mark
the stream as failed, reject the promise, and destroy writeStream to prevent
background writes; apply the same failed-state guard in the writeStream error
handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 1535648f-93eb-4b42-9923-3903385ff7f5

📥 Commits

Reviewing files that changed from the base of the PR and between 0c5ab2b and a755a3c.

📒 Files selected for processing (1)
  • scripts/manage-mailpit.js

- Reinstall when the installed binary cannot report its version
- Extract zip entries to .partial files, renaming into place only
  after the write is fully flushed
- Log async spawn failures instead of crashing
@rlorenzo
rlorenzo force-pushed the fix/mailpit-self-heal branch from a755a3c to 008d1d0 Compare July 17, 2026 01:34
@rlorenzo

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@rlorenzo

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/manage-mailpit.js`:
- Around line 246-277: Update the extraction failure handling around the
writeStream close/error callbacks to remove partialPath whenever reading,
writing, or finalizing fails. Ensure cleanup occurs before rejecting, while
preserving the existing successful rename and extraction flow.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b5233fe5-7485-4cac-8d20-ae88b92d35ae

📥 Commits

Reviewing files that changed from the base of the PR and between a755a3c and 008d1d0.

📒 Files selected for processing (1)
  • scripts/manage-mailpit.js

Comment thread scripts/manage-mailpit.js
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.

2 participants