Skip to content

[dev] Adopt moonrepo#809

Open
SlexAxton wants to merge 17 commits into
mainfrom
alex/dev/moon
Open

[dev] Adopt moonrepo#809
SlexAxton wants to merge 17 commits into
mainfrom
alex/dev/moon

Conversation

@SlexAxton

Copy link
Copy Markdown
Contributor

Replace custom script orchestration with moon

Migrates the monorepo from hand-rolled task running (scripts/ws.ts, per-package
npm scripts, concurrently, husky, artifact-passing CI) to
moon v2.3.3, pinned in .prototools and enforced via
versionConstraint. Every human/agent/CI entry point is now a moon task;
package.json scripts are npm lifecycle hooks only.

Command surface

Before After
bun ws <project> <task> moon run <project>:<task> / moonx <project>:<task>
bun run tsc (root or package) moonx <project>:typecheck / moon run :typecheck
bun run lint / format / lint:css moon run root:lint / root:format / root:lint-css
bun run wt … moonx root:wt -- …
test:e2e, dev:demo, icons:sprite, … kebab-case: test-e2e, dev-demo, icons, …

Task names are flat kebab-case with one meaning everywhere: build produces dist,
dev is the persistent watcher, test is bun test, tsc is renamed typecheck.

Task architecture

  • .moon/tasks/bun-common.ymltest + typecheck for every bun project, both
    with deps: ['^:build'] (cross-package imports and types resolve through each
    dependency's built dist; this replaces every build:deps script and fixed a
    real race where diffshub:test could run before theming:build finished).
  • .moon/tasks/tag-tsdown.ymlbuild/dev for the tsdown packages; theming and
    trees override to append their post-build asserts.
  • .moon/tasks/tag-publishable.ymlprepublish guard chain
    (assert-bun-version + build) behind each package's prepublishOnly.
  • Per-project moon.yml for everything else (apps, benchmarks, e2e, publish).

App dev tasks depend on ^:build + ^:dev: moon finishes dependency builds,
then runs dependency watchers alongside the app server — concurrently is gone.
Port offsets still come from .env.worktree (loaded via task envFile);
wt.ts, run-dev.sh, and load-worktree-env.mjs survive unchanged in role.

CI

One affected-aware lane (plus the unchanged actions-pinned guard):

  • moon ci --include-relations <explicit targets> computes affected projects from
    the VCS diff and expands through graph relations, so a package change re-runs its
    dependents' builds/tests/typechecks — including trees e2e when affected.
  • Caching via Blacksmith sticky disks: ~/.proto, bun's download cache, moon's
    portable cache (hashes + outputs), playwright browsers. No more artifact
    upload/download between jobs.
  • Toolchain bootstrap is proto-only (moonrepo/setup-toolchain, SHA-pinned); moon
    installs dependencies itself with a frozen lockfile. The bun-version awk parsing,
    setup-bun, and manual bun install are gone.
  • The playwright install is skipped (fail-open) when e2e is unaffected.

Hooks

moon-managed (vcs.hooks, worktree-compatible — hooks land in .moon/hooks with a
worktree-local core.hooksPath; no install-time prepare). Pre-commit runs
moon exec :typecheck --affected --status staged (now also checks dependents,
which precommit-tsc.ts missed) + lint-staged. Pre-push keeps the git-lfs guard;
generated hook scripts forward "$@" and stdin. husky and precommit-tsc.ts are
deleted.

Vercel

Builds run via the GitHub integration, so each app ships a vercel.json whose
buildCommand runs the app's moon build (moon comes from a pinned root
devDependency; PATH covers both repo-root and app-dir Root Directory layouts).
The two docs projects share one file and dispatch on NEXT_PUBLIC_SITE.
docs distDir is now per-site in every mode (.next/diffs / .next/trees),
making the two site builds disjoint and independently cacheable; they share a
mutex only for the site-dependent public/llms*.txt.

Deliberate behavior changes

  • tsctypecheck everywhere.
  • assert-bun-version guards all five published packages (was diffs-only);
    storage-elements-next gains its first prepublish guard.
  • theming is now watched during docs/diffshub dev (was imported but unwatched).
  • path-store's build is renamed build-demo (it builds the demo site).
  • Type-aware root:lint now orders after package builds (it resolves @pierre/*
    types through dist; previously raced tsdown --clean).
  • CI consolidates six checks into two.
  • runInCI policy: graph-edge-free local tasks are 'always' (work in CI-marked
    agent shells); graph-connected ones (dev/prod, e2e variants, publish guards)
    stay 'skip' and need a CI= prefix in such shells (CI= moonx docs:dev-diffs).

Verification

  • Full pipeline green locally, cold and warm (33 targets; warm rerun 5.2s,
    30 cache hits); affected-skip verified on isolated changes.
  • trees e2e: 58 passed through the real CI path (nested moon run webServer).
  • Worktree dev smoke: 4 watchers + next on offset port, hot reload on theming edit.
  • bun publish --dry-run × 5 fires the prepublish chains (trees' guard correctly
    blocks direct publish).
  • Hook smoke: staged type error blocks the commit; hooks ran on every commit here.
  • Gates: every scripts block is lifecycle-only; zero bun ws/husky/
    concurrently references in live code; all embedded invokers (playwright
    webServers, profiler/test spawns, trees publish flow) point at moon tasks.
  • Reviewed by an 8-agent pass; all confirmed findings fixed (cache-input gaps,
    broken spawns, lint/build race, Vercel PATH robustness, CI lane consolidation).

Merge checklist (externally gated)

  • Vercel dashboard audit per project: Root Directory, Install Command,
    NEXT_PUBLIC_SITE env (and whether demo is a fourth project)
  • Preview deploys green for all docs/diffshub(/demo) projects on this PR
  • First Blacksmith run: stickydisk mounts + PR base detection (fallback:
    explicit --base in the workflow)
  • One real LFS push through the moon pre-push hook

Follow-ups (deferred by design)

  • Depot remote cache when wall-clock or local cache hydration warrants it
    (stickydisk remains as L1).
  • docs test-e2e in CI — enable deliberately, not as migration fallout.
  • TypeScript project-reference syncing — separate project if typecheck time hurts.

SlexAxton added 12 commits June 11, 2026 15:14
Each docs site variant (diffs, trees) now builds into its own
`.next/<site>` directory in production, not just dev. This lets the
two site builds run concurrently and be cached independently by the
task runner, and makes `next start` serve the build matching the
serving env's NEXT_PUBLIC_SITE instead of whichever variant was built
last. Vercel resolves the custom distDir from next.config.mjs, with
NEXT_PUBLIC_SITE set per project; verify via preview deploys on both
docs projects before relying on it.
Add the moon v2 workspace config (project globs for packages/* and
apps/*, plus the repo root as the `root` project for repo-wide
tooling), the toolchains config (javascript + bun, versions inherited
from .prototools, frozen-lockfile installs), and gitignore entries for
moon's generated cache and hooks directories.

moon 2.3.3 is pinned in .prototools and enforced via
versionConstraint so the proto-managed binary and the workspace
config move together.
Add inherited task files and per-project moon.yml configs covering
every npm script that is a real task:

- bun-common (all bun projects): test, typecheck. Typecheck depends
  on ^:build because cross-package types resolve through each
  dependency's published dist, replacing every build:deps script.
- tag-tsdown (diffs, theming, truncate, trees): build + dev watcher.
  theming/trees override build to append their post-build asserts;
  trees also watches the inlined path-store sources.
- tag-publishable (diffs, theming, truncate, trees,
  storage-elements-next): prepublish guard chain (assert-bun-version
  + build) for the prepublishOnly lifecycle hook. trees appends
  assert-safe-publish. assert-bun-version previously only guarded
  diffs; it now covers every published package.
- Apps define build/dev/start/prod with ^:build and ^:dev deps; moon
  runs dependency watchers alongside the app server, replacing the
  concurrently pipelines. Port math and run-dev.sh stale-listener
  cleanup live in task scripts with PIERRE_PORT_OFFSET loaded from
  /.env.worktree.
- The two docs site builds write disjoint .next/<site> outputs but
  share a mutex because generate-llms-txt writes site-dependent
  content to the shared public/ dir.
- path-store's build is renamed build-demo (it builds the demo site,
  not a library), so consumers' ^:build never drags it in.
- Repo-wide lint/format/stylelint/icons/clean/chrome/wt live on the
  root project; lint and format checks always run in CI.
- docs test-e2e and path-store test-demo keep runInCI: skip for
  parity with the previous CI setup.
Playwright webServer commands now spawn moon tasks instead of npm
scripts (moon normalizes the working directory to the project root,
matching what bun run did). prepublishOnly in every published package
delegates to its moon prepublish guard chain; storage-elements-next
gains the guard for the first time.

moon is now a pinned root devDependency so environments without proto
(Vercel builders) get the binary from bun install; the bunfig
release-age gate excludes @moonrepo packages since the version is
exact-pinned everywhere it is consumed. Each Vercel-deployed app gets
a vercel.json whose buildCommand runs the app's moon build from
node_modules/.bin. The two docs Vercel projects share one file,
dispatching on NEXT_PUBLIC_SITE to pick the site build task. Cutover
is atomic at merge since Vercel reads vercel.json from the deploying
commit; verify via preview deploys before merging.
Running the full affected pipeline surfaced four issues:

- test tasks raced dependency builds: bun resolves workspace imports
  through each dependency's dist, so test now depends on ^:build like
  typecheck (diffshub:test failed when theming:build had not finished).
- Explicit input globs are filesystem-walked and ignore gitignore, so
  the repo-wide root tasks and the shared typecheck inputs now negate
  node_modules and generated output trees instead of hashing them.
- Tasks spawned by playwright webServer (trees:test-e2e-server,
  path-store:test-demo-server, docs:start) must not use the server
  preset: moon run refuses runInCI-disabled tasks in CI environments,
  which broke e2e there. They now set persistent/cache/runInCI
  explicitly. Nothing depends on them and no CI lane targets them, so
  they never enter a moon ci pipeline.
- demo has no test suite and now excludes the inherited test task;
  docs' build outputs dropped the vestigial fumadocs .source dir that
  nothing generates (it failed moon's output validation).
Replace the six-job pipeline (build + artifact fan-out) with two moon
lanes plus the unchanged actions-pinned guard:

- ci: moon ci --include-relations over every CI task except trees
  e2e. Affected projects come from the VCS diff with graph relations
  enabled, so a package change re-runs its dependents' builds, tests,
  and typechecks; unchanged work restores from the moon cache.
- e2e: moon ci --include-relations trees:test-e2e in parallel, so the
  slowest suite never serializes behind builds and skips itself when
  trees is unaffected.

The setup action drops bun-version parsing, setup-bun, actions/cache,
and the manual bun install: moonrepo/setup-toolchain provisions the
.prototools toolchain via proto, moon installs dependencies itself
with a frozen lockfile, and Blacksmith sticky disks persist ~/.proto,
bun's download cache, and .moon/cache across runs. Artifact
upload/download steps are gone; outputs flow through the moon cache.
Runners consolidate to one 8vcpu machine per lane.
moon now generates .moon/hooks (gitignored) from vcs.hooks in the
workspace config and points the worktree-local core.hooksPath at it,
so every clone and linked worktree gets hooks from any moon command —
no install-time prepare script. Verified: generated hook scripts
inline commands with "$@" intact and inherit stdin, so the git-lfs
pre-push guard keeps receiving its ref list.

pre-commit replaces scripts/precommit-tsc.ts with
`moon exec :typecheck --affected --status staged`, which also
typechecks dependents of the staged projects (the old script only
checked workspaces containing staged files), then runs lint-staged
as before. husky and the prepare lifecycle script are removed.
Every package.json scripts block is now lifecycle-only: the five
published packages keep exactly prepublishOnly (pointing at their
moon prepublish guard), and every other manifest has no scripts.
All human/agent/CI entrypoints are moon tasks (`moon run`, `moonx`).

Deleted along with the scripts:
- scripts/ws.ts, the custom workspace runner moon replaces (worktree
  env loading is handled by task envFile options, and TTY/signal
  babysitting by moon's pipeline).
- concurrently (catalog + three app devDependencies): moon runs
  dependency watchers alongside app servers as persistent task deps.
- husky's catalog entry (the dep itself left with the hooks change).

CLI help text, code comments, and the profiler's chrome launcher now
reference moon tasks or invoke scripts directly; the file-tree
profiler help test asserts the new strings.
AGENTS.md now documents the moon command surface (moon run / moonx,
task discovery), tells agent sessions to unset CI (moon refuses to
run local-only tasks like dev servers and formatters in CI-detected
environments), and rewrites the verification baseline as moon tasks.

Skills (tooling-and-dependencies, testing-and-verification,
worktrees-and-dev-servers, typescript-monorepo), scripts/README.md,
and package docs (trees README/TESTING/PUBLISHING, path-store and
diffs READMEs) drop every bun ws / npm-script reference in favor of
the moon equivalents, including the renamed tasks (tsc -> typecheck,
colon-separated names -> kebab-case).

trees' publish.ts now builds through moon run trees:build (it spawned
the deleted build script), and assert-safe-publish's guidance points
at moonx trees:publish.
Cache correctness (stale-cache and miss-invalidation bugs):
- tsdown builds hash the package tsconfig and root tsconfig.options
  (tsdown loads them for dts emit); theming/trees overrides match.
- typecheck inputs exclude *.tsbuildinfo and demo/dist; root:lint
  hashes tsconfig.options.json; root:icons hashes svgo.config.js.
- tree-test-data:test hashes ../path-store/src (index.js imports the
  builder directly; a manifest dep would cycle with path-store's
  devDependency on this package).

Execution correctness:
- Type-aware oxlint resolves @pierre/* through built dist exactly like
  typecheck, so the root project now dependsOn the dist-producing
  packages and root:lint/lint-fix order after ^:build (lint raced
  tsdown --clean and saw error-types). stylelint ignores generated CSS
  for the same build-race reason.
- profileFileTree.ts and theming's dist-guards test spawned deleted
  npm scripts (bun run build / test:e2e:server); profileDemo.ts
  spawned a script that never existed. All now spawn moon tasks.

runInCI policy, after discovering that moon ci --include-relations
runs affected runInCI-enabled tasks even when unrequested:
- Tasks with no graph edges (formatters, benchmarks, wt, chrome,
  clean, icons, playwright-spawned servers) set runInCI 'always' so
  they work in CI-marked shells (agent harnesses export CI=1) and
  never enter the pipeline.
- Graph-connected local tasks (dev/prod, e2e variants, lint-fix, the
  publish guard chain — trees' assert-safe-publish fails by design
  outside a real publish) stay runInCI 'skip'; CI-marked shells run
  them with a CI= prefix. The previous `unset CI` advice did not
  survive per-command env injection in agent harnesses and is gone.

CI workflow:
- Collapsed to a single lane: e2e joins via graph relations whenever
  trees is affected, so a separate lane double-ran it. The playwright
  install is gated on a fail-open affected query.
- Sticky disks persist only moon's portable cache state (hashes,
  outputs), not machine-specific run state.
- vercel.json build commands prefix PATH with both the repo-root and
  app-relative node_modules/.bin so they work whether the Vercel root
  directory is the repo or the app.
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
pierre-docs-diffs Ready Ready Preview Jun 12, 2026 1:39am
pierre-docs-diffshub Ready Ready Preview Jun 12, 2026 1:39am
pierre-docs-trees Ready Ready Preview Jun 12, 2026 1:39am
pierrejs-diff-demo Ready Ready Preview Jun 12, 2026 1:39am

Request Review

@socket-security

socket-security Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​moonrepo/​cli@​2.3.39910010096100

View full report

Root cause of the CI failures (diffs/trees tests and trees:typecheck
failing to resolve @pierre/theming moments after theming:build
completed): theming's dist-guards test spawned a nested
`moon run theming:build`, which cache-hit and re-hydrated — cleared
and re-unpacked — packages/theming/dist while sibling tasks were
resolving @pierre/theming through it. Verified locally by sampling
dist during a cold pipeline: it went missing for ~300ms mid-run, the
exact clear-then-untar signature and the same window as the CI
failures.

- dist-guards no longer builds or restores anything; theming:test
  declares a dep on theming:build and the test just asserts dist
  exists. Invariant recorded in the workflow: no task may write
  another task's outputs mid-pipeline.
- storage-elements builds use `tsgo --build --force`: a stale
  tsconfig.tsbuildinfo with a deleted dist made tsgo skip emit and
  fail moon's output validation; task-level caching owns
  incrementality anyway.
- docs site builds declare explicit inputs excluding public/llms*:
  moon only filters a task's OWN outputs from greedy inputs, so each
  site build was hashing its sibling's llms output and the two hashes
  ping-ponged, forcing rebuilds every pipeline. All four app builds
  now cache-hit on warm reruns.
- CI workflow reverted to a single moon ci phase (a two-phase split
  added during diagnosis was illusory: moon ci executes every
  affected runInCI-enabled task regardless of requested targets).
  Comments and skills now state those semantics precisely: targets
  add entry points, never subtract; anything CI-unsafe must be
  runInCI skip.

Validated cold (rm dist/tsbuildinfo/.next + empty moon cache): 33
tasks, 0 failures, dist written exactly once; warm rerun: 31/33
cached, 0 failures.
Vercel's Next builder collects output from the literal .next
directory and does not honor the env-dependent distDir when locating
routes-manifest.json, so both docs deployments failed after the
per-site distDir change. Per-site build dirs only matter where both
variants share a workspace (local dev and CI); on Vercel each project
builds exactly one site in an isolated container, so distDir now
falls back to .next when VERCEL is set.

The moon tasks declare their .next outputs as globs because globs are
optional: on Vercel the per-site directory never exists and exact-dir
outputs would fail moon's output validation after a successful build.

Unrebased PRs are unaffected: they deploy with their own commit's
config (old prod distDir = .next, npm scripts intact) and the Vercel
dashboard settings are unchanged; only commits carrying vercel.json
get the new build path.

Verified: VERCEL=1 moon run docs:build produces
.next/routes-manifest.json and passes; normal runs keep .next/diffs
and cache hits.
moon 2.3.3 fails output validation when any declared output matches
nothing — including glob outputs, despite the error hint — so the
docs site-build tasks cannot run on Vercel where the build writes
plain .next and the per-site directory never exists (the trees
deployment failed with task_runner::missing_outputs after a
successful next build).

Split the responsibilities instead: a new docs:build-deps aggregator
(noop + ^:build, CI-enabled because Vercel builders set CI=1) gives
Vercel the dependency graph, and its buildCommand then runs
generate-llms-txt and next build directly — which is also what
Vercel's builder expects to own (it applies modifyConfig to the next
build it observes). The command is cwd-agnostic so it works whether
the project's Root Directory is the repo root or apps/docs, and it
no longer needs the NEXT_PUBLIC_SITE dispatch since the env alone
selects the site.

The docs moon tasks get their strict exact-dir outputs back
(.next/diffs, .next/trees); they are simply never invoked on Vercel.

Verified: the buildCommand passes from both candidate root
directories with VERCEL=1 CI=1 (routes-manifest.json lands at
apps/docs/.next/); local moon builds keep per-site dirs and full
cache hits.
@SlexAxton SlexAxton requested a review from amadeus June 12, 2026 00:27

@amadeus amadeus left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

👯 lfg 👯

CONTRIBUTING.md covers the full fresh-clone path: installing proto
(with a link to the official guide), git-lfs, `proto use` against the
.prototools pins, bun install with the catalog convention, the
moon-managed git hooks, the moon command surface with a common-tasks
table, and the pre-push verification baseline.

AGENTS.md and the tooling skill gain a Toolchain section: versions are
pinned in .prototools and managed by proto (never installed globally),
moon's pin is additionally enforced by workspace versionConstraint and
mirrored in the @moonrepo/cli catalog entry for Vercel builders, and
CI resolves the identical toolchain via moonrepo/setup-toolchain.

A sweep for stale workflow commands (bun run format, bun ws, npm run)
found none left; remaining npm/pnpm mentions are docs-site content and
fixtures about external repos, not repo workflow.

@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: 825405ab7b

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/workflows/ci.yml
Comment on lines +55 to +57
moon ci --include-relations :build docs:build-trees-site
path-store:build-demo :test :typecheck trees:test-e2e root:lint
root:lint-css root:format-check

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep app builds on package changes

With the CI lane now running only moon ci --include-relations, a package-only change no longer explicitly targets the app builds that used to run unconditionally in this workflow (docs and diffshub). Since --include-relations expands from affected projects to related tasks, it will not make unaffected downstream apps build when the triggering task is :build; for example, changing packages/diffs/src/... can be satisfied by the package build/test/typecheck targets and skip docs:build, docs:build-trees-site, and diffshub:build, so broken Next integration or transpilePackages regressions in consumers would no longer be caught. Please include downstream app build targets for affected package changes, or otherwise make these app builds depend on the package targets in CI.

Useful? React with 👍 / 👎.

bun run lint:fix
bun run lint:css
bun run lint:css:fix
moon run root:format-check

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.

Is this different from how moon works in the monorepo (e.g, iirc, moon :format-check etc) or just another way of doing the same thing?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

it's a little differently setup. the short version is that our formatters and linters are so fast that it didn't make a ton of sense to not just run them from root every time. it's probably true in monorepo as well now, but still has configuration from when it wasn't true.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

that said moonx :format-check will run format-check in all the places it exists, which should just be root anyways. so it should work fine that way.

bun ws <project> <task>
bun ws <project> <task> --some --flag
moon run <project>:<task>
moonx <project>:<task> # shorthand for moon exec

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.

what's the difference between moon exec <project>:<task> and moon <project>:<task>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

moon run === moonx

CI failed with `Process git failed: short read while indexing
.moon/cache/hashes/<hash>.js`: root:lint's input glob (`**/*.js` and
friends) and root:format-check's `**/*` are filesystem-walked and
matched moon's own cache directory — including the hash manifests
moon writes concurrently while the pipeline runs. git hash-object
then raced a manifest mid-write and the whole moon ci invocation
died.

Negate `.moon/cache/**` and `.moon/hooks/**` in both input sets.
`.moon/*.yml` configs stay included in format-check's inputs since
oxfmt formats them. Verified with a cold full pipeline (maximum
concurrent manifest writes): 34 tasks green, and none of the 41
generated hash manifests reference .moon/cache anymore.
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.

3 participants