Back to Moon

moon v2.4 - Task checks, Ruby toolchain, storage APIs, and much more

website/blog/2026-07-05-moon-v2.4.mdx

2.4.08.1 KB
Original Source

This release is all about control and trust. We're giving tasks a way to gate, skip, and fingerprint themselves with plain shell scripts, letting projects set default task options in one place, and rounding out the toolchain lineup with Ruby and Poetry support. Behind the scenes, the daemon and VCS layers also got a substantial hardening pass so moon behaves reliably in the messy edge cases real repositories throw at it.

<!--truncate-->

Task checks

Tasks often need to answer a question before (or instead of) actually running: "is this tool installed?", "should this even run right now?", "did the environment change since last time?". Previously you'd have to bake that logic into the task's command itself. In v2.4, we're introducing a checks setting for tasks that runs shell scripts before the task, and reacts to the result based on the type of check.

yaml
tasks:
  deploy:
    command: './deploy.sh'
    checks:
      # A plain string is shorthand for a `requirement` check
      - 'command -v aws'

      # Skip the task entirely if the condition script exits successfully
      - check: 'condition'
        script: './scripts/already-deployed.sh'

      # Fold the script's stdout into the task's cache hash
      - check: 'fingerprint'
        script: 'openssl version'
        hash: stdout

Requirements

A requirement check runs a script, and if it fails, the task fails and does not run at all. This is the default behavior when you provide a plain string instead of an object, making it the quickest way to assert a precondition, like a required binary being on PATH.

Conditions

A condition check is the inverse: if all condition scripts in the list pass, the task is skipped instead of running. This is useful for idempotency checks, such as "has this migration already been applied?" or "is the output already up to date?", where running the task again would be redundant or wasteful.

Fingerprints

A fingerprint check always runs alongside the task, but instead of gating it, its output is mixed into the task's cache hash. This means a change in the script's output — like a different tool version — invalidates the cache, even when none of the task's declared inputs changed. You can control exactly what gets hashed (exit-code, stdout, stderr, or all output) via the hash field.

Checks are inherited like any other task setting, and a new mergeChecks task option controls whether inherited checks are appended, prepended, replaced, or preserved.

Project-level task options

Task options have always been configurable per task, but if you wanted the same option (say, disabling the cache, or bumping the retry count) applied across every task in a project, you had to repeat it on each one. v2.4 adds a top-level taskOptions setting to moon.* that defines defaults for every task in that project, which individual tasks can still override.

yaml
taskOptions:
  retryCount: 2
  cache: false

tasks:
  test:
    command: 'jest'
    # Inherits retryCount: 2 and cache: false

  lint:
    command: 'eslint .'
    options:
      cache: true # Overrides the project-level default

Unstable Ruby toolchain support

Ruby joins the toolchain lineup as an unstable, experimental integration. Like our other toolchains, it supports the full range of tiers — from basic language detection in tier 1, up through building from source in tier 3.

Configure it via unstable_ruby in .moon/toolchains.*. Major thanks to @lamalex for the contribution!

yaml
unstable_ruby: {}

Run moon toolchain info unstable_ruby for the full list of available settings.

Poetry support for the Python toolchain

The Python toolchain now supports Poetry as a package manager, alongside the existing pip and uv support, and works across tiers 1 through 3. Opt in by setting packageManager: 'poetry', and fine-tune behavior through the new unstable_poetry settings.

yaml
unstable_python:
  packageManager: 'poetry'

unstable_poetry: {}

Run moon toolchain info unstable_poetry for the full list of available settings.

New cache storage APIs

A new internal storage layer landed this release, unifying how local and remote caches interoperate:

  • When a remote cache hit occurs, moon now warms the local cache with the hydrated manifest and its blobs, so the next run resolves locally instead of round-tripping to the remote again.
  • When a blob is missing from one cache, moon will now attempt to retrieve it from the other, improving hit rates in mixed local/remote scenarios.
  • Copying files to/from the CAS now uses OS reflinks when available, improving performance and reducing disk usage.
  • Remote cache blob streaming now supports compression, and falls back to uncompressed ("identity") transfer instead of disabling the cache entirely when the server doesn't support the configured compression.

Daemon improvements

moon's background daemon powers a lot of the persistent, cross-run functionality. In v2.4 we reworked how the daemon establishes and maintains its identity so it behaves correctly under real-world churn:

  • The daemon now takes exclusive ownership of its workspace through an advisory file lock held for its entire lifetime, replacing PID-liveness checks that could be fooled by zombie processes, reused PIDs, or processes owned by another user.
  • Whether the daemon is running is now determined by connecting to it rather than probing a PID, with its metadata recorded in a daemon.json state file (replacing moond.pid).
  • Connecting to the daemon and starting it are now a single operation — a command that needs the daemon will start one itself if a background pre-warm hasn't already, and concurrent starts coordinate so only one daemon is ever spawned.
  • On connect, the client now checks the running daemon's version against its own, and restarts it on a mismatch, so a daemon left over from before a moon upgrade doesn't keep serving the old binary.
  • The daemon now retires itself after a long idle period, and exits immediately if its workspace is deleted, so an abandoned workspace no longer leaves it running forever.
  • Server log files now rotate up to 7 times, with older files automatically cleaned up.

VCS improvements

Git interactions got a hardening pass across the board:

  • All executed Git commands now validate revisions against argument injection, credential prompts fail immediately instead of hanging, and the fsmonitor daemon is disabled so it can't block process pipes.
  • Merge base resolution was reworked to be more accurate and performant — the most recent divergence point is now preferred when local and remote branches have drifted, and a warning is logged when a merge base can't be resolved, since diffs may be inaccurate without one.
  • Renames are now reported properly when diffing between revisions: the old path shows as "deleted" and the new path as "added" (instead of both showing as "modified"), and for status output, the old path shows as "deleted" instead of being dropped entirely. Type changes and unmerged files are now reported too, instead of being silently omitted.

Other changes

View the official release for a full list of changes.

  • Added a --update-constraint option to moon upgrade that updates the workspace config's version constraint to the latest version after upgrading.
  • Toolchain executable location lookups are now cached, instead of being re-resolved for every single task that uses them.
  • Updated --log level handling: debug covers most day-to-day debugging needs, while trace now includes significantly more detail, geared towards agents and deep diagnostics.
  • Updated proto to v0.58.2.