{"record":{"id":"0cc5e062101e5353","repo":"heygen-com/hyperframes","slug":"s3transport-tar-source-must-be-an-existing-direc","errorCode":null,"errorMessage":"[s3Transport] tar source must be an existing directory: ${sourceDir}","messagePattern":"\\[s3Transport\\] tar source must be an existing directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/aws-lambda/src/s3Transport.ts","lineNumber":272,"sourceCode":"function isS3PreconditionFailed(error: unknown): boolean {\n  if (!isRecord(error)) return false;\n  const metadata = isRecord(error.$metadata) ? error.$metadata : undefined;\n  return error.name === \"PreconditionFailed\" || metadata?.httpStatusCode === 412;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n  return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\n/**\n * Pack a directory into a `.tar.gz` at `destTarball`. Uses the `tar` npm\n * package (pure JS over `node:zlib`) rather than spawning a system tar\n * binary — the AWS Lambda Node 22 base image ships a minimal set of\n * userland tools and does NOT include `tar` in `/usr/bin`.\n */\nexport async function tarDirectory(sourceDir: string, destTarball: string): Promise<void> {\n  if (!existsSync(sourceDir) || !statSync(sourceDir).isDirectory()) {\n    throw new Error(`[s3Transport] tar source must be an existing directory: ${sourceDir}`);\n  }\n  mkdirSync(dirname(destTarball), { recursive: true });\n  await tar.create({ gzip: true, file: destTarball, cwd: sourceDir }, [\".\"]);\n}\n\n/**\n * Extract a `.tar.gz` produced by {@link tarDirectory} into `destDir`.\n * The directory is created (or cleared) before extraction so a retried\n * invocation doesn't observe stale files from a prior run on the same\n * warm Lambda container.\n */\nexport async function untarDirectory(tarballPath: string, destDir: string): Promise<void> {\n  if (!existsSync(tarballPath)) {\n    throw new Error(`[s3Transport] tarball missing: ${tarballPath}`);\n  }\n  // Wipe target so the warm container's prior planDir doesn't bleed into\n  // the new invocation. Lambda re-uses /tmp across invocations on the same\n  // container.","sourceCodeStart":254,"sourceCodeEnd":290,"githubUrl":"https://github.com/heygen-com/hyperframes/blob/c2996c8626135db5253519359d8a063d3bafad8d/packages/aws-lambda/src/s3Transport.ts#L254-L290","documentation":"Thrown by tarDirectory when the sourceDir argument either does not exist or is not a directory (isDirectory() false). tarDirectory is the pack step used by deploySite and by the Lambda handler to gzip a planDir or project tree. Because the tar npm package is invoked with cwd: sourceDir, a non-directory path would cause a confusing ENOTDIR error inside the tar stream; this guard fails fast with the actual offending path.","triggerScenarios":"deploySite calls tarDirectory with opts.projectDir; the Lambda handler calls it with a planDir path. The error fires when the path points to a regular file, a broken symlink, or a path that was never created. Also triggered when a relative path resolves against the wrong working directory (Lambda cwd vs /tmp).","commonSituations":"Passing a tarball path instead of its unpacked directory; a planDir cleanup race where rmSync removed the dir between existence check and tar; relative paths on Lambda where cwd is /var/task but the artifact is under /tmp; symlink to a deleted target.","solutions":["Log statSync(sourceDir) or readdirSync before the call to confirm the path resolves to a directory in the current cwd.","Use an absolute path (e.g. path.join(os.tmpdir(), ...)) rather than a relative one, especially inside Lambda.","Ensure the directory-creation step (mkdirSync recursive) completed before tarDirectory is invoked.","If the path is a symlink, resolve it first with fs.realpathSync to catch broken links."],"exampleFix":"// before\nawait tarDirectory(projectDir, tarball);\n\n// after\nconst abs = path.resolve(projectDir);\nif (!existsSync(abs) || !statSync(abs).isDirectory()) {\n  throw new Error(`projectDir not a directory: ${abs}`);\n}\nawait tarDirectory(abs, tarball);","handlingStrategy":"validation","validationCode":"import { existsSync, statSync } from 'node:fs';\nimport { resolve } from 'node:path';\nfunction assertTarrableDir(sourceDir: string): string {\n  const abs = resolve(sourceDir);\n  if (!existsSync(abs) || !statSync(abs).isDirectory()) {\n    throw new Error(`not a directory: ${abs}`);\n  }\n  return abs;\n}","typeGuard":"import { statSync } from 'node:fs';\nconst isDirectory = (p: string): boolean => {\n  try { return statSync(p).isDirectory(); } catch { return false; }\n};","tryCatchPattern":null,"preventionTips":["Resolve sourceDir to an absolute path before the call.","Await the directory-creation step before tarring.","Use realpathSync to detect broken symlinks before tar."],"tags":["tar","filesystem","validation","deploy","aws-lambda"],"backgroundTag":null,"analyzedSha":"c2996c8626135db5253519359d8a063d3bafad8d","analyzedAt":"2026-08-12T22:18:56.877Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}