Skip to main content

moon v2.3 - Task tags, dep cache strategies, native file hashing, CAS cache, and more

· 7 min read
Miles Johnson
Founder, developer

This release focuses on giving you more control over how tasks are organized, referenced, and cached. We're introducing first-class tags for tasks, a new cache strategy for task dependencies, and a pair of experimental layers — native file hashing and a local CAS — that lay the groundwork for further performance improvements.

Task tags

Projects have supported tags since v1, but tasks themselves have never had first-class support for tags, until now. In v2.3, we're introducing tags for tasks, allowing you to label and organize tasks in a more flexible way. This opens up new possibilities for filtering, grouping, and managing tasks based on their tags.

moon.yml
tasks:
lint:
# ...
tags: ['quality', 'ci']

To start, targets have been updated to support the # tag syntax in the task scope, allowing you to do crazy things, such as:

  • :#tag - Reference all tasks with the tag tag.
  • ^:#tag - Reference all tasks with the tag tag in upstream projects.
  • project:#tag - Reference all tasks with the tag tag in a specific project.
  • #tag1:#tag2 - Reference all tasks with the tag2 tag, in all projects with the tag1 tag.

These targets can be utilized on the command line, enabling you to curate exactly which tasks to run based on their tags.

$ moon run ':#quality'

Or they can be used in task dependencies, which will be expanded at runtime to a list of fully-qualified targets.

moon.yml
tasks:
build:
# ...
deps: ['shared:#prereqs']

In addition to the new tags setting, we've also added an options.mergeTags setting to control how tags are merged during task inheritance, a taskTag field to MQL for querying tasks by their tags, and a --tags filter for moon query tasks.

info

Heads up: the existing MQL tag field has been renamed to projectTag to disambiguate from the new taskTag field.

Task dependency cache strategies

When a task depends on another task, that dependency's changes need to invalidate the current task's cache — but how aggressively? Until now, moon always mixed the dependency's hash into the current task's hash. This meant that a build task depending on an upstream build would re-run any time the upstream task's inputs changed, even if the upstream task's outputs ended up identical. Even worse, depending on output-less tasks like lint or test invalidated builds for no real reason.

In v2.3, task dependencies now support a cacheStrategy field that controls exactly how a dependency contributes to cache invalidation:

  • hash - Use the dependency task's hash for cache invalidation. The current task is invalidated whenever the dependency changes (inputs, command, args, env, etc.). This is the historical behavior.
  • ignored - Ignore the dependency task's hash entirely. The current task is never invalidated by this dependency's changes — the dependency is purely a sequencing edge.
  • outputs - Use the dependency task's outputs instead of its hash. The current task is only invalidated when the dependency's output files change, not when its inputs change.
moon.yml
tasks:
build:
command: 'webpack'
deps:
- target: '^:build'
cacheStrategy: 'outputs'

For build dependencies in particular, outputs is the strategy you almost certainly want. A change to an upstream project's source files that doesn't affect its compiled output should not force a downstream rebuild — and now it won't.

Behavior change

To make this work for everyone out of the box, the default cacheStrategy is now chosen for you based on whether the dependency declares outputs:

  • A dependency with outputs (e.g. a build task) defaults to hash.
  • A dependency without outputs (e.g. a lint or test task) defaults to ignored.

This is a behavior change from v2.2 and earlier, where the strategy was always effectively hash. In practice, this means tasks depending on output-less tasks will see fewer (correct!) cache invalidations going forward. If you want to restore the old behavior for a specific dependency, set cacheStrategy: 'hash' explicitly.

Experimental native file hashing

For as long as moon has existed, file hashing has been delegated to the VCS (typically Git). This was a pragmatic choice — Git already has a hash for every file it tracks, so why hash them again? But shelling out to Git for every hashing operation has overhead, and Git's hashing doesn't run on moon's task pool, which means it can't take advantage of the parallelism we've spent the last few releases building out.

In v2.3, we're introducing an experimental native file hashing implementation that runs directly within moon's task pool, sidestepping the VCS entirely. In our benchmarks, this delivers a 10-50% improvement depending on workspace size and file count.

Enable it in your workspace configuration:

.moon/workspace.yml
experiments:
nativeFileHashing: true

The CAS hasher can also be tuned via the new top-level cache setting — see the workspace config docs for the available knobs.

Experimental local CAS for task outputs

Speaking of caching — moon has shipped a remote content-addressable storage (CAS) backend since v1.30 for remote caching, but locally, task outputs were still archived as tarballs. In v2.3, we're introducing an experimental local CAS layer that stores task outputs in the same content-addressed format used by the remote cache. This means deduplicated storage across tasks, and a unified cache shape locally and remotely.

Enable it in your workspace configuration:

.moon/workspace.yml
experiments:
casOutputsCache: true

This is a foundational change — most of the work in this release went into reshaping the cache, remote, and hashing internals around a shared CAS abstraction. Expect this to become the default in a future release once it's been battle-tested.

warning

At this time, the local CAS may be slower than the tarball approach, as we have more guardrails to prevent data loss and cache corruption. Additionally, archiving and hydration currently happen in the main thread, and not through the daemon. In subsequent releases, we plan to optimize the local CAS for performance and make it the default.

MCP tools for template discovery

For AI agents working in moon repositories, code generation is one of the highest-leverage operations they can perform — but only if they know which templates exist and what variables those templates accept. v2.3 adds two new tools to the MCP server that close this gap:

  • get_templates - Lists available templates with id, title, and description. Supports an optional case-insensitive filter regex (mirroring moon templates --filter).
  • get_template - Returns the merged variable schema for a given template id, resolving the extends chain. Variables marked internal: true are omitted.

Together, these let an agent discover what's available, inspect a template's contract, and then call generate with the right inputs — no more guessing at template ids or required variables.

Thanks to ateirney-nz for this contribution!

Unstable uv pip support for Python

In the Python ecosystem, pip is the default package manager, but tools like uv are gaining traction for their speed and modern features. To better support Python developers, we've added experimental support for uv pip as a package manager in the Python toolchain, alongside pip and uv.

This new option can be enabled via the unstable_python.packageManager setting in your project configuration:

.moon/toolchains.yml
unstable_python:
packageManager: 'uv-pip'

Because uv pip is a combination of uv and pip, that offers the speed and efficiency of uv while maintaining compatibility with pip, the way it is configured is quite unique. For install related args, the unstable_pip toolchain is used (because pip compatibility), while sync and venv related args, the unstable_uv toolchain is used (because uv is used directly).

.moon/toolchains.yml
unstable_python:
packageManager: 'uv-pip'

unstable_pip:
installArgs: ['--group', 'dev']

unstable_uv:
syncArgs: ['--check']
venvArgs: ['--clear']

Other changes

View the official release for a full list of changes.

  • Added Deno v2.8 support to the JavaScript toolchain.
  • Added Git SHA256 support for commit hashes. This is in preparation for Git's transition to SHA256 as the default hash algorithm.
  • Reduced task target memory footprint by 50-100%, further trimming the in-memory size of the project and task graphs.
  • Fixed a glob regression where unbounded walks could be up to 10x slower.