DEV Community

Cover image for Distributing AnalogJS CI with Nx Agents and One Prompt
Brandon Roberts
Brandon Roberts

Posted on

Distributing AnalogJS CI with Nx Agents and One Prompt

AnalogJS has been an Nx monorepo since its very first commit, back in July 2022 — so for nearly four years the build tool has known the full dependency graph between every package and app. CI, though, never used any of that. It was six GitHub Actions jobs running mostly in series, each one checking out the repo and re-installing the world before it could do any real work. The workflow had grown into that job-per-concern shape over those years, and once it worked, nobody had a reason to revisit it.

I'd been meaning to move it onto Nx Agents for a while. What finally made me do it was getting a little shamed by a post from Altan Stalker — a pointed argument that CI pipelines exactly like mine reinvent a build graph the tool already has. Mine did, so I tried something: hand the whole migration to my coding agent from a single prompt and see how far careful analysis — not clever YAML — would get.

The short version is that it got the whole way. The more interesting part was watching why it made the choices it did.

The starting point

The old ci.yml had a job per concern — prettier, lint, build, build-windows, unit, and e2e. Every Linux job opened with the same boilerplate:

- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: corepack enable
- run: pnpm install --frozen-lockfile --prefer-offline
Enter fullscreen mode Exit fullscreen mode

Then e2e was hand-gated behind build with needs: build. That needs: edge is the heart of what Altan calls out: CI was re-encoding a dependency graph the build tool already knows. And stage gating has a quieter cost — when build fails, e2e is skipped, so you don't even find out whether e2e would have passed until the next push. Six runners, duplicated setup, serial --parallel=1 steps, and dead time between the stages.

CI before: six serial GitHub Actions jobs

One prompt

The prompt I gave my coding agent was deliberately about behavior, not YAML: analyze the existing CI and produce a correct, conservative Nx Agents setup based on the pipeline's real commands, dependencies, secrets, and execution behavior. Then I let it do the analysis itself. A few things it did stood out, because they're the things I'd want a careful human to do:

  • It read the real pipeline, not an idealized one — ci.yml, the package.json scripts each job actually invoked, nx.json targetDefaults, .node-version, and the package-manager pin.
  • It generated the Nx task graph (nx run-many -t build test e2e lint --graph) and inspected the edges to prove that test and e2e already depend on build — for example, storybook-angular:test → vite-plugin-angular:build. That evidence is what justified deleting the needs: build gate: Nx schedules the ordering, so CI doesn't have to.
  • It refused to over-consolidate. Each target has a distinct project filter — lint excludes content,platform,router, build excludes astro-app, e2e is limited to two projects. A single run-many can't express those, so the commands stayed separate. Correctness over a tidier diff.
  • It translated constraints, not bottlenecks. The old --parallel=1 limits became per-agent task concurrency rather than global serialization, because the constrained resource — memory, browser ports — is local to a machine, and each agent is its own machine.

That last distinction is the one I most expected it to get wrong, and the one that matters most. An old single-runner --parallel=1 is easy to misread as "this must never run concurrently anywhere." Usually it just means "this machine only has so much RAM."

The result

CI is now a single coordinator job that starts a distributed run and hands the cacheable Nx work to agents:

- name: Start CI run
  run: >
    pnpm exec nx-cloud start-ci-run
    --distribute-on="3 analog-linux"
    --require-explicit-completion
    --stop-agents-after="e2e"
Enter fullscreen mode Exit fullscreen mode

A new .nx/workflows/agents.yaml defines one analog-linux agent template — Node pinned from .node-version, corepack and pnpm, Playwright and Cypress browsers — so any agent can pick up a lint, build, test, or e2e task. The build-windows smoke build stays a native runner job; it needs a separate OS plane, and the agent left it alone.

CI after: one coordinator distributing work across Nx Agents

The payoff is on the part I changed. On the first run — cold cache, agents starting from scratch — the Linux pipeline dropped from about 10 minutes to 7 and a half, just from letting the graph schedule across agents instead of chaining build → e2e in YAML. It should keep dropping as the Nx Cloud cache warms up.

That left one long pole: the Windows smoke build, still around 12 minutes. It was rebuilding all 20 projects, one at a time, just to verify a single app's output. So I scoped it to that app — nx build blog-app, the six tasks it actually needs — and moved the full Windows build to a nightly run. It dropped from ~12 minutes to under 4. Time to green went from ~12 minutes to about 7 and a half.

Conclusion

Let the build tool own the graph. Nx already knows test depends on build. Restating that in YAML just keeps the same fact in two places, and only one of them is checked.

The rest was separating meaning from shape. Old job boundaries and --parallel numbers tell you how the last setup happened to work, not what the next one has to keep. And the analysis — read the scripts, generate the task graph, decide what not to merge — turned out to be the part worth handing off. Point a tool at the real pipeline instead of a mental model of it, and it does that part well. That's the part to lean on.

If you enjoyed this post, click the ❤️ so other people will see it. Follow AnalogJS and Brandon Roberts on Bluesky, and subscribe to my YouTube Channel for more content!

Top comments (0)