Background
The simlin-serve cross-build, npm-packaging, and release plumbing was deliberately copied from simlin-mcp during Phase 1 of the server-rewrite (see docs/implementation-plans/2026-04-05-server-rewrite/phase_01.md, Subcomponent F, Tasks 18-21). The mirror was intentional to minimize churn and stay close to a working pattern, with the explicit understanding that shared infrastructure would be factored out once two consumers existed.
That second consumer now exists. This is the follow-up to do the factor-out.
Affected files
src/simlin-mcp/Dockerfile.cross, src/simlin-serve/Dockerfile.cross
src/simlin-mcp/scripts/cross-build.sh, src/simlin-serve/scripts/cross-build.sh
src/simlin-mcp/build-npm-packages.sh, src/simlin-serve/build-npm-packages.sh
.github/workflows/mcp-release.yml, .github/workflows/serve-release.yml
scripts/release-mcp.sh, scripts/release-serve.sh
Shared logic
Across both crates the duplicated pieces are:
- The four-platform table (
darwin-arm64, linux-arm64, linux-x64, win32-x64) with their Rust target triples
- The Dockerfile shape: pinned Rust + Zig + cargo-zigbuild
- The
cargo zigbuild --locked --release --target ... per-target loop
- The
build-npm-packages.sh per-platform package.json template
- The release-script flow: bump Cargo.toml, refresh Cargo.lock, bump wrapper package.json + optionalDependencies, regenerate platform packages, sanity-check version agreement, commit, tag (no push)
- The CI workflow shape: validate -> matrix build -> publish-platform -> publish-wrapper, with
npm view rerunnability guards and --provenance on every publish
Divergences to preserve
- simlin-serve's image bundles Node + pnpm because its
build.rs shells to pnpm to produce web/dist/ for rust-embed. simlin-mcp has no embedded frontend.
- simlin-serve's
cross-build.sh streams the repo into the container via tar instead of bind-mounting it read-only, since pnpm and vite need to write inside the workspace during the build.
The abstraction must accommodate these without re-introducing copy-paste.
Suggested approach
- Move the parameterized parts (target list, binary name, repo path, optional pre-build hook for the SPA) into a shared bash library at
scripts/lib/cross-build.sh that both crate-level scripts source.
- Keep per-crate
Dockerfile.cross files (image contents legitimately differ) but extract a shared base layer if multi-stage Dockerfile composition is feasible.
- Workflow YAMLs are harder to share without composite actions or reusable workflows; consider a single reusable workflow at
.github/workflows/_npm-release.yml that takes the package name and binary name as inputs.
- Generalize the version-bump-and-tag logic in
scripts/release-{mcp,serve}.sh into scripts/lib/release.sh.
Discovery context
Filed as the follow-up explicitly called out in Phase 1 Subcomponent F of the server-rewrite plan, and mentioned at line 267 of docs/design-plans/2026-04-05-server-rewrite.md.
Background
The simlin-serve cross-build, npm-packaging, and release plumbing was deliberately copied from simlin-mcp during Phase 1 of the server-rewrite (see
docs/implementation-plans/2026-04-05-server-rewrite/phase_01.md, Subcomponent F, Tasks 18-21). The mirror was intentional to minimize churn and stay close to a working pattern, with the explicit understanding that shared infrastructure would be factored out once two consumers existed.That second consumer now exists. This is the follow-up to do the factor-out.
Affected files
src/simlin-mcp/Dockerfile.cross,src/simlin-serve/Dockerfile.crosssrc/simlin-mcp/scripts/cross-build.sh,src/simlin-serve/scripts/cross-build.shsrc/simlin-mcp/build-npm-packages.sh,src/simlin-serve/build-npm-packages.sh.github/workflows/mcp-release.yml,.github/workflows/serve-release.ymlscripts/release-mcp.sh,scripts/release-serve.shShared logic
Across both crates the duplicated pieces are:
darwin-arm64,linux-arm64,linux-x64,win32-x64) with their Rust target triplescargo zigbuild --locked --release --target ...per-target loopbuild-npm-packages.shper-platform package.json templatenpm viewrerunnability guards and--provenanceon every publishDivergences to preserve
build.rsshells to pnpm to produceweb/dist/for rust-embed. simlin-mcp has no embedded frontend.cross-build.shstreams the repo into the container viatarinstead of bind-mounting it read-only, since pnpm and vite need to write inside the workspace during the build.The abstraction must accommodate these without re-introducing copy-paste.
Suggested approach
scripts/lib/cross-build.shthat both crate-level scripts source.Dockerfile.crossfiles (image contents legitimately differ) but extract a shared base layer if multi-stage Dockerfile composition is feasible..github/workflows/_npm-release.ymlthat takes the package name and binary name as inputs.scripts/release-{mcp,serve}.shintoscripts/lib/release.sh.Discovery context
Filed as the follow-up explicitly called out in Phase 1 Subcomponent F of the server-rewrite plan, and mentioned at line 267 of
docs/design-plans/2026-04-05-server-rewrite.md.