website/blog/2026-08-14_moon-v2.5.mdx
This release is about speed you can feel and visibility you can graph. The async engine we've been iterating on over the last few releases is now the default, heavy cache operations have moved into the daemon's background, Git worktrees can finally share a cache, and the entire pipeline can now be exported to your observability stack through OpenTelemetry.
<!--truncate-->moon's pipeline has been internally instrumented for a while — it's the same span data that powers
--dump and verbose logging — but until now, that data never left your machine. In v2.5, moon can
export OpenTelemetry (OTEL) traces, metrics (we don't emit any yet), and logs over OTLP, for
integration with observability backends like Grafana, Honeycomb, or Datadog. The entire pipeline —
action graph construction, task execution, plugin calls, and more — is reported as a span tree, so
you can finally answer questions like "what is CI actually spending its time on?" with a flame
graph instead of a hunch.
This is opt-in, and enabled with a handful of new global options (or their environment variable counterparts):
--otel (MOON_OTEL) - Export traces and metrics over OTLP.--otel-logs (MOON_OTEL_LOGS) - Also export log events as OTLP logs. Respects --log, so a
lower log level exports fewer events.--otel-service-name (MOON_OTEL_SERVICE_NAME) - The service name to report.The destination and transport are configured with the standard OTEL_EXPORTER_OTLP_* environment
variables, and both HTTP (http/protobuf) and gRPC (grpc) transports are supported.
$ export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4318"
$ moon --otel run app:build
Jump over to the official documentation for the full list of supported environment variables and transports.
Over the past few releases, we've been rebuilding moon's hottest code paths as asynchronous, task-pool driven implementations, and shipping them behind experiment flags. They've now been battle-tested enough that in v2.5, the following experiments are enabled by default:
asyncGraphBuilding (added in v2.2) - Builds the
project and task graphs asynchronously, improving performance by 100-170% in large workspaces.asyncAffectedTracking (added in v2.2) - Tracks
affected projects and tasks asynchronously, improving performance by 100-150%.nativeFileHashing (added in v2.3) - Hashes files
natively within moon's task pool instead of shelling out to the VCS, improving performance by
10-50% depending on workspace size and file count.If you've been running with the defaults, this release should simply feel faster. If you run into an issue, please report it, and disable the offending experiment to keep working while we investigate.
experiments:
asyncGraphBuilding: false
Git worktrees have become a staple of modern workflows — a worktree per branch, per review, or per
AI agent grinding away in parallel. But each worktree has its own .moon/cache, which means every
new worktree starts with a stone-cold cache and re-builds artifacts that already exist on the same
machine.
In v2.5, the new cache.unstable_sharedWorktreeCache setting shares the content-addressable
storage (CAS) cache between all worktrees of a repository on the same machine. Only blobs and
manifests are shared, as they're portable, while hashes, locks, and states remain
worktree-specific. The shared cache lives in the base checkout's .moon/cache directory, or in
~/.moon/cache/shared when the repository has no base checkout (bare clones).
This builds on the local CAS layer, so the
casOutputsCache experiment is required.
experiments:
casOutputsCache: true
cache:
unstable_sharedWorktreeCache: true
With this enabled, a task that was cached in one worktree hydrates instantly in every other — spin up a fresh worktree, run your build, and watch it complete in seconds.
Additionally, cache settings can now be controlled with
environment variables: MOON_CACHE_CAS_MAX_SIZE, MOON_CACHE_CAS_VERIFY_INTEGRITY, and
MOON_CACHE_SHARED_WORKTREE_CACHE.
When we introduced the local CAS in v2.3, we warned that archiving and hydration ran on the main thread, and promised to fix it. Promise kept! In v2.5, task output archiving (after a task runs) and hydration (on a cache hit) are offloaded to the background daemon, so these heavy file system operations no longer block the pipeline.
One caveat with this change: since the work now happens in the background, you'll need to inspect the daemon server logs to understand when something fails during archiving or hydrating, as the main process no longer surfaces these errors directly.
Until now, the project graph was a single directed acyclic graph, and any dependency cycle between projects was an error — even when the relationship was perfectly valid. The classic example comes from Go: a package is depended on in production, while its tests depend on a test-utility package that imports it right back. Not actually a problem at build time, but moon would either fail with a cycle error, or silently drop dependency edges.
In v2.5, the project graph validates cycles per dependency scope partition. Production scoped
dependencies (production, peer) and development scoped dependencies (development, build,
root) are now tracked as separate internal graphs, so relationships that cross the boundary no
longer fail, while cycles within a partition are still rejected as before.
The projects setting has always supported globs for locating
project folders, but folder names alone can't answer questions like "is this a Node.js project?".
In v2.5, project globs can now match files, where the parent folder of a matched file becomes the
project root. This allows for far more precise project discovery.
projects:
# Only folders with a package.json
- 'apps/*/package.json'
# Only crates with a Cargo manifest
- 'crates/*/Cargo.toml'
# .NET projects, wherever they live
- 'src/**/*.csproj'
This does not apply to the workspace root itself. For a root-level project, only a matched moon configuration file counts.
Task inheritance via .moon/tasks/**/* is one of moon's most powerful
features, and in v2.5, it learns a new trick: an env setting. These
environment variables are inherited by all matching projects, and are merged into each project's
env, with project-level variables taking precedence.
env:
NODE_ENV: 'production'
And since inheritance always raises the question of "how do these merge?", we've also added a
workspace.mergeStrategies setting to
moon.*, which controls how a project's settings are merged with inherited
workspace-level settings. It currently supports env and fileGroups, using the same merge
strategies as task merging: append, prepend, preserve, and replace.
workspace:
mergeStrategies:
env: 'replace'
fileGroups: 'preserve'
The JavaScript ecosystem never sleeps, and the latest package manager on the block is Nub, a fast standalone binary. In v2.5, the JavaScript toolchain gains unstable support for it:
nub.lock (pnpm lockfile format), but will locate dependency roots using other
package manager lockfiles that nub can operate on.pnpm-workspace.yaml when present, otherwise from
package.json.javascript:
packageManager: 'nub'
unstable_nub: {}
Run moon toolchain info unstable_nub for the full list of available settings.
Nub isn't the only toolchain news — the rest of the lineup received a batch of quality-of-life improvements as well.
Project relationships got much smarter this release:
go.mod module path plus the project's relative directory), and go list -deps results are matched against those by longest prefix. This makes relationships resolvable in repositories that share a single go.mod across all projects.go.work no longer create project relationships, since those builds consume the published module rather than the local source. When the go binary is unavailable, projects with their own go.mod under a workspace go.work fall back to resolving relationships from their direct requires.replace directives keep their meaning in the new model: a require replaced by a local directory always links to the project at that location (it consumes local source even without a go.work), while a require replaced by another module never links.go list -deps ./... enumerates packages belonging to projects nested inside the scanned project, which previously inferred an edge from the parent to every nested child — forming a cycle whenever a child declared dependsOn on its parent.force option for bins entries is now respected, and binaries deleted outside of moon are
now reinstalled..venv directories for non-focused projects (those that
were not explicitly scaffolded).bins whose binaries were uninstalled or deleted outside of moon are now reinstalled.
Only missing binaries are installed.lib.rs or main.rs files.View the official release for a full list of changes, but here's a few more worth calling out:
moon setup to also setup toolchain environments when their
dependency root is the workspace root itself.moon exec (and related pipeline commands) to display action
failures in a summary at the bottom, instead of interleaved within all actions.VirtualPath type was reworked from the ground up into a newtype
wrapper around PathBuf (a breaking change), alongside a new RealPath sibling and
convert_to_virtual_path / convert_to_real_path helpers. Toolchains can also now declare
requirements for the setup environment and setup toolchain actions.