
How to Ship a Major Version People Actually Upgrade To
Releasing a major version sounds like a technical problem, and it’s very tempting to treat it like one. You bump a number, clean up APIs, document breaking changes, and ship, assuming the hardest part is the work that happens in code.
In practice, the code is rarely the hard part. What usually makes a major release difficult is everything around it: trust, timing, fear, and the very real concern that hundreds of people might suddenly regret upgrading on a random Tuesday because of a decision you made weeks earlier.
This post is about what it actually takes to ship a major version of a widely used library in a way that people are willing to upgrade to – and the things that surprised us along the way.
When a Backlog Stops Being a Backlog
We ran into this problem while working on Vibe, monday.com’s design system, during the v3 release.
Vibe serves a complex ecosystem: hundreds of microfrontends across multiple products, teams moving at very different speeds, and a broad set of external consumers as an open-source library. That mix mostly matters because it raises the stakes. When you break something at this scale, you don’t just break a repo – you break confidence.
We work with semantic versioning, and we take it seriously. Minor and patch releases are supposed to be safe. Developers should be able to upgrade without worrying that their app will break. That contract builds trust, and it works – but it has a side effect.
Breaking changes don’t disappear; they accumulate over time, especially when you are disciplined about not pushing them into minor or patch releases. In our case, that showed up in very concrete ways: accessibility fixes in components like Tooltip and Modal that we couldn’t safely ship behind minors, typography and Heading APIs that had drifted far enough to be confusing, and long‑standing inconsistencies that everyone had learned to work around instead of fixing.
To avoid breaking changes, we often introduce new props rather than modifying existing ones. That kept releases safe in the short term, but over time, it made APIs harder to reason about, with multiple props telling slightly different stories about the same component. None of these issues is urgent on its own, but together they form a wall.
When a Major is Necessary, But Not Enough
There’s a common rule of thumb: it’s time for a major when not releasing it causes more harm than releasing it. We agree with that, but we also learned it’s not enough. You can be right and still fail.
A few other things mattered just as much. Trust was a big one. A major version is a withdrawal from trust – you’re asking people to do work. If that work feels pointless, confusing, or chaotic, adoption stalls fast.
Timing mattered too. Not because we follow a release cadence (we don’t), but because ecosystems have moods. Sometimes teams have bandwidth. Sometimes they don’t. Shipping a major when nobody can absorb it is how you end up maintaining two versions forever.
And then there was scope. We had to be honest about what this major was for. Fixing everything is tempting, but migrations don’t fail because of one big breaking change – they fail because of death by a thousand cuts.
Over time, we kept coming back to the same underlying feeling: staying put had started to feel riskier than changing. But what really mattered wasn’t just deciding to release a major – it was being confident that we could actually guide people through it. That second part turned out to be the harder one.
The Wrong Assumption We Made Early
At scale, versioning is less about APIs and more about how much uncertainty you push onto other teams.
Here’s where we were wrong, and it took us longer than it probably should have to see it clearly.
We assumed the hardest part would be the changes themselves – the components, the APIs, the behavior tweaks. Things like updating Button and TextField props, changing Heading semantics, or cleaning up legacy defaults felt like the core of the work.
It turned out not to be.
The actual code changes in Vibe 3 were relatively straightforward. What took time, and what ended up defining the success of the release, was everything around it: how predictable the migration felt, how visible future breakage was, and how much ambiguity we could remove before people even started upgrading.
Early on, we spent a lot of time thinking about how far documentation could realistically take us. A thorough changelog and a clear migration guide were table stakes, and we knew we needed them. But it also became clear – even before release – that documentation alone wouldn’t scale to the amount of ambiguity we were introducing. Even when everything is written down, teams still get stuck when they can’t tell how a change should apply to their specific usage.
That realization changed how we approached the entire release.
We stopped thinking about “shipping a version” and started thinking about shipping a migration system.

Vibe 3 project board
Codemods Removed the Mechanical Fear
Before automation even became useful, there was something more basic we had to get right: being explicit about what was coming.
As soon as we knew certain changes would not be safely automatable, we warned about them ahead of time. Not as part of the release itself, but earlier, in development-only warnings. Things like components we knew were going away, or usage patterns that would no longer be supported in the next major release. For example, EditableInput, which was removed in favor of EditableText, or ResponsiveList, which was deprecated in favor of an alternative custom hook. The code changes themselves were usually simple, but choosing the right replacement often depended on usage patterns and design intent – something we wanted teams to think about ahead of time, not discover in the middle of a migration.
That didn’t magically solve the problem, but it changed how teams approached it. Instead of discovering surprises during migration, they could plan, coordinate with designers if needed, and make intentional decisions. At that point, automation started to make sense.
Codemods ended up being one of the biggest enablers of the migration. They allowed us to handle large classes of mechanical changes – prop renames across components, import path updates after package restructuring, and removal of deprecated props – in a way that was fast, predictable, and consistent across codebases.
We marked every change that could be handled mechanically in the changelog and built codemods to cover them. The effect was immediate. Migrations that used to take days were suddenly down to minutes for a first pass.
A typical migration started to look like this: run the codemods, see that most of the diff was already handled, and then focus on a small number of real decisions instead of hundreds of mechanical edits. That shift alone changed how teams felt about the upgrade.

CLI running the migration codemods
Codemods didn’t solve everything – but they removed the most boring, error‑prone part of the migration. And that matters, because fear often comes from volume, not complexity.
Making Decisions Explicit
By this point, it was clear that the real problem wasn’t a lack of information, but unstructured information. Teams didn’t just need to know what changed – they needed help understanding where decisions were required and where things were safe to automate.
That’s how we started thinking about documentation differently. Not as a deliverable, but as a way to make implicit knowledge explicit. The changelog became the source of truth, and from it we generated a migration guide that was component‑oriented and explicit about intent, tradeoffs, and alternatives – not just API diffs.
This wasn’t only about helping humans read less or faster. By making decisions, defaults, and breaking points explicit, we were turning ambiguity into structured context – something that tools could later consume, not just people.
The Cases Codemods Couldn’t Handle
Codemods handle the predictable layer, but there is always another category of breaking changes: the ones where intent matters and behavior depends on context.
In early migrations, we kept seeing teams stop at the same place. Tooltip changes weren’t mechanically complex, but they forced a decision: were teams relying on the old default behavior, or was it accidental? The migration guide explained what changed, but it couldn’t answer that question for them.
For example, Tooltip underwent several accessibility‑driven behavior changes: keyboard interactions were enabled by default, the open/close behavior changed in dialogs, and the internal structure was modified in ways that could affect layouts. A codemod can’t know whether a team relied on the old behavior or layout.

Tooltip component’s changelog having many changes
Another example was Modal. Defaults like unmountOnClose changed, and overlays inside modals started rendering within the modal container by default. Whether that’s correct depends entirely on the product: state preservation, layering assumptions, and existing UX patterns.
The MCP we built exposed structured, authoritative context from the repository: component props and metadata, accessibility requirements, usage examples, design tokens, and migration instructions. The migration tool could scan an entire codebase, identify which Vibe components were actually used, flag only the relevant breaking changes, and generate concrete migration steps and codemod commands.
With that context available, AI coding assistants could do something meaningfully different than just reading docs. Instead of guessing, they could explain why a specific usage broke, show a recommended replacement pattern using real component examples, and explicitly call out when a human decision was required. Automation handled the repetition; humans stayed responsible for intent.
We’re very explicit about this: AI doesn’t replace review. We still look at the changes. We still test. What it replaces is the grind of manually chasing down every last ambiguous case.
The result was dramatic. Migration time went from days or weeks to hours with codemods, to minutes for a solid first pass with AI assistance backed by MCP. More importantly, it made the process more reliable, even when the breaking change itself wasn’t. We consistently got feedback like “this was easier than we thought.”
Codemods removed the boring certainty. MCP handled the uncomfortable ambiguity.
From Shipping a Version to Shipping a System
Shipping Vibe 3 changed how we think about major versions.
The hardest part wasn’t the breaking changes themselves, but everything around them: removing uncertainty, making decisions explicit, and giving teams a way to move forward without feeling exposed.
The real win wasn’t Vibe 3 itself. It was building a system for shipping majors – one that treats migrations as infrastructure, not an afterthought. That’s why Vibe 4 already takes a fraction of the effort. Not because it’s smaller, but because the rails are finally in place.
Follow the project on GitHub, and don’t forget to give us a star ⭐


