Skip to content

feat: ship per-arch standalone binaries on each release#73

Open
ja-818 wants to merge 1 commit intostripe:mainfrom
gethouston:add-binary-releases
Open

feat: ship per-arch standalone binaries on each release#73
ja-818 wants to merge 1 commit intostripe:mainfrom
gethouston:add-binary-releases

Conversation

@ja-818
Copy link
Copy Markdown

@ja-818 ja-818 commented May 4, 2026

Summary

Adds a release workflow that attaches single-file standalone link-cli binaries to every GitHub Release, plus a manifest.json with sha256 checksums and per-target download URLs. Targets: darwin-arm64, darwin-x64, linux-x64, windows-x64.

Motivation

We're shipping Houston (a desktop platform for non-technical founders to run AI agents) with Link as a first-class payments surface. Houston bundles three CLIs today (codex, composio, claude-code); for each we download a signed binary from upstream, sha256-verify, sign + notarize alongside our .app, and stage it into Contents/Resources/bin/. That works well because each upstream ships per-arch standalone binaries.

link-cli only ships on npm. Bundling a non-Node binary today means owning a bun build --compile (or pkg) pipeline per consumer, which means every consumer hitting the same packaging edges (the react-devtools-core resolution issue under ink, update-notifier external handling, viem's WASM + dynamic imports). Solving it once upstream is a much smaller surface than every agent platform doing it independently.

Tracking issue with full context: #72.

What this PR does

  1. scripts/build-binary.ts: invokes Bun.build against the existing packages/cli/dist/cli.js entrypoint, with a tiny in-process plugin that stubs two optional deps (react-devtools-core is dynamically loaded by ink only when DEV=true; update-notifier is already external in tsup.config.ts). Output: dist-bin/link-cli-<target>[.exe].

  2. scripts/generate-manifest.ts: walks dist-bin/, sha256s every binary, emits manifest.json with version, generated_at, and per-target { file, sha256, url }.

  3. .github/workflows/release-binaries.yml: triggers on release.published (the event changesets/action fires on every npm publish). On a single Ubuntu runner, builds all four targets via Bun cross-compile, generates the manifest, and uploads everything to the existing release via gh release upload. workflow_dispatch is provided for manual rebuilds against an existing tag.

  4. README.md: short "Standalone binaries" section under Installation pointing at the latest release + the manifest URL pattern.

  5. .gitignore: adds dist-bin/.

Verification

Run locally on macOS arm64 (Bun 1.3.10, Node 22, pnpm 10.32):

pnpm install --frozen-lockfile
pnpm turbo run build
for t in darwin-arm64 darwin-x64 linux-x64 windows-x64; do
  bun run scripts/build-binary.ts $t
done
bun run scripts/generate-manifest.ts 0.4.2

Result on darwin-arm64 host:

  • All four targets compile; total time < 10s after pnpm turbo run build.
  • dist-bin/link-cli-darwin-arm64 --help and --version work.
  • dist-bin/link-cli-darwin-arm64 mpp decode --challenge 'Payment id="ch_001", realm="merchant.example", method="stripe", intent="charge", request="..."' runs the viem-backed challenge decoder successfully (validation reaches the per-field "amount: missing" check, which is the expected behavior for an incomplete test challenge).
  • Sizes (minified): darwin-arm64 60 MB, darwin-x64 64 MB, linux-x64 101 MB, windows-x64 111 MB. Linux/Windows are heavier because Bun ships more runtime polyfills for non-host targets.

CI gauntlet on this branch passes locally:

  • pnpm biome check . — clean (96 files)
  • pnpm turbo run typecheck — pass
  • pnpm turbo run test — 100/100 pass
  • pnpm turbo run build — pass

Notes

  • I pinned Bun to 1.3.10 in the workflow to keep release outputs reproducible. Happy to change that to latest or whatever your team prefers.
  • The script uses // @ts-expect-error on one line because the compile option is supported by Bun.build at runtime but not yet in the public TypeScript types.
  • Stub plugin replaces module contents, not the import path, so the bundled output has no leftover references to either optional dep on disk.
  • I haven't touched release.yml — the existing changesets-driven flow is unchanged. This new workflow is fully additive and runs after the release event the existing flow already produces.
  • I'll sign the CLA on first comment.

Future work (not in this PR)

  • Lazy-load viem inside mpp decode so the static bundle is smaller for non-MPP users (mentioned in the tracking issue).
  • Linux arm64 + Windows arm64 if there's demand. Bun supports both targets; happy to add in a follow-up.

🤖 Co-authored with Claude Code.

@cla-assistant
Copy link
Copy Markdown

cla-assistant Bot commented May 4, 2026

CLA assistant check
All committers have signed the CLA.

Adds a release-binaries workflow that produces single-file standalone
link-cli binaries for darwin-arm64, darwin-x64, linux-x64, and
windows-x64 every time a GitHub Release is published, plus a
manifest.json with sha256 checksums and download URLs.

Motivation: agent platforms and desktop apps that want to bundle
link-cli currently have to build their own binaries from the npm
package, which means owning a CI pipeline that compiles ink + viem +
update-notifier into a single executable. Shipping binaries upstream
lets downstream consumers pin against a manifest URL the same way they
pin any other vendored CLI (codex, claude-code, etc).

How it works:
- scripts/build-binary.ts runs bun build --compile against the existing
  packages/cli/dist/cli.js entrypoint. A small plugin stubs two
  optional deps that ink and update-notifier reference but that are
  not needed at runtime in a bundled context (react-devtools-core only
  loads when DEV=true; update-notifier is already external in tsup).
- scripts/generate-manifest.ts emits dist-bin/manifest.json with
  version, generated_at, and per-target { file, sha256, url }.
- .github/workflows/release-binaries.yml triggers on
  release.published (changesets/action publishes a GH release on every
  npm release), builds all four targets in parallel inside a single
  Ubuntu runner via Bun cross-compile, and attaches the binaries +
  manifest to the release.

Verified locally: each binary runs --help, --version, and exercises
the viem-dependent mpp decode path successfully on darwin-arm64.

Sizes (minified): darwin-arm64 60 MB, darwin-x64 64 MB, linux-x64
101 MB, windows-x64 111 MB. Linux/Windows are heavier because Bun
ships more runtime polyfills for non-host targets.
@ja-818 ja-818 force-pushed the add-binary-releases branch from 17e5b3f to 23fdf0a Compare May 5, 2026 14:10
@ja-818
Copy link
Copy Markdown
Author

ja-818 commented May 5, 2026

Force-pushed with the correct git author email so the commit links to my GitHub account. Going to sign the CLA now.

done

- name: Generate manifest
run: bun run scripts/generate-manifest.ts "${{ steps.tag.outputs.version }}"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

doesn't this script need two params?

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.

2 participants