DEV Community

Cover image for How an Unrelated App Broke Our Nx Deployment
Ango Jeffrey
Ango Jeffrey

Posted on

How an Unrelated App Broke Our Nx Deployment

Introduction

Ever had a build pass perfectly on your local machine only to watch it crumble in the CI/CD pipeline? I recently hit a wall at work while deploying a web app from an Nx monorepo. The culprit was not even the code I was deploying. Instead, it was a configuration mismatch in a completely different mobile app within the same workspace.

Wait, What is Nx and a Monorepo?

Before we dive into the error, let’s talk about the setup. A monorepo is a single repository that houses multiple projects. In my case at work, I have a monorepo setup that houses our web and mobile apps. Nx is a powerful build system designed to manage these monorepos efficiently.

To give you a visual, here is a simplified look at the folder structure:

my-monorepo/
├── apps/
│   ├── web/                # The app we were trying to deploy
│   │   └── project.json
│   └── mobile/             # The project that broke the build
│       ├── eslint.config.js
│       └── project.json
├── package.json            # Root dependencies (ESLint 8 lived here)
├── nx.json                 # Global Nx configuration
└── package-lock.json
Enter fullscreen mode Exit fullscreen mode

One of its primary features is the Project Graph. Nx automatically scans your entire codebase to understand how projects relate to one another. This allows it to perform "affected" commands, only testing or building the parts of the repo that actually changed.

Logically Unrelated, but Toolchain-Coupled
It is easy to think of a mobile app and a web app as "unrelated" because they don't share code. However, in an Nx workspace, they are toolchain-coupled. Because Nx evaluates plugins, configs, and inferred targets across the whole workspace, a configuration error in one project can actually break the deployment for another.

In this post, I’ll break down why the Nx Project Graph failed, how ESLint version skew caused it, and how to harden your CI workflow to prevent it from happening again.

The Problem: The "Ghost" of Other Projects

Even if you're working on one app in the repo and you build that app locally and everything passes fine, Nx must compute that workspace-wide project graph. If any project in your repo has a broken configuration or an incompatible plugin, the entire graph fails to initialize.

Our deployment was failing with a cryptic error:

NX Failed to process project graph
Enter fullscreen mode Exit fullscreen mode

Digging into the verbose logs, we found the real villain:

apps/mobile/eslint.config.js: Package subpath './config' is not defined by "exports" in .../node_modules/eslint/package.json
Enter fullscreen mode Exit fullscreen mode

The Root Cause: ESLint Version Skew

We were running a "split" toolchain across the monorepo:

  • Root/Web: Resolving to ESLint 8.

  • Mobile: Using ESLint 9 with the Flat Config style.

Because Nx uses plugin inference to build the graph, it tried to evaluate the mobile app's ESLint config using the ESLint version resolved in the CI environment. The mobile config used import patterns incompatible with the CI's ESLint version, which caused the graph and the entire deployment to crash.

The issue was compounded by a non-deterministic CI strategy. We were deleting the lockfile in the pipeline and using npm install --legacy-peer-deps, which increased the risk of version drift.

The Fix: Isolation and Hardening

When faced with this toolchain break, there were a few paths we could have taken. However, our choice was driven by the reality of our team structure and technical constraints.

Why we chose Isolation

We ultimately decided to isolate the mobile app from Nx plugin inference. Here is why the other common paths didn't work for us:

  • Why not align toolchains? Upgrading everything to ESLint 9 would have been the "cleanest" path, but our web app (Next.js) had potential version clashes with the new ESLint flat config.
  • Why not separate workspaces? Moving the mobile app to its own repo would offer true isolation, but we chose the monorepo approach specifically to leverage shared resources. A huge portion of our core infrastructure—including our APIs, API wrappers, shared state logic, and utility functions are shared by both web and mobile. Splitting them would have meant duplicating code or managing complex private NPM packages.
  • The Team Factor: In our case, the mobile and web apps are owned by different teams. Forcing a global upgrade would have required cross-team coordination that would have delayed our deployment further.

By isolating the mobile app, we allowed both teams to move at their own pace without breaking the global toolchain.

1. Isolate Plugin Inference

To do this, we told Nx to stop trying to "infer" the mobile app's configuration during graph creation. In nx.json, we excluded the mobile app from the ESLint plugin:

"@nx/eslint/plugin": {
  "exclude": ["apps/mobile/**"]
}
Enter fullscreen mode Exit fullscreen mode

2. Restore Functionality with Explicit Targets

By excluding the project from the plugin, we lost the automatic lint target. We restored this by adding an explicit target in apps/mobile/project.json using run-commands:

"lint": {
  "executor": "nx:run-commands",
  "options": {
    "cwd": "apps/mobile",
    "command": "npm run lint"
  }
}
Enter fullscreen mode Exit fullscreen mode

This keeps the mobile linting as a first-class citizen in Nx without breaking the global graph.

3. Deterministic CI Installs

Stop deleting your lockfile in CI. We switched from npm install to npm ci to ensure the environment exactly matches the local development state.

Key Takeaways for Monorepo Maintainers

Keep Nx Versions Aligned: Ensure nx and all @nx/* packages stay on the same version.

Use npm ci: Determinism is your best friend in a pipeline.

Isolate Boundaries: If you maintain split toolchains, use the exclude property in nx.json to prevent cross-contamination during graph builds.

Debug with Verbosity: When the graph fails, use npx nx report and npx nx build [app name] --verbose to find the hidden error.

Wrap Up
Monorepos offer incredible power but they require a "global" mindset. A failure in a project you are not even touching can block your entire pipeline. By hardening your CI and isolating plugin inference, you can ensure your deployments stay green.

Have you encountered Nx graph issues? Let’s discuss in the comments!

Top comments (0)