website/docs/migrate/2.0.mdx
To ease the migration process from moon v1 to v2, we've compiled a list of all breaking changes and important changes that you should be aware of. Please read through these carefully before upgrading your workspace.
To automate some of the migration process, we've created the moon migrate v2 command that will
migrate all applicable settings in configuration files.
$ moon migrate v2
moonx binary to use moon exec instead of moon run under the hood.--logLevel -> --log-level--platform -> --toolchainmoon nodemoon migrate from-package-json (use the migrate-turborepo extension instead)moon query hash (use moon hash instead)moon query hash-diff (use moon hash instead)moon action-graph--json to a new JSON structure (now matches the project and task graphs).moon checkmoon exec under the hood, with some arguments/options pre-filled.--closest.--update-cache, -u -> --force, -fmoon cimoon exec under the hood, with some arguments/options pre-filled.--include-relations to include them.--update-cache, -u -> --force, -fmoon generate--to option.
moon generate <id> ./dist -> moon generate <id> --to ./distmoon initmoon toolchain add
command instead.--to (use a positional argument instead)moon mcpget_projects tool to no longer have an includeTasks option.get_projects tool to return a list of project fragments, instead of the whole
project object. This was required as the response was too large for MCP.get_tasks tool to return a list of task fragments, instead of the whole task object.
This was required as the response was too large for MCP.moon query projects--dependentsmoon runmoon exec under the hood, with some arguments/options pre-filled.--include-relations to include them.--dependents now requires a value, either deep or direct--update-cache, -u -> --force, -f--no-bail (use moon exec instead)--profile--remote (use --affected remote instead)moon templatesmoon run~: scope instead.
moon run build -> moon run ~:build.moon/workspace.*codeowners.orderBy value project-name -> project-idcodeowners.syncOnRun -> codeowners.syncconstraints.enforceProjectTypeRelationships -> constraints.enforceLayerRelationshipsdocker.prune.installToolchainDeps -> docker.prune.installToolchainDependenciesdocker.scaffold.include -> docker.scaffold.configsPhaseGlobsrunner -> pipelineunstable_remote -> remotevcs.manager -> vcs.clientvcs.syncHooks -> vcs.syncdocker.scaffold.copyToolchainFilesexperiments.*hasher.batchSizepipeline.archivableTargets.moon/toolchain.*.moon/toolchains.* (plural) to reflect that multiple toolchains are
configured. This also aligns with the new .moon/extensions.* file.unstable_ prefix must be removed
from identifiers.The bun, deno, and node toolchains now require the javascript toolchain to be defined as
well. All shared settings have been moved to the javascript toolchain.
In addition, all node package managers are no longer nested under the node toolchain, but are
now top-level settings. These are only required when the javascript.packageManager setting is
defined.
bun.dependencyVersionFormat -> javascript.dependencyVersionFormatbun.inferTasksFromScripts -> javascript.inferTasksFromScriptsbun.rootPackageOnly -> javascript.rootPackageDependenciesOnlybun.syncProjectWorkspaceDependencies -> javascript.syncProjectWorkspaceDependenciesnode.dependencyVersionFormat -> javascript.dependencyVersionFormatnode.dedupeOnLockfileChange -> javascript.dedupeOnLockfileChangenode.inferTasksFromScripts -> javascript.inferTasksFromScriptsnode.packageManager -> javascript.packageManagernode.rootPackageOnly -> javascript.rootPackageDependenciesOnlynode.syncPackageManagerField -> javascript.syncPackageManagerFieldnode.syncProjectWorkspaceDependencies -> javascript.syncProjectWorkspaceDependenciesnode.bun -> bunnode.npm -> npmnode.pnpm -> pnpmnode.yarn -> yarnnode.binExecArgs -> node.executeArgsbun.packagesRootdeno.depsFiledeno.lockfilenode.addEnginesConstraintnode.packagesRoot# Before
node:
version: '22.14.0'
packageManager: 'yarn'
inferTasksFromScripts: false
syncPackageManagerField: true
syncProjectWorkspaceDependencies: true
yarn:
version: '4.8.0'
# After
javascript:
packageManager: 'yarn'
inferTasksFromScripts: false
syncPackageManagerField: true
syncProjectWorkspaceDependencies: true
node:
version: '22.14.0'
yarn:
version: '4.8.0'
All the Python toolchains are still unstable, and still require the unstable_ prefix. We're not
Python experts, so we would love help from the community to test and improve the implementation. If
you're interested in helping out, please reach out in our Discord server.
python.pip -> unstable_pippython.uv -> unstable_uvpython.rootVenvOnly# Before
python:
version: '3.12.0'
packageManager: 'uv'
uv:
version: '0.10.2'
# After
unstable_python:
version: '3.12.0'
packageManager: 'uv'
unstable_uv:
version: '0.10.2'
.moon/extensions.*extensions setting from .moon/workspace.* has been moved (and flattened) to its own file,
.moon/extensions.*.download, migrate-nx, and migrate-turborepo must now be enabled in
the configuration file before they can be used. Simply set an empty object.
download: {}
migrate-nx: {}
In v1 when determining the affected state of a task (via --affected), the graph relations
(dependencies and dependents) were always included in the calculation. This meant that tasks were
affected, even if there were no changed files or environment variables, just because they were
related to other affected tasks. This was sometimes confusing to users, as it seemed like the
pipeline was over-zealously running tasks that it shouldn't (even though it was correct).
In v2 and later, the graph relations are no longer included by default, and only changed files and
environment variables are taken into account. To include graph relations in the affected
calculation, you can pass the --include-relations (-g) flag.
Tasks have an option called runInCI that indicates whether the task should be run in CI or not,
and can be configured with more granulary conditions. However, in v1 this option was only taken into
account in the moon ci command, and not in moon run. There was no way to force this option to be
respected in moon run, which meant that users had to be careful to not accidentally run CI-only
tasks locally.
In v2, we've improved the implementation of this option, and how we detect CI environments. The following changes have been made.
moon ci, moon check, moon run, and moon exec now all respect the runInCI option.moon ci is always forced in CI mode. The other commands are dependent on the CI environment
variable.
CI environment variable to true, or pass the --ci flag.runInCI option will be applied when in an CI environment. The exception to this is the
"only" condition, which also applies locally.runInCI option can be disabled entirely with moon exec --ignore-ci-checks.moon.*docker.scaffold.include -> docker.scaffold.sourcesPhaseGlobsproject.name -> project.titletype -> layertoolchain -> toolchainsplatform -> toolchains.defaultThe primary language is now detected from toolchains, instead of being a hardcoded implementation.
The result may now differ, as the first toolchain in the list will be used. Additionally, languages
that don't have a toolchain yet, like PHP or Ruby, will not be detected and must be explicitly
configured.
# After
language: 'ruby'
The project.metadata setting has been removed, but all custom metadata fields can now be defined
at the root of the project setting.
# Before
project:
metadata:
customField: 'value'
# After
project:
customField: 'value'
The toolchain.*.disabled setting was removed. Instead set the toolchain itself to null/false.
# Before
toolchain:
typescript:
disabled: true
# After
toolchains:
typescript: null
moon.*, .moon/tasks.moon/tasks.yml has been removed. If you want to support tasks that are inherited by
all projects, then move this to .moon/tasks/all.yml and do not configure the new
inheritedBy setting.$projectName -> $projectTitle$projectType -> $projectLayer$taskPlatform -> $taskToolchaintasks.*.local -> tasks.*.preset using server valuetasks.*.platform -> tasks.*.toolchainstasks.*.options.affectedPassInputs -> tasks.*.options.affectedFiles.passInputsWhenNoMatchtasks.*.preset value watchertasks.deps.*.args no longer supports a string. Use a list of strings instead.tasks.*.options.affectedFiles now supports an object for more granular control.tasks.*.options.envFile now defaults to a list of files, instead of a single file, when
true. Refer to the blog post for more information.tasks.*.options.inferInputs now defaults to false instead of true.tasks.*.options.shell now defaults to true instead of false.tasks.*.options.unixShell now defaults to bash instead of nothing.tasks.*.options.windowsShell now defaults to pwsh instead of nothing.We've reworked command (and args) to only support simple commands. A simple command is an
executable (binary, file, etc) followed by zero or many arguments.
Complex commands that involve shell features like piping (|), redirection (>, <), chaining
(&&, ||, ;), and environment variable assignment (KEY=value) are no longer supported
directly in the command setting. Use script instead.
# Before
tasks:
example:
command: 'echo "foo" | grep "f"'
# Before
tasks:
example:
script: 'echo "foo" | grep "f"'
Shell features like expansion, globbing, and substitution is still supported in
command.
The local task setting has been removed as the name was confusing. Users assumed it meant "only
run locally", but it actually meant "this is a persistent server that should only run locally".
Instead, use the preset setting with a value of server.
# Before
tasks:
dev:
command: 'start-dev'
local: true
# Before
tasks:
dev:
command: 'start-dev'
preset: 'server'
If you want a task that is simply "local only" without other options changes, use options.runInCI
directly.
tasks:
dev:
command: 'start-dev'
options:
runInCI: false
Tasks now run in a shell by default, and will use Bash on Unix (options.unixShell), and pwsh on
Windows (options.windowsShell). You can disable this behavior by setting options.shell to
false.
tasks:
dev:
command: 'start-dev'
options:
shell: false
The syntax and behavior for substituting (expanding/interpolation) environment variables has
changed, to better align with the standard of .env files. The biggest change is that flagless
tokens ($VAR) and ? flag tokens ($VAR?) swapped functionality. Refer to the following table:
| Syntax | v1 | v2 |
|---|---|---|
$VAR | Substitute with variable syntax ($VAR) if variable empty | 💥 Substitute with empty string if variable empty |
$VAR! | Don't substitute and keep variable syntax ($VAR) | 💥 Removed syntax |
$VAR? | Substitute with empty string if variable empty | 💥 Removed syntax |
${VAR} | Substitute with variable syntax ($VAR) if variable empty | 💥 Substitute with empty string if variable empty |
${VAR!} | Don't substitute and keep variable syntax ($VAR) | ~ |
${VAR?} | Substitute with empty string if variable empty | 💥 Substitute with variable syntax ($VAR) if variable empty |
${VAR:default} | Use default value if variable empty | ~ |
${VAR:-default}, ${VAR-default} | ⛔️ Not supported | Use default value if variable empty |
${VAR:+alternate}, ${VAR+alternate} | ⛔️ Not supported | Use alternate value if variable non-empty |
Legend:
~ indicates the syntax/functionality is the same as v1💥 indicates breaking change⛔️ indicates not supportedThe order of precedence for environment variables has slightly changed when running tasks, as it was confusing to users. The new order of precedence is as follows, from lowest to highest tier, with the latter overwriting the former:
.env files
options.envFileenv or deps.*.env.bashrc, .zshrc, etc)KEY=value moon ...In regards to variable substitution, each tier can reference variables within the same tier, or the higher tier(s), but not from lower tier(s). This is because variables are processed in reverse order. Refer to the following table:
| Tier | Can reference | Evaluated during |
|---|---|---|
| Dotenv | Dotenv, Task, System | Before task execution |
| Task | Task, System | After task creation |
| System | System | CLI startup |
If you don't want to inherit a system variable, you can override it in the task with a null value.
tasks:
dev:
env:
EXAMPLE: null
.env filesIn v1, when options.envFile was enabled, the .env file(s) were loaded at the time of task
creation, during the building of project/task graphs. This meant that if the .env file changed
between the time of graph creation and task execution, the changes would not be reflected.
In v2 and later, .env files are loaded just before task execution, ensuring that any changes to
the file are picked up.
:::caution
Because of this change, task env variables will continue to override .env file variables, BUT
can no longer reference them for substitution. This is because the .env files are loaded later in
the process.
:::
How toolchains work and get detected for projects and tasks has been heavily reworked in v2.
Ideally, you should never configure the toolchains setting for a task directly. This can cause
unintended consequences, and instead you should rely on the toolchain detection system itself.
For example, in moon v1, the platform: node setting represented Node.js, the configured package
manager (npm, pnpm, yarn), and JavaScript itself. That's 3 different layers of functionality in 1
implementation. In v2, these layers are now split into separate toolchains (plugins), and act
independently.
So if you had the following configuration in v2:
tasks:
build:
# ...
toolchains: 'node'
This would only apply the Node.js toolchain, and NOT the JavaScript toolchain, NOR the package manager toolchain. This may cause unexpected issues. The correct configuration should be:
tasks:
build:
# ...
toolchains: ['node', 'javascript', 'npm']
But adding all those toolchains is cumbersome, so we suggest omitting toolchains entirely and let
the detection do its thing.
options.affectedFiles is enabled, the list of files will be joined with the OS path
separator (: on Unix, ; on Windows) instead of a comma (,) when passed as the
MOON_AFFECTED_FILES environment variable.In v1, when inheriting tasks, all global configs (those in .moon) were shallow merged into a
single config ignoring merge task options, and then merged with the local config (moon.*) using
merge task options. This was not intuitive, as users expected all configs to be merged in sequence.
To demonstrate this, take the following example configs, in order of inherited:
# .moon/tasks.yml
tasks:
build:
command: 'build --cache'
options:
mutex: 'build'
# .moon/tasks/tag-a.yml
tasks:
build:
args: '--force'
# .moon/tasks/tag-b.yml
tasks:
build:
args: '--clean'
Users would expect the final build task to be build --cache --force --clean with the mutex
option set. However, since the tasks setting was shallow merged, only the last config (tag-b)
would be used, resulting in the task being noop --clean without the mutex. The noop pops up
because the command setting was not configured in the last config.
To remedy this, and to improve task composition overall, global configs are no longer shallow merged into a single config before merging with the local config. Instead, all configs are merged in sequence, while respecting the task merge options. This is the order of operations for the new system:
.moon) into a list, in order, based on the inheritedBy setting.moon.*).extend and other composition settings.Because of the new deep merging behavior, file groups defined in inherited configs are now merged together, instead of being replaced by the following config in the sequence. For example, given the configs:
# .moon/tasks/tag-a.yml
fileGroups:
sources:
- 'src/**'
# .moon/tasks/tag-b.yml
fileGroups:
sources:
- 'docs/**'
In v1, the final sources file group would only include docs/**, as the second config would
replace the first. In v2, the final sources file group includes both src/** and docs/**.
We've rewritten our Git hooks from the ground up to be based around the core.hooksPath setting.
The following changes have been made:
.git/hooks directory..moon/hooks and Git is configured to use this directory..sh.:::caution
We currently don't have an easy way to clean the previous implementation of hooks. You may need to
manually remove the old scripts from .git/hooks and .moon/hooks if they are causing issues.
:::
We renamed the terminology "touched files" to "changed files" throughout the codebase and documentation. This better aligns with common VCS terminology and reduces confusion. Because of this, the following changes were made:
moon query touched-files CLI command to moon query changed-files.get_touched_files MCP tool to get_changed_files.touchedFiles run report field to changedFiles.# Before
$ moon query touched-files
# After
$ moon query changed-files
.moon/docker/workspace directory was renamed to .moon/docker/configs.moon docker file command will now loop through all toolchains and use the first image found,
otherwise it defaults to "scratch". If you want to be explicit, set the docker.file.image
setting.projectName -> projectIdprojectType -> projectLayertaskPlatform -> taskToolchain# Before
projectType=application && taskPlatform=node
# After
projectLayer=application && taskToolchain=node
tool.* events. Use toolchain.* events instead.runtime field from dependencies.* events. Use the toolchain field instead.