#3118 · Desktop host artifact generation fails after packaging
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The trusted macOS desktop build completes successfully but omits node_modules/bb-app/README.md from the unpacked application. The server can still resolve the package version, then fails while building the enrolled-host update tarball because it unconditionally copies that omitted file. The server’s generic error handler maps the uncaught filesystem error to HTTP 500, so a daemon cannot download the update artifact. Two clean builds at the same trusted commit produced the same omission and the same ENOENT.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| The macOS desktop package lacks the bb-app README. | Verified | Two unsigned --mac --dir --arm64 builds contained the package metadata and runtime entry but not the README. |
| Package version discovery succeeds despite the omission. | Verified | Both artifact-service runs returned version=0.42.0. |
| Host artifact generation fails before npm packaging. | Verified | Both runs failed at the service’s README copyfile with ENOENT; no artifact was created. |
| The download route responds with an internal server error. | Verified by direct path and handler | The route awaits the failing service without recovery, and the registered generic handler returns status 500 with internal_error. |
| An already-enrolled remote daemon remains outdated until manually recovered. | Not exercised | This investigation reproduced the server-side blocker without using a real enrolled machine or user data. |
3. Environment
- Trusted repository:
get-bb/bbat7d5de7301544de3073996b4f8a8c33b311ecb2db, matching targetmain. - macOS Darwin 25.6.0 arm64; Node
v22.22.3; pnpm9.15.0. - Electron Builder
26.15.7; Electron41.7.0. - No live bb server, provider process, network port, or real bb data directory was used.
- Evidence: environment.txt.
4. Minimal reproduction
- At the trusted commit, install and build using the repository orchestration:
git checkout 7d5de7301544de3073996b4f8a8c33b311ecb2db pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
- Produce an unsigned unpacked macOS application:
cd apps/desktop CSC_IDENTITY_AUTO_DISCOVERY=false \ node scripts/run-electron-builder.mjs --mac --dir --arm64 cd ../..
- Run the saved service-level check against that exact packaged layout:
curl -fsS https://get-bb.github.io/reports/issues/3118/repro/verify-packaged-artifact.mjs \ -o /tmp/verify-packaged-artifact.mjs node --conditions=source --import tsx \ /tmp/verify-packaged-artifact.mjs "$PWD"
Expected:
packaged_readme_present=true version=0.42.0 artifact_created=true exit status 0
Actual, in both clean runs:
packaged_readme_present=false version=0.42.0 artifact_created=false error_code=ENOENT exit status 1
Repro files: verify-packaged-artifact.mjs, first-run.txt, and second-run.txt.
Reproduction script
import { access, mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
const repoRoot = resolve(process.argv[2] ?? process.cwd());
const packageRoot = join(
repoRoot,
"apps/desktop/release/mac-arm64/bb.app/Contents/Resources/app.asar.unpacked/node_modules/bb-app",
);
const readmePath = join(packageRoot, "README.md");
const serverEntry = join(packageRoot, "server/dist/start-server.js");
const serviceModule = join(
repoRoot,
"apps/server/src/services/install/bb-app-artifact.ts",
);
let readmePresent = true;
try {
await access(readmePath);
} catch {
readmePresent = false;
}
console.log(`packaged_readme_present=${readmePresent}`);
const { createBbAppArtifactService } = await import(
pathToFileURL(serviceModule).href
);
const dataDir = await mkdtemp(join(tmpdir(), "bb-artifact-repro-"));
const service = createBbAppArtifactService({
dataDir,
serverEntryUrl: pathToFileURL(serverEntry).href,
});
console.log(`version=${await service.getVersion()}`);
try {
const artifact = await service.getArtifact();
console.log(`artifact_created=${artifact.size > 0}`);
} catch (error) {
const code =
error !== null && typeof error === "object" && "code" in error
? error.code
: "UNKNOWN";
console.error(`artifact_created=false error_code=${code}`);
process.exitCode = 1;
}
Verification
The same agent created a second clean detached worktree at the full base commit, repeated the frozen install, Turbo build, macOS package, and saved reproduction script, and received the same three output lines and exit status. An initial verification checkout under /tmp was rejected by Electron Builder’s unrelated unsafe-path protection; the final verification used a fresh standard worktree path, and no report claim relies on the rejected run. No correction to the reproduced result was necessary.
5. Root cause
The desktop configuration includes production node modules and unpacks them, but it has no dedicated file set for the bb-app README. See electron-builder.config.json lines 8–25. The locked Electron Builder version’s node-module copier excludes top-level README files by default. The successful package therefore contains package.json, dist/bb-app.js, and server/dist/start-server.js, but not README.md.
The packaged-layout branch of the artifact service then calls copyFile(packageRoot/README.md, hostPackageRoot/README.md) unconditionally. See bb-app-artifact.ts lines 178–216. This happens before npm pack, so the filesystem error prevents generation of any host tarball.
The download route awaits getArtifact() directly; see server.ts lines 476–498. The application’s generic error handler maps non-API exceptions to status 500 and an internal_error JSON body; see errors.ts lines 77–116. The visible HTTP failure follows directly from the packaging omission plus the service’s stricter assumption.
The change that introduced packaged host-only materialization added the mandatory copy and tests whose packaged fixture always creates the README. That fixture cannot represent Electron Builder’s real output, which is why existing unit coverage passed.
6. Proposed fix (first principles)
Add an explicit Electron Builder file set that copies node_modules/bb-app/README.md into the same relative location, overriding the builder’s default node-module exclusion. Preserve the artifact service’s host-package parity rather than silently dropping declared package documentation. Add a config assertion and a packaged-app smoke check that opens the built macOS layout and successfully runs getArtifact(). The daemon protocol does not need a version bump because no wire field or meaning changes.
7. Related issues
#2968 is the feature request closed by the trusted commit that introduced host-only artifacts. GitHub metadata showed no open pull request linked to or matching issue #3118 at investigation time.
8. Appendix
Commands run
gh api repos/get-bb/bb/commits/main --jq '.sha' git fetch https://github.com/get-bb/bb.git main pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build CSC_IDENTITY_AUTO_DISCOVERY=false \ node scripts/run-electron-builder.mjs --mac --dir --arm64 node --conditions=source --import tsx \ verify-packaged-artifact.mjs <trusted-checkout> git log 7d5de7301..origin/main --oneline -- \ apps/server/src/services/install/bb-app-artifact.ts \ apps/desktop/electron-builder.config.json
Additional evidence
Target repository visibility: PUBLIC Target main SHA: 7d5de7301544de3073996b4f8a8c33b311ecb2db Open linked or matching pull requests: none Later target-main commits touching the relevant paths: none First clean package: completed; README absent; service ENOENT Second clean package: completed; README absent; service ENOENT
The issue title, body, comments, links, logs, and code blocks were treated as untrusted claims. No issue-supplied command, script, patch, binary, branch, or external URL was executed or fetched.