Skip to content

[Experiment] Migrate Bun to Go#78

Draft
travis-hoover-glean wants to merge 3 commits into
mainfrom
go-migration
Draft

[Experiment] Migrate Bun to Go#78
travis-hoover-glean wants to merge 3 commits into
mainfrom
go-migration

Conversation

@travis-hoover-glean
Copy link
Copy Markdown
Contributor

Problem

glean-mdm is built with bun (bun build --compile). We want to move off the bun toolchain to the native Go toolchain while preserving behavior exactly. A hard constraint: already-deployed bun binaries self-update by downloading the next release, so the Go binaries must keep identical output filenames and release URLs, and reproduce the runtime contract the E2E scripts assert.

Solution

Functionally-identical Go rewrite in the same repo, cross-compiled with go build (no bun --compile). Clean replacement: TypeScript sources, tsconfig, vitest, and bun lockfiles are removed.

Behavior preserved (covered by the reused ci/e2e-*.sh scripts):

  • --version prints the bare version string.
  • Host log lines (Configured JSON/TOML/YAML: <path>, Already up to date, etc.).
  • mcp-config.json shape (array of {serverName, url}, 2-space indent, trailing newline) and skip/append/dedup semantics.
  • Idempotent host writes; self-update + re-exec with --skip-update; launchd/systemd/schtasks scheduling.

The MCP client registry (the tricky part)

The original depends on @gleanwork/mcp-config-glean@gleanwork/mcp-config-schema, whose builder injects per-client default fields that are not present in the raw config data (e.g. Goose YAML gets enabled, timeout, env_keys, available_tools). A hand-written Go builder would silently drift from upstream.

Instead, the npm package stays the source of truth:

  • scripts/gen-registry.mjs (dev-only Node, pinned to @gleanwork/mcp-config-glean@4.3.0) runs the real builder and snapshots each user-configurable HTTP client into internal/registry/registry.json.
  • The Go binary embeds that JSON via go:embed and, at runtime, only substitutes the server-name and server-URL tokens — every other per-client field is preserved verbatim.
  • A CI job regenerates the snapshot and fails if it is stale vs. the pinned dependency.

Layout

cmd/glean-mdm (cobra) + internal/{cli,config,configwriter,registry,hosts,extensions,users,updater,scheduler,uninstaller,platform,logger,jsonutil,version}. Library mapping: commander→cobra, zod→manual validation, smol-toml→go-toml/v2, yaml→yaml.v3, fetch→net/http, createHash→crypto/sha256, BUILD_VERSION define→-ldflags -X .../version.BuildVersion.

Build & CI

  • build.sh cross-compiles all 5 targets with identical filenames and regenerates version.json with sha256 checksums.
  • Workflows swap setup-bun for setup-go (+ a Node step for registry generation). The two Bun.serve E2E mock servers are rewritten as Go net/http programs; all ci/e2e-*.sh scripts are reused. The e2e-update job tolerates a pre-Go main so it stays green on this migration PR (it then exercises the real bun→Go self-update path).

Testing

  • go vet ./..., go test ./..., and gofmt -l are clean locally. Unit tests port the *.spec.ts suites (configurator merge/dedup/idempotency, updater version compare/shouldUpdate, config validation, registry substitution incl. Goose's embedded name, scheduler args/plist).
  • ./build.sh produces all binaries + version.json; the binary was exercised for --version, config, and run --dry-run (10 hosts configured, JetBrains correctly skipped).
  • System-level E2E flows (sudo, /usr/local/bin, real home dirs) run on the CI matrix (ubuntu/macos/windows), not locally.

Notes / risks

  • The per-client builder output is frozen in the committed registry.json; bumping the schema version requires re-running the generator (gated in CI).
  • The server-name normalization (glean_ prefix unless already present) is implemented directly in Go rather than codegen — it is a small, stable rule, unit-tested, and exercised end-to-end via Goose's embedded name field.
  • JSON/TOML/YAML key ordering becomes deterministic (sorted) vs. the TS writer; semantically equivalent and still idempotent (what the E2E checks assert).

Replace the TypeScript/bun implementation with a functionally-identical Go
port, cross-compiled with the native Go toolchain. Output binary names and
release URLs are unchanged so already-deployed bun binaries self-update
straight into the Go binary.

The MCP client registry stays sourced from @gleanwork/mcp-config-schema: a
build-time Node generator (scripts/gen-registry.mjs) snapshots it into
internal/registry/registry.json, which the Go binary embeds. At runtime Go only
substitutes the server name/URL tokens, preserving every per-client default
field verbatim.
@travis-hoover-glean travis-hoover-glean changed the title [refactor] Migrate glean-mdm CLI from TypeScript/bun to Go [Experiment] Migrate Bun to Go May 29, 2026
…riven configurators

Behavior-preserving cleanups to the new Go code:
- Collapse configureJSON/TOML/YAMLFile into one codec-driven configureFile
- Extract shared ResolveWritePath/AtomicWrite into internal/fsutil, used by
  hosts and configwriter
- Add a fail(err) closure in updater downloadAndInstall
- Derive MCP/MDM config and Windows log paths from GetDefaultConfigDir()
- Unify timeout-exec helpers into internal/executil (RunWithTimeout/ErrTimeout)
- Share serve/writePort between the two e2e mock servers via ci/mockutil

Verified with go build, go vet, gofmt -l, and go test ./...
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.

1 participant