<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Carlos Santillana</title>
    <description>The latest articles on DEV Community by Carlos Santillana (@carlossantillana).</description>
    <link>https://dev.to/carlossantillana</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1055782%2F5e7cb73e-30e0-486a-811d-624fa78bfc72.jpeg</url>
      <title>DEV Community: Carlos Santillana</title>
      <link>https://dev.to/carlossantillana</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/carlossantillana"/>
    <language>en</language>
    <item>
      <title>How to open source code from a private monorepo</title>
      <dc:creator>Carlos Santillana</dc:creator>
      <pubDate>Wed, 03 May 2023 16:13:26 +0000</pubDate>
      <link>https://dev.to/carlossantillana/how-to-open-source-code-from-a-private-monorepo-262o</link>
      <guid>https://dev.to/carlossantillana/how-to-open-source-code-from-a-private-monorepo-262o</guid>
      <description>&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;p&gt;This post is about open-sourcing modules from a private monorepo. It’s a hard problem with a decent number of gotchas. We want to share our experience wiring this up to help others who venture down the same path.&lt;/p&gt;

&lt;h2&gt;
  
  
  How we structure our monorepo
&lt;/h2&gt;

&lt;p&gt;A bit of context about Dopt before we dive in: Dopt is a web application for designing user*-*state machines on a canvas, paired with APIs and SDKs for utilizing those machines at runtime. The idea is that you can instantiate these machines per user of your product. We let you progress the user through the machine and handle the persistence of the user’s state in each machine for you. You can iterate on and version your machines, and we’ll handle migrating your users across machines’ versions. This should be deep enough to contextualize any Dopt-specific bits in this article (but if you’re interested in diving deeper, you can check out our &lt;a href="https://docs.dopt.com/what-is-dopt/"&gt;docs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;We develop in a monorepo at Dopt. Our monorepo is home to the apps that live on &lt;a href="http://dopt.com"&gt;dopt.com&lt;/a&gt; and the packages/services they share. It’s the source of truth for all things Dopt.&lt;/p&gt;

&lt;p&gt;We use &lt;a href="https://pnpm.io/"&gt;pnpm&lt;/a&gt; at Dopt, and we utilize &lt;a href="https://pnpm.io/workspaces"&gt;pnpm workspaces&lt;/a&gt;. The pnpm-workspace.yaml defines the root of our workspace and allows us to define/constrain where packages can live in the monorepo. It looks something like this&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;packages:
  - "apps/**/*"
  - "packages/**/*"
  - "apis/**/*"
  - "services/**/*"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our monorepo's structure is app-centric in nature, e.g., we are building products as opposed to reusable libraries. This is reflected in our folder structure&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── apps          (apps that live on dopt.com)
├── services      (internal services used by app(s) and API(s))
├── apis          (public APIs hosted on dopt subdomains)
└── packages      (packages shared by apps, services, and APIs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Children directories in these folders correspond to package scopes e.g.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── apps
│   ├── @app
│   ├── @www
│   ├── @blog
│   └── @docs
├── services
│   ├── @gateway
│   └── @transitions
├── apis
│   ├── @users
│   └── @blocks
└── packages
    ├── @bodo
    └── @dopt

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and their children correspond to packages themselves&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;├── apps
│   ├── @app
│   ├─────── client
│   ├─────── server
│   ├─────── database
│   ├── @www
│   ├─────── app
│   ├── @blog
│   ├─────── app
│   ├── @docs
│   └─────── app
├── services
│   ├── @gateway
│   ├─────── service
│   ├─────── definition
│   ├── @transitions
│   ├─────── service
│   └─────── definition
├── apis
│   ├── @users
│   ├─────── service
│   ├─────── definition
│   ├── @blocks
│   ├─────── service
│   └─────── definition
└── packages
    ├── @bodo
    ├─────── alert
    ├─────── box
    ├─────── ...
    ├── @dopt
    ├─────── app-client-sdk
    ├─────── app-middleware
    ├─────── block-client-sdk
    └─────── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scopes in the &lt;code&gt;app&lt;/code&gt; directory correspond directly to apps on subdomains of &lt;a href="http://dopt.com/"&gt;dopt.com&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;└─── apps
   ├── @app         (app.dopt.com)
   ├── @www         (www.dopt.com)
   ├── @blog        (blog.dopt.com)
   └── @docs        (docs.dopt.com)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scopes in the &lt;code&gt;apis&lt;/code&gt; directory correspond Public APIs hosted on subdomains of dopt&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;└─── apis
   ├── @blocks        (blocks.dopt.com)
   └── @users         (users.dopt.com)

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Problem: open sourcing from a private monorepo means filtering and syncing commits
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;apis&lt;/code&gt; directory is home to services that power our public APIs. These are the APIs folks use to drive our user-state machines.&lt;/p&gt;

&lt;p&gt;There are really three ways to use said APIs.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Directly (e.g. &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, or some other language-specific tool for making HTTP requests)&lt;/li&gt;
&lt;li&gt;API Clients (i.e., generated language-specific abstractions for talking to our REST API)&lt;/li&gt;
&lt;li&gt;SDKs (i.e., framework-specific libraries that go beyond basic REST requests, e.g., they create socket connections).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For API clients and SDKs, we need to get that code into folks’ hands. That means publishing already built versions of those packages to language-specific registries (e.g., npm, pip, etc.) for usage in their codebase and open-sourcing the raw, uncompiled source so folks can peruse, build, debug, contribute, and file issues.&lt;/p&gt;

&lt;p&gt;Packages in a monorepo can depend on other packages in a monorepo. This is the benefit of a monorepo, e.g., you can easily create reusable/shareable code by extracting things out into their own modules.&lt;/p&gt;

&lt;p&gt;Open-sourcing a package from a monorepo means open-sourcing everything that the package depends on as well. This concept is likely intuitive, but it’s worth pointing out a few implications of this that are perhaps less intuitive.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The open-source repository will itself need to be a monorepo&lt;/li&gt;
&lt;li&gt;Any change to an open-source package OR one of its dependent packages (directly or indirectly) requires code to be synced.&lt;/li&gt;
&lt;li&gt;Uni-directional (e.g., private to public) syncing is straightforward. Bi-directional syncing (e.g, private to public and public to private) adds some cognitive overhead.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I started to talk about “syncing” above. To expand a bit, given that we use git as our version control system, we will be syncing commits.&lt;/p&gt;

&lt;p&gt;This is the problem within the problem, e.g., &lt;strong&gt;how do you safely sync commits between two repositories?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rest of this post will explore the required steps for us to make this happen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Coming up with a convention for configuring and statically identifying open-source packages&lt;/li&gt;
&lt;li&gt;Identification of an open-source package’s dependent packages (direct and indirect)&lt;/li&gt;
&lt;li&gt;Syncing commits between repositories&lt;/li&gt;
&lt;li&gt;Automating the process&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Marking packages to be open-sourced
&lt;/h2&gt;

&lt;p&gt;Given that we use pnpm’s workspace concept, our monorepo’s packages are all node modules, independent of whether their src code is written in JavaScript. Said another way, every package in our monorepo has a &lt;code&gt;package.json&lt;/code&gt;, the dependencies of which define our workspace's topology.&lt;/p&gt;

&lt;p&gt;The schema/type definition for a package.json file is &lt;a href="https://github.com/unjs/pkg-types/blob/main/src/types/packagejson.ts#L158"&gt;quite flexible&lt;/a&gt;, allowing us to add arbitrary fields to the top-level json.&lt;/p&gt;

&lt;p&gt;To mark packages as open source we introduced an optional boolean field named &lt;code&gt;openSource&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="nx"&gt;DoptPackageJson&lt;/span&gt; &lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;PackageJson&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;openSource&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see the usage of this field in our &lt;a href="https://www.npmjs.com/package/@dopt/blocks-javascript-client"&gt;JavaScript Block API client&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
 &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@dopt/blocks-javascript-client"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A generated JavaScript API client for Dopt's blocks API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"openSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;lt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;field&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;configuring&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;package&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;open&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;source&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Having marked packages, we need a way to identify them programmatically. Whether using pnpm, yarn, or npm to configure workspaces, they all offer some tooling, albeit primitive, for listing packages in the workspace, etc.&lt;/p&gt;

&lt;p&gt;We ended up wrapping pnpm’s functionality in a package called &lt;a href="https://github.com/dopt/odopt/tree/main/packages/%40dopt/wutils"&gt;@dopt/wutils&lt;/a&gt;, which is conveniently open-sourced from our private monorepo. The code to locate open-source packages in the monorepo looks something like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPackageLocationsSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dopt/wutils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openSourcePackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getPackageLocationsSync&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;workspacePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cwd&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;workspacePath&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/package.json`&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;pkg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;openSource&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Static analysis: filtering to open source packages and their dependents
&lt;/h2&gt;

&lt;p&gt;Now that we’ve marked open source packages as such and came up with a way of filtering down the monorepo to that set of packages, we should be able to script a solution for identifying a package's dependent packages (both direct and indirect).&lt;/p&gt;

&lt;p&gt;We have two options for tools that can help us with this part, &lt;a href="https://pnpm.io/"&gt;pnpm&lt;/a&gt; or &lt;a href="https://turbo.build/repo"&gt;turborepo&lt;/a&gt;. We use pnpm as our package manager and turbo as our monorepo build tool. Since both solve monorepo-related problems, they both offer tools for filtering the monorepo and doing static analysis related to dependencies. See links below.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pnpm.io/filtering"&gt;pnpm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://turbo.build/repo/docs/core-concepts/monorepos/filtering"&gt;turbo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Turbo's Filter API design is heavily inspired by &lt;a href="https://pnpm.io/filtering"&gt;pnpm&lt;/a&gt;’s. In this case we ended up using pnpm, but either would have worked just fine.&lt;/p&gt;

&lt;p&gt;Attached below is an extension of the example above that illustrates how to use pnpm’s &lt;code&gt;...&lt;/code&gt; filtering syntax to select a package and its dependencies (direct and non-direct).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Use the function above to filter workspace packages to open source packages&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openSourcePackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;openSourcePackages&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Use the open source packages to template pnpm filtering&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templatedPnpmFilter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`pnpm ls -r --depth -1 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;openSourcePackages&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;` --filter &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;... `&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt; --json`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Execute the templated pnpm command&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;targetPackages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templatedPnpmFilter&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Git 🪄: filtering and syncing commits
&lt;/h2&gt;

&lt;p&gt;We have the open source packages and their dependent packages. The next step is to identify changes that impacted those packages. In git terms, “changes” are going to be git commits. So we are going to want to operate on the git commit history.&lt;/p&gt;

&lt;p&gt;Operations like this are aptly termed “history rewrites” and there are a few tools that are meant to help with this, given how complicated and dangerous it can become.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/docs/git-filter-branch"&gt;git filter-branch&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/newren/git-filter-repo"&gt;git-filter-repo&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The former &lt;a href="https://git-scm.com/docs/git-filter-branch#_warning"&gt;warns against its usage&lt;/a&gt; and suggests using the latter instead, which is exactly what we did.&lt;/p&gt;

&lt;p&gt;Our goal with this tool will be to extract the git history for each path associated with the open source packages and their dependents.&lt;/p&gt;

&lt;p&gt;Building from the code example in the previous section, we can leverage the collection of &lt;code&gt;targetPackages&lt;/code&gt; (which contains the path to that package) to form the &lt;code&gt;git filter-repo&lt;/code&gt; query.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;getPackagesSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@dopt/wutils&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;PackageDef&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Get all the packages in the monorepo&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PackageDef&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;getPackagesSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Filter all packages by target packages and form query&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;templatedGitFilterRepoQuery&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`git filter-repo &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;packages&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;targetPackages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(({&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`--path &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Execute the query, rewriting your git history on the&lt;/span&gt;
&lt;span class="c1"&gt;// current branch to only the desired commits.&lt;/span&gt;
&lt;span class="nx"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templatedGitFilterRepoQuery&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nx"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This all happened on a clean clone of our private monorepo and we now have a history that has been filtered down to what I’ll call open source commits. How do we sync these commits over to our other repository?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a fresh clone of the private monorepo&lt;/span&gt;
git clone git@github.com:&amp;lt;org&amp;gt;/&amp;lt;private_monorepo&amp;gt;.git
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$private_monorepo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Run the git commit filter script&lt;/span&gt;
node ./filter-to-open-source-commits.mjs

&lt;span class="c"&gt;# Move up a directory so the repose are siblings&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Create a fresh clone of the open source monorepo&lt;/span&gt;
git clone git@github.com:&amp;lt;org&amp;gt;/&amp;lt;opensource_monorepo&amp;gt;.git
&lt;span class="nb"&gt;cd&lt;/span&gt; &lt;span class="nv"&gt;$opensource_monorepo&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; sync-commits-from-private-monorepo&lt;span class="p"&gt;;&lt;/span&gt;
git remote add &lt;span class="nb"&gt;source&lt;/span&gt; ../private_monorepo&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c"&gt;# Replace "main" with the branch name in the private monorepo&lt;/span&gt;
git pull &lt;span class="nb"&gt;source &lt;/span&gt;main &lt;span class="nt"&gt;--allow-unrelated-histories&lt;/span&gt; &lt;span class="nt"&gt;--strategy-option&lt;/span&gt; theirs &lt;span class="nt"&gt;--no-edit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c"&gt;# Finally, set your remote and push!&lt;/span&gt;
git remote set-url &lt;span class="nt"&gt;--push&lt;/span&gt;  origin https://github.com/&amp;lt;org&amp;gt;/&amp;lt;opensource_monorepo&amp;gt;.git
git push &lt;span class="nt"&gt;--set-upstream&lt;/span&gt; origin sync-commits-from-private-monorepo&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Automating this workflow
&lt;/h2&gt;

&lt;p&gt;With everything working manually, our next goal is to “set and forget” this workflow/pipeline. We solved this in CI/CD, creating a GitHub action that is invoked on any merge to the main branch of our private monorepo.&lt;/p&gt;

&lt;p&gt;Check it out the code for the GitHub Action below. Additionally, here’s a recent &lt;a href="https://github.com/dopt/odopt/pull/613"&gt;link&lt;/a&gt; to one of the pull requests this action created in the open source repository.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Example CI/CD&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sync OSS Packages&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;merge&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out the monorepo&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dopt/monorepo&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./monorepo&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check out the open monorepo&lt;/span&gt;
      &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dopt/odopt&lt;/span&gt;
        &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./odopt&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Filter the open-source packages&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;monorepo/&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node ./filter.js;&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sync commits&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;odopt/&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;git checkout -b sync/${{ github.run_id }};&lt;/span&gt;
        &lt;span class="s"&gt;git remote add source ../monorepo;&lt;/span&gt;
        &lt;span class="s"&gt;git pull source main --allow-unrelated-histories --strategy-option theirs --no-edit;&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Push commits&lt;/span&gt;
      &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;odopt/&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
        &lt;span class="s"&gt;git remote set-url --push  origin https://github.com/dopt/odopt.git&lt;/span&gt;
        &lt;span class="s"&gt;git push --set-upstream origin sync/${{ github.run_id }};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Learnings
&lt;/h2&gt;

&lt;p&gt;First and foremost, we learned that you can have your cake and eat it too, i.e., enjoy all the benefits of a monorepo while still open-sourcing parts of it.&lt;/p&gt;

&lt;p&gt;It definitely requires a decent bit of setup and cognitive overhead to understand the problems you need to solve, but by following a similar pattern you can also share pieces of your monorepo with the community with little continued maintenance. We’ve not touched this pipeline since putting it in place!&lt;/p&gt;

&lt;p&gt;Lastly, our use case was primarily open-source API clients and SDKs, but having this pipeline has promoted a healthy habit of open-sourcing packages/modules that we think the community would benefit from e.g.,&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;please (&lt;a href="https://github.com/dopt/odopt/tree/main/packages/%40dopt/please"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@dopt/please?activeTab=readme"&gt;npm&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;mercator (&lt;a href="https://github.com/dopt/odopt/tree/main/packages/%40dopt/mercator"&gt;GitHub&lt;/a&gt;, &lt;a href="https://www.npmjs.com/package/@dopt/mercator"&gt;npm&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;wutils (&lt;a href="https://github.com/dopt/odopt/tree/main/packages/%40dopt/wutils"&gt;GitHub&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out &lt;a href="https://github.com/dopt/odopt"&gt;odopt&lt;/a&gt; to see what else we are building and sharing!&lt;/p&gt;

</description>
      <category>git</category>
      <category>cicd</category>
      <category>devops</category>
      <category>development</category>
    </item>
  </channel>
</rss>
