{"id":20805,"date":"2025-08-07T08:32:01","date_gmt":"2025-08-07T13:32:01","guid":{"rendered":"https:\/\/zocdoctech.wpenginepowered.com\/?p=20805"},"modified":"2025-12-22T14:42:51","modified_gmt":"2025-12-22T19:42:51","slug":"lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna","status":"publish","type":"post","link":"https:\/\/www.zocdoc.com\/techblog\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/","title":{"rendered":"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna"},"content":{"rendered":"\r\n<p id=\"3914\"><a href=\"https:\/\/www.zocdoc.com\/techblog\/monorepo-magic-escaping-version-hell-by-decoupling-dependencies\/\"><strong>In part 1<\/strong><\/a>, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an existing codebase into it and the lessons we learned.<\/p>\r\n\r\n\r\n\r\n<p id=\"0219\"><em>Spoiler: we like it, but it required some work to get to that point.<\/em><\/p>\r\n\r\n\r\n\r\n<h1 id=\"67f7\" class=\"wp-block-heading\">The Pilot Project<\/h1>\r\n\r\n\r\n\r\n<p id=\"9448\">The tool most commonly used in the Javascript ecosystem to manage monorepos is\u00a0<a href=\"https:\/\/lerna.js.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Lerna<\/a>, which is what we ultimately decided to move forward with.<\/p>\r\n\r\n\r\n\r\n<p id=\"4aa5\">We were a little hesitant to commit to a full-out migration without a trial period, so we decided on a mini-migration of about half a dozen components to make sure that a monorepo was the right solution.<\/p>\r\n\r\n\r\n\r\n<p id=\"8e32\">Our goals for the pilot migration were relatively straightforward:<\/p>\r\n\r\n\r\n\r\n<ol class=\"wp-block-list\">\r\n<li>Move half a dozen specific components and utils from our existing design system repo into a monorepo. Include small components, large components, components that depend on other components, and components we expect to be under active development so we can stress-test lerna.<\/li>\r\n\r\n\r\n\r\n<li>Make sure unit testing, screenshot testing, interaction testing, and publishing packages are in good shape, and are scalable.<\/li>\r\n\r\n\r\n\r\n<li>Find all repos that consume the migrated code from the legacy design system, and switch the imports to use the versions in the monorepo.<\/li>\r\n<\/ol>\r\n\r\n\r\n\r\n<p id=\"5057\">However, migrating a few components independently as individual packages is a bit more complicated than migrating a whole repository at once.<\/p>\r\n\r\n\r\n\r\n<h1 id=\"7364\" class=\"wp-block-heading\">Code Organization<\/h1>\r\n\r\n\r\n\r\n<p id=\"ad31\">It\u2019s common for the main source code of a non-monorepo repository to live in a\u00a0<code>src\/<\/code>\u00a0directory. Instead, Lerna encourages you to use a\u00a0<strong>packages\u00a0<\/strong>directory. Generally the packages folder is a flat listing of every package in the repo, but we found that a bit more structure was ideal. Since this was common front-end code, we decided to break everything down into four categories, with the caveat that we could add more if there was a good case:<\/p>\r\n\r\n\r\n\r\n<ol class=\"wp-block-list\">\r\n<li><strong>assets<\/strong>\u00a0for images\/icons\/fonts\/etc<\/li>\r\n\r\n\r\n\r\n<li><strong>components<\/strong>\u00a0for our react and javascript components<\/li>\r\n\r\n\r\n\r\n<li><strong>styles<\/strong>\u00a0for anything aesthetic<\/li>\r\n\r\n\r\n\r\n<li><strong>utils<\/strong>\u00a0as a catch-all for functions and constants we might want to be available from multiple places<\/li>\r\n<\/ol>\r\n\r\n\r\n\r\n<p>This allowed us to create a structure and force developers to easily adhere to it. We found we could get lerna to follow this structure by setting the packages property in the\u00a0<code>lerna.json<\/code>:<\/p>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">\"packages\": [\r\n    \"packages\/assets\/**\",\r\n    \"packages\/components\/**\",\r\n    \"packages\/styles\/**\",\r\n    \"packages\/utils\/**\",\r\n    \"packages\/node\/**\"\r\n]<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">packages<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #d8dee9ff;\">: [<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">packages\/assets\/**<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #eceff4;\">,<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">packages\/components\/**<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #eceff4;\">,<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">packages\/styles\/**<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #eceff4;\">,<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">packages\/utils\/**<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #eceff4;\">,<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">packages\/node\/**<\/span><span style=\"color: #eceff4;\">\"<\/span><\/span>\r\n<span class=\"line\"><span style=\"color: #d8dee9ff;\">]<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<p>This directory structure was significantly different from the organization of the code in our legacy design system, which is a small issue we had to deal with when importing components. A much more complicated issue was git history.<\/p>\r\n\r\n\r\n\r\n<p>Migration With Git History<\/p>\r\n\r\n\r\n\r\n<p id=\"e02b\">We weren\u2019t willing to lose the git history of our legacy Design System library in the migration process. It was important to us that after migrating code, we could still identify the original authors and when they made changes.<br \/>The\u00a0<code>lerna import<\/code>\u00a0command maintains history, but it is designed for migrating an entire repository into a single package in the monorepo. We only wanted to migrate a few components for the pilot, and we wanted each component to have its own package. In order to migrate components one at a time, we wrote a shell script that took in a few command line parameters:<\/p>\r\n\r\n\r\n\r\n<ul class=\"wp-block-list\">\r\n<li><code><mark class=\"has-inline-color has-black-color\" style=\"background-color: #abb8c3;\"><strong>$PACKAGE_NAME<\/strong><\/mark><\/code><strong>\u00a0<\/strong>(the name for the new package post migration)<\/li>\r\n\r\n\r\n\r\n<li><code><strong><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">$SRC_REPO<\/mark><\/strong><\/code><strong>\u00a0<\/strong>(a url that can be used to clone the old repository)<\/li>\r\n\r\n\r\n\r\n<li><code><strong><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">$IMPORT_SUBFOLDER<\/mark><\/strong><\/code>\u00a0(the path to the code to migrate)<\/li>\r\n\r\n\r\n\r\n<li><code><strong><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">$CATEGORY<\/mark><\/strong><\/code>\u00a0(utils, components, styles, assets, as outlined above)<\/li>\r\n<\/ul>\r\n\r\n\r\n\r\n<p id=\"1893\">The script ran in a few different steps:<\/p>\r\n\r\n\r\n\r\n<ul class=\"wp-block-list\">\r\n<li>Create a fresh clone of the source repo, for us usually our design system<\/li>\r\n<\/ul>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">git clone $SRC_REPO<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88c0d0;\">git<\/span> <span style=\"color: #a3be8c;\">clone<\/span> <span style=\"color: #d8dee9;\">$SRC_REPO<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<ul class=\"wp-block-list\">\r\n<li>Filter the git history of this clone to just the desired component<\/li>\r\n<\/ul>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">git filter-repo --prune-empty always --subdirectory-filter ${IMPORT_SUBFOLDER}\r\ngit tag -d `git tag | grep -E \u2018.\u2019`<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88c0d0;\">git<\/span> <span style=\"color: #a3be8c;\">filter-repo<\/span> <span style=\"color: #a3be8c;\">--prune-empty<\/span> <span style=\"color: #a3be8c;\">always<\/span> <span style=\"color: #a3be8c;\">--subdirectory-filter<\/span> <span style=\"color: #81a1c1;\">${<\/span><span style=\"color: #d8dee9;\">IMPORT_SUBFOLDER<\/span><span style=\"color: #81a1c1;\">}<\/span><\/span>\r\n<span class=\"line\"><span style=\"color: #88c0d0;\">git<\/span> <span style=\"color: #a3be8c;\">tag<\/span> <span style=\"color: #a3be8c;\">-d<\/span> <span style=\"color: #eceff4;\">`<\/span><span style=\"color: #88c0d0;\">git<\/span><span style=\"color: #a3be8c;\"> tag <\/span><span style=\"color: #81a1c1;\">|<\/span> <span style=\"color: #88c0d0;\">grep<\/span><span style=\"color: #a3be8c;\"> -E \u2018.\u2019<\/span><span style=\"color: #eceff4;\">`<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<ul class=\"wp-block-list\">\r\n<li>Commit a new package.json for this component with at least:<\/li>\r\n<\/ul>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">{\r\n    name: \"@your-monorepo-domain\/category-package-name\",\r\n    version: \"1.0.0\"\r\n}<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #eceff4;\">{<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #88c0d0;\">name:<\/span> <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">@your-monorepo-domain\/category-package-name<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">,<\/span><\/span>\r\n<span class=\"line\">    <span style=\"color: #88c0d0;\">version:<\/span> <span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">1.0.0<\/span><span style=\"color: #eceff4;\">\"<\/span><\/span>\r\n<span class=\"line\"><span style=\"color: #eceff4;\">}<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<ul class=\"wp-block-list\">\r\n<li>Rename the clone folder to <code><mark class=\"has-inline-color has-black-color\" style=\"background-color: #abb8c3;\"><strong>${PACKAGE_NAME}<\/strong><\/mark><\/code><\/li>\r\n\r\n\r\n\r\n<li>Use lerna to import the mutated clone:<\/li>\r\n<\/ul>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">lerna import ${PACKAGE_NAME} --dest=\"\/monorepo\/packages\/${CATEGORY}\/${PACKAGE_NAME}\" --preserve-commit --flatten -y<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88c0d0;\">lerna<\/span> <span style=\"color: #a3be8c;\">import<\/span> <span style=\"color: #81a1c1;\">${<\/span><span style=\"color: #d8dee9;\">PACKAGE_NAME<\/span><span style=\"color: #81a1c1;\">}<\/span> <span style=\"color: #a3be8c;\">--dest=<\/span><span style=\"color: #eceff4;\">\"<\/span><span style=\"color: #a3be8c;\">\/monorepo\/packages\/<\/span><span style=\"color: #81a1c1;\">${<\/span><span style=\"color: #d8dee9;\">CATEGORY<\/span><span style=\"color: #81a1c1;\">}<\/span><span style=\"color: #a3be8c;\">\/<\/span><span style=\"color: #81a1c1;\">${<\/span><span style=\"color: #d8dee9;\">PACKAGE_NAME<\/span><span style=\"color: #81a1c1;\">}<\/span><span style=\"color: #eceff4;\">\"<\/span> <span style=\"color: #a3be8c;\">--preserve-commit<\/span> <span style=\"color: #a3be8c;\">--flatten<\/span> <span style=\"color: #a3be8c;\">-y<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<p>At this point, all the code that began in the design system repo in\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">\/src\/${PACKAGE_NAME}<\/mark><\/code>\u00a0would have been migrated to the monorepo in\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">\/packages\/${CATEGORY}\/${PACKAGE_NAME}<\/mark><\/code>.<\/p>\r\n\r\n\r\n\r\n<p id=\"bc06\">The approach described above imports only one folder at a time from a legacy repo into the monorepo. We initially tried\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">git filter-branch<\/mark><\/code>\u00a0, which can import a set of many folders at once with their git history using the<mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">\u00a0<code>--index-filter<\/code><\/mark>\u00a0flag, but this was slow, so we switched to mostly using the\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">--subdirectory-filter<\/mark><\/code>\u00a0flag and ultimately the better supported\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">git filter-repo<\/mark><\/code>\u00a0command.<\/p>\r\n\r\n\r\n\r\n<p id=\"37ed\">Once a component was published from the monorepo, we updated consumer references from the legacy design system package to the newly released monorepo package. To prevent users from trying to iterate on the legacy component, we went into the legacy repo, deleted the code, and replaced it with a reference to the new package. After enough time passed and all consumers were consuming the new package, we deleted this safety net reference too.<\/p>\r\n\r\n\r\n\r\n<h1 id=\"8b1c\" class=\"wp-block-heading\">Bootstrapping<\/h1>\r\n\r\n\r\n\r\n<p id=\"48a2\">Lerna\u2019s\u00a0<code>lerna bootstrap<\/code>\u00a0command installs all dependencies from each package\u2019s package.json file and symlinks together packages that depend on each other. It also allows individual packages to specify\u00a0<a href=\"https:\/\/nodejs.org\/es\/blog\/npm\/peer-dependencies\/\" target=\"_blank\" rel=\"noreferrer noopener\">peer dependencies<\/a>.<\/p>\r\n\r\n\r\n\r\n<p id=\"eb60\">Each of our web applications depends on third-party libraries like\u00a0<a href=\"https:\/\/reactjs.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">React<\/a>,\u00a0<a href=\"https:\/\/redux.js.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Redux<\/a>, and\u00a0<a href=\"https:\/\/styled-components.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">Styled-Components<\/a>. Some of these libraries require applications to rely on a singleton instance of the library at runtime, even though many front-end components may independently import from the library. To allow many components to consume a library but ensure that only one instance of the library is present in a web application at runtime, we specify the library as a peer dependency of packages that use it. We also specify a version of the library as a dev dependency in the package.json at the root of the monorepo; that version is installed to test against in the monorepo in CI and on dev machines.<\/p>\r\n\r\n\r\n\r\n<p id=\"d39c\">We created these guidelines for specifying package dependencies:<\/p>\r\n\r\n\r\n\r\n<ol class=\"wp-block-list\">\r\n<li>Is the dependency used only for testing, like Jest or Enzyme? If it is, it goes in a package\u2019s\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">devDependencies<\/mark><\/code>. If it\u2019s external (not written at Zocdoc), it also goes in\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">devDependencies<\/mark><\/code>\u00a0of the root\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">package.json<\/mark><\/code>.<\/li>\r\n\r\n\r\n\r\n<li>Otherwise, is the <mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">dependency<\/mark> another package in the monorepo? If yes, it should be in\u00a0<code>dependencies<\/code>\u00a0in your package\u2019s <code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">package.json<\/mark><\/code>.<\/li>\r\n\r\n\r\n\r\n<li>Otherwise, put it in\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">peerDependencies<\/mark><\/code>\u00a0in your package\u2019s\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">package.json<\/mark><\/code>\u00a0and\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">devDependencies<\/mark><\/code>\u00a0in the root\u00a0<code><code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">package.json<\/mark><\/code><\/code>.<\/li>\r\n<\/ol>\r\n\r\n\r\n\r\n<p id=\"b859\">These rules have worked well, with one major caveat. Sometimes, developers who import a package into a web application without having installed its peer dependencies are confused by failures due to the missing peer dependencies. Our guidance that external libraries should be peer dependencies makes confusion more likely, but it prevents us from installing multiple versions of the same dependencies in our consuming web apps.<\/p>\r\n\r\n\r\n\r\n<h1 id=\"d534\" class=\"wp-block-heading\">Continuous Integration (CI)<\/h1>\r\n\r\n\r\n\r\n<p id=\"e2a6\">A monorepo doesn\u2019t operate solely on developers\u2019 machines. We also needed to set up a continuous integration pipeline for it.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"5c60\" class=\"wp-block-heading\">Useful Lerna Commands<\/h2>\r\n\r\n\r\n\r\n<p id=\"9332\">Lerna provides many tools via CLI commands that are well documented in the project\u2019s\u00a0<a href=\"https:\/\/github.com\/lerna\/lerna\" target=\"_blank\" rel=\"noreferrer noopener\">Github readme<\/a>. We found these commands to be useful for migrating code to a new monorepo and building a CI\/CD pipeline:<\/p>\r\n\r\n\r\n\r\n<ul class=\"wp-block-list\">\r\n<li><strong>lerna init<\/strong>: create a new lerna project within a repo<\/li>\r\n\r\n\r\n\r\n<li><strong>lerna import<\/strong>: imports an external repository as a package<\/li>\r\n\r\n\r\n\r\n<li><strong>lerna bootstrap<\/strong>: install dependencies and symlink between packages<\/li>\r\n\r\n\r\n\r\n<li><strong>lerna ls<\/strong>: lists packages, with filter options<\/li>\r\n\r\n\r\n\r\n<li><strong>lerna exec<\/strong>: run a command in all package directories<\/li>\r\n\r\n\r\n\r\n<li><strong>lerna version<\/strong>: bump versions of packages<\/li>\r\n\r\n\r\n\r\n<li><strong>lerna publish<\/strong>: publish a new package<\/li>\r\n<\/ul>\r\n\r\n\r\n\r\n<p id=\"b9e2\">We\u2019ve already mentioned a few of these. These commands are enough to create a Lerna monorepo, import existing code, manage dependencies on external libraries and between packages, and create a CI \/ CD pipeline that runs automated tests and publishes releases.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"59ef\" class=\"wp-block-heading\">Running Tests On Changed Packages<\/h2>\r\n\r\n\r\n\r\n<p id=\"c14b\">In our legacy design system, as the number of components increased so did the number of tests. We ran every test for every component whenever any change was made to any component, even though most tests were totally unrelated to most code changes. As the number of components increased, CI runtimes and the frequency of flaky CI run failures increased in parallel. We did not want a linear increase in the number of packages in the monorepo to linearly increase our CI runtime.<\/p>\r\n\r\n\r\n\r\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:802\/1*apCMcVhhT5nrYD4hEp-a9A.jpeg\" alt=\"\" \/><\/figure>\r\n\r\n\r\n\r\n<p id=\"73e2\">The solution seemed clear: why not just run tests pertaining to the packages where code has changed? After all, if a change is made to a Button, there\u2019s no reason I should need to test the SearchBar, which doesn\u2019t use it.<\/p>\r\n\r\n\r\n\r\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:832\/1*yuPWTIETo5D74PedLQf-bg.jpeg\" alt=\"\" \/><\/figure>\r\n\r\n\r\n\r\n<p id=\"7bab\">However, any package with a changed dependency does need to be tested, even if it doesn\u2019t contain changed code itself. Timesgrid has Buttons, so if we change how a Button works, we need to run tests against Timesgrid using<\/p>\r\n\r\n\r\n\r\n<figure class=\"wp-block-image\"><img decoding=\"async\" src=\"https:\/\/miro.medium.com\/v2\/resize:fit:1022\/1*CRGTPsfCLTlhoYLeNzDoxQ.jpeg\" alt=\"\" \/><\/figure>\r\n\r\n\r\n\r\n<p id=\"8b69\">To do that, our script can use lerna to find all affected packages:<\/p>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">BRANCH_POINT=\"$(git merge-base $(git rev-parse --abbrev-ref HEAD) $(git describe origin\/master))\"\r\nchangedPackages=\"$(npx lerna ls -p --since $BRANCH_POINT --include-dependents)\"<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #d8dee9;\">BRANCH_POINT<\/span><span style=\"color: #81a1c1;\">=<\/span><span style=\"color: #eceff4;\">\"$(<\/span><span style=\"color: #88c0d0;\">git<\/span><span style=\"color: #a3be8c;\"> merge-base <\/span><span style=\"color: #eceff4;\">$(<\/span><span style=\"color: #88c0d0;\">git<\/span><span style=\"color: #a3be8c;\"> rev-parse --abbrev-ref HEAD<\/span><span style=\"color: #eceff4;\">)<\/span> <span style=\"color: #eceff4;\">$(<\/span><span style=\"color: #88c0d0;\">git<\/span><span style=\"color: #a3be8c;\"> describe origin\/master<\/span><span style=\"color: #eceff4;\">))\"<\/span><\/span>\r\n<span class=\"line\"><span style=\"color: #d8dee9;\">changedPackages<\/span><span style=\"color: #81a1c1;\">=<\/span><span style=\"color: #eceff4;\">\"$(<\/span><span style=\"color: #88c0d0;\">npx<\/span><span style=\"color: #a3be8c;\"> lerna ls -p --since <\/span><span style=\"color: #d8dee9;\">$BRANCH_POINT<\/span><span style=\"color: #a3be8c;\"> --include-dependents<\/span><span style=\"color: #eceff4;\">)\"<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<p id=\"f905\">The first command identifies where the current feature branch diverged from master, and the second command uses lerna to identify all the packages affected by code changes on the feature branch since that divergence point.<\/p>\r\n\r\n\r\n\r\n<p id=\"50dc\">Now that we know what packages should be tested, we want each of our testing frameworks to run the tests in these packages, ignoring other tests.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"4d13\" class=\"wp-block-heading\">Unit tests<\/h2>\r\n\r\n\r\n\r\n<p id=\"a694\">We use Jest as our test runner. In the monorepo, our Jest configuration is specified in a\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">jest.config.js<\/mark><\/code>\u00a0file. We use a JS config file instead of JSON to allow dynamic behavior at runtime, since we want to run a different set of tests on each commit.<\/p>\r\n\r\n\r\n\r\n<p id=\"448a\">Our\u00a0<code><code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">jest.config.js<\/mark><\/code><\/code>\u00a0file reads the changed packages that lerna identified through an environment variable and passes this into the \u201croots\u201d configuration that Jest exposes.<\/p>\r\n\r\n\r\n\r\n<p id=\"abb8\">Our Jest configuration was also affected by our use of multiple libraries. Our legacy design system imported some code from libraries in other Zocdoc repositories that published separate packages. The monorepo depended on that code as well, so we configured Jest to transpile those dependencies using babel before running tests. Jest has a\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">packages<\/mark><\/code>\u00a0option as an alternative to\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">roots<\/mark><\/code>, and in many cases, the\u00a0<code><code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">packages<\/mark><\/code><\/code>\u00a0configuration may be the best choice. In our case, though, using\u00a0<code><code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">packages<\/mark><\/code><\/code>\u00a0caused Jest to transpile the same dependencies multiple times (once for each package in which tests were run) instead of transpiling once and reusing the output for all tests, which was the behavior when we used\u00a0<code><code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">roots<\/mark><\/code><\/code>. Therefore, we used\u00a0<code><code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">roots<\/mark><\/code><\/code>\u00a0to achieve a fast runtime without having to immediately change how our other libraries were published or migrate them into the monorepo, although those are both steps we may take in the future.<\/p>\r\n\r\n\r\n\r\n<p id=\"0064\">Both Lerna and Jest can parallelize work. We invoke Jest once and let it parallelize tests instead of using\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">lerna exec<\/mark><\/code>\u00a0to invoke Jest multiple times. This was also to ensure that Jest transpiled each dependency only once.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"a6f8\" class=\"wp-block-heading\">Linting<\/h2>\r\n\r\n\r\n\r\n<p id=\"e233\">Linting is the fastest step in our testing pipeline, so we took the naive approach and sequentially invoked ESLint on each affected package.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"a505\" class=\"wp-block-heading\">Interaction tests<\/h2>\r\n\r\n\r\n\r\n<p id=\"a46e\">For interaction tests, we use\u00a0<a href=\"https:\/\/www.cypress.io\/\" target=\"_blank\" rel=\"noreferrer noopener\">Cypress<\/a>. We use a bash script to search for Cypress test files in the changed packages and then pass those into the\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">\u2014-spec<\/mark>\u00a0<\/code>flag in the\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">cypress run<\/mark><\/code>\u00a0CLI command.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"a31f\" class=\"wp-block-heading\">Screenshot tests<\/h2>\r\n\r\n\r\n\r\n<p id=\"2476\">Finally, we use\u00a0<a href=\"https:\/\/storybook.js.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">Storybook<\/a>\u00a0with\u00a0<a href=\"https:\/\/percy.io\/\" target=\"_blank\" rel=\"noreferrer noopener\">Percy<\/a>\u00a0for screenshot testing. This presented an interesting challenge. The baseline for Percy screenshots is the last run on master. On pull requests, we only want to evaluate screenshots in packages that have changed, but we must always establish a complete baseline. We briefly entertained the idea of creating a new Percy project for each package, but decided that wasn\u2019t a scalable solution. We decided to use a single Percy project, and run partial builds on feature branches.<\/p>\r\n\r\n\r\n\r\n<p id=\"9028\">To ensure that we generate a complete baseline, all screenshots are generated on the master branch. There\u2019s no manual approval of diffs on master, so we can publish our packages without waiting for Percy to finish.<\/p>\r\n\r\n\r\n\r\n<p id=\"cebd\">When we invoke its\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">build-storybook<\/mark><\/code>\u00a0CLI command, Storybook runs our\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">.storybook\/config.js<\/mark><\/code>\u00a0file to load stories. We dynamically invoke the Webpack\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">require.context<\/mark><\/code>\u00a0command to load the appropriate stories using the value of\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">STORYBOOK_SOURCE_DIR<\/mark><\/code>. Because calls to this Webpack API are meant to be statically analyzable, this was difficult to get working.<\/p>\r\n\r\n\r\n\r\n<p id=\"d57a\">Finally, we invoke\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">percy-storybook<\/mark><\/code>\u00a0to upload whichever stories were built to Percy for screenshot testing by comparison against the last baseline.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"1e2e\" class=\"wp-block-heading\">Dependency Validation<\/h2>\r\n\r\n\r\n\r\n<p id=\"1ea5\">Lerna packages each have their own\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">package.json<\/mark><\/code>, which is what allows each component to version independently. Unfortunately, this also means that each package must explicitly specify the other monorepo packages it imports from. If one of its dependencies is not specified, the package will not work when it is installed as a dependency in a web application if the application does not already have a version of that dependency, and it may behave unexpectedly if the application has an incompatible version of the dependency.<\/p>\r\n\r\n\r\n\r\n<p id=\"e310\">To ensure that developers specify all dependencies, we run\u00a0<a href=\"https:\/\/www.npmjs.com\/package\/depcheck\" target=\"_blank\" rel=\"noreferrer noopener\">depcheck<\/a>\u00a0in our linting step in CI. If a package imports from any dependency not referenced in the package.json, depcheck will identify this and report a failure. We also wrote a script which developers could run to automatically update the appropriate package.json files to fix any issues that depcheck identifies.<\/p>\r\n\r\n\r\n\r\n<h1 id=\"afc1\" class=\"wp-block-heading\">Publishing Updated Package Versions<\/h1>\r\n\r\n\r\n\r\n<p id=\"615e\">We publish new releases by running the following commands:<\/p>\r\n\r\n\r\n\r\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" style=\"font-size: .875rem; font-family: Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace; line-height: 1.25rem; --cbp-tab-width: 2; tab-size: var(--cbp-tab-width, 2);\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\">\r\n<pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" readonly=\"readonly\" aria-hidden=\"true\">npx lerna version -y --conventional-commit --create-release github\r\nnpx lerna publish -y from-package<\/textarea><\/pre>\r\n<pre class=\"shiki nord\" style=\"background-color: #2e3440ff;\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #88c0d0;\">npx<\/span> <span style=\"color: #a3be8c;\">lerna<\/span> <span style=\"color: #a3be8c;\">version<\/span> <span style=\"color: #a3be8c;\">-y<\/span> <span style=\"color: #a3be8c;\">--conventional-commit<\/span> <span style=\"color: #a3be8c;\">--create-release<\/span> <span style=\"color: #a3be8c;\">github<\/span><\/span>\r\n<span class=\"line\"><span style=\"color: #88c0d0;\">npx<\/span> <span style=\"color: #a3be8c;\">lerna<\/span> <span style=\"color: #a3be8c;\">publish<\/span> <span style=\"color: #a3be8c;\">-y<\/span> <span style=\"color: #a3be8c;\">from-package<\/span><\/span><\/code><\/pre>\r\n<\/div>\r\n\r\n\r\n\r\n<p id=\"cdae\">We use semantic versioning at Zocdoc. The\u00a0<code>\u2014-conventional-commits<\/code>\u00a0flag causes lerna to analyze commit messages and choose the version increment (major\/minor\/patch) that seems appropriate.<\/p>\r\n\r\n\r\n\r\n<p id=\"0835\">At this point, lerna has published a new package, whose release version has been incremented based on its current version on the master branch.\u00a0We now have a working process to publish new packages and ensure that future changes will be versioned correctly. These steps aren\u2019t difficult to learn, but let\u2019s face it \u2014 expecting every other developer to remember to do this whenever they make a change, particularly if they\u2019re in a rush or they don\u2019t touch the monorepo often, is probably asking for too much. Our solution was to automate the process.<\/p>\r\n\r\n\r\n\r\n<p id=\"b5f2\">Automating it sounds great, but\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">lerna version<\/mark><\/code>\u00a0and\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">lerna publish<\/mark><\/code>\u00a0are both interactive, and need user input. You can see we\u2019ve included an argument, -y, which indicates to lerna that it should select the default options for us if we haven\u2019t specified something already, like\u00a0<code><mark class=\"has-inline-color\" style=\"background-color: #abb8c3;\">--conventional-commits<\/mark><\/code>.<\/p>\r\n\r\n\r\n\r\n<h1 id=\"9e42\" class=\"wp-block-heading\">What we Lerna\u2019d<\/h1>\r\n\r\n\r\n\r\n<h2 id=\"213f\" class=\"wp-block-heading\">Multiple Migrations Are Harder Than One<\/h2>\r\n\r\n\r\n\r\n<p id=\"d4ad\">Our legacy design system was inconsistent; it used 3 different frameworks to render screenshots and 2 types of interaction tests. We only added support to the monorepo CI\/CD pipeline for one framework for each purpose. As a result, migrating components required us to rewrite many tests.<\/p>\r\n\r\n\r\n\r\n<p id=\"eeb2\">This strategy had some benefits. Integrating lerna with each testing framework to only run tests affected by changed code was a large effort. Limiting the monorepo to support a minimal set of preferred frameworks helped us quickly complete the setup. Also, individual developers who built new components from scratch in the monorepo did not need to learn several frameworks, and could focus on mastering the few supported tools.<\/p>\r\n\r\n\r\n\r\n<p id=\"a67c\">Over time, however, the cost of this choice became clear. Developers who migrated components to the monorepo in preparation for other work found that the migration took much longer than expected. A significant reason for that was our choice to restrict which CI frameworks to support; rewriting tests took much more time than importing git history or updating dependent apps. As a result, we did most of the migration as a dedicated project. Until we could allocate time to complete that, we were in a hybrid state with some components in the old design system and others in the monorepo, which caused significant growing pains.<\/p>\r\n\r\n\r\n\r\n<p id=\"5b95\">If you do consider moving to a monorepo, we recommended moving your code over as-is, warts and all. Once in the monorepo, the independent versioning of packages makes it easy to progressively refactor code to remove undesired patterns without blocking the migration effort. If entire components are deprecated, however, the migration is a great opportunity to leave them behind. Our legacy design system had close to 100 independent units of code, and 10\u201315% had been replaced with better alternatives and were no longer recommended for use. Because we left this code in the legacy design system, developers are less likely to discover or use it now. If we need to use any of this code again someday, we can always migrate it then.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"c882\" class=\"wp-block-heading\">We\u2019re Happy With The Monorepo<\/h2>\r\n\r\n\r\n\r\n<p id=\"2261\">After several months of using the monorepo and the legacy design system in parallel, we saw the benefits of the monorepo outweighing its costs, so we committed to a full migration. This has already paid off.<\/p>\r\n\r\n\r\n\r\n<p id=\"67e1\">To give an example of where the monorepo really shines, let\u2019s talk about the\u00a0Zocdoc Video Service\u00a0(ZVS), which we built earlier this year. The service\u2019s code was split up into a few different webapps; a webapp that handles the provider\u2019s login flow, and webapps that handle the patient\u2019s login flow. ZVS was being built rapidly, and changes were constantly being made. The core logic and many of the components used during the video session needed to be shared between the provider and the patient, so using a shared library made a lot of sense. If we did this using our legacy shared component system, we might have run into a lot of problems, blocking or being blocked by other developers for unrelated changes. The monorepo helped us write the shared code we needed, and we managed to get the service up and running in a few weeks.<\/p>\r\n\r\n\r\n\r\n<p><em>\u201cWe couldn\u2019t have done ZVS in the time it took us without the help of the monorepo.\u201d \u2014 Zack Banton, Team Lead<\/em><\/p>\r\n\r\n\r\n\r\n<p>I hope we\u2019ve shown you that adopting a monorepo for your front-end components can make your developers more productive than a classic component library. However, it is important to note that we\u2019re not done. The monorepo\u2019s drawbacks \u2014 such as managing many more package.json files \u2014 caused fewer productivity issues than our old design system, but these new drawbacks are still frustrating. We embarked on this project because we saw consistent issues with the way our old system worked, and found that our monorepo could in fact help solve them. Our job now is to look at our new system and try to figure out what can make us more efficient at doing what we love to do \u2014 deliver high quality products and features to patients.<\/p>\r\n\r\n\r\n\r\n<h1 id=\"2ce3\" class=\"wp-block-heading\">About the Authors<\/h1>\r\n\r\n\r\n\r\n<h2 id=\"a5fa\" class=\"wp-block-heading\">Anand Sundaram<\/h2>\r\n\r\n\r\n\r\n<p id=\"6d26\"><strong>Anand Sundaram<\/strong>\u00a0is a Senior Software Engineer at Zocdoc. When he\u2019s not bringing to life Zocdoc designers\u2019 beautiful visions for improving the experience of booking doctors\u2019 appointments, he\u2019s probably reading nonfiction or playing a real-time strategy video game.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"69ff\" class=\"wp-block-heading\">Gil Varod<\/h2>\r\n\r\n\r\n\r\n<p id=\"65c8\"><strong>Gil Varod<\/strong>\u00a0is a former Principal Front-End Engineer at Zocdoc, meaning somehow there are now only two Gil\u2019s at the company. What a pity.<\/p>\r\n\r\n\r\n\r\n<h2 id=\"fd7d\" class=\"wp-block-heading\">Tim Chu<\/h2>\r\n\r\n\r\n\r\n<p id=\"ab30\">Tim Chu is a Senior Software Engineer at Zocdoc. He\u2019s spent most of his tenure working to improve the patient experience, and is currently on rotation with the Android app team, working to improve the patient experience\u2026 but on Android!<\/p>\r\n\r\n\r\n\r\n<p><strong>Zocdoc is hiring!\u00a0<\/strong>Visit our\u00a0<a href=\"https:\/\/www.zocdoc.com\/about\/careers-list\/\" target=\"_blank\" rel=\"noreferrer noopener\"><strong>Careers page<\/strong><\/a>\u00a0for open positions!<\/p>\r\n","protected":false},"excerpt":{"rendered":"<p>In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an existing codebase into it and the lessons we learned. Spoiler: we like it, but it required some work to get to that point. The Pilot Project The [&hellip;]<\/p>\n","protected":false},"author":120,"featured_media":20867,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1],"tags":[],"class_list":["post-20805","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-general"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v26.7 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna - General<\/title>\n<meta name=\"description\" content=\"In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna - General\" \/>\n<meta property=\"og:description\" content=\"In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an\" \/>\n<meta property=\"og:url\" content=\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\" \/>\n<meta property=\"og:site_name\" content=\"Tech Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-08-07T13:32:01+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2025-12-22T19:42:51+00:00\" \/>\n<meta name=\"author\" content=\"Zocdoc\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Zocdoc\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"15 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\"},\"author\":{\"name\":\"Zocdoc\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/#\/schema\/person\/e2613adb0dc1626c91325a0c2fbd8d53\"},\"headline\":\"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna\",\"datePublished\":\"2025-08-07T13:32:01+00:00\",\"dateModified\":\"2025-12-22T19:42:51+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\"},\"wordCount\":3111,\"image\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg\",\"articleSection\":[\"General\"],\"inLanguage\":\"en-US\"},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\",\"url\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\",\"name\":\"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna - General\",\"isPartOf\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg\",\"datePublished\":\"2025-08-07T13:32:01+00:00\",\"dateModified\":\"2025-12-22T19:42:51+00:00\",\"author\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/#\/schema\/person\/e2613adb0dc1626c91325a0c2fbd8d53\"},\"description\":\"In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an\",\"breadcrumb\":{\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage\",\"url\":\"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg\",\"contentUrl\":\"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg\",\"width\":1080,\"height\":720},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/zocdoctech.zocdoc.com\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"General\",\"item\":\"https:\/\/zocdoctech.zocdoc.com\/category\/general\/\"},{\"@type\":\"ListItem\",\"position\":3,\"name\":\"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/#website\",\"url\":\"https:\/\/zocdoctech.zocdoc.com\/\",\"name\":\"Tech Blog\",\"description\":\"Zocdoc\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/zocdoctech.zocdoc.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/zocdoctech.zocdoc.com\/#\/schema\/person\/e2613adb0dc1626c91325a0c2fbd8d53\",\"name\":\"Zocdoc\",\"url\":\"https:\/\/www.zocdoc.com\/techblog\/author\/zocdoc\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna - General","description":"In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/","og_locale":"en_US","og_type":"article","og_title":"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna - General","og_description":"In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an","og_url":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/","og_site_name":"Tech Blog","article_published_time":"2025-08-07T13:32:01+00:00","article_modified_time":"2025-12-22T19:42:51+00:00","author":"Zocdoc","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Zocdoc","Est. reading time":"15 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#article","isPartOf":{"@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/"},"author":{"name":"Zocdoc","@id":"https:\/\/zocdoctech.zocdoc.com\/#\/schema\/person\/e2613adb0dc1626c91325a0c2fbd8d53"},"headline":"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna","datePublished":"2025-08-07T13:32:01+00:00","dateModified":"2025-12-22T19:42:51+00:00","mainEntityOfPage":{"@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/"},"wordCount":3111,"image":{"@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage"},"thumbnailUrl":"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg","articleSection":["General"],"inLanguage":"en-US"},{"@type":"WebPage","@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/","url":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/","name":"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna - General","isPartOf":{"@id":"https:\/\/zocdoctech.zocdoc.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage"},"image":{"@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage"},"thumbnailUrl":"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg","datePublished":"2025-08-07T13:32:01+00:00","dateModified":"2025-12-22T19:42:51+00:00","author":{"@id":"https:\/\/zocdoctech.zocdoc.com\/#\/schema\/person\/e2613adb0dc1626c91325a0c2fbd8d53"},"description":"In part 1, we described the problems we set out to solve with a lerna monorepo. Part 2 describes how we created an actual monorepo and migrated an","breadcrumb":{"@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#primaryimage","url":"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg","contentUrl":"https:\/\/www.zocdoc.com\/techblog\/wp-content\/uploads\/2025\/12\/Tech-Blog-post_06.svg","width":1080,"height":720},{"@type":"BreadcrumbList","@id":"https:\/\/zocdoctech.zocdoc.com\/lerna-you-a-monorepo-the-nuts-and-bolts-of-building-a-ci-pipeline-with-lerna\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/zocdoctech.zocdoc.com\/"},{"@type":"ListItem","position":2,"name":"General","item":"https:\/\/zocdoctech.zocdoc.com\/category\/general\/"},{"@type":"ListItem","position":3,"name":"Lerna You a Monorepo: The Nuts and Bolts of Building a CI Pipeline with Lerna"}]},{"@type":"WebSite","@id":"https:\/\/zocdoctech.zocdoc.com\/#website","url":"https:\/\/zocdoctech.zocdoc.com\/","name":"Tech Blog","description":"Zocdoc","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/zocdoctech.zocdoc.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/zocdoctech.zocdoc.com\/#\/schema\/person\/e2613adb0dc1626c91325a0c2fbd8d53","name":"Zocdoc","url":"https:\/\/www.zocdoc.com\/techblog\/author\/zocdoc\/"}]}},"_links":{"self":[{"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/posts\/20805","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/users\/120"}],"replies":[{"embeddable":true,"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/comments?post=20805"}],"version-history":[{"count":0,"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/posts\/20805\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/media\/20867"}],"wp:attachment":[{"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/media?parent=20805"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/categories?post=20805"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/zocdoctech.zocdoc.com\/wp-json\/wp\/v2\/tags?post=20805"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}