---
slug: moon-v2.4
title: moon v2.4 - Task checks, Ruby toolchain, storage APIs, and much more
authors: [milesj]
tags: [tasks, checks, options, toolchains, ruby, python, poetry, daemon, vcs, cache]
image: ./img/moon/v2.4.png
---

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`](/docs/config/project#checks) setting for tasks that runs shell scripts _before_ the task, and reacts to
the result based on the type of check.

```yaml title="moon.yml"
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`](/docs/config/project#mergechecks) task option controls
whether inherited checks are appended, prepended, replaced, or preserved.

## Project-level task options

Task [`options`](/docs/config/project#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`](/docs/config/project#taskoptions) setting to
`moon.*` that defines defaults for every task in that project, which individual tasks can still
override.

```yaml title="moon.yml"
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`](/docs/config/toolchain#unstable_ruby) in
[`.moon/toolchains.*`](/docs/config/toolchain). Major thanks to [@lamalex](https://github.com/lamalex) for the contribution!

```yaml title=".moon/toolchains.yml"
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](https://python-poetry.org) 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`](/docs/config/toolchain#unstable_poetry) settings.

```yaml title=".moon/toolchains.yml"
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](/docs/guides/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`](/docs/commands/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](https://github.com/moonrepo/moon/releases/tag/v2.4.0) 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](https://github.com/moonrepo/proto/releases/tag/v0.58.0).
