Skip to content

feat: standalone single-file executables (Bun) for every supported platform - #25

Open
ErRusso wants to merge 8 commits into
napi-bindings:mainfrom
ErRusso:feat/standalone-executable
Open

ErRusso wants to merge 8 commits into
napi-bindings:mainfrom
ErRusso:feat/standalone-executable

Conversation

@ErRusso

@ErRusso ErRusso commented Oct 18, 2026

Copy link
Copy Markdown
Contributor

Closes #21

Distributes Marmotta as a self-contained executable per platform alongside the npm package, which remains the primary distribution channel.

Spike: Node SEA vs Bun (both kept buildable)

Both runtimes were implemented and run through the identical scripts/smoke-test.mjs (version, help, install, list, configure, build, addon loads in host Node, clean, rebuild, remove, exit codes, under hostile NODE_OPTIONS + stray .env/bunfig.toml):

Node SEA Bun 1.4.3
Full smoke test pass pass
Targets host-only cross all 8 targets build
Startup (100 × --version) 2.57 s 0.46 s
Binary size 66 MB 93 MB

Chosen: Bun per the issue's decision rule — it covers the full 8-platform Bun matrix (matching Zig's target support), cross-compiles from one machine, and starts 5.6× faster. scripts/build-sea.mjs remains in the repo as the fallback path.

What's in here

  • Runtime groundwork — src/standalone.ts detects SEA/Bun standalone builds and reads embedded assets; src/node-api-headers.ts resolves headers from node_modules under npm and extracts the embedded node-api-headers.tar into ~/.marmotta/node-api-headers/<version>/ in standalone mode (Zig compiles in a separate process and needs real files); src/version.ts + scripts/check-version.mjs keep the reported version in step with package.json.
  • Build scripts — build:headers (archives the node-api-headers dependency's include/ + def/node_api.def, not vendored), build:bun (8 targets, --bytecode, --asset, .env/bunfig autoload disabled, ad-hoc codesign + entitlements on macOS), build:sea (esbuild + node --build-sea, execArgvExtension: none).
  • .github/workflows/release-binaries.yml — tag-triggered; each target builds on a runner matching its platform (ubuntu, ubuntu-arm, macos-15-intel, macos-latest, windows, windows-11-arm) and is smoke-tested natively, with the musl pair quick-tested in Alpine containers; publishes binaries + SHA256SUMS + actions/attest-build-provenance attestation to the release; tag must match package.json.
  • Docs — README "Standalone binary" section (platform table, checksum/attestation verification, macOS quarantine note) and CONTRIBUTING release notes.

Testing

  • npm test: 31 pass, 0 fail, 5 skipped (Zig/Deno/Bun not on PATH locally; covered by the existing CI matrix).
  • Full smoke test passes on both built runtimes; all 8 Bun targets compile (6 validated cross from linux-x64, darwin/windows additionally validated by the release workflow's native jobs).

Out of scope (per issue)

marmotta self-update, Homebrew/Scoop/winget manifests, signing certificates (macOS binaries are ad-hoc signed only).

…bles

A standalone executable (Node.js SEA or bun build --compile) has no
node_modules and cannot read package.json, so:

- src/version.ts holds the version constant; check-version.mjs and a
  CLI test keep it in step with package.json.
- src/standalone.ts detects Node SEA and Bun standalone builds and
  reads assets embedded in the executable.
- src/node-api-headers.ts resolves the headers from node_modules under
  npm and extracts the embedded archive into the Marmotta home when
  running standalone, since Zig compiles in a separate process and
  needs real files.

The build scripts produce the embedded headers archive (build:headers)
and the executables for both runtimes (build:sea, build:bun); the
smoke test runs a built executable the way a user without Node.js
would.
- The fixture assigns napi_status before assert(): `zig cc -O2` defines
  NDEBUG, so an assert() wrapping the call compiled it away and the
  built addon exposed no properties.
- BUN_OPTIONS is out of the hostile environment: Bun parses it before
  the entrypoint, no compiled executable can opt out, and the issue's
  determinism criteria cover NODE_OPTIONS, .env and bunfig.toml - all
  of which both binaries ignore.
- build-bun keeps --bytecode but drops the bare --sourcemap (Bun 1.4
  requires a value) and skips codesign with a warning when a darwin
  target is cross-built off macOS.

Spike results: both runtimes pass the full smoke test on linux-x64;
Bun additionally builds all eight targets from this machine, starts
5.6x faster (100 x --version: 0.46s vs 2.57s), at the cost of a
larger binary (93 MB vs 66 MB).
The tag-triggered Release binaries workflow compiles all eight targets
with bun build --compile - Bun won the spike over Node SEA: both
pass the identical smoke test, and Bun additionally cross-compiles
every Zig-supported platform and starts 5.6x faster - then runs
scripts/smoke-test.mjs against each executable (natively on a
matching runner, in an Alpine container for the musl pair), and
attaches SHA256SUMS plus provenance attestations to the release.

Executables are named marmotta-vX.Y.Z-<platform> so downloads and
the release tag share the same version spelling; both build scripts
follow that pattern. The tag must match package.json or the workflow
fails before building.
Cover where to download them, which file matches which platform,
checksum and attestation verification, and the behavioral notes
that differ from an npm install: no Node.js required, NODE_OPTIONS
and stray project files are ignored, macOS quarantine handling.
@ErRusso
ErRusso marked this pull request as draft October 18, 2026 21:31
@ErRusso
ErRusso marked this pull request as ready for review October 18, 2026 22:07

@NickNaso NickNaso left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: standalone executables (PR #25)

Thanks for this. Embedding the Node-API headers, keeping the version in src/version.ts with a drift check, and smoke-testing every binary on a matching runner are all good choices, and the comments explain the reasoning well.

My main concerns are about the headers cache in the Marmotta home and about the release workflow on Windows and Alpine. Items 1–4 should be fixed before the first tagged release; items 2 and 3 are worth confirming with a manual workflow_dispatch run first. Items 5–8 are smaller fixes, and 9–11 are cleanups.

Must fix

1. A header upgrade never re-extracts

src/node-api-headers.ts:44-64 (and the comment at lines 12-14)

findInstalledHeaders returns the first complete directory under ~/.marmotta/node-api-headers and never compares it with the version embedded in the running executable. The comment says "a header upgrade re-extracts automatically", but it doesn't:

  1. A user runs a binary that embeds node-api-headers-1.3.0, which gets extracted.
  2. They upgrade to a binary that embeds node-api-headers-1.5.0.
  3. readdir finds the 1.3.0 directory, both access() checks pass, and that directory is returned.

From then on every build compiles against the stale include/ and generates the Windows host imports from the stale node_api.def. If several versions are present, readdir order decides which one is used, so the result also depends on the file system.

Suggestion: look up the exact directory the running binary expects instead of scanning. One option is to name the destination after a hash of the embedded archive (the asset is already in memory, so hashing it is cheap). Another is to generate the expected directory name at build time, the same way src/version.ts carries the CLI version. Old directories can then be removed, or simply ignored.

2. tar with C:\... paths will probably fail on the Windows release jobs

.github/workflows/release-binaries.yml:90-106, scripts/build-headers-asset.mjs:29, src/node-api-headers.ts:92, src/toolchain.ts:255

Every step in the release workflow uses shell: bash. On windows-latest and windows-11-arm, Git Bash puts Git's /usr/bin first on PATH, so a spawned tar resolves to GNU tar rather than C:\Windows\System32\tar.exe (bsdtar). GNU tar reads an archive name with a colon before any slash as host:path, so tar -cf C:\a\marmotta\build\node-api-headers.tar ... fails with Cannot connect to C: resolve failed.

This would break npm run build:headers first. Under the same shell, the native smoke test would hit the same problem in marmotta install (Zig extraction) and in the first build (headers extraction). The regular ci.yml uses the default shell (pwsh) on Windows, which is probably why this hasn't shown up yet.

Suggestions:

  • Pass tar a relative archive name and set cwd to its directory (e.g. spawnSync('tar', ['-cf', 'node-api-headers.tar', '-C', staging, directoryName], { cwd: buildDir })). Both GNU tar and bsdtar handle that, while --force-local only exists in GNU tar.
  • Or drop shell: bash from the Windows steps that don't need it.

The runtime code has the same exposure for users whose PATH puts GNU tar (Git, MSYS2, Cygwin) ahead of System32, so I'd fix it in run('tar', ...) too and not only in the workflow.

3. The musl smoke test runs in an Alpine image without libstdc++

.github/workflows/release-binaries.yml:113, :118

Bun's musl executables are dynamically linked against libstdc++ and libgcc, which alpine:latest doesn't ship. docker run alpine:latest /out/marmotta-...-linux-x64-musl --version is then expected to fail with Error loading shared library libstdc++.so.6, which blocks the release. Even if the smoke test were skipped, users on a stock Alpine image would hit the same error.

Suggestions:

  • Install the libraries in the smoke container (apk add --no-cache libstdc++ libgcc) and document the requirement in the README.
  • Pin the image (e.g. alpine:3.22) so the smoke test is reproducible like the pinned Bun version.

4. Concurrent first-run extractions can delete headers that are in use

src/node-api-headers.ts:99-102

await rm(destination, { recursive: true, force: true });
await rename(join(stagingDir, directories[0].name), destination);

The comment says no partially written directory is ever visible, but rm followed by rename isn't atomic as a pair. Picture two marmotta build processes starting on a fresh machine (a monorepo script or a parallel CI matrix):

  1. Process A renames its copy into place and starts zig cc -I <destination>/include.
  2. Process B runs rm(destination) before its own rename, and A's compile fails with node_api.h not found.
  3. If A's rename lands between B's rm and B's rename, B's rename fails with ENOTEMPTY/EPERM and B reports a directory error.

Suggestion: once the destination is version-specific (item 1), every copy has the same content, so the first writer can win. Try the rename first. If it fails because the destination exists, check that the destination is complete and use it. Only remove a destination that is incomplete. downloadAndInstallZig (src/toolchain.ts:261-262) has the same pattern, so the fix could cover both (see item 10).

Should fix

5. A failed bun build or codesign leaves the tarball in dist/

scripts/build-bun.mjs:63, :80

process.exit() ends the process immediately, so the finally { rm(stagedHeaders) } block at line 86 never runs. After a failed build, dist/node-api-headers.tar stays behind. package.json publishes dist, so a later npm publish from the same checkout would ship the tarball in the npm package.

Suggestion: set process.exitCode and break/return out of the loop, or throw and let the finally run before exiting.

6. The smoke test doesn't exercise the .env / bunfig.toml protection

scripts/smoke.mjs:50-51, :77-92

The hostile .env and bunfig.toml are written to the temporary project directory, but run() spawns the binary without cwd, so it runs in the caller's working directory. Bun auto-loads these files from the process cwd, not from -C <directory>. If a later change dropped --no-compile-autoload-dotenv, this test would still pass.

Suggestions:

  • Pass cwd: directory to the project commands (configure, build, clean, rebuild).
  • Also set MARMOTTA_HOME to a temporary directory. The smoke test then always extracts the headers fresh (which would have caught item 1), and it stops writing into the developer's real ~/.marmotta when run locally.

7. Under Node SEA a missing asset throws a raw Node error

src/standalone.ts:40 and src/node-api-headers.ts:77-80

sea.getAsset(key) throws when the asset doesn't exist; it doesn't return undefined. So under SEA the if (!archive) branch with the InternalError is unreachable. A SEA built without the asset fails marmotta build with a raw Node error instead of the intended Marmotta error, and since line 77 is outside the try, it also skips directoryError handling.

Suggestion: wrap the getAsset call and return undefined (or throw the InternalError directly) when the asset is missing.

8. The release workflow grants write permissions to every job

.github/workflows/release-binaries.yml:14-17

contents: write, id-token: write and attestations: write are set at workflow level, so the eight build jobs get them too. Those jobs run npm ci, the test suite and the build scripts, none of which need write access.

Suggestion: set permissions: contents: read at the top, and grant the three write permissions only to the release job.

Nice to have

9. Resolve the Marmotta home once and don't cache failures

src/node-api-headers.ts:17-25, :67

extractEmbeddedHeaders calls marmottaHome() again instead of reusing the home ensureZig already resolved. src/toolchain.ts states that "the Marmotta home is resolved once by ensureZig and travels with the command". Passing zig.home from build.ts into resolveNodeApiHeaders(home) would keep that rule.

Also, resolved keeps a rejected promise forever. For the CLI that doesn't matter, but in programmatic use one transient failure (for example EACCES) makes every later call fail. Clear resolved when the promise rejects.

10. The extraction logic duplicates downloadAndInstallZig

src/node-api-headers.ts:81-109 vs src/toolchain.ts:248-267

Both functions run the same sequence: create a mkdtemp under the root, write the archive, tar -xf into a staging directory, check for one top-level directory, rm the destination, rename it into place, and clean up. Items 2 and 4 would need to be fixed in both places. A shared helper in toolchain.ts (something like installArchive(home, root, archivePath)) would keep them in one place.

11. SEA artifact names and existence checks

scripts/build-sea.mjs:20-35, scripts/build-bun.mjs:32-37

  • The comment at line 21 says the output is "named after the target platform, not the build host", but the name comes from process.platform / process.arch, which is the build host. (--build-sea can only build for the host anyway, so the comment is what's wrong.)
  • The names also don't match the Bun artifacts: marmotta-v0.6.0-win32-x64.exe vs marmotta-v0.6.0-windows-x64.exe. Mapping win32 to windows would keep checksums and download scripts consistent.
  • Both scripts check that the header archive exists by reading the whole file with readFile, and build-sea.mjs wraps a single file in a loop. await access(headers) does the same job.

Runtime

- Name the extracted headers directory after a hash of the embedded archive
  instead of scanning the Marmotta home, so a binary that embeds a newer
  node-api-headers version never builds against a stale extraction.
- Share the archive installation between Zig and the headers: unpack into a
  private staging directory and move the result into place with one rename.
  A complete destination is reused as is; only an incomplete one is replaced,
  so concurrent first runs cannot delete files another build is compiling with.
- Pass the home Zig resolved into the header resolution instead of reading
  MARMOTTA_HOME again, and stop caching a rejected header promise.
- Wrap sea.getAsset, which throws for a missing asset, so a SEA built without
  the headers archive reports INTERNAL_ERROR instead of a raw Node.js error.
- Read the embedded archive from Bun.embeddedFiles. With `--bytecode`,
  import.meta.url keeps the build machine's path, so the previous lookup only
  worked when the archive happened to exist next to the original sources.
- Pass tar relative paths and a working directory: GNU tar (Git for Windows,
  MSYS2, Cygwin) reads a colon before the first slash in an archive name as
  the remote syntax host:path, so an absolute Windows path failed.

Build scripts and workflow

- Let the staged dist/node-api-headers.tar be removed when a bun build or
  codesign fails, so a later publish cannot ship it.
- Smoke-test the binary with a stray .env and bunfig.toml in its working
  directory and with a private MARMOTTA_HOME, which always extracts the
  headers fresh instead of reusing the developer's cache.
- Install libstdc++ in the Alpine container (Bun's musl binaries link it) and
  pin the image; document the requirement.
- Grant write permissions and the attestation scopes only to the release job.
- Check for the headers archive with access(), drop the single-iteration loop,
  and name the SEA output windows-x64 like the Bun artifacts.

Tests

- Cover the shared archive installer: reuse of a complete destination,
  replacement of an incomplete one, and concurrent installers of one archive.
@ErRusso

ErRusso commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review — all eleven items are addressed in 1764857, and item 6 turned up a bug that had been hiding behind the developer's cache.

Must fix

  1. Header upgrade — the extracted directory is now named after a SHA-256 of the embedded archive (node-api-headers-<hash>), which is the only name the executable can know before extracting. The lookup is exact instead of a readdir scan, so a binary embedding a different node-api-headers version installs beside the old copy instead of reusing it. Old copies are left alone rather than removed, since a concurrent process may still be compiling against one.
  2. tar and C:\... — fixed at all four call sites (installArchive, build-headers-asset.mjs, and both test helpers): tar is given relative paths and a cwd, which GNU tar and bsdtar both accept.
  3. musl smoke test — confirmed with readelf: the musl binary needs libstdc++.so.6. The Alpine step now runs apk add --no-cache libstdc++ libgcc (with libgcc for libgcc_s.so.1) on alpine:3.22, and the README documents the requirement.
  4. Concurrent first run — the new installArchive helper reuses a complete destination and only removes an incomplete one. The destination is content-specific, so a race resolves to "someone else already installed an identical copy".

Should fix

  1. Leftover tarball — build-bun.mjs now records the exit code, breaks out of the loop, and exits after the finally has removed dist/node-api-headers.tar.
  2. Smoke test — the project commands run with cwd set to the project directory and a private MARMOTTA_HOME, so the planted .env/bunfig.toml are actually in the process working directory and the headers are always extracted fresh.
  3. SEA getAsset — wrapped; a SEA without the asset now reports INTERNAL_ERROR ("The standalone build does not embed node-api-headers.tar."). Built one without the asset to confirm.
  4. Workflow permissions — contents: read at the top, and the three write scopes on the release job only.

Nice to have

  1. resolveNodeApiHeaders(home) takes the home Zig already resolved, and resolved is cleared when the promise rejects.
  2. installArchive in toolchain.ts is shared by Zig and the headers; items 2 and 4 are fixed once for both. Added test/install-archive.test.js covering reuse, replacement of an incomplete destination, and four concurrent installers.
  3. SEA output names use windows and the comment now says the name comes from the platform this Node.js build targets; both scripts use access() and the single-iteration loop is gone.

Bug found through item 6

Making the smoke test extract the headers fresh exposed that the Bun binaries could not read their embedded asset at all. With --bytecode, import.meta.url in a compiled executable keeps the build machine's path, so readFile(join(entryDirectory, name)) only worked in the developer's checkout (or when the home already held an extraction, which is why earlier runs passed). readEmbeddedAsset now uses Bun.embeddedFiles, and bun/SEA binaries both pass the full smoke test with an empty MARMOTTA_HOME.

@NickNaso

Copy link
Copy Markdown
Member

@ErRusso please resolve the conflicts.

…cutable

# Conflicts:
#	package-lock.json
#	src/cli.ts
#	src/toolchain.ts
@ErRusso

ErRusso commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor Author

@NickNaso conflicts are now solved.

@NickNaso NickNaso left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review: pr/25, standalone executables (Bun / Node SEA)

Summary

This branch adds standalone executables for 8 platforms. They embed the Node-API headers, extract them once into the Marmotta home, and are built, smoke-tested and attested by a new release workflow. Along the way, the Zig installer now uses a shared, atomic installArchive routine. The work is careful and well commented, and failure modes like tar's C: handling, NDEBUG removing asserts and NODE_OPTIONS leaking in were clearly hit and fixed. I found nothing that blocks merging. The suggestions below are hardening.

Verified locally: check:version passes, and install-archive, standalone and cli tests show 30 pass, 0 fail, 2 skipped for platform reasons. I had to run npm ci first because the local node_modules was missing pino from the upstream merge; that was an environment problem, not a problem in the branch. I couldn't run the Bun build because Bun isn't installed here. The release workflow's smoke step gates that path.

Critical Issues

None.

Suggestions

# File Line Suggestion
1 entitlements.plist / build-bun.mjs — codesign runs without --options runtime, so the hardened runtime is off and none of these entitlements does anything. The comments say otherwise: "without allow-jit the executable is terminated" is only true under the hardened runtime. disable-library-validation is justified as "Loads Zig", but Zig is started as a separate process, not loaded as a library, so library validation never applies to it. If someone later adds the hardened runtime for notarization, these entitlements quietly allow libraries to be injected into a binary that runs compilers. Either drop disable-library-validation and fix the comments, or turn on the hardened runtime on purpose and keep only allow-jit, plus whatever else Bun really needs.
2 build-bun.mjs 338 dist/node-api-headers.tar is cleaned up in a finally, but that block doesn't run on SIGINT, SIGKILL or a crashed runner, and dist is in files. Add "!dist/node-api-headers.tar" to files in package.json as a guard, so an interrupted build can never publish the archive to npm.
3 workflows/release-binaries.yml 159 The release job has contents: write and id-token: write, but its third-party actions (softprops/action-gh-release@v2, attest-build-provenance@v3) are pinned by tag. A moved tag could then produce validly attested malicious binaries. Pin them to commit SHAs, at least in this job. ci.yml uses tags too, but it has no write or OIDC permissions.
4 release-binaries.yml 90 Unlike ci.yml, this workflow doesn't set up Zig, so npm test here skips every Zig integration test. That's acceptable, since smoke.mjs does the real end-to-end check, but say so in a comment so no one assumes the whole suite ran on all 8 runners.
5 node-api-headers.ts 782 The cached promise ignores home after the first call. A later call with a different MARMOTTA_HOME silently gets the first home's paths. This doesn't matter today, because the npm path never uses home and the standalone binary handles one CLI run per process. If programmatic use is a goal, as the "callers may retry" comment suggests, key the cache by home.path.
6 smoke.mjs 593 The workflow no longer uses --quick, because the Alpine step calls docker run directly. On top of that, node scripts/smoke.mjs --quick bin would treat --quick as the binary, and running it with no argument resolves to the current directory and fails with a confusing spawn error. Either remove --quick, or parse arguments properly and print usage when no path is given.
7 version.ts 4 npm version bumps package.json but not src/version.ts. The test and the tag check do catch the drift, but only after the bump. A "version" lifecycle script that rewrites the constant and runs git add src/version.ts would make the two impossible to get out of step.
8 build-sea.mjs 473 The SEA build writes to build/ while the Bun build writes to out/. Use out/ for both, so .gitignore, the smoke commands and the docs all point to one place.

What Looks Good

  • The installArchive refactor. Files are unpacked into a staging directory and moved into place with one rename, the destination is named after its contents, and the fallback "if another process already finished it, keep its copy" handles Windows, where renaming onto an existing directory fails. A test covers concurrent installers.
  • Least privilege in CI. Only the release job can write, the build jobs that run the tests and scripts are read-only, and needs: build with fail-fast: false means one failed target blocks the whole release instead of publishing part of it.
  • Hostile-environment smoke test. It plants NODE_OPTIONS, .env and bunfig.toml, uses a private MARMOTTA_HOME, and loads the compiled addon in a real Node.js process. It's a strong end-to-end check, and the comment explaining why BUN_OPTIONS can't be blocked is a nice touch.
  • Version discipline. check:version, the tag-vs-package.json check and the --version test give three separate safeguards.
  • Clear error paths. A missing embedded asset is reported as an InternalError, filesystem failures go through directoryError, and a failed header resolution is no longer cached.

Verdict

Approve with suggestions. #1 to #3 are worth fixing before the first tagged release: the misleading entitlements, the npm leak guard and SHA pinning. The rest can come later.s

Address the second review of the standalone executables, and repair the SEA
fallback that the structured-logging merge broke.

macOS signing
- Sign the Bun binaries with `codesign --options runtime` so the JIT
  entitlements actually apply, and drop disable-library-validation: Zig is a
  separate process, so library validation never covers it.
- Document the hardened runtime and the missing notarization in the README.

npm package
- Exclude dist/node-api-headers.tar in "files", covering interrupted builds
  whose cleanup never ran.
- Add a `version` lifecycle script that rewrites src/version.ts and stages it,
  so `npm version` cannot leave the executables reporting the old version.

Release workflow
- Pin the release job's actions to commit SHAs; that job holds write and OIDC
  permissions.
- Say that Zig/Deno/Bun are intentionally absent from the build jobs and that
  the smoke test is the end-to-end check there.

Runtime and scripts
- Key the resolved headers cache by Marmotta home instead of caching the first
  home forever; add a test for two homes.
- Drop the unused smoke-test --quick mode and print usage without a binary.
- Write the SEA executable to out/ like the Bun artifacts, and give the ESM
  bundle a real require() so pino's require('node:os') no longer throws
  "Dynamic require is not supported" under Node SEA.
@ErRusso

ErRusso commented Oct 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks — all eight suggestions are addressed in 44f92d6.

Critical / worth fixing before the tag

  1. Entitlements — the macOS binaries are now signed with codesign --options runtime, so the JIT entitlements actually apply. disable-library-validation is gone: Zig runs as a separate process, so library validation never covered it. The plist and build-script comments no longer claim otherwise, and the README says the binaries are hardened-runtime signed but not notarized.
  2. npm leak guard — "!dist/node-api-headers.tar" is in files. I confirmed with npm pack --dry-run that the archive is excluded even when present.
  3. SHA pinning — the release job's three actions are pinned to commit SHAs (download-artifact 4.3.0, attest-build-provenance v3, action-gh-release 2.6.2), with a comment explaining why.

Suggestions

  1. The release build step now says Zig/Deno/Bun are intentionally absent, which tests skip, and that smoke.mjs is the end-to-end check there.
  2. The headers promise cache is keyed by home.path, and test/standalone.test.js now checks that two homes do not share cached paths.
  3. --quick is removed (the workflow uses docker run directly), and smoke.mjs prints usage and exits 2 without a binary argument.
  4. Added a version lifecycle script (scripts/sync-version.mjs) that rewrites src/version.ts and stages it. Verified with npm version patch --no-git-tag-version that the constant follows package.json.
  5. The SEA output now defaults to out/, like the Bun artifacts; build/ holds only the bundle, SEA config and headers archive.

One regression found while smoke-testing the fallback

The SEA build no longer started after the structured-logging merge: esbuild's ESM output has no scope-level require, so pino's require('node:os') threw Dynamic require of "node:os" is not supported. build-sea.mjs now injects a createRequire banner; the full scripts/smoke.mjs passes for both the SEA and Bun binaries on linux-x64 with an empty MARMOTTA_HOME. npm test is 61 pass, 0 fail, 7 skipped locally (no Zig/Deno/Bun).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Distribute Marmotta as a standalone executable (single-file binary)

2 participants