Skip to content

build: migrate runtime, tooling and tests from node to bun - #3560

Draft
Pooya Raki (PooyaRaki) wants to merge 2 commits into
mainfrom
claude/serene-hawking-1t7c89
Draft

Pooya Raki (PooyaRaki) wants to merge 2 commits into
mainfrom
claude/serene-hawking-1t7c89

Conversation

@PooyaRaki

@PooyaRaki Pooya Raki (PooyaRaki) commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Replaces Node.js, Yarn 4, Vitest and dd-trace with Bun 1.4.2 (latest release) as runtime, package manager and test runner. It also replaces Datadog's dd-trace with OpenTelemetry. This is one PR, not a split series, because the request was for a single migration PR.

The branch is rebased onto current main. It includes #3476's move to TypeScript 7 (typescript@7.0.2 is kept for bun run typecheck), #3551's #/ imports, #3573's blocklist removal and #3482's Biome 2.5.15. #3476's tsc-alias/concurrently/dist build and the Yarn bumps are superseded, because Bun runs the sources directly.

#/ imports. Bun 1.4.2 does not resolve package.json imports subpaths, but it does resolve tsconfig.json paths. So #/* → ./src/* and #/abis/* → ./abis/* live in tsconfig.json paths, and the Node-only imports map (which pointed at dist/) is gone. The production image ships tsconfig.json and keeps WORKDIR /app (#3570).

Why OpenTelemetry. Datadog does not support dd-trace on Bun and recommends the OpenTelemetry SDK instead. Native Bun.otel is still an unmerged Bun PR. I checked on Bun 1.4.2 that the standard instrumentations produce spans for:

  • the HTTP server (Fastify loads node:http via require, so it can be patched)
  • pg, redis and ioredis (the instrumentation supports ioredis <7, so main's ioredis 6 is covered)
  • amqplib

Spans go over OTLP/HTTP to the Datadog Agent's OTLP receiver. Log lines carry dd.trace_id/dd.span_id, so Datadog can link each log to its trace.

Deploy prerequisite. Before this ships, enable the Agent's OTLP/HTTP receiver, e.g. DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_HTTP_ENDPOINT=0.0.0.0:4318. Traces go to http://$DD_AGENT_HOST:4318/v1/traces unless OTEL_EXPORTER_OTLP_(TRACES_)ENDPOINT is set. DD_SERVICE/DD_ENV/DD_VERSION become the resource's service, environment and version. The default is to sample every trace; use OTEL_TRACES_SAMPLER/OTEL_TRACES_SAMPLER_ARG to lower that.

Supply-chain settings carry over from .yarnrc.yml:

Yarn Bun
npmMinimalAgeGate (7 days) bunfig.toml minimumReleaseAge = 604800 (7 days)
enableScripts: false trustedDependencies: []
enableHardenedMode / --immutable bun install --frozen-lockfile (bun.lock integrity hashes)

A new CI job, ci / audit, runs bun audit (currently: no vulnerabilities). The earlier fastify override is dropped: @nestjs/platform-fastify@12.1.2 from main already pins the patched fastify@5.12.5. Dependabot now uses the bun ecosystem.

Formatter/linter. Bun 1.4.2 has no built-in formatter or linter, so Biome stays.

Behaviour changes for reviewers:

  • No build step. Bun runs the TypeScript sources: no tsc emit, no dist/, no tsconfig.build.json. TypeORM loads migrations/*.ts. The image is oven/bun:1.4.2-alpine with production dependencies only. One-off scripts run as bun scripts/generate-token.ts.
  • Undici dispatcher removed. Bun's native fetch ignores undici's global dispatcher (verified), so the undici Agent, its shutdown hook and the UNDICI_* variables were no-ops and are gone. Bun's fetch pools keep-alive connections itself, and was 2.5–5× faster than undici-on-Bun in a local benchmark. Per-request timeouts (AbortSignal.timeout) are unchanged.
  • ES module semantics. Bun loads the app as ES modules. Some import cycles that CommonJS tolerated, by silently leaving a binding undefined, now fail to load. They are broken with z.lazy (User↔Wallet, notification schemas), TypeORM's Relation<T> on relation properties, forwardRef(UserIdentityResolverModule), and a dedicated fetch-client.entity.ts for FetchClientToken.
  • Type imports. Type-only names used in decorator metadata are now import type. tsconfig uses verbatimModuleSyntax/module: Preserve/moduleResolution: bundler/noEmit. chain.entity.ts had been importing a type as a value.
  • Bun transpiler bug. Undecorated constructor params ahead of the first @Inject are emitted as Object when the class has no class decorator. The only affected class, KilnApiManager, got @Injectable().

Tests. All suites run on bun:test with explicit imports and --parallel file isolation.

Suite Before (Vitest) After (Bun, local, on this branch)
Unit ~3 min 6,962 pass in ~30s
Integration — 1,827 pass, 0 fail (105 files, ~2.2 min unsharded)
  • Spec type-checking stays in CI, as on main: bun run typecheck is tsc --noEmit -p tsconfig.json, specs included. To keep main's specs compiling under bun:test's stricter typings, src/__tests__/mocks.ts erases generics from a mock's implementation type (as Vitest/Jest do) and test/bun-test.d.ts gives the equality matchers Jest's generic expected (so a datasource's Raw<T> can be compared with T).
  • Integration runs as three bun test --shard jobs in CI, matching main's split.
  • Integration parity. A per-file comparison against Vitest on main showed the same tests run. Two undici-dispatcher tests were removed. 34 migration specs under migrations/ that Vitest's src/** glob never ran now run too.
  • e2e. e2e (not in CI) has no new failures compared with main in the same environment.
  • Bun 1.4.2 bugs worked around in test/setup.ts:
    • An AsyncLocalStorage#enterWith() during test-file evaluation (nestjs-cls does one at import) hangs that file's tests.
    • jest.resetAllMocks() drops jest.fn(impl) implementations. Vitest 4 kept them, and shared pass-through doubles rely on that.
  • Fake timers. bun:test has no toFake option, so the entitlements spec that faked only Date (Postgres needs real timers) uses setSystemTime instead.
  • Vacuous assertions fixed:
    • checkGuardIsApplied passed on any first guard, because Vitest's expect returns a truthy object.
    • A route-logger assertion built a stringMatching regex from [, which matched almost anything.
  • Faker-dependent flakes fixed (separate commit; both also flaky on main):
    • The notifications "token is invalid" spec used faker.string.sample(), which can contain two dots and then parses as a three-part JWT ("invalid token" instead of "jwt malformed"). It now uses faker.string.alphanumeric() like its siblings. CI seed 7016067276651539 reproduced it.
    • SafeSignature > should return the v value drew a random v; v 0 marks a contract signature, which needs a dynamic part (about 1 in 256 runs). v is now 1–255.
  • Other test edits: e2e specs are renamed *.e2e.spec.ts, because Bun only discovers *.spec.*. Biome's test-globals list is removed now that every spec imports from bun:test.

Changes

  • package.json, bun.lock, bunfig.toml: Bun scripts and supply-chain settings, the OpenTelemetry dependencies, and typescript@7.0.2 for type-checking. Removed yarn.lock, .yarn/, .yarnrc.yml, .nvmrc, vitest.config.ts, tsconfig.build.json, test/faker-setup.ts, test/shared-module-cache-setup.ts, Vitest, SWC, tsc-alias, concurrently, @types/node, undici and dd-trace.
  • tsconfig.json: Bun types, #/ paths, verbatimModuleSyntax, noEmit.
  • src/tracing/:
    • preloaded OpenTelemetry bootstrap
    • route naming, health probes skipped, query values redacted, and db/cache spans only inside a parent span
    • Datadog log correlation, an outbound client-span helper, and span flush on shutdown
  • src/config/, .env.sample.json: added tracing config (OTEL_SDK_DISABLED, OTLP endpoints, DD_AGENT_HOST/DD_SERVICE/DD_ENV/DD_VERSION); removed undici.
  • src/datasources/network/: outbound requests wrapped in client spans with traceparent propagation; undici dispatcher removed.
  • src/logging/: dd.trace_id/dd.span_id on log lines.
  • ES-module fixes:
    • src/modules/{users,wallets,notifications} schemas: z.lazy
    • **/*.entity.db.ts: Relation<T>
    • src/modules/spaces/spaces.module.ts: forwardRef
    • src/modules/staking/datasources/kiln-api.manager.ts: @Injectable()
    • type-only imports across 27 files
  • Specs (~540 files): Vitest to bun:test, including those added or changed on main since. Adds src/__tests__/mocks.ts (Mocked* types, mocked(), automock()), test/setup.ts (preload) and test/bun-test.d.ts (matcher typings), plus new specs for the tracing helpers.
  • Dockerfile, .dockerignore: oven/bun image, TS sources, and the --preload ./src/tracing/tracing.ts CMD.
  • .github/workflows/_ci.yml (renamed from _ci-node.yml, same job names plus audit); pull-request.yml, devstaging.yml and production.yml callers; .github/dependabot.yml.
  • .devcontainer/, .vscode/, .husky/pre-commit: Bun.
  • Docs: README.md (new Observability section), AGENTS.md (pre-commit is now bun run format / lint / typecheck / test), TESTING.md, docs/agents/*, .claude/skills/*, .specify/, specs/, module READMEs. Also adds SPDX headers to the touched files that lacked one.

🤖 Generated with Claude Code

https://claude.ai/code/session_01GStCQiW4fcycjN6k2KmRQz

@PooyaRaki
Pooya Raki (PooyaRaki) requested a review from a team as a code owner October 7, 2026 17:44
@PooyaRaki
Pooya Raki (PooyaRaki) marked this pull request as draft October 9, 2026 08:06
@PooyaRaki
Pooya Raki (PooyaRaki) force-pushed the claude/serene-hawking-1t7c89 branch 5 times, most recently from 078c080 to cd87467 Compare October 9, 2026 16:23
Runs the app, scripts and migrations on Bun 1.4.2 straight from the
TypeScript sources, swaps Yarn for Bun's package manager (frozen
lockfile, exact versions, a 7-day minimum release age, no dependency
lifecycle scripts, `bun audit` in CI) and moves every suite from Vitest
to bun:test. dd-trace is replaced by OpenTelemetry, exported over OTLP
to the Datadog Agent, since Datadog does not support dd-trace on Bun.

Rebased onto main's `#/` imports: Bun resolves them through
tsconfig.json `paths`, and `bun run typecheck` keeps main's guarantee
of type-checking the specs, with Jest-compatible mock and matcher
typings for bun:test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GStCQiW4fcycjN6k2KmRQz
The notifications "token is invalid" spec built its token with
`faker.string.sample()`, which can contain two dots and then parses as a
three-part JWT that jsonwebtoken rejects as "invalid token" rather than
"jwt malformed"; it now uses `faker.string.alphanumeric()` like its
siblings. The SafeSignature v-value spec drew a random v, and a v of 0
marks a contract signature that needs a dynamic part; v is now 1-255.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GStCQiW4fcycjN6k2KmRQz
@PooyaRaki
Pooya Raki (PooyaRaki) force-pushed the claude/serene-hawking-1t7c89 branch from cd87467 to 06bb398 Compare October 9, 2026 16:24

This branch has not been deployed

No deployments
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