Skip to content

chore(repo): migrate from bun to pnpm - #129

Merged
haydenshively merged 5 commits into
mainfrom
chore/migrate-to-pnpm
Aug 7, 2026
Merged

chore(repo): migrate from bun to pnpm#129
haydenshively merged 5 commits into
mainfrom
chore/migrate-to-pnpm

Conversation

@haydenshively

@haydenshively haydenshively commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Part 1 of 2 for CRTR-2822. pnpm takes over installs, workspace resolution, the version catalog, and
script running. bun stays as the runtime and test runner, so this PR touches no test file and
bun test is unchanged. Part 2 removes bun (vitest, esbuild bundles, Node runtime).

Splitting it this way lands the entire security motivation now, without waiting on a 136-file vitest
port.

Why

A npm supply-chain attack prompted a team-wide decision (Slack #curators, 2026-07-17) to
standardize on pnpm. The deciding property is pnpm's default-deny on dependency lifecycle scripts: a
compromised transitive dependency cannot run arbitrary code at install time. bun has no equivalent.

The decision was already written up in docs/decisions/TIB-2026-07-20-migrate-to-pnpm.md, which
lived only on the unmerged claude/migrate-repo-pnpm-e5f4c1 (3 commits, @cashd, 2026-07-20, no PR).
That branch is 190 commits behind main and predates four workspaces, so this re-derives from it
rather than rebasing. The TIB is carried over here with Addendum A recording what changed.

Three settings the original branch omitted

Setting Why it matters
strictDepBuilds: true Without it an un-opted-in build script is a warning, not a failure — the deciding security property of the TIB would have been advisory only.
enablePrePostScripts: true pnpm does not run npm-style pre/post scripts by default. prestart builds @repo/contracts' gitignored dist/; without this the bots cannot resolve their own workspace dependency.
allowBuilds: {} Starts empty, not {esbuild: true} — nothing in the current tree declares preinstall/install/postinstall, and esbuild is absent entirely. It becomes the first entry in Part 2.

No dependency upgrades ride along

Regenerating the lockfile re-resolves every caret range, which would contradict the TIB's non-goal.
Catalog entries that drifted are pinned to what bun.lock had resolved:

  • solc 0.8.35 → pinned. This one is not just parity: solc fixes the compiled bytecode of the
    soltag sol\``` lens templates and the Executor, so a silent bump would invalidate the recorded
    explorer-verification settings (0.8.35, optimizer runs=200).
  • @internationalized/date 3.12.1, date-fns 4.1.0, @types/bun 1.3.13 — pinned.
  • Remaining drift is dev-tooling transitives inside their declared ranges. The only runtime-reachable
    one is valibot 1.4.1 → 1.4.2 (patch, via @morpho-org/viem-dlc).
  • Package set is otherwise identical; pnpm correctly skips one Windows-only optional binary.

Docker

Keeping FROM oven/bun was not viable: pnpm is activated through corepack, which those images do not
provide, and bun install --frozen-lockfile is no fallback once bun.lock is deleted. All three
images move to node:24.14.1-slim with the bun binary copied in. Both runtimes must stay on PATH
in the final image
for this PR, because CMD fires prestart, which shells out to pnpm. Part 2
drops the bun binary and switches CMD to node dist/src/index.js.

Evidence

  • Default-deny actually blocks CI — not a vacuous check. An esbuild probe on a clean
    node_modules exits 1 with ERR_PNPM_IGNORED_BUILDS; probe reverted, lockfile byte-identical
    after.
  • prestart still runs — wiped packages/contracts/dist/, ran
    pnpm --filter @morpho-org/blue-liquidation run start, watched pnpm fire
    pnpm -r --parallel --if-present run build and rebuild it before reaching the bot's own config check.
  • Tests byte-identical to main1412 pass, 3 skip, 3 fail, 1 error, 7732 expect() calls, 1418 tests across 136 files on both main (in a baseline worktree) and this branch. Same failing
    test names. The 3 failures are a pre-existing missing RPC_URL_8453 for the fork suites; CI
    supplies it as a secret.
  • pnpm install --frozen-lockfile on a clean checkout: clean.
  • pnpm -r run typecheck 12/12 Done · pnpm lint 0 warnings 0 errors · pnpm knip clean · pnpm format clean.
  • pnpm --filter @repo/contracts run generate re-run; the only diff in the tracked generated sources
    is the command string in the header.
  • pnpm -- forwarding verified against the market-making CLI (--json is honored), before rewriting
    ~22 README examples.
  • Pre-commit hook exercised on the real commit: lint-staged + knip both ran under pnpm.

Containers run unprivileged

oven/bun ends with USER bun; the official node images define a node user (uid 1000) but do
not switch to it, so the base swap would have promoted a process holding a funded liquidator key to
root. Each Dockerfile now runs corepack enable as root (its shim lands in /usr/local/bin),
chowns /repo, then drops to USER node — every layer after that, the install, and CMD are
unprivileged.

Docker is verified

All three images build, docker run … id reports uid=1000(node) gid=1000(node), and the
blue-liquidation container runs prestart — writing @repo/contracts' gitignored dist/ as
non-root — before reaching its fail-loud Missing required env var: CHAIN_ID. Both pnpm 11.1.1 and
bun 1.3.12 are on PATH in the final image, as this intermediate state requires.

Not verified

Nothing outstanding.

Follow-ups

🤖 Generated with Claude Code

@haydenshively haydenshively self-assigned this Aug 5, 2026
@haydenshively
haydenshively requested a review from cashd August 5, 2026 13:40
@haydenshively
haydenshively marked this pull request as ready for review August 5, 2026 13:41

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread bots/blue-liquidation/Dockerfile

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 88ae719a35

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/actions/setup/action.yml
haydenshively added a commit that referenced this pull request Aug 5, 2026
Addresses review on #129.

- All three images dropped to the unprivileged `node` user. The oven/bun base
  ended with `USER bun`; node-slim defines `node` (uid 1000) but does not switch
  to it, so the base swap would have run a process holding a funded EOA key as
  root. `corepack enable` stays root (its shim lands in /usr/local/bin), then
  /repo is chowned and every later layer, install, and CMD runs as `node`.
- CI installs pnpm with `pnpm/setup` v2 instead of `pnpm/action-setup`, which
  upstream now scopes to pnpm v10 and older. v2 also fetches pnpm's
  self-contained release binary against GitHub's published SHA-256 digest rather
  than bootstrapping through an `npm ci` of `@pnpm/exe` — one fewer npm artifact
  on the path this migration exists to shorten.

Verified: all three images build; `docker run` reports uid=1000(node); the
blue-liquidation container runs `prestart` (writing the gitignored
@repo/contracts dist as non-root) and reaches its fail-loud config check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
haydenshively added a commit that referenced this pull request Aug 5, 2026
No file changes. After #129 and this branch were pushed in quick succession,
GitHub cached mergeable_state=dirty for this PR even though its base
(chore/migrate-to-pnpm@176a6a6) is a strict ancestor of this head — a
fast-forward that cannot conflict. The stale state also suppressed the Checks
workflow, which only fires on pull_request opened/synchronize. This empty
commit forces both to recompute.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively
haydenshively force-pushed the chore/migrate-to-pnpm branch from 176a6a6 to c39786f Compare August 5, 2026 18:20
haydenshively and others added 3 commits August 7, 2026 10:54
pnpm 11.1.1 takes over installs, workspace resolution, the version catalog,
and script running; bun 1.3.12 remains the runtime and test runner. pnpm's
default-deny on dependency lifecycle scripts is the point of the migration,
per the team supply-chain decision in #curators.

Three settings the original migration branch omitted are set explicitly:
strictDepBuilds makes an un-opted-in build script a hard install failure
rather than a warning (proven: an esbuild probe exits 1 with
ERR_PNPM_IGNORED_BUILDS); enablePrePostScripts keeps prestart alive, which
builds @repo/contracts' gitignored dist/; and allowBuilds starts empty
because nothing in the tree declares an install script.

Catalog entries that a fresh resolution would have bumped are pinned to what
bun.lock had resolved, so the migration rides no dependency upgrades. solc
is pinned exactly because it fixes the compiled bytecode of the soltag lens
templates and the Executor.

The bot images move to a Node base carrying a copied bun binary: pnpm is
activated through corepack, which oven/bun does not ship, and both runtimes
must stay on PATH because start fires prestart.

Test suite is untouched and byte-identical to main: 1412 pass, 3 skip,
3 fail, 1 error across 136 files on both (the failures are a pre-existing
missing RPC_URL_8453 for the fork suites).

CRTR-2822

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses review on #129.

- All three images dropped to the unprivileged `node` user. The oven/bun base
  ended with `USER bun`; node-slim defines `node` (uid 1000) but does not switch
  to it, so the base swap would have run a process holding a funded EOA key as
  root. `corepack enable` stays root (its shim lands in /usr/local/bin), then
  /repo is chowned and every later layer, install, and CMD runs as `node`.
- CI installs pnpm with `pnpm/setup` v2 instead of `pnpm/action-setup`, which
  upstream now scopes to pnpm v10 and older. v2 also fetches pnpm's
  self-contained release binary against GitHub's published SHA-256 digest rather
  than bootstrapping through an `npm ci` of `@pnpm/exe` — one fewer npm artifact
  on the path this migration exists to shorten.

Verified: all three images build; `docker run` reports uid=1000(node); the
blue-liquidation container runs `prestart` (writing the gitignored
@repo/contracts dist as non-root) and reaches its fail-loud config check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…etup

The switch to pnpm/setup failed every job at the Setup step: morpho-org's
GitHub Actions allowlist permits `pnpm/action-setup@*` and not `pnpm/setup@*`.
action-setup installs pnpm 11.1.1 correctly, so this reverts to it and records
the constraint plus the follow-up (an org admin allowlisting the successor) in
the action and the TIB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@haydenshively
haydenshively force-pushed the chore/migrate-to-pnpm branch from c39786f to 1b7deea Compare August 7, 2026 15:55
@haydenshively
haydenshively merged commit 8bbb8c3 into main Aug 7, 2026
3 of 4 checks passed
@haydenshively
haydenshively deleted the chore/migrate-to-pnpm branch August 7, 2026 18:52
@linear-code

linear-code Bot commented Aug 7, 2026

Copy link
Copy Markdown

CRTR-2971

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