Skip to content

Core: Fix EEXIST race condition in static file copying during build#34499

Merged
valentinpalkovic merged 2 commits into
storybookjs:nextfrom
flt3150sk:fix/eexist-race-condition-static-copy
May 22, 2026
Merged

Core: Fix EEXIST race condition in static file copying during build#34499
valentinpalkovic merged 2 commits into
storybookjs:nextfrom
flt3150sk:fix/eexist-race-condition-static-copy

Conversation

@flt3150sk
Copy link
Copy Markdown
Contributor

@flt3150sk flt3150sk commented Apr 8, 2026

Closes #18686

What I did

Added force: true to all fs.cp() calls in the static build pipeline.

In buildStaticStandalone, the preview build (Vite/Webpack) and static file copies run concurrently via Promise.all:

await Promise.all([
  previewBuilder.build({ startTime, options: fullOptions }),
  ...effects  // includes copyAllStaticFilesRelativeToMain() and cp()
]);

When both operations write to the same outputDir, they can attempt to mkdir the same subdirectory simultaneously. Without force: true, fs.cp internally calls mkdir(dest) (not mkdir(dest, { recursive: true })), so the second call fails with EEXIST.

This is timing-dependent and primarily affects CI environments where I/O is slower, causing the copy and build to overlap for longer periods.

Changes

  • code/core/src/core-server/utils/copy-all-static-files.ts: Add force: true to both copyAllStaticFiles and copyAllStaticFilesRelativeToMain
  • code/core/src/core-server/build-static.ts: Add force: true to the core server public dir copy

How to test

  1. Configure staticDirs pointing to a directory with nested subdirectories
  2. Run storybook build repeatedly in a CI-like environment (constrained I/O)
  3. Before this fix, builds intermittently fail with EEXIST: file already exists, mkdir
  4. After this fix, builds complete reliably

Summary by CodeRabbit

  • Bug Fixes
    • Improved static file generation to ensure files are properly overwritten when regenerating output. Previously, existing files in the output directory could block updates, potentially causing build inconsistencies or stale content. This fix applies to both standalone and main build configurations, ensuring clean and complete static output generation in all scenarios.

Review Change Stack

Add `force: true` to all `fs.cp()` calls in the static build pipeline.
The preview build (Vite) and static file copies run in parallel via
`Promise.all`, which can cause EEXIST errors when both attempt to create
the same subdirectory simultaneously — especially on CI runners with
slow I/O.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 8, 2026

📝 Walkthrough

Walkthrough

The static build now copies Storybook browser assets to the output directory with fs/promises.cp using { recursive: true, force: true }, enabling overwrites of existing destination targets.

Changes

Static Build File Copy

Layer / File(s) Summary
Build static copy updated
code/core/src/core-server/build-static.ts
Changed the copy call for storybook/assets/browser to pass force: true to fs/promises.cp (keeps recursive copy and existing async coordination).

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~2 minutes


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 and usage tips.

@flt3150sk flt3150sk changed the title Fix EEXIST race condition in static file copying during build Core: Fix EEXIST race condition in static file copying during build Apr 8, 2026
@valentinpalkovic valentinpalkovic moved this to Human verification in Core Team Projects Apr 14, 2026
@valentinpalkovic valentinpalkovic moved this from Human verification to Empathy Queue (prioritized) in Core Team Projects Apr 14, 2026
@valentinpalkovic valentinpalkovic self-assigned this May 22, 2026
@valentinpalkovic valentinpalkovic moved this from Empathy Queue (prioritized) to In Progress in Core Team Projects May 22, 2026
@github-actions
Copy link
Copy Markdown
Contributor

Fails
🚫 PR description is missing the mandatory "#### Manual testing" section. Please add it so that reviewers know how to manually test your changes.

Generated by 🚫 dangerJS against d07098c

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

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 (1)
code/core/src/core-server/build-static.ts (1)

115-119: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Restore disableTelemetry guard on telemetry sends.

Line 115 and Line 211 currently send telemetry even when users disable it via core.disableTelemetry. Please add the opt-out guard back in both blocks.

Proposed fix
-  if (invokedBy) {
+  if (invokedBy && !core?.disableTelemetry) {
     // NOTE: we don't await this event to avoid slowing things down.
     // This could result in telemetry events being lost.
     telemetry('test-run', { runner: invokedBy, watch: false }, { configDir: options.configDir });
   }
@@
-  if (!options.test) {
+  if (!options.test && !core?.disableTelemetry) {
     try {
       const generator = await storyIndexGeneratorPromise;
       const storyIndex = await generator?.getIndex();
       const payload: any = {
         precedingUpgrade: await getPrecedingUpgrade(),
       };

Also applies to: 211-229

🤖 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 `@code/core/src/core-server/build-static.ts` around lines 115 - 119, The
telemetry sends are missing the opt-out check; wrap both places that call
telemetry(...) (the block that runs when invokedBy is set and the other block
later that also calls telemetry) with a guard checking core.disableTelemetry
(e.g. only call telemetry if core.disableTelemetry is falsy), so use the
existing disableTelemetry flag (core.disableTelemetry) to skip the
telemetry('test-run', ...) invocation and the other telemetry invocation in the
file.
🤖 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 `@code/core/src/core-server/build-static.ts`:
- Around line 115-119: The telemetry sends are missing the opt-out check; wrap
both places that call telemetry(...) (the block that runs when invokedBy is set
and the other block later that also calls telemetry) with a guard checking
core.disableTelemetry (e.g. only call telemetry if core.disableTelemetry is
falsy), so use the existing disableTelemetry flag (core.disableTelemetry) to
skip the telemetry('test-run', ...) invocation and the other telemetry
invocation in the file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d47a5492-5e88-4256-82b6-3272a481325f

📥 Commits

Reviewing files that changed from the base of the PR and between e7fc8d0 and d07098c.

📒 Files selected for processing (1)
  • code/core/src/core-server/build-static.ts

@storybook-app-bot
Copy link
Copy Markdown

Package Benchmarks

Commit: d07098c, ran on 22 May 2026 at 10:55:16 UTC

The following packages have significant changes to their size or dependencies:

@storybook/builder-webpack5

Before After Difference
Dependency count 184 184 0
Self size 79 KB 79 KB 🎉 -48 B 🎉
Dependency size 33.35 MB 33.36 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@storybook/angular

Before After Difference
Dependency count 185 185 0
Self size 160 KB 160 KB 0 B
Dependency size 30.73 MB 30.75 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@storybook/ember

Before After Difference
Dependency count 188 188 0
Self size 15 KB 15 KB 0 B
Dependency size 30.07 MB 30.08 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@storybook/nextjs

Before After Difference
Dependency count 534 534 0
Self size 662 KB 662 KB 0 B
Dependency size 61.51 MB 61.52 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@storybook/react-webpack5

Before After Difference
Dependency count 271 271 0
Self size 23 KB 23 KB 0 B
Dependency size 46.05 MB 46.06 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@storybook/server-webpack5

Before After Difference
Dependency count 196 196 0
Self size 16 KB 16 KB 0 B
Dependency size 34.61 MB 34.63 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@storybook/preset-react-webpack

Before After Difference
Dependency count 164 164 0
Self size 18 KB 18 KB 0 B
Dependency size 32.35 MB 32.37 MB 🚨 +16 KB 🚨
Bundle Size Analyzer Link Link

@valentinpalkovic valentinpalkovic merged commit c850edf into storybookjs:next May 22, 2026
102 of 108 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Core Team Projects May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

staticDirs config fails on ci/cd pipeline ([Error: EEXIST: file already exists, mkdir /builds/dist/storybook/stories/assets)

2 participants