{"record":{"id":"4c7df9ed899e6538","repo":"Hmbown/CodeWhale","slug":"downloaded-release-artifact-must-be-a-regular-file","errorCode":null,"errorMessage":"Downloaded release artifact must be a regular file: ${source}","messagePattern":"Downloaded release artifact must be a regular file: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/release/assemble-release-assets.js","lineNumber":135,"sourceCode":"\nasync function assemble(inputDirectory, outputDirectory) {\n  const expected = allReleaseAssetNames();\n  const generated = new Set([WINDOWS_LAUNCHER, CHECKSUM_MANIFEST]);\n  const copiedNames = expected.filter((name) => !generated.has(name));\n  const sources = new Map();\n  for (const name of copiedNames) {\n    const source = intermediateArtifactPath(inputDirectory, name);\n    let sourceStat;\n    try {\n      sourceStat = await fs.lstat(source);\n    } catch (error) {\n      if (error && error.code === \"ENOENT\") {\n        throw new Error(`Downloaded release artifacts are missing ${name} at ${source}`);\n      }\n      throw error;\n    }\n    if (!sourceStat.isFile()) {\n      throw new Error(`Downloaded release artifact must be a regular file: ${source}`);\n    }\n    sources.set(name, source);\n  }\n\n  await fs.mkdir(outputDirectory, { recursive: true });\n  const existing = await fs.readdir(outputDirectory);\n  if (existing.length > 0) {\n    throw new Error(`Output directory must be empty: ${outputDirectory}`);\n  }\n\n  for (const name of copiedNames) {\n    await fs.copyFile(sources.get(name), path.join(outputDirectory, name));\n  }\n  await fs.writeFile(\n    path.join(outputDirectory, WINDOWS_LAUNCHER),\n    windowsLauncherContents(),\n    \"utf8\",\n  );","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/8880682c63083a91624de936797efa3ce9e498fd/scripts/release/assemble-release-assets.js#L117-L153","documentation":"assemble() lstat()ed an expected intermediate artifact and it exists but is not a regular file — a directory, symlink, fifo, or similar. Because lstat does not follow symlinks, a symlink to the real binary is rejected just like a directory. The guard ensures only real bytes are copied into the release.","triggerScenarios":"INPUT_DIR/<name>/<name> resolving to a directory because the artifact unpacked with an extra nesting level; artifacts replaced by symlinks from dedup tooling or manual 'save disk' setups; a fifo or socket accidentally created at the path.","commonSituations":"An unpack/download step creating directories where files are expected; artifact caching layers that dedupe via symlinks; workflow changes altering the artifact layout.","solutions":["Make the path a real regular file: replace the symlink with its target's bytes (cp -L) or flatten the extra directory level","Fix the unpack step so each artifact lands as a file at INPUT_DIR/<name>/<name>","Re-run assemble after correcting the INPUT_DIR layout"],"exampleFix":"# before: artifacts/codewhale-x86_64/codewhale-x86_64.tar.gz is a symlink -> rejected\n\n# after\ncp -L artifacts/codewhale-x86_64/codewhale-x86_64.tar.gz /tmp/real.tar.gz\nmv /tmp/real.tar.gz artifacts/codewhale-x86_64/codewhale-x86_64.tar.gz","handlingStrategy":"validation","validationCode":"const fs = require(\"node:fs/promises\");\nconst path = require(\"node:path\");\nasync function assertRegularFile(file) {\n  const stat = await fs.lstat(file); // lstat, like the script: does not follow symlinks\n  if (!stat.isFile()) throw new Error(`${file} is not a regular file; flatten or copy with cp -L`);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Disable symlink-based dedupe/caching for artifact directories used as assemble input","After unpacking, assert each expected path is a file before invoking assemble","Prefer downloading artifacts over re-extracting archives when possible — downloads preserve file types"],"tags":["release","filesystem","ci","artifacts"],"backgroundTag":null,"analyzedSha":"8880682c63083a91624de936797efa3ce9e498fd","analyzedAt":"2026-08-16T11:31:27.956Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}