Skip to content

Publishing is impossible on Windows: syncDirectory() throws EPERM on directory fsync #42

Description

@ryanzegalia

Summary

On Windows, every upload PUT fails with HTTP 500 and artifact_commit_upload then refuses the commit, so no artifact can ever be published. The cause is a directory fsync in syncDirectory(), which Windows cannot perform. The fix is a three-line platform guard.

Reproduced against 2251e42 (v0.1.0) on Windows 10 22H2, Node 24.15.0, running the local service via the MCP stdio adapter.

Symptom

  1. artifact_create_upload succeeds and returns an upload plan.
  2. PUTing the exact bytes to the returned uploadUrl returns HTTP 500 INTERNAL_ERROR. The service log records failure_tag: "StagingStorageFailure".
  3. artifact_commit_upload then fails with UPLOAD_INCOMPLETE: Every declared upload file must be verified before commit.

Cause

syncDirectory() in src/storage/verified-file.ts:144 opens the directory as a file handle and calls sync() on it:

export async function syncDirectory(directory: string): Promise<void> {
  const handle = await open(directory, "r");
  try {
    await handle.sync();
  } finally {
    await handle.close();
  }
}

A directory fsync is a POSIX durability barrier. Windows has no equivalent and cannot open a directory as a file handle, so open()/sync() throws EPERM: operation not permitted, fsync. Minimal repro, independent of this project:

import {open} from "node:fs/promises";
const h = await open("C:/some/existing/directory", "r");
await h.sync();   // EPERM on win32

Both storage paths route through this one function, so a single guard covers both:

  • src/storage/local-staging-store.ts:69 — upload staging (this is what breaks the PUT)
  • src/storage/local-blob-store.ts:119 — commit into the blob store

Suggested fix

Early-return on win32:

export async function syncDirectory(directory: string): Promise<void> {
  // Windows cannot open a directory as a file handle, so handle.sync() throws
  // EPERM. A directory fsync is a POSIX durability barrier with no Windows
  // equivalent, so skip it there.
  if (process.platform === "win32") {
    return;
  }
  const handle = await open(directory, "r");
  try {
    await handle.sync();
  } finally {
    await handle.close();
  }
}

Durability note

This drops a crash-durability barrier, but only on the platform that cannot provide it anyway, and only for the directory entry. The file bytes are still fsynced via file.sync() before the rename, so committed artifacts remain durable. The worst case after a hard power loss is a staged upload that has to be redone.

Verification

With the guard applied and pnpm build re-run, the same sequence returns HTTP 200 with {"status":"verified"} on the PUT, and artifact_commit_upload returns a normal artifact with working review and version links. Publishing has been reliable since.

Happy to open a PR with the guard plus a process.platform test if that is useful.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions