<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" version="2.0">
    <channel>
        
        <title>
            <![CDATA[ monorepo - freeCodeCamp.org ]]>
        </title>
        <description>
            <![CDATA[ Browse thousands of programming tutorials written by experts. Learn Web Development, Data Science, DevOps, Security, and get developer career advice. ]]>
        </description>
        <link>https://www.freecodecamp.org/news/</link>
        <image>
            <url>https://cdn.freecodecamp.org/universal/favicons/favicon.png</url>
            <title>
                <![CDATA[ monorepo - freeCodeCamp.org ]]>
            </title>
            <link>https://www.freecodecamp.org/news/</link>
        </image>
        <generator>Eleventy</generator>
        <lastBuildDate>Wed, 06 May 2026 22:13:41 +0000</lastBuildDate>
        <atom:link href="https://www.freecodecamp.org/news/tag/monorepo/rss.xml" rel="self" type="application/rss+xml" />
        <ttl>60</ttl>
        
            <item>
                <title>
                    <![CDATA[ How to Build Reusable Architecture for Large Next.js Applications ]]>
                </title>
                <description>
                    <![CDATA[ Every Next.js project starts the same way: you run npx create-next-app, write a few pages, maybe add an API route or two, and things feel clean. Then the project grows. Features multiply. A second app ]]>
                </description>
                <link>https://www.freecodecamp.org/news/reusable-architecture-for-large-nextjs-applications/</link>
                <guid isPermaLink="false">69d00029e466e2b762517489</guid>
                
                    <category>
                        <![CDATA[ Next.js ]]>
                    </category>
                
                    <category>
                        <![CDATA[ architecture ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monorepo ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ Abisoye Alli-Balogun ]]>
                </dc:creator>
                <pubDate>Fri, 03 Apr 2026 18:00:09 +0000</pubDate>
                <media:content url="https://cdn.hashnode.com/uploads/covers/5e1e335a7a1d3fcc59028c64/0a713a19-a418-4954-bfca-9b8bc1e77a03.png" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>Every Next.js project starts the same way: you run <code>npx create-next-app</code>, write a few pages, maybe add an API route or two, and things feel clean.</p>
<p>Then the project grows. Features multiply. A second app appears, maybe a separate admin dashboard, a marketing site, or a mobile-facing API. Suddenly, you're copying components between repos, duplicating business logic, arguing over where auth utilities belong, and asking yourself: <em>where did it all go wrong?</em></p>
<p>The answer is almost always architecture, or rather, the absence of one. Not the kind that lives in a Notion doc but the kind baked into your folder structure, your module boundaries, and the tools you reach for at the start of a project (not after it's already broken).</p>
<p>This article is a practical guide to building layered, reusable architecture in Next.js.</p>
<p>You'll learn about the App Router's colocation model, building scalable folder structures around features, sharing logic across apps with Turborepo, drawing clean data-fetching boundaries using Server Components, designing a testing strategy that matches your layer structure, and wiring up a CI/CD pipeline that only builds and tests what actually changed.</p>
<p>By the end, you'll have a blueprint you can actually use, not just admire.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-the-core-problem-coupling-without-intention">The Core Problem: Coupling Without Intention</a></p>
</li>
<li><p><a href="#heading-layer-1-the-app-router-and-colocation">Layer 1: The App Router and Colocation</a></p>
</li>
<li><p><a href="#heading-layer-2-feature-based-folder-structure">Layer 2: Feature-Based Folder Structure</a></p>
</li>
<li><p><a href="#heading-layer-3-monorepo-with-turborepo-sharing-logic-across-apps">Layer 3: Monorepo with Turborepo (Sharing Logic Across Apps)</a></p>
</li>
<li><p><a href="#heading-layer-4-server-components-and-data-fetching-boundaries">Layer 4: Server Components and Data-Fetching Boundaries</a></p>
</li>
<li><p><a href="#heading-layer-5-testing-strategy-for-a-layered-codebase">Layer 5: Testing Strategy for a Layered Codebase</a></p>
</li>
<li><p><a href="#heading-layer-6-cicd-with-turborepo">Layer 6: CI/CD with Turborepo</a></p>
</li>
<li><p><a href="#heading-putting-it-all-together-the-full-blueprint">Putting It All Together: The Full Blueprint</a></p>
</li>
<li><p><a href="#heading-common-pitfalls-and-how-to-avoid-them">Common Pitfalls and How to Avoid Them</a></p>
</li>
<li><p><a href="#heading-final-thoughts">Final Thoughts</a></p>
</li>
</ul>
<h2 id="heading-the-core-problem-coupling-without-intention">The Core Problem: Coupling Without Intention</h2>
<p>When a component reaches directly into a global store, when a page imports a utility from three directories away, when your auth logic is spread across <code>/lib</code>, <code>/helpers</code>, and <code>/utils</code> with no clear owner, every file knows too much about every other file.</p>
<p>The app still runs. But now changing one thing breaks three others, onboarding takes a week, and adding a second app means copying half the first one.</p>
<p>Layered architecture solves this by giving everything a place, and making those places mean something.</p>
<h2 id="heading-layer-1-the-app-router-and-colocation">Layer 1: The App Router and Colocation</h2>
<p>Next.js 13+ introduced the App Router with a file-system-based routing model that does something subtly powerful: it lets you colocate everything related to a route <em>inside</em> that route's folder.</p>
<p>Before the App Router, pages lived in <code>/pages</code>, components lived in <code>/components</code>, and data fetching was scattered. The App Router flips this. A route segment can now own its layout, its loading and error states, its server actions, and even its local components, all in one place.</p>
<h3 id="heading-what-colocation-actually-means">What Colocation Actually Means</h3>
<p>Consider a <code>/dashboard</code> route. In the App Router model, its folder might look like this:</p>
<pre><code class="language-plaintext">app/
  dashboard/
    page.tsx              # The route entry point
    layout.tsx            # Dashboard-specific shell/navigation
    loading.tsx           # Streaming loading state
    error.tsx             # Error boundary
    components/
      StatsCard.tsx       # Used only within dashboard
      ActivityFeed.tsx
    lib/
      queries.ts          # Data fetching for this route only
      formatters.ts       # Dashboard-specific transforms
</code></pre>
<p>The key insight: <code>StatsCard.tsx</code> and <code>queries.ts</code> don't belong to your whole application, they belong to <code>/dashboard</code>. When you delete or refactor the dashboard, you delete or refactor one folder. Nothing else breaks.</p>
<p>This is colocation. It's not a new idea, but the App Router makes it idiomatic in Next.js for the first time.</p>
<h3 id="heading-the-rule-of-proximity">The Rule of Proximity</h3>
<p>A good heuristic: <em>a file should live as close as possible to where it's used.</em> If it's used in one route, it lives in that route's folder. If it's used by two routes under the same parent segment, it moves up one level. If it's used across the entire app, it belongs in a shared layer (more on that shortly).</p>
<pre><code class="language-plaintext">app/
  (marketing)/          # Route group , no URL segment
    layout.tsx          # Shared layout for marketing pages
    page.tsx
    about/
      page.tsx
  (dashboard)/
    layout.tsx          # Different shell for app routes
    dashboard/
      page.tsx
    settings/
      page.tsx
</code></pre>
<p>Route groups (folders wrapped in parentheses) let you share layouts across segments without polluting the URL. This is a clean way to separate concerns, marketing pages and app pages can have entirely different shells without any URL trickery.</p>
<h2 id="heading-layer-2-feature-based-folder-structure">Layer 2: Feature-Based Folder Structure</h2>
<p>Colocation handles the route level. But large applications have cross-cutting concerns – things that don't belong to any single route but aren't generic utilities either.</p>
<p>This is where most projects fall apart: the <code>/components</code> folder becomes a dumping ground, <code>/lib</code> becomes a junk drawer, and nobody agrees on where <code>useAuth</code> should live.</p>
<p>Feature-based folder structure brings order to this chaos.</p>
<h3 id="heading-organising-by-domain-not-by-file-type">Organising by Domain, Not by File Type</h3>
<p>Instead of grouping files by what they <em>are</em> (components, hooks, utils), group them by what they <em>do</em>.</p>
<pre><code class="language-plaintext">src/
  features/
    auth/
      components/
        LoginForm.tsx
        AuthGuard.tsx
      hooks/
        useAuth.ts
        useSession.ts
      lib/
        tokenStorage.ts
        validators.ts
      types.ts
      index.ts            # Public API , only export what others need

    billing/
      components/
        PricingTable.tsx
        SubscriptionBadge.tsx
      hooks/
        useSubscription.ts
      lib/
        stripe.ts
      types.ts
      index.ts

    notifications/
      ...
</code></pre>
<p>Each feature folder is a self-contained unit. It has its own components, hooks, utilities, and types. Crucially, it has a barrel file (<code>index.ts</code>) that defines its <em>public API</em>, the things other parts of the app are allowed to import.</p>
<h3 id="heading-enforcing-boundaries-with-barrel-exports">Enforcing Boundaries with Barrel Exports</h3>
<p>The <code>index.ts</code> is not optional. It's the mechanism that prevents features from becoming entangled.</p>
<pre><code class="language-typescript">// features/auth/index.ts
export { LoginForm } from './components/LoginForm';
export { AuthGuard } from './components/AuthGuard';
export { useAuth } from './hooks/useAuth';
export type { AuthUser, AuthState } from './types';

// NOT exported, internal implementation detail:
// tokenStorage.ts, validators.ts
</code></pre>
<p>Now, the rest of your app imports from <code>@/features/auth</code>, never from <code>@/features/auth/lib/tokenStorage</code>. If you refactor how tokens are stored internally, nothing outside the feature breaks. This is the essence of encapsulation, not just as a theoretical principle, but as a structural one enforced by your folder layout.</p>
<h3 id="heading-shared-vs-feature">Shared vs. Feature</h3>
<p>Not everything belongs in a feature. Truly generic utilities: a <code>cn()</code> classname helper, a date formatter, or a base HTTP client, for example, belong in a shared layer:</p>
<pre><code class="language-plaintext">src/
  shared/
    components/
      Button.tsx
      Modal.tsx
      Spinner.tsx
    hooks/
      useDebounce.ts
      useMediaQuery.ts
    lib/
      http.ts
      dates.ts
    ui/              # shadcn/ui or design system components
</code></pre>
<p>The rule: <code>shared/</code> has zero knowledge of any feature. Features can import from <code>shared/</code>. <code>shared/</code> never imports from a feature.</p>
<h2 id="heading-layer-3-monorepo-with-turborepo-sharing-logic-across-apps">Layer 3: Monorepo with Turborepo (Sharing Logic Across Apps)</h2>
<p>Single-repo architecture gets you far, but most teams eventually end up with multiple apps: a customer-facing Next.js app, an admin panel, a separate marketing site, maybe a set of API services.</p>
<p>The question becomes: <em>how do you share code between them without copy-pasting?</em></p>
<p>The answer is a monorepo with shared packages, and Turborepo is currently the best tool for Next.js teams doing this.</p>
<h3 id="heading-the-monorepo-shape">The Monorepo Shape</h3>
<p>A well-structured Turborepo looks like this:</p>
<pre><code class="language-plaintext">my-platform/
  apps/
    web/              # Customer-facing Next.js app
    admin/            # Internal admin panel (also Next.js)
    marketing/        # Marketing site
  packages/
    ui/               # Shared component library
    config/           # Shared ESLint, TypeScript, Tailwind configs
    auth/             # Shared auth utilities and types
    database/         # Prisma client + query helpers
    utils/            # Generic utilities
  turbo.json
  package.json        # Root workspace config
</code></pre>
<p><code>apps/</code> contains deployable applications. <code>packages/</code> contains shared code that apps depend on. Neither app imports directly from the other, all sharing flows through <code>packages/</code>.</p>
<h3 id="heading-setting-up-a-shared-package">Setting Up a Shared Package</h3>
<p>A package is just a folder with a <code>package.json</code> that other workspace members can depend on.</p>
<pre><code class="language-json">// packages/ui/package.json
{
  "name": "@my-platform/ui",
  "version": "0.0.1",
  "main": "./src/index.ts",
  "types": "./src/index.ts",
  "exports": {
    ".": "./src/index.ts"
  }
}
</code></pre>
<pre><code class="language-typescript">// packages/ui/src/index.ts
export { Button } from './Button';
export { Modal } from './Modal';
export { Card } from './Card';
</code></pre>
<p>Now your apps consume it like any npm package:</p>
<pre><code class="language-json">// apps/web/package.json
{
  "dependencies": {
    "@my-platform/ui": "*"
  }
}
</code></pre>
<pre><code class="language-tsx">// apps/web/app/dashboard/page.tsx
import { Card, Button } from '@my-platform/ui';
</code></pre>
<p>Change <code>Card</code> once in <code>packages/ui</code>, and every app that uses it gets the update, no copy-pasting, no drift.</p>
<p><strong>Important:</strong> Because the package points directly at TypeScript source files (not compiled output), each consuming Next.js app must tell the bundler to transpile it. Add this to your Next.js config:</p>
<pre><code class="language-ts">// apps/web/next.config.ts
const config: import('next').NextConfig = {
  transpilePackages: ['@my-platform/ui', '@my-platform/auth', '@my-platform/utils'],
};

export default config;
</code></pre>
<p>Without this, the build fails with syntax errors, Next.js doesn't transpile packages from <code>node_modules</code> or workspace dependencies by default. The alternative is compiling each package to <code>dist/</code> and pointing <code>exports</code> there, but that adds a build step to every package and slows down the dev feedback loop. For internal monorepo packages, <code>transpilePackages</code> is the simpler tradeoff.</p>
<h3 id="heading-the-turbojson-pipeline">The <code>turbo.json</code> Pipeline</h3>
<p>Turborepo's real power is its build pipeline. It understands the dependency graph between your packages and apps, caches build outputs, and runs tasks in parallel where possible.</p>
<pre><code class="language-json">// turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "tasks": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [".next/**", "dist/**"]
    },
    "lint": {
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "type-check": {
      "dependsOn": ["^build"],
      "outputs": []
    }
  }
}
</code></pre>
<p>The <code>^build</code> syntax means: <em>before building this package, build all its dependencies first.</em> So if <code>apps/web</code> depends on <code>packages/ui</code>, Turborepo ensures <code>packages/ui</code> is built before <code>apps/web</code> starts. Remote caching means if <code>packages/ui</code> hasn't changed, Turborepo skips rebuilding it entirely, even across CI runs and team members' machines.</p>
<h3 id="heading-what-goes-in-a-package-vs-an-app">What Goes in a Package vs. an App</h3>
<p>A useful litmus test:</p>
<table>
<thead>
<tr>
<th>Lives in <code>packages/</code></th>
<th>Lives in <code>apps/</code></th>
</tr>
</thead>
<tbody><tr>
<td>Design system / UI primitives</td>
<td>Route definitions</td>
</tr>
<tr>
<td>Auth utilities and types</td>
<td>App-specific layouts</td>
</tr>
<tr>
<td>Database client and queries</td>
<td>Feature-specific pages</td>
</tr>
<tr>
<td>Shared TypeScript configs</td>
<td>API route handlers</td>
</tr>
<tr>
<td>Analytics abstractions</td>
<td>Environment-specific config</td>
</tr>
<tr>
<td>Generic hooks (useDebounce)</td>
<td>App-specific business logic</td>
</tr>
</tbody></table>
<p>If two apps need the same logic, it goes in a package. If only one app needs it, it stays in that app, even if you <em>think</em> the other app might need it someday. Premature abstraction is just as damaging as none at all.</p>
<h2 id="heading-layer-4-server-components-and-data-fetching-boundaries">Layer 4: Server Components and Data-Fetching Boundaries</h2>
<p>The App Router's Server Components model is arguably the most architecturally significant change Next.js has ever shipped, and also the most misunderstood.</p>
<p>Most developers approach it as a performance optimisation. It is that, but it's more importantly an <em>architectural boundary</em>. Understanding where that boundary sits, and designing around it deliberately, is what separates scalable App Router codebases from ones that fight the framework.</p>
<h3 id="heading-the-mental-model-two-worlds">The Mental Model: Two Worlds</h3>
<p>Every component in the App Router lives in one of two worlds:</p>
<p><strong>Server Components</strong> (default) run exclusively on the server. They can <code>await</code> data directly, access databases, read environment variables, and reduce the JavaScript sent to the browser. They can't use browser APIs, <code>useState</code>, <code>useEffect</code>, or event handlers.</p>
<p><strong>Client Components</strong> (<code>'use client'</code>) run in the browser (and also during SSR/hydration). They can use hooks, handle events, and access browser APIs. They can't directly <code>await</code> server-side resources.</p>
<p>The directive <code>'use client'</code> doesn't mean <em>"this runs only in the browser"</em> , it means <em>"this is the boundary where the server-to-client handoff begins."</em> Any module <em>imported</em> by a Client Component becomes part of the client bundle.</p>
<p>But Server Components <em>passed as props</em> (typically via <code>children</code>) retain their server-only nature, they're rendered on the server and streamed as HTML, not included in the client bundle. This distinction is what makes the composition pattern below work.</p>
<h3 id="heading-designing-the-boundary">Designing the Boundary</h3>
<p>The goal is to push the <code>'use client'</code> boundary as far down the tree as possible, keeping data fetching and heavy logic on the server, and reserving Client Components for genuinely interactive leaves.</p>
<p>A pattern that works well in practice:</p>
<pre><code class="language-tsx">// app/dashboard/page.tsx , Server Component
// Fetches data, no 'use client' directive needed

import { getMetrics } from '@/features/analytics/lib/queries';
import { MetricsDashboard } from './components/MetricsDashboard';

export default async function DashboardPage() {
  const metrics = await getMetrics();   // Direct DB call , no API round-trip
  return &lt;MetricsDashboard data={metrics} /&gt;;
}
</code></pre>
<pre><code class="language-tsx">// app/dashboard/components/MetricsDashboard.tsx , Server Component
// Composes layout, delegates interactivity to leaves

import { StatsCard } from './StatsCard';
import { ChartSection } from './ChartSection';

export function MetricsDashboard({ data }) {
  return (
    &lt;div className="grid gap-6"&gt;
      &lt;StatsCard value={data.revenue} label="Revenue" /&gt;
      &lt;ChartSection points={data.trend} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<pre><code class="language-tsx">// app/dashboard/components/ChartSection.tsx , Client Component
// Interactive chart needs browser APIs

'use client';

import { useState } from 'react';
import { LineChart, RangeSelector } from '@my-platform/ui';

export function ChartSection({ points }) {
  const [range, setRange] = useState('7d');
  return (
    &lt;div&gt;
      &lt;RangeSelector value={range} onChange={setRange} /&gt;
      &lt;LineChart data={points.filter(/* range logic */)} /&gt;
    &lt;/div&gt;
  );
}
</code></pre>
<p>The data flows from server to client in one direction. The server does the expensive work (database query), passes serialisable data down as props, and the client receives a ready-to-render dataset – no loading spinners, no client-side fetch waterfalls.</p>
<h3 id="heading-colocating-data-fetching-with-routes">Colocating Data Fetching with Routes</h3>
<p>A powerful pattern enabled by Server Components is colocating data fetching directly with the route that needs it, eliminating the need for global state management in many cases.</p>
<pre><code class="language-plaintext">app/
  orders/
    page.tsx              # await getOrders() , renders list
    [id]/
      page.tsx            # await getOrder(id) , renders single order
      loading.tsx         # Streaming skeleton while awaiting
      components/
        OrderTimeline.tsx  # Server Component , renders timeline data
        CancelButton.tsx  # 'use client' , needs click handler
</code></pre>
<p>Each page fetches its own data, scoped to what it needs. Nested layouts and pages can fetch concurrently when using <code>Promise.all</code> or parallel route segments. And <code>loading.tsx</code> gives you streaming suspense boundaries without writing a single <code>&lt;Suspense&gt;</code> wrapper manually.</p>
<h3 id="heading-when-to-use-a-fetch-layer-vs-direct-queries">When to Use a Fetch Layer vs. Direct Queries</h3>
<p>As apps scale, you'll want a consistent approach to data access. A practical pattern:</p>
<pre><code class="language-typescript">// packages/database/src/queries/orders.ts
// Runs on the server , can be imported in any Server Component

import { db } from '../client';

export async function getOrdersByUser(userId: string) {
  return db.order.findMany({
    where: { userId },
    include: { items: true },
    orderBy: { createdAt: 'desc' },
  });
}
</code></pre>
<pre><code class="language-typescript">// packages/database/src/index.ts
export { getOrdersByUser } from './queries/orders';
export { getProductById } from './queries/products';
// ...
</code></pre>
<p>Your Server Components import from <code>@my-platform/database</code>. Your Client Components never touch this package: they call API routes or Server Actions if they need to mutate data. This keeps the boundary clean and auditable.</p>
<h3 id="heading-server-actions-for-mutations">Server Actions for Mutations</h3>
<p>Data fetching flows through Server Components, but mutations need their own boundary. Server Actions (<code>'use server'</code>) let you define server-side functions that Client Components can call directly – no API route boilerplate needed.</p>
<pre><code class="language-typescript">// app/orders/[id]/actions.ts
'use server';

import { db } from '@my-platform/database';
import { revalidatePath } from 'next/cache';

export async function cancelOrder(orderId: string) {
  await db.order.update({
    where: { id: orderId },
    data: { status: 'cancelled', cancelledAt: new Date() },
  });

  revalidatePath(`/orders/${orderId}`);
}
</code></pre>
<pre><code class="language-tsx">// app/orders/[id]/components/CancelButton.tsx
'use client';

import { cancelOrder } from '../actions';
import { useTransition } from 'react';

export function CancelButton({ orderId }: { orderId: string }) {
  const [isPending, startTransition] = useTransition();

  return (
    &lt;button
      disabled={isPending}
      onClick={() =&gt; startTransition(() =&gt; cancelOrder(orderId))}
    &gt;
      {isPending ? 'Cancelling...' : 'Cancel Order'}
    &lt;/button&gt;
  );
}
</code></pre>
<p>The architectural decision:</p>
<ul>
<li><p><strong>use Server Actions for mutations that are colocated with a specific route</strong> (cancelling an order, updating a profile).</p>
</li>
<li><p><strong>Use API routes for mutations that are consumed by external clients</strong> (webhooks, mobile apps, third-party integrations).</p>
</li>
</ul>
<p>Server Actions keep mutation logic close to the UI that triggers it. API routes provide a stable contract for external consumers.</p>
<p>This completes the data flow picture: Server Components handle reads, Server Actions handle writes, and Client Components are the interactive surface that connects them.</p>
<h2 id="heading-layer-5-testing-strategy-for-a-layered-codebase">Layer 5: Testing Strategy for a Layered Codebase</h2>
<p>The testing pyramid is one of those concepts that sounds obvious in theory but falls apart in practice, usually because the codebase doesn't have clear boundaries to test against. When everything is tangled, every test becomes an integration test by accident.</p>
<p>The layered architecture you've built changes this: each layer has a defined surface area, so you can test each one at the right level of abstraction.</p>
<h3 id="heading-test-each-layer-at-the-right-granularity">Test Each Layer at the Right Granularity</h3>
<p>The layered architecture maps naturally onto the testing pyramid:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Test Type</th>
<th>Tools</th>
</tr>
</thead>
<tbody><tr>
<td><code>packages/</code> (utils, db queries)</td>
<td>Unit tests</td>
<td>Vitest</td>
</tr>
<tr>
<td><code>features/</code> (hooks, lib, components)</td>
<td>Unit + Integration</td>
<td>Vitest + React Testing Library</td>
</tr>
<tr>
<td>App Router pages (Server Components)</td>
<td>Integration</td>
<td>Vitest + custom render</td>
</tr>
<tr>
<td>Critical user flows (checkout, auth)</td>
<td>End-to-end</td>
<td>Playwright</td>
</tr>
</tbody></table>
<p>The goal: test shared packages exhaustively, test features thoroughly, test pages for integration correctness, and use E2E only for the flows that matter most.</p>
<p>Not everything needs an E2E test, and treating E2E as the default testing strategy is one of the most expensive mistakes a team can make.</p>
<h3 id="heading-unit-testing-shared-packages">Unit Testing Shared Packages</h3>
<p>Packages in <code>packages/</code> are the easiest to test. They're pure TypeScript with no framework coupling. Use Vitest:</p>
<pre><code class="language-typescript">// packages/utils/src/dates.test.ts
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { formatRelativeDate } from './dates';

describe('formatRelativeDate', () =&gt; {
  beforeEach(() =&gt; {
    // Pin the clock to avoid flaky results near midnight
    vi.useFakeTimers();
    vi.setSystemTime(new Date('2026-03-15T12:00:00Z'));
  });

  afterEach(() =&gt; {
    vi.useRealTimers();
  });

  it('returns "today" for dates within the current day', () =&gt; {
    expect(formatRelativeDate(new Date())).toBe('today');
  });

  it('returns "yesterday" for dates on the previous day', () =&gt; {
    const yesterday = new Date('2026-03-14T15:00:00Z');
    expect(formatRelativeDate(yesterday)).toBe('yesterday');
  });
});
</code></pre>
<p>Keep package tests colocated with the source file. A <code>dates.ts</code> file has a <code>dates.test.ts</code> sibling. No separate <code>__tests__</code> folders, those are relics of less structured codebases.</p>
<h3 id="heading-testing-feature-modules">Testing Feature Modules</h3>
<p>Features are where most of your business logic lives, so they get the most test coverage. The key rule: test the public API of the feature, not its internals.</p>
<pre><code class="language-typescript">// features/auth/hooks/useAuth.test.ts
import { renderHook, act } from '@testing-library/react';
import { useAuth } from '../hooks/useAuth';
import { createWrapper } from '@/test/utils'; // your test provider wrapper

describe('useAuth', () =&gt; {
  it('returns authenticated state when session exists', async () =&gt; {
    const { result } = renderHook(() =&gt; useAuth(), {
      wrapper: createWrapper({ session: mockSession }),
    });

    expect(result.current.isAuthenticated).toBe(true);
    expect(result.current.user.email).toBe(mockSession.user.email);
  });

  it('redirects to login when session is null', async () =&gt; {
    const { result } = renderHook(() =&gt; useAuth(), {
      wrapper: createWrapper({ session: null }),
    });

    expect(result.current.isAuthenticated).toBe(false);
  });
});
</code></pre>
<p>Notice that the test imports from the hook directly, not from the feature's <code>index.ts</code> barrel. Feature index exports are public APIs. They're tested through integration tests that consume the feature as a whole. Internal hooks and utilities are tested at the unit level. Both are valid, and the distinction is intentional.</p>
<h3 id="heading-testing-server-components">Testing Server Components</h3>
<p>Server Components are async functions that return JSX. Testing them directly is still an evolving story. React's test renderer doesn't natively handle async components, and calling <code>await DashboardPage()</code> then passing the result to <code>render()</code> produces subtle issues (missing context, <code>act()</code> warnings, or outright failures depending on your setup).</p>
<p>The most reliable approach today is to <strong>test the layers separately</strong>: mock the data layer to verify it's called correctly, and test the presentational component with static props.</p>
<pre><code class="language-typescript">// app/dashboard/components/MetricsDashboard.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import { MetricsDashboard } from './MetricsDashboard';

describe('MetricsDashboard', () =&gt; {
  it('renders revenue metric from provided data', () =&gt; {
    render(
      &lt;MetricsDashboard data={{ revenue: 84200, trend: [] }} /&gt;
    );

    expect(screen.getByText('£84,200')).toBeInTheDocument();
  });
});
</code></pre>
<pre><code class="language-typescript">// features/analytics/lib/queries.test.ts
import { describe, it, expect } from 'vitest';
import { getMetrics } from './queries';

describe('getMetrics', () =&gt; {
  it('returns revenue and trend data', async () =&gt; {
    const metrics = await getMetrics();

    expect(metrics.revenue).toBeGreaterThan(0);
    expect(Array.isArray(metrics.trend)).toBe(true);
  });
});
</code></pre>
<p>The key insight: mock at the data layer boundary, not at the database or network layer. The data query has its own tests in <code>packages/database</code>. The presentational component has its own tests with static props. The Server Component page wires them together, and that wiring is verified by your E2E tests, which are better suited to catching integration issues across the async boundary.</p>
<h3 id="heading-end-to-end-tests-with-playwright">End-to-End Tests with Playwright</h3>
<p>Reserve Playwright for the flows that touch multiple layers and where a breakage would be catastrophic: authentication, checkout, and form submission with side effects. Don't use it for visual regressions or static content, as that's expensive and slow.</p>
<pre><code class="language-typescript">// e2e/auth.spec.ts
import { test, expect } from '@playwright/test';

test('user can log in and reach dashboard', async ({ page }) =&gt; {
  await page.goto('/login');
  await page.fill('[name="email"]', 'test@example.com');
  await page.fill('[name="password"]', 'password123');
  await page.click('button[type="submit"]');

  await expect(page).toHaveURL('/dashboard');
  await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
});
</code></pre>
<p>Colocate E2E tests in a top-level <code>e2e/</code> folder at the monorepo root. They span apps and don't belong inside any single app's directory.</p>
<h3 id="heading-configuring-vitest-across-the-monorepo">Configuring Vitest Across the Monorepo</h3>
<p>Each package and app has its own <code>vitest.config.ts</code>, but they can share a base config via a shared package:</p>
<pre><code class="language-typescript">// packages/config/vitest.base.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    environment: 'jsdom',
    globals: true,
    setupFiles: ['./src/test/setup.ts'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'lcov'],
    },
  },
});
</code></pre>
<pre><code class="language-typescript">// apps/web/vitest.config.ts
import { mergeConfig } from 'vitest/config';
import base from '@my-platform/config/vitest.base';

export default mergeConfig(base, {
  test: {
    include: ['src/**/*.test.{ts,tsx}', 'app/**/*.test.{ts,tsx}'],
  },
});
</code></pre>
<p>This ensures consistent test configuration across every app and package without duplication.</p>
<h2 id="heading-layer-6-cicd-with-turborepo">Layer 6: CI/CD with Turborepo</h2>
<p>A well-designed monorepo without a smart CI pipeline is just a big repo. Turborepo's real power emerges in CI, where it can cut build and test times dramatically through caching and intelligent task scheduling.</p>
<h3 id="heading-the-core-insight-only-run-what-changed">The Core Insight: Only Run What Changed</h3>
<p>Traditional CI pipelines run everything on every commit. In a monorepo, this means running tests for <code>apps/admin</code> when you only changed a utility in <code>apps/web</code>. Turborepo's dependency graph awareness eliminates this.</p>
<p>When you run <code>turbo test</code>, Turborepo:</p>
<ol>
<li><p>Builds the dependency graph from your <code>package.json</code> files</p>
</li>
<li><p>Checks which packages have changed (against the last cached state)</p>
</li>
<li><p>Runs tests only for changed packages and their dependents</p>
</li>
<li><p>Caches results. If nothing changed, it restores from cache instantly.</p>
</li>
</ol>
<p>A change to <code>packages/ui</code> triggers tests for <code>packages/ui</code>, <code>apps/web</code>, and <code>apps/admin</code> (since both depend on it). A change only to <code>apps/web</code> triggers tests for <code>apps/web</code> only.</p>
<h3 id="heading-remote-caching">Remote Caching</h3>
<p>Without remote caching, Turborepo's local cache doesn't help in CI – each run starts fresh. With remote caching, build and test artifacts are stored in the cloud and shared across all CI runners and developers' machines.</p>
<pre><code class="language-bash"># Authenticate with Turborepo remote cache (Vercel)
npx turbo login
npx turbo link
</code></pre>
<p>Or use a self-hosted cache server if you need to keep artifacts on your own infrastructure. Once configured, a CI run on a branch that touched only <code>apps/web</code> might take 45 seconds instead of 8 minutes, because every <code>packages/*</code> task restores from cache.</p>
<h3 id="heading-a-production-ready-github-actions-pipeline">A Production-Ready GitHub Actions Pipeline</h3>
<p>Here's a complete pipeline that uses Turborepo's caching, runs affected tasks only, and splits lint, test, and build into parallel jobs:</p>
<pre><code class="language-yaml"># .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM: ${{ secrets.TURBO_TEAM }}

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npx turbo lint --filter="...[origin/main]"

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npx turbo test --filter="...[origin/main]"

  build:
    name: Build
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npx turbo build --filter="...[origin/main]"

  e2e:
    name: E2E Tests
    runs-on: ubuntu-latest
    needs: [build]
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npx playwright install --with-deps

      - name: Build the app (restores from Turborepo cache if unchanged)
        run: npx turbo build --filter="apps/web"

      - name: Run E2E tests
        run: npx turbo e2e
</code></pre>
<p>The E2E job assumes Playwright's <code>webServer</code> config handles starting the app automatically. Configure this in your <code>playwright.config.ts</code>:</p>
<pre><code class="language-typescript">// playwright.config.ts
export default defineConfig({
  webServer: {
    command: 'npm run start --prefix apps/web',
    port: 3000,
    reuseExistingServer: !process.env.CI,
  },
});
</code></pre>
<p>This way Playwright starts the production server before tests run and tears it down afterwards – no manual server management in CI.</p>
<p>The <code>--filter="...[origin/main]"</code> flag is the critical piece. It tells Turborepo to run tasks only for packages that have changed since the <code>main</code> branch, plus all packages that depend on those changed packages. This is the most impactful optimisation in the whole pipeline.</p>
<h3 id="heading-filtering-strategies">Filtering Strategies</h3>
<p>Turborepo's <code>--filter</code> flag is flexible and worth understanding:</p>
<pre><code class="language-bash"># Only run tasks for packages that changed vs main
turbo test --filter="...[origin/main]"

# Run tasks for a specific app and all its dependencies
turbo build --filter="apps/web..."

# Run tasks for everything except a specific app
turbo test --filter="!apps/admin"

# Run tasks for all apps (not packages)
turbo build --filter="./apps/*"
</code></pre>
<p>For most CI pipelines, <code>--filter="...[origin/main]"</code> on feature branches and <code>turbo run test build</code> (no filter) on <code>main</code> merges is the right split. You want fast feedback on PRs and confidence that everything still works on main.</p>
<h3 id="heading-deployment-pipeline-with-per-app-filtering">Deployment Pipeline with Per-App Filtering</h3>
<p>When deploying to Vercel, Netlify, or any platform with per-app deployments, Turborepo lets you detect which apps actually changed and skip deployments for unchanged ones:</p>
<pre><code class="language-yaml"># .github/workflows/deploy.yml
- name: Check if web app changed
  id: check-web
  run: |
    CHANGED=$(npx turbo run build --filter="apps/web...[origin/main]" --dry=json | jq '.packages | length')
    echo "changed=\(CHANGED" &gt;&gt; \)GITHUB_OUTPUT

- name: Deploy web
  if: steps.check-web.outputs.changed != '0'
  run: vercel deploy --prod
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
</code></pre>
<p>This ensures your admin app doesn't trigger a deployment when only the marketing site changed, reducing deploy times, costs, and the blast radius of any deployment failure.</p>
<h3 id="heading-environment-variable-management">Environment Variable Management</h3>
<p>One of the trickier parts of a monorepo CI setup is environment variables: each app needs its own secrets, but some are shared across apps.</p>
<p>A clean convention:</p>
<pre><code class="language-plaintext"># .env (repo root , shared across all apps in local dev)
DATABASE_URL=...
REDIS_URL=...

# apps/web/.env.local (web-specific overrides)
NEXT_PUBLIC_APP_URL=https://app.example.com
STRIPE_KEY=...

# apps/admin/.env.local (admin-specific)
NEXT_PUBLIC_APP_URL=https://admin.example.com
ADMIN_SECRET=...
</code></pre>
<p>In CI, store shared secrets as organisation-level GitHub secrets and app-specific secrets as repository-level secrets scoped to the appropriate environment.</p>
<p>Never store secrets in <code>turbo.json</code> or any committed file. Instead, use <code>env</code> in your pipeline steps and Turborepo's <code>globalEnv</code> field in <code>turbo.json</code> to declare which env vars should bust the cache when they change:</p>
<pre><code class="language-json">// turbo.json
{
  "globalEnv": ["NODE_ENV", "DATABASE_URL"],
  "tasks": {
    "build": {
      "env": ["NEXT_PUBLIC_APP_URL", "STRIPE_KEY"],
      "dependsOn": ["^build"],
      "outputs": [".next/**"]
    }
  }
}
</code></pre>
<p>This tells Turborepo: if <code>DATABASE_URL</code> changes, invalidate the cache for all tasks. If <code>NEXT_PUBLIC_APP_URL</code> changes, only invalidate the <code>build</code> task. Without this, you risk Turborepo restoring a cached build that was compiled against a different environment, a subtle and painful bug.</p>
<h2 id="heading-putting-it-all-together-the-full-blueprint">Putting It All Together: The Full Blueprint</h2>
<p>Here's what the complete architecture looks like assembled:</p>
<pre><code class="language-plaintext">my-platform/
  apps/
    web/
      app/
        (marketing)/
          layout.tsx
          page.tsx
          about/page.tsx
        (app)/
          layout.tsx            # Auth-protected shell
          dashboard/
            page.tsx            # Server Component , fetches data
            loading.tsx
            components/
              MetricsDashboard.tsx
              ChartSection.tsx  # 'use client'
          orders/
            page.tsx
            [id]/
              page.tsx
              components/
                OrderTimeline.tsx
                CancelButton.tsx  # 'use client'
      src/
        features/
          auth/
            components/
            hooks/
            lib/
            index.ts
          billing/
            ...
        shared/
          components/
          hooks/
          lib/
    admin/
      app/
        ...                     # Same layer structure
      src/
        features/
          ...
  packages/
    ui/                         # Shared primitives
    auth/                       # Shared auth logic
    database/                   # Prisma + queries
    config/                     # ESLint, TS, Tailwind configs
    utils/                      # Generic helpers
  turbo.json
  package.json
</code></pre>
<p>Notice how the <code>'use client'</code> boundary appears only at the interactive leaves: <code>ChartSection.tsx</code> needs <code>useState</code>, and <code>CancelButton.tsx</code> needs a click handler and <code>useTransition</code>. Everything above them (<code>MetricsDashboard.tsx</code>, <code>OrderTimeline.tsx</code>, the page components) stays on the server, fetching data and composing layout without shipping any JavaScript to the browser.</p>
<p>The layers stack cleanly:</p>
<ol>
<li><p><strong>Turborepo packages</strong>: the lowest layer. Generic, reusable, no app-specific knowledge.</p>
</li>
<li><p><strong>Shared feature layer</strong>: cross-cutting app concerns. Can consume packages, knows nothing of routes.</p>
</li>
<li><p><strong>Feature modules</strong>: domain logic, encapsulated behind barrel exports.</p>
</li>
<li><p><strong>App Router</strong>: routes, layouts, colocation. Consumes features and packages. Data flows through Server Components, interactivity is delegated to Client Component leaves.</p>
</li>
</ol>
<h2 id="heading-common-pitfalls-and-how-to-avoid-them">Common Pitfalls and How to Avoid Them</h2>
<p><strong>"I'll just put it in</strong> <code>/utils</code> <strong>for now."</strong> This is how junk drawers form. If you can't name what a utility belongs to, it probably needs a new feature folder, not a generic dumping ground.</p>
<p><strong>Over-extracting packages too early</strong>: Not everything needs to be a shared package. Start in the app, extract to a package only when a second consumer appears. The cost of premature abstraction is maintenance overhead and false coupling.</p>
<p><strong>Client Components at the top of every tree</strong>: If your route's <code>page.tsx</code> has <code>'use client'</code> at the top, you've lost most of what Server Components give you. Push the directive down to the interactive leaf.</p>
<p><strong>Circular package dependencies</strong>: If <code>packages/auth</code> imports from <code>packages/database</code> and <code>packages/database</code> imports from <code>packages/auth</code>, you have a cycle. Keep the dependency graph a DAG: each package should have one clear level of abstraction.</p>
<p><strong>Barrel files that export everything</strong>: The barrel file is a public API, not an index of every file in the folder. Export only what other parts of the app are meant to use.</p>
<h2 id="heading-final-thoughts">Final Thoughts</h2>
<p>Good architecture isn't about finding the perfect structure, it's about making the right decisions easy and the wrong decisions hard.</p>
<ul>
<li><p><strong>Colocation</strong> makes it easy to find what you need.</p>
</li>
<li><p><strong>Feature modules</strong> make it hard to accidentally couple unrelated domains.</p>
</li>
<li><p><strong>Turborepo</strong> makes it easy to share code and hard to duplicate it.</p>
</li>
<li><p><strong>Server Components</strong> make it easy to fetch data where you need it and hard to send unnecessary JavaScript to the browser.</p>
</li>
</ul>
<p>None of these ideas are new. Layered architecture, separation of concerns, and encapsulation are decades-old principles. What Next.js and Turborepo give you is a modern toolkit to express them idiomatically in a JavaScript codebase.</p>
<p>The best time to set this up is at the start of a project. The second best time is now, before the next feature makes untangling things twice as hard.</p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ How to Set Up Continuous Integration for a Monorepo Using Buildkite ]]>
                </title>
                <description>
                    <![CDATA[ By subash adhikari A monorepo is a single repository that holds all the code and multiple projects in a single Git repository.  This setup is quite nice to work with because of its flexibility and ability to manage various services and frontends in o... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/how-to-set-up-continuous-integration-for-monorepo-using-buildkite/</link>
                <guid isPermaLink="false">66d4614c3dce891ac3a96828</guid>
                
                    <category>
                        <![CDATA[ AWS ]]>
                    </category>
                
                    <category>
                        <![CDATA[ Continuous Integration ]]>
                    </category>
                
                    <category>
                        <![CDATA[ GitHub ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monorepo ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Fri, 02 Apr 2021 20:33:51 +0000</pubDate>
                <media:content url="https://www.freecodecamp.org/news/content/images/2021/03/cover-1.jpeg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By subash adhikari</p>
<p>A monorepo is a single repository that holds all the code and multiple projects in a single Git repository. </p>
<p>This setup is quite nice to work with because of its flexibility and ability to manage various services and frontends in one single repository. It also eliminates the hassle of tracking changes in multiple repositories and updating dependencies as projects change.</p>
<p>On the other hand, monorepos also come with their challenges, specifically as relates to Continuous Integration. As individual sub-projects within the monorepo change, we need to identify which sub-projects changed to build and deploy them. </p>
<p>This post will serve as a step by step guide to:</p>
<ol>
<li>Configure Continuous Integration for monorepos in Bulidkite.</li>
<li>Deploy Buildkite Agents to AWS EC2 instances with autoscaling.</li>
<li>Configure GitHub to trigger Bulidkite CI pipelines.</li>
<li>Configure Buildkite to trigger appropriate pipelines when sub-projects within a monorepo change.</li>
<li>Automate all of the above using bash scripts.</li>
</ol>
<h3 id="heading-pre-requisites">Pre-requisites</h3>
<ol>
<li><a target="_blank" href="https://aws.amazon.com/free/"><strong>AWS</strong></a> account to deploy the Buildkite agents.</li>
<li>Configure <a target="_blank" href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html"><strong>AWS CLI</strong></a> to talk to AWS Account.</li>
<li><a target="_blank" href="https://buildkite.com/"><strong>Buildkite</strong></a> account to create continuous integration pipelines.</li>
<li><a target="_blank" href="https://github.com/"><strong>GitHub</strong></a> account to host the monorepo sourcecode.</li>
</ol>
<p>The complete source code is available in the <a target="_blank" href="https://github.com/adikari/buildkite-monorepo"><strong>buildkite-monorepo</strong></a> in GitHub.</p>
<h2 id="heading-project-setup">Project Setup</h2>
<p>The Buildkite workflow consists of <a target="_blank" href="https://buildkite.com/docs/pipelines">Pipelines</a> and Steps. The top-level containers for modeling and defining workflows are called Pipelines. Steps run individual tasks or commands.</p>
<p>The following diagram lists the pipelines we are setting up, their associated triggers, and each step that the pipeline runs.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-109.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-pull-request-workflow">Pull Request Workflow</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-110.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The above diagram visualizes the workflow for the Pull Request pipeline. </p>
<p>Creating a new Pull Request in GitHub triggers the <code>pull-request</code> pipeline in Buildkite. This pipeline then runs <code>git diff</code> to identify which folders (projects) within the monorepo have changed. </p>
<p>If it detects changes, then it will dynamically trigger the appropriate Pull Request pipeline defined for that project. Buildkite reports the status of each pipeline back to <a target="_blank" href="https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/about-status-checks">GitHub status check.</a></p>
<h3 id="heading-merge-workflow">Merge Workflow</h3>
<p>The Pull Request is merged when all status checks in Github pass. Merging Pull Request triggers the <code>merge</code> pipeline in Buildkite.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-111.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Similar to the previous pipeline, the merge pipeline identifies the projects that have changed and triggers the corresponding <code>deploy</code> pipeline for it. The Deploy pipeline initially deploys changes to the staging environment. </p>
<p>Once the deployment to staging is complete, production deployment is manually released.</p>
<h3 id="heading-final-project-structure">Final project structure</h3>
<p>├── .buildkite<br>│   ├── diff<br>│   ├── merge.yml<br>│   ├── pipelines<br>│   │   ├── deploy.json<br>│   │   ├── merge.json<br>│   │   └── pull-request.json<br>│   └── pull-request.yml<br>├── bar-service<br>│   ├── .buildkite<br>│   │   ├── deploy.yml<br>│   │   ├── merge.yml<br>│   │   └── pull-request.yml<br>│   └── bin<br>│       └── deploy<br>├── bin<br>│   ├── create-pipeline<br>│   ├── create-secrets-bucket<br>│   ├── deploy-ci-stack<br>│   └── stack-config<br>└── foo-service<br>    ├── .buildkite<br>    │   ├── deploy.yml<br>    │   ├── merge.yml<br>    │   └── pull-request.yml<br>    └── bin<br>        └── deploy</p>
<h3 id="heading-set-up-the-project">Set Up the Project</h3>
<p>Create a new Git project and push it to GitHub. Run the following commands in the CLI.</p>
<pre><code class="lang-bash">mkdir buildkite-monorepo-example
<span class="hljs-built_in">cd</span> buildkite-monorepo-example
git init
<span class="hljs-built_in">echo</span> node_modules/ &gt; .gitignore
git add .
git commit -m <span class="hljs-string">"initialize repository"</span>
git remote add origin &lt;YOUR_GITHUB_REPO_URL&gt;
git push origin master
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-112.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-set-up-the-buildkite-infrastructure">Set up the Buildkite infrastructure</h3>
<ol>
<li>Create a bin directory with some executable scripts inside it.</li>
</ol>
<pre><code class="lang-bash">mkdir bin 
<span class="hljs-built_in">cd</span> bin
touch create-pipeline create-secrets-bucket deploy-ci-stack
chmod +x ./*
</code></pre>
<ol start="2">
<li>Copy the following contents into <code>create-secrets-bucket</code>.</li>
</ol>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -eou pipefail

CURRENT_DIR=$(<span class="hljs-built_in">pwd</span>)
ROOT_DIR=<span class="hljs-string">"<span class="hljs-subst">$( dirname <span class="hljs-string">"<span class="hljs-variable">${BASH_SOURCE[0]}</span>"</span> )</span>"</span>/..

BUCKET_NAME=<span class="hljs-string">"buildkite-secrets-adikari"</span>
KEY=<span class="hljs-string">"id_rsa_buildkite"</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"creating bucket <span class="hljs-variable">$BUCKET_NAME</span>.."</span>
aws s3 mb s3://<span class="hljs-variable">$BUCKET_NAME</span>

<span class="hljs-comment"># Generate SSH Key</span>
ssh-keygen -t rsa -b 4096 -f <span class="hljs-variable">$KEY</span> -N <span class="hljs-string">''</span>

<span class="hljs-comment"># Copy SSH Keys to S3 bucket</span>
aws s3 cp --acl private --sse aws:kms <span class="hljs-variable">$KEY</span> <span class="hljs-string">"s3://<span class="hljs-variable">$BUCKET_NAME</span>/private_ssh_key"</span>
aws s3 cp --acl private --sse aws:kms <span class="hljs-variable">$KEY</span>.pub <span class="hljs-string">"s3://<span class="hljs-variable">$BUCKET_NAME</span>/public_key.pub"</span>


<span class="hljs-keyword">if</span> [[ <span class="hljs-string">"<span class="hljs-variable">$OSTYPE</span>"</span> == <span class="hljs-string">"darwin"</span>* ]]; <span class="hljs-keyword">then</span>
  pbcopy &lt; id_rsa_buildkite.pub
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"public key contents copied in clipboard."</span>
<span class="hljs-keyword">else</span>
  cat id_rsa_buildkite.pub
<span class="hljs-keyword">fi</span>

<span class="hljs-comment"># Move SSH Keys to ~/.ssh directory</span>
mv ./<span class="hljs-variable">$KEY</span>* ~/.ssh
chmod 600 ~/.ssh/<span class="hljs-variable">$KEY</span>
chmod 644 ~/.ssh/<span class="hljs-variable">$KEY</span>.pub

<span class="hljs-built_in">cd</span> <span class="hljs-variable">$CURRENT_DIR</span>
</code></pre>
<p>The above script creates an S3 bucket that is used to store the ssh keys. Buildkite uses this key to connect to the Github repo. The script also generates an ssh key and sets its permission correctly.</p>
<h3 id="heading-run-the-script">Run the script</h3>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-113.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The script copies the generated public and private keys to the <code>~/.ssh</code> folder. These keys can be used later to ssh into the EC2 instance, running the Buildkite agent for debugging.</p>
<p>Next, verify that the bucket exists and the keys are present in the new S3 bucket.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-114.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Navigate to <a target="_blank" href="https://github.com/settings/keys">https://github.com/settings/keys</a>, add a new SSH key, then paste in the contents of <code>id_rsa_buildkite.pub</code> .</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-115.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-deploy-aws-elastic-ci-cloudformation-stack">Deploy AWS Elastic CI Cloudformation Stack</h3>
<p>The folks at Buildkite have created the <a target="_blank" href="https://github.com/buildkite/elastic-ci-stack-for-aws"><strong>Elastic CI Stack for AWS</strong></a><strong>,</strong> which creates a private, autoscaling Buildkite Agent cluster in AWS. Let's deploy the infrastructure to our AWS Account.</p>
<p>Create a new file <code>bin/deploy-ci-stack</code> and copy the contents of the following script in it.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -euo pipefail

[ -z <span class="hljs-variable">$BUILDKITE_AGENT_TOKEN</span> ] &amp;&amp; { <span class="hljs-built_in">echo</span> <span class="hljs-string">"BUILDKITE_AGENT_TOKEN is not set."</span>; <span class="hljs-built_in">exit</span> 1;}

CURRENT_DIR=$(<span class="hljs-built_in">pwd</span>)
ROOT_DIR=<span class="hljs-string">"<span class="hljs-subst">$( dirname <span class="hljs-string">"<span class="hljs-variable">${BASH_SOURCE[0]}</span>"</span> )</span>"</span>/..
PARAMETERS=$(cat ./bin/stack-config | envsubst)

<span class="hljs-built_in">cd</span> <span class="hljs-variable">$ROOT_DIR</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"downloading elastic ci stack template.."</span>
curl -s https://s3.amazonaws.com/buildkite-aws-stack/latest/aws-stack.yml -O

aws cloudformation deploy \
  --capabilities CAPABILITY_NAMED_IAM \
  --template-file ./aws-stack.yml \
  --stack-name <span class="hljs-string">"buildkite-elastic-ci"</span> \
  --parameter-overrides <span class="hljs-variable">$PARAMETERS</span>

rm -f aws-stack.yml

<span class="hljs-built_in">cd</span> <span class="hljs-variable">$CURRENT_DIR</span>
</code></pre>
<p>You can get the <code>BUILDKITE_AGENT_TOKEN</code> from the <strong>Agents</strong> tab in Buildkite's Console.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-116.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Next, create a new file called <code>bin/stack-config</code>. Configuration in this file overrides the Cloudformation parameters. The complete list of parameters is available in the <a target="_blank" href="https://s3.amazonaws.com/buildkite-aws-stack/latest/aws-stack.yml">Cloudformation template</a> used by Elastic CI.</p>
<p>On line 2, replace the bucket name with the bucket created earlier.</p>
<pre><code class="lang-bash">BuildkiteAgentToken=<span class="hljs-variable">$BUILDKITE_AGENT_TOKEN</span>
SecretsBucket=buildkite-secrets-adikari
InstanceType=t2.micro
MinSize=0
MaxSize=3
ScaleUpAdjustment=2
ScaleDownAdjustment=-1
</code></pre>
<p>Next, run the script in the CLI to deploy the Cloudformation stack.</p>
<pre><code class="lang-bash">./bin/deploy-ci-stack
</code></pre>
<p>The script will take some time to finish. Open up the AWS Cloudformation console to view the progress.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-117.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-118.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The Cloudformation stack would have created an Autoscaling Group that Buildkite will use to spawn up EC2 instances. The Buildkite Agents and the builds run inside those EC2 instances.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-119.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-create-build-pipelines-in-bulidkite">Create build pipelines in Bulidkite</h3>
<p>At this point, we have the infrastructure ready that's required to run Buildkite. Next, we configure Buildkite and create some Pipelines.</p>
<p>Create an nAPI Access Token at <a target="_blank" href="https://buildkite.com/user/api-access-tokens">https://buildkite.com/user/api-access-tokens</a> and set the scope to <code>write_builds</code>, <code>read_pipelines</code>, and <code>write_pipelines</code>. More information about agent tokens is in this <a target="_blank" href="https://buildkite.com/docs/agent/v3/tokens">document</a>.</p>
<p>Ensure the <code>BUILDKITE_API_TOKEN</code> is set on the environment. Either use <a target="_blank" href="https://www.npmjs.com/package/dotenv">dotenv</a> or export it to the environment before running the script.</p>
<p>Copy the contents of the following script to <code>bin/create-pipeline</code>. Pipelines can be created manually in the Buildkite Console, but it is always better to automate and create reproducible infrastructure.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

<span class="hljs-built_in">set</span> -euo pipefail

<span class="hljs-built_in">export</span> SERVICE=<span class="hljs-string">"."</span>
<span class="hljs-built_in">export</span> PIPELINE_TYPE=<span class="hljs-string">""</span>
<span class="hljs-built_in">export</span> REPOSITORY=git@github.com:adikari/buildkite-docker-example.git

CURRENT_DIR=$(<span class="hljs-built_in">pwd</span>)
ROOT_DIR=<span class="hljs-string">"<span class="hljs-subst">$( dirname <span class="hljs-string">"<span class="hljs-variable">${BASH_SOURCE[0]}</span>"</span> )</span>"</span>/..
STATUS_CHECK=<span class="hljs-literal">false</span>
BUILDKITE_ORG_SLUG=adikari <span class="hljs-comment"># update to your buildkite org slug</span>

USAGE=<span class="hljs-string">"USAGE: <span class="hljs-subst">$(basename <span class="hljs-string">"<span class="hljs-variable">$0</span>"</span>)</span> [-s|--service] service_name [-t|--type] pipeline_type
Eg: create-pipeline --type pull-request
    create-pipeline --type merge --service foo-service
    create-pipeline --type merge --status-checks
NOTE: BUILDKITE_API_TOKEN must be set in environment
ARGUMENTS:
    -t | --type           buildkite pipeline type &lt;merge|pull-request|deploy&gt; (required)
    -s | --service        service name (optional, default: deploy root pipeline)
    -r | --repository     github repository url (optional, default: buildkite-docker-example)
    -c | --status-checks      enable github status checks (optional, default: true)
    -h | --help           show this help text"</span>

[ -z <span class="hljs-variable">$BUILDKITE_API_TOKEN</span> ] &amp;&amp; { <span class="hljs-built_in">echo</span> <span class="hljs-string">"BUILDKITE_API_TOKEN is not set."</span>; <span class="hljs-built_in">exit</span> 1;}

<span class="hljs-keyword">while</span> [ <span class="hljs-variable">$#</span> -gt 0 ]; <span class="hljs-keyword">do</span>
    <span class="hljs-keyword">if</span> [[ <span class="hljs-variable">$1</span> =~ <span class="hljs-string">"--"</span>* ]]; <span class="hljs-keyword">then</span>
        <span class="hljs-keyword">case</span> <span class="hljs-variable">$1</span> <span class="hljs-keyword">in</span>
            --<span class="hljs-built_in">help</span>|-h) <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$USAGE</span>"</span>; <span class="hljs-built_in">exit</span>; ;;
            --service|-s) SERVICE=<span class="hljs-variable">$2</span>;;
            --<span class="hljs-built_in">type</span>|-t) PIPELINE_TYPE=<span class="hljs-variable">$2</span>;;
            --repository|-r) REPOSITORY=<span class="hljs-variable">$2</span>;;
            --status-check|-c) STATUS_CHECK=<span class="hljs-variable">${2:-true}</span>;;
        <span class="hljs-keyword">esac</span>
    <span class="hljs-keyword">fi</span>
    <span class="hljs-built_in">shift</span>
<span class="hljs-keyword">done</span>

[ -z <span class="hljs-string">"<span class="hljs-variable">$PIPELINE_TYPE</span>"</span> ] &amp;&amp; { <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$USAGE</span>"</span>; <span class="hljs-built_in">exit</span> 1; }

<span class="hljs-built_in">export</span> PIPELINE_NAME=$([ <span class="hljs-variable">$SERVICE</span> == <span class="hljs-string">"."</span> ] &amp;&amp; <span class="hljs-built_in">echo</span> <span class="hljs-string">""</span> || <span class="hljs-built_in">echo</span> <span class="hljs-string">"<span class="hljs-variable">$SERVICE</span>-"</span>)<span class="hljs-variable">$PIPELINE_TYPE</span>

BUILDKITE_CONFIG_FILE=.buildkite/pipelines/<span class="hljs-variable">$PIPELINE_TYPE</span>.json
[ ! -f <span class="hljs-string">"<span class="hljs-variable">$BUILDKITE_CONFIG_FILE</span>"</span> ] &amp;&amp; { <span class="hljs-built_in">echo</span> <span class="hljs-string">"Invalid pipeline type: File not found <span class="hljs-variable">$BUILDKITE_CONFIG_FILE</span>"</span>; <span class="hljs-built_in">exit</span>; }

BUILDKITE_CONFIG=$(cat <span class="hljs-variable">$BUILDKITE_CONFIG_FILE</span> | envsubst)

<span class="hljs-keyword">if</span> [ <span class="hljs-variable">$STATUS_CHECK</span> == <span class="hljs-string">"false"</span> ]; <span class="hljs-keyword">then</span>
  pipeline_settings=<span class="hljs-string">'{ "provider_settings": { "trigger_mode": "none" } }'</span>
  BUILDKITE_CONFIG=$((echo <span class="hljs-variable">$BUILDKITE_CONFIG</span>; echo <span class="hljs-variable">$pipeline_settings</span>) | jq -s add)
fi
cd <span class="hljs-variable">$ROOT_DIR</span>
echo "Creating <span class="hljs-variable">$PIPELINE_TYPE</span> pipeline.."
RESPONSE=$(curl -s POST "https://api.buildkite.com/v2/organizations/<span class="hljs-variable">$BUILDKITE_ORG_SLUG</span>/pipelines" \
  -H "Authorization: Bearer <span class="hljs-variable">$BUILDKITE_API_TOKEN</span>" \
  -d "<span class="hljs-variable">$BUILDKITE_CONFIG</span>"
)
[[ "<span class="hljs-variable">$RESPONSE</span>" == *errors* ]] &amp;&amp; { echo <span class="hljs-variable">$RESPONSE</span> | jq; exit <span class="hljs-number">1</span>; }
echo <span class="hljs-variable">$RESPONSE</span> | jq
WEB_URL=$(echo <span class="hljs-variable">$RESPONSE</span> | jq -r '.web_url')
WEBHOOK_URL=$(echo <span class="hljs-variable">$RESPONSE</span> | jq -r '.provider.webhook_url')
echo "Pipeline url: <span class="hljs-variable">$WEB_URL</span>"
echo "Webhook url: <span class="hljs-variable">$WEBHOOK_URL</span>"
echo "<span class="hljs-variable">$PIPELINE_NAME</span> pipeline created."
cd <span class="hljs-variable">$CURRENT_DIR</span>
unset REPOSITORY
unset PIPELINE_TYPE
unset SERVICE
unset PIPELINE_NAME
</code></pre>
<p>Make the script executable by setting the correct permission (chmod +x). Run <code>./bin/create-pipeline -h</code> in the CLI for help.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-120.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The script uses <a target="_blank" href="https://buildkite.com/docs/apis/rest-api">Buildkite REST API</a> to create the pipelines with the given configuration. The script uses a pipeline configuration defined as a  <code>json</code> document and posts it to the REST API. Pipeline configurations live in the <code>.bulidkite/pipelines</code> folder.</p>
<p>To define the configuration for the <code>pull-request</code> pipeline, create <code>.buildkite/pipelines/pull-request.json</code> with the following content:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"$PIPELINE_NAME"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Pipeline for $PIPELINE_NAME pull requests"</span>,
  <span class="hljs-attr">"repository"</span>: <span class="hljs-string">"$REPOSITORY"</span>,
  <span class="hljs-attr">"default_branch"</span>: <span class="hljs-string">""</span>,
  <span class="hljs-attr">"steps"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"script"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">":buildkite: $PIPELINE_TYPE"</span>,
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"buildkite-agent pipeline upload $SERVICE/.buildkite/$PIPELINE_TYPE.yml"</span>
    }
  ],
  <span class="hljs-attr">"cancel_running_branch_builds"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"skip_queued_branch_builds"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"branch_configuration"</span>: <span class="hljs-string">"!master"</span>,
  <span class="hljs-attr">"provider_settings"</span>: {
    <span class="hljs-attr">"trigger_mode"</span>: <span class="hljs-string">"code"</span>,
    <span class="hljs-attr">"publish_commit_status_per_step"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"publish_blocked_as_pending"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"pull_request_branch_filter_enabled"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"pull_request_branch_filter_configuration"</span>: <span class="hljs-string">"!master"</span>,
    <span class="hljs-attr">"separate_pull_request_statuses"</span>: <span class="hljs-literal">true</span>
  }
}
</code></pre>
<p>Next, create <code>./buildkite/pipelines/merge.json</code> with the following content:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"$PIPELINE_NAME"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Pipeline for $PIPELINE_NAME merge"</span>,
  <span class="hljs-attr">"repository"</span>: <span class="hljs-string">"$REPOSITORY"</span>,
  <span class="hljs-attr">"default_branch"</span>: <span class="hljs-string">"master"</span>,
  <span class="hljs-attr">"steps"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"script"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">":buildkite: $PIPELINE_TYPE"</span>,
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"buildkite-agent pipeline upload $SERVICE/.buildkite/$PIPELINE_TYPE.yml"</span>
    }
  ],
  <span class="hljs-attr">"cancel_running_branch_builds"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"skip_queued_branch_builds"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-attr">"branch_configuration"</span>: <span class="hljs-string">"master"</span>,
  <span class="hljs-attr">"provider_settings"</span>: {
    <span class="hljs-attr">"trigger_mode"</span>: <span class="hljs-string">"code"</span>,
    <span class="hljs-attr">"build_pull_requests"</span>: <span class="hljs-literal">false</span>,
    <span class="hljs-attr">"publish_blocked_as_pending"</span>: <span class="hljs-literal">true</span>,
    <span class="hljs-attr">"publish_commit_status_per_step"</span>: <span class="hljs-literal">true</span>
  }
}
</code></pre>
<p>Finally, create <code>.buildkite/pipelines/deploy.yml</code> with the following content:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"name"</span>: <span class="hljs-string">"$PIPELINE_NAME"</span>,
  <span class="hljs-attr">"description"</span>: <span class="hljs-string">"Pipeline for $PIPELINE_NAME deploy"</span>,
  <span class="hljs-attr">"repository"</span>: <span class="hljs-string">"$REPOSITORY"</span>,
  <span class="hljs-attr">"default_branch"</span>: <span class="hljs-string">"master"</span>,
  <span class="hljs-attr">"steps"</span>: [
    {
      <span class="hljs-attr">"type"</span>: <span class="hljs-string">"script"</span>,
      <span class="hljs-attr">"name"</span>: <span class="hljs-string">":buildkite: $PIPELINE_TYPE"</span>,
      <span class="hljs-attr">"command"</span>: <span class="hljs-string">"buildkite-agent pipeline upload $SERVICE/.buildkite/$PIPELINE_TYPE.yml"</span>
    }
  ],
  <span class="hljs-attr">"provider_settings"</span>: {
    <span class="hljs-attr">"trigger_mode"</span>: <span class="hljs-string">"none"</span>
  }
}
</code></pre>
<p>Now, run the <code>./bin/create-pipeline</code> command to create a pull-request pipeline.</p>
<pre><code class="lang-bash">./bin/create-pipeline --<span class="hljs-built_in">type</span> pull-request --status-checks
./bin/create-pipeline --<span class="hljs-built_in">type</span> merge --status-checks
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-121.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Copy the <code>Webhook url</code> from the console output and create a webhook integration in GitHub. The webhook URL is available in the pipeline settings in the Buildkite console if needed in the future. </p>
<p>We need to configure the webhook only for the <code>pull-request</code> and <code>merge</code> pipelines. All other pipelines are triggered dynamically.</p>
<p>Navigate to the GitHub repository <code>Settings &gt; Webhooks</code> and add a webhook. Select <code>Just the push event</code>, then add the webhook. Repeat this for both pipelines.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-122.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Now in the Buildkite Console, there should be two newly created pipelines. 🎉</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-123.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Next, add GitHub integration to allow Buildkite to send status updates to GitHub. You only need to set up this integration once per account. It is available at <code>Setting &gt; Integrations &gt; Github</code> in the Buildkite Console.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-124.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Next, create the remaining pipelines. These pipelines will be dynamically triggered by the <code>pull-request</code> and <code>merge</code> pipelines, so we do not need to create GitHub integration.</p>
<pre><code class="lang-bash"><span class="hljs-comment"># foo service pipelines</span>
./bin/create-pipeline --<span class="hljs-built_in">type</span> pull-request --service foo-service
./bin/create-pipeline --<span class="hljs-built_in">type</span> merge --service foo-service
./bin/create-pipeline --<span class="hljs-built_in">type</span> deploy --service foo-service

<span class="hljs-comment"># bar service pipelines</span>
./bin/create-pipeline --<span class="hljs-built_in">type</span> pull-request --service bar-service
./bin/create-pipeline --<span class="hljs-built_in">type</span> merge --service bar-service
./bin/create-pipeline --<span class="hljs-built_in">type</span> deploy --service bar-service
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-125.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The Buildkite Console should now have all the pipelines listed. 🥳</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-126.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h3 id="heading-set-up-buildkite-steps">Set up Buildkite Steps</h3>
<p>Now that the pipelines are ready, let's configure steps to run for each pipeline.</p>
<p>Add the following script in <code>.buildkite/diff</code>. This script diffs between all the files changed in a commit against the master branch. The output of the script is used to trigger respective pipelines dynamically.</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/bash</span>

[ <span class="hljs-variable">$#</span> -lt 1 ] &amp;&amp; { <span class="hljs-built_in">echo</span> <span class="hljs-string">"argument is missing."</span>; <span class="hljs-built_in">exit</span> 1; }

COMMIT=<span class="hljs-variable">$1</span>

BRANCH_POINT_COMMIT=$(git merge-base master <span class="hljs-variable">$COMMIT</span>)

<span class="hljs-built_in">echo</span> <span class="hljs-string">"diff between <span class="hljs-variable">$COMMIT</span> and <span class="hljs-variable">$BRANCH_POINT_COMMIT</span>"</span>
git --no-pager diff --name-only <span class="hljs-variable">$COMMIT</span>..<span class="hljs-variable">$BRANCH_POINT_COMMIT</span>
</code></pre>
<p>Change the permission of the script to make it executable.</p>
<pre><code class="lang-bash">chmod +x .buildkite/diff
</code></pre>
<p>Create a new file <code>.buildkite/pullrequest.yml</code> and add the following step configuration. We use the <a target="_blank" href="https://github.com/chronotc/monorepo-diff-buildkite-plugin">buildkite-monorepo-diff</a> plugin to run the <code>diff</code> script and automatically upload and trigger the respective pipelines.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Triggering pull request pipeline"</span>
    <span class="hljs-attr">plugins:</span>
      <span class="hljs-string">chronotc/monorepo-diff#v1.1.1:</span>
        <span class="hljs-attr">diff:</span> <span class="hljs-string">".buildkite/diff ${BUILDKITE_COMMIT}"</span>
        <span class="hljs-attr">wait:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">watch:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">"foo-service"</span>
            <span class="hljs-attr">config:</span>
              <span class="hljs-attr">trigger:</span> <span class="hljs-string">"foo-service-pull-request"</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">"bar-service"</span>
            <span class="hljs-attr">config:</span>
              <span class="hljs-attr">trigger:</span> <span class="hljs-string">"bar-service-pull-request"</span>
</code></pre>
<p>Now create the configuration for the merge pipeline by adding the following content in <code>.buildkite/merge.yml</code>.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Triggering merge pipeline"</span>
    <span class="hljs-attr">plugins:</span>
      <span class="hljs-string">chronotc/monorepo-diff#v1.1.1:</span>
        <span class="hljs-attr">diff:</span> <span class="hljs-string">"git diff --name-only HEAD~1"</span>
        <span class="hljs-attr">wait:</span> <span class="hljs-literal">false</span>
        <span class="hljs-attr">watch:</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">"foo-service"</span>
            <span class="hljs-attr">config:</span>
              <span class="hljs-attr">trigger:</span> <span class="hljs-string">"foo-service-merge"</span>
          <span class="hljs-bullet">-</span> <span class="hljs-attr">path:</span> <span class="hljs-string">"bar-service"</span>
            <span class="hljs-attr">config:</span>
              <span class="hljs-attr">trigger:</span> <span class="hljs-string">"bar-service-merge"</span>
</code></pre>
<p>At this point, we have configured the topmost level <code>pull-request</code> and <code>merge</code> pipelines. Now we need to configure individual pipelines for each service.</p>
<p>We'll configure pipelines for <code>foo-service</code> first. Create <code>foo-service/.buildkite/pull-request.yml</code> with the following content. When the <code>pull-request</code> pipeline for foo service runs, specify that the <code>lint</code> and <code>test</code> commands should run. The <code>command</code> option can also trigger other scripts.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Foo service pull request"</span>
    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"echo linting"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"echo testing"</span>
</code></pre>
<p>Next, setup a merge pipeline for the foo service by adding the following content in <code>foo-service/.buildkite/merge.yml</code>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Run sanity checks"</span>
    <span class="hljs-attr">command:</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"echo linting"</span>
      <span class="hljs-bullet">-</span> <span class="hljs-string">"echo testing"</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Deploy to staging"</span>
    <span class="hljs-attr">trigger:</span> <span class="hljs-string">"foo-deploy"</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">env:</span>
        <span class="hljs-attr">STAGE:</span> <span class="hljs-string">"staging"</span>

  <span class="hljs-bullet">-</span> <span class="hljs-string">wait</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">block:</span> <span class="hljs-string">":rocket: Release to Production"</span>

  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Deploy to production"</span>
    <span class="hljs-attr">trigger:</span> <span class="hljs-string">"foo-deploy"</span>
    <span class="hljs-attr">build:</span>
      <span class="hljs-attr">env:</span>
        <span class="hljs-attr">STAGE:</span> <span class="hljs-string">"production"</span>
</code></pre>
<p>When the <code>foo-service-merge</code> pipeline runs, here is what happens:</p>
<ol>
<li>The pipeline runs the sanity check.</li>
<li>Then <code>foo-deploy</code> pipeline is dynamically triggered. We pass the <code>STAGE</code> environment to identify which environment to run the deployment against.</li>
<li>Once the deployment to staging is complete, the pipeline is blocked and the following pipeline is not triggered automatically. The pipeline can be resumed by pressing the “Release to Production” button.</li>
<li>Unblocking the pipeline triggers <code>foo-deploy</code> pipeline again, but this time with <code>production</code> stage.</li>
</ol>
<p>Finally, add configuration for the <code>foo-deploy</code> pipeline by adding <code>foo-service/.buildkite/deploy.yml</code>. In the deploy configuration, we trigger a bash script and pass the <code>STAGE</code> variable which was received from the <code>foo-service-merge</code> pipeline.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">steps:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-attr">label:</span> <span class="hljs-string">"Deploying foo service to ${STAGE}"</span>
    <span class="hljs-attr">command:</span> <span class="hljs-string">"./foo-service/bin/deploy ${STAGE}"</span>
</code></pre>
<p>Now, create the deploy script <code>foo-service/bin/deploy</code> and add the following content:</p>
<pre><code class="lang-yaml"><span class="hljs-comment">#!/bin/bash</span>

<span class="hljs-string">set</span> <span class="hljs-string">-euo</span> <span class="hljs-string">pipefail</span>

<span class="hljs-string">STAGE=$1</span>

<span class="hljs-string">echo</span> <span class="hljs-string">"Deploying foo service to $STAGE"</span>
</code></pre>
<p>Make the deploy script executable like this:</p>
<pre><code class="lang-bash">chmod +x ./foo-service/bin/deploy
</code></pre>
<p>The pipeline and steps configuration for <code>foo-service</code> are now complete. Repeat all the above steps above to configure pipelines for <code>bar service</code>.</p>
<h3 id="heading-test-the-overall-workflow">Test the overall workflow</h3>
<p>We have configured Buildkite and GitHub and we've set up the appropriate infrastructure to run the builds. Next, test the entire workflow and see it in action.</p>
<p>To test the workflow, start by creating a new branch and modifying some file in <code>foo-service</code>. Push the changes to GitHub and create a Pull Request.</p>
<pre><code class="lang-bash">git checkout -b change-foo-service
<span class="hljs-built_in">cd</span> foo-service &amp;&amp; touch test.txt
<span class="hljs-built_in">echo</span> testing &gt;&gt; test.txt
git add .
git commit -m <span class="hljs-string">'making some change'</span>
git push origin master
</code></pre>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-127.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Pushing changes to GitHub should trigger the <code>pull-request</code> pipeline in Buildkite, which then triggers the <code>foo-service-pull-request</code> pipeline. </p>
<p>GitHub should report the status in GitHub checks. You can enable GitHub's branch protection to require the checks to pass before merging the Pull Request.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-128.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>Once all the checks have passed in GitHub, merge the Pull Request. This merge will trigger the <code>merge</code> pipeline in Buildkite.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-129.png" alt="Image" width="600" height="400" loading="lazy"></p>
<p>The changes in the foo service are detected, and <code>foo-service-merge</code> pipeline is triggered. The pipeline will eventually be blocked when the <code>foo-service-deploy</code> runs against the staging environment. </p>
<p>Unblock the pipeline by manually clicking the <code>Release to Production</code> button to run deployment against production.</p>
<p><img src="https://www.freecodecamp.org/news/content/images/2021/03/image-130.png" alt="Image" width="600" height="400" loading="lazy"></p>
<h2 id="heading-summary">Summary</h2>
<p>In this post, we set up a continuous integration pipeline for a monorepo using Buildkite, Github, and AWS. </p>
<p>The pipeline gets our code from the development machine to staging, then to production. The build agents and steps run in autoscaled AWS EC2 instances. </p>
<p>We also created a bunch of bash scripts to create easily reproducible versions of this setup. </p>
<p>As an improvement to the current design, consider using the <a target="_blank" href="https://github.com/buildkite-plugins/docker-compose-buildkite-plugin">buildkite-docker-compose-plugin</a> to isolate the builds in Docker containers.</p>
<p><em>Follow me on</em> <a target="_blank" href="https://twitter.com/adikari"><em>Twitter</em></a> <em>or check out my projects on</em> <a target="_blank" href="https://github.com/adikari"><em>Github</em></a><em>.</em></p>
 ]]>
                </content:encoded>
            </item>
        
            <item>
                <title>
                    <![CDATA[ Essentials of monorepo development ]]>
                </title>
                <description>
                    <![CDATA[ By Ovidiu Bute The word monorepo is a combination between “mono”, as in the Greek word mónos (in translation, alone) and an abbreviation of the word repository. A simple concept if taken verbatim: one lonely repository. The domain is software enginee... ]]>
                </description>
                <link>https://www.freecodecamp.org/news/monorepo-essentials/</link>
                <guid isPermaLink="false">66d4608cc7632f8bfbf1e471</guid>
                
                    <category>
                        <![CDATA[ Code Quality ]]>
                    </category>
                
                    <category>
                        <![CDATA[ development ]]>
                    </category>
                
                    <category>
                        <![CDATA[ monorepo ]]>
                    </category>
                
                    <category>
                        <![CDATA[ scalability ]]>
                    </category>
                
                <dc:creator>
                    <![CDATA[ freeCodeCamp ]]>
                </dc:creator>
                <pubDate>Thu, 13 Jun 2019 18:51:48 +0000</pubDate>
                <media:content url="https://cdn-media-2.freecodecamp.org/w1280/5f9ca20a740569d1a4ca522a.jpg" medium="image" />
                <content:encoded>
                    <![CDATA[ <p>By Ovidiu Bute</p>
<p>The word monorepo is a combination between “<em>mono</em>”, as in the Greek word <em>mónos</em> (in translation, <strong>alone</strong>) and an abbreviation of the word <strong>repository</strong>. A simple concept if taken verbatim: one lonely repository. The domain is software engineering so we’re referring to a home for source code, multimedia assets, binary files, and so on. But this definition is just the tip of the iceberg, since a monorepo in practice is so much more.</p>
<p>In this article I plan to distill the pros and cons of having every piece of code your company owns in the same repository. At the end you should have a good idea about why you should consider working like this, what challenges you’ll face, what problems it’ll solve, and how much you’ll need to invest in it.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*pCRpcpi3mLE2I-e4FOnp5w.png" alt="Image" width="600" height="400" loading="lazy">
<em>Relative interest in the term “monorepo” since 2004, source: Google Trends</em></p>
<p>The term itself, as visible in the chart above, looks to be as new as 2017. However it would be a mistake to think that previously nobody was storing all of their code in one place. In fact during my first job back in 2009, the company I worked at stored every project in a single SVN repository, one directory per project. Indeed you may well be able to trace this practice back even further. But how can we explain the recent explosive popularity, then?</p>
<p>The reality is that storing code in a single spot is not the main selling point. In the past years the major tech companies — Google, Facebook, or Dropbox have been showing off their way of working together within the same repository at massive scale. Organizations of tens of thousands of engineers collaborating within one repository is an awesome sight. And a difficult engineering problem. So difficult in fact that these companies invest a lot of money into tools and systems that allow developers to work productively. These systems in turn have solved problems that you may not even realize you had. This is what fascinates people during tech talks. This is what’s been driving searches since 2017.</p>
<ul>
<li>Front-end development at Google, Alex Eagle: <a target="_blank" href="https://medium.com/@Jakeherringbone/you-too-can-love-the-monorepo-d95d1d6fcebe">https://medium.com/@Jakeherringbone/you-too-can-love-the-monorepo-d95d1d6fcebe</a></li>
<li>Google monorepo presentation, Rachel Potvin: <a target="_blank" href="https://www.youtube.com/watch?v=W71BTkUbdqE">https://www.youtube.com/watch?v=W71BTkUbdqE</a></li>
<li>Scaling Mercurial to the size of Facebook’s codebase, Durham Goode: <a target="_blank" href="https://code.fb.com/core-data/scaling-mercurial-at-facebook/">https://code.fb.com/core-data/scaling-mercurial-at-facebook/</a></li>
</ul>
<p>I’ve identified a few core features that a Google or a Facebook vetted monorepo offers. This is surely not an exhaustive list, but it’s a great starting point. When discussing each of one of these points, I took into consideration what life looks like without them, and what exactly do they solve. Certainly in our field of work everything is a trade-off, nothing’s free. For every pro that I list someone will find use-cases that directly contradict me, butI’m OK with that.</p>
<h4 id="heading-all-your-code-regardless-of-language-is-located-in-one-repository">All your code, regardless of language, is located in one repository</h4>
<p>The first advantage of storing everything in once place may not be immediately obvious, but as a developer, simply being able to freely browse through everything is of great impact. It helps foster a sort of team spirit and is also a very valuable and cheap way to distribute information. Have you ever asked yourself what projects are in development at your company? Past and present? Curious what a certain team is up to? How have they solved a particular engineering problem? How are they writing unit-tests?</p>
<p>In direct opposition to the monorepo we have the <strong>multirepo</strong> structure. Each project or module gets its own separate space. In such a system developers can spend quite a bit of time getting answers to the questions I listed above. The distributed nature of the work means there’s no single source of information that you can subscribe to.</p>
<p>There are companies that have transitioned from a multi to a monorepo layout by following only this feature from my list. Such a structure should not be confused with the topic of this article though. I’d define it instead as a <strong>collocated multirepo.</strong> Yes, everything is in one place, but the rest of the features on this list are far more interesting.</p>
<h4 id="heading-youre-able-to-organize-dependencies-between-modules-in-a-controlled-and-explicit-way">You‘re able to organize dependencies between modules in a controlled and explicit way</h4>
<p>The traditional, battle tested way of handling dependencies is by publishing versions to a separate storage system from continuous integration systems, or even manually, from development machines. These are versioned (or tagged) to make it easier to search later on. Now in a multirepo setup, each project has a set of dependencies of external origins (third parties) or internal, as in, published from inside the same company.</p>
<p>In order for one team to depend on another one’s code, everything needs to pass through a dependency management storage system. Examples of this are npm, MavenCentral, or PyPi. I said earlier that you can easily build a collocated multirepo just by storing everything in one place. Such a system is <strong>indirectly observable.</strong> Let’s examine why that’s important.</p>
<p>As developers, our time is split very unequally between reading and writing code. Now imagine having to debug an issue that has its root cause inside of a dependency. We can rule out third parties here, since that’s a difficult problem as it is. No, this problem occurs in a package published by another team in your company. If your project depends on the latest version, you’re in luck! Just navigate to the respective directory and grab a cup of coffee.</p>
<blockquote>
<p>“Indeed, the ratio of time spent reading versus writing is well over 10 to 1. We are constantly reading old code as part of the effort to write new code. …[Therefore,] making it easy to read makes it easier to write.”</p>
</blockquote>
<p>― Robert C. Martin, <a target="_blank" href="https://www.goodreads.com/work/quotes/3779106">Clean Code: A Handbook of Agile Software Craftsmanship</a></p>
<p>More often though you might depend on an older version. So now what do you do? Do you try and use your VCS to read through the older code? Do you try and read the actual artifact instead of the original code? What if it’s minified, as is usually the case with JavaScript?</p>
<p>Contrast this with Google’s system, for example — since code dependencies are direct, as in, there are essentially no versions anywhere, one can say the system is <strong>directly observable</strong>. The code you’re looking at is pretty much your entire world. I say mostly because of course there are always going to be minor exceptions to this rule, such as external dependencies that would be prohibitive to host yourself. But that shouldn’t take anything away from this discussion.</p>
<hr>
<p>While we’re on the topic of dependency management we should touch upon the subject of restrictions. Imagine a project where you’re able to depend on any source file you need. Nothing is off limits, you can import anything. For those of you that started their careers at least 10 years ago, this sounds like business as usual for the time. This is an almost complete definition of a <strong>monolith</strong>.</p>
<p>The name implies grandeur, scale, but more importantly, singularity. Practically every source file inside of a monolith cannot live outside of it. There’s a fundamental reason for this is relevant to our discussion: you don’t have an explicit and audit-able way of managing dependencies inside of a monolith. Everything is up for grabs, and it feels free and cheap. So naturally, developers end up creating a complex graph of imports and includes.</p>
<p>Nowadays practically everyone is doing microservices, there can be little doubt about that. Given sufficient scale, a codebase becomes a beast, as everything is inexorably linked to each other. I’m sure many developers will provide counter-arguments that monoliths can be managed in a clean, reasonable way without falling into this trap. But exceptions simply reinforce the initial statement. Microservices solve this by defining clear boundaries and responsibilities, and a monorepo is a natural extension of this philosophy. Typically modules offer a set of public exports, or APIs, and other modules are only able to use those as part of their contracts.</p>
<h4 id="heading-software-modules-reuse-common-infrastructure">Software modules reuse common infrastructure</h4>
<p>This is a topic that’s very near and dear to my heart. I’ll define <em>infrastructure</em> in this context, that of a software codebase, as the essential tools necessary to ensure productivity and code quality.</p>
<p>One of the reasons why I think betting your company on multirepos is a mistake has to do with a set of basic requirements any software engineering project should meet:</p>
<ul>
<li>A build system to be able to reliably produce a deliverable artifact.</li>
<li>A way to run automated tests.</li>
<li>A way to statically analyze code for common mistakes, potential bugs, and enforce best practices.</li>
<li>A way to install and manage third party dependencies, i.e. software modules which are external to your company.</li>
</ul>
<p>If you have your code split in multiple repositories, <strong>you need to replicate this work everywhere</strong>. Don’t underestimate how much work this involves! All of the features listed above require at the very minimum a set of configuration files which need to be maintained in perpetuity. Having them copied across more than two places basically guarantees you will always generate technical debt.</p>
<p>I know that some companies go to extreme lengths to minimize the impact of this. They’ll have their configurations bundled as scaffolding (<em>a la</em> create-react-app or yeoman), and use them to setup new repositories. But as we’ve seen in the section before this one, there’s no way to enforce that everyone’s on the latest version of these boilerplate dependencies! The amount of time spent upgrading each repository individually increases linearly in large codebases. Given sufficient scale, practically all published versions of an internal package will be depended on at the same time!</p>
<p>There’s a quote I absolutely love that relates to this conundrum:</p>
<blockquote>
<p>At scale, statistics are not your friend. The more instances of anything you have, the higher the likelihood one or more of them will break. Probably at the same time.</p>
</blockquote>
<p>— <a target="_blank" href="https://thenewstack.io/distributed-systems-hard/">Anne Curie</a></p>
<p>If you think distributed systems just refers to web services, I would disagree. Your codebase is an interconnected, living system. Tens, hundreds, or thousands of engineers are racing to get their code into production each day, all the while struggling to keep the build green and the code quality up. If anything, to me this sounds even scarier than a set of microservices :)</p>
<h4 id="heading-changes-are-always-reflected-throughout-the-entire-repository">Changes are always reflected throughout the entire repository</h4>
<p>This is highly dependent on the rest of the features. It’s one of the benefits that’s easier to understand through example.</p>
<p>Let’s say I work at a company that builds web applications for customers all around the world. Everything is organized into modules, as is exemplified below via the popular open-source project <a target="_blank" href="https://github.com/babel/babel/">Babel</a>. At this company we all use ReactJS for front-end work, and out of pure coincidence, all of our projects are on the same version of it.</p>
<p><img src="https://cdn-media-1.freecodecamp.org/images/1*SURhmpcSs3ZlS4AfRpBqSA.png" alt="Image" width="600" height="400" loading="lazy">
_Babel’s myriad of modules: [https://github.com/babel/babel/tree/master/packages](https://github.com/babel/babel/tree/master/packages" data-href="https://github.com/babel/babel/tree/master/packages" class="markup--anchor markup--figure-anchor" rel="nofollow noopener noopener" target="<em>blank)</em></p>
<p>But the folks at Facebook publish the latest version of React and we realize that upgrading to it is not trivial. To be more productive, we’ve built a library of reusable components that resides as a separate module. All projects depend on it. This new React version brings lots of breaking changes that affect it. What options do we have for doing the upgrade?</p>
<p>This is typically where monorepo adversaries would shoot down the entire concept. It’s easy to say that we’ve worked ourselves into a corner and that the multirepo structure would’ve been a superior choice given the circumstances. Indeed in the latter case what we would do is just gradually adopt the new React version in our projects one by one, preceded by a major version upgrade of our core components module.</p>
<p>But I would say this creates more problems than it solves. <strong>A core dependency breaking change release creates a schism in your engineering team</strong>. You now have two cores to maintain: the new one, which is used by a couple, brave teams in a few projects, and the older one, still depended on by almost the entire company.</p>
<p>Let’s take this problem to a bigger scale for further analysis. Our company may have some projects which are still in production, but are just in maintenance mode, and don’t have any active development teams assigned to them. These projects will probably be the last ones to migrate, extending the time window in which you keep working on two cores at the same time. The old version will still receive bugs or security fixes even though it’s deprecated, as you can’t risk your customers’ businesses.</p>
<p>All of this is to say that <strong>a multirepo solution promotes and enables a constant state of technical debt</strong>. There are lots of migrations going on, modules that depend on older versions of other modules, and many, many deprecation policies which may or may not be enforceable.</p>
<p>Let’s now consider an alternative solution to the React upgrade problem. By having all of the code in one place, and dependent on each other directly, without versioning, we’re left with one option: we have to do all of the work upfront, in all modules simultaneously.</p>
<p>If that sounds like a scary proposition, I don’t blame you. It’s terrifying to think about, at first. However the advantage is clear: no migrations, no technical debt, less confusion around the state of our codebase. In practical terms, there is one obstacle to overcome with this solution — there may be hundreds, thousands, or millions of lines of code that need to be changed all at once. By having separate projects we avoid the sheer volume of work by doing it piece by piece. It’s still the same total amount of changes, but we’re naturally inclined to think it would be easier to do that over time, rather than in one push.</p>
<p>To solve this last problem large companies have turned to <em>codemods</em> — programmatic transformations of source code that can run at very large scale. There are numerous tutorials out there if you’re interested, but the gist of it is — you write code that first detects certain patterns in your source code, and then applies specific changes to it. To take our React example further, you could write a codemod that replaces a deprecated API with a newer one, and even apply logic changes if necessary. Indeed this is how Facebook recommends you migrate from one version of their library to the next. It’s how they’re doing it internally. Check out their <a target="_blank" href="https://github.com/reactjs/react-codemod">open-source examples</a>.</p>
<p>Viewed from this angle, a migration doesn’t seem as scary as before. You do all of your research upfront, you define how you want to essentially rewrite the affected code, and apply the changes more or less all at once. This to me is a robust solution. I’ve seen it in action, it can be done. It’s indeed amazing when it works and lately more and more companies are adopting it.</p>
<h4 id="heading-drawbacks">Drawbacks</h4>
<p>The old adage of <em>“there’s no such thing as a free lunch”</em> certainly applies here, as well. I’ve talked about a lot of pros, but there are some cons which you need to think about.</p>
<p>Given that everyone is working in the same place, and everything is interconnected, <strong>tests</strong> become the blood of the whole system. Trying to make a change that impacts potentially thousands of lines of code (or more) without the safety net of automated tests is simply not possible.</p>
<p>Why is this any different from traditional ways of storing code? I’d say that versioned modules hide this particular problem, at the expense of creating technical debt. If you own a module that depends on another team’s code, by way of a strict version number, then you’re in charge of upgrading it. If you don’t have sufficient test coverage, you’ll err on the side of caution and simply <strong>delay upgrading</strong> until you’re confident the module doesn’t affect your own project. As we’ve discussed earlier, this has a serious long term consequences, but it’s a viable strategy nonetheless. Especially if your business doesn’t actually promote long term projects.</p>
<p>We mentioned the benefit of every contributor being able to access all of the source code in your organization. If we flip that around, this can also be a problem for some types of work. There’s no easy way you can restrict access to projects. This is important if you consider government or military contracts as they typically have strict security requirements.</p>
<p>Finally let’s consider continuous integration. You may be using a system such as Jenkins, Travis, or CircleCI, to manage the way your code is tested and delivered to customers. When you have more than one repository you typically set up one pipeline for each. Some teams even go further and have one dedicated CI instance per project. This is a flexible system that can adapt to the needs of each team. Your billing team may deploy to production once a week, while your web team would move faster and deploy multiple times a day.</p>
<p>If you’re considering moving to a monorepo, be wary of your CI system’s capabilities. It will have to do <strong>a lot of work</strong>. Simple tasks such as checking out the code, or building an artifact may become long running tasks which impact productivity. Google developed and runs its own custom CI solution, and for good reason. Nothing available on the market was good enough.</p>
<p>Now before you conclude that this is a blocker, I’d recommend you carefully analyse your project and the tools you use. If you’re using git, for example, there’s a myth going around that it can’t handle big repositories. This is demonstrably inaccurate, as best exemplified by the project that inspired git in the first place, the Linux Kernel.</p>
<p>Make your own research and see how many files and lines of code you have, and try to predict how much your project will grow. If you’re nowhere near the scale of the Kernel, then you’re OK. You could also make the point that git isn’t very good at storing binaries. <a target="_blank" href="https://git-lfs.github.com/">LFS</a> aims to solve that. You can also rewrite your history to delete old binaries in order to optimize performance.</p>
<p>In a similar vein, open-source CI systems are much more powerful than you think. Jenkins for example can scale to hundreds of jobs, dozens of workers, and can serve the needs of a large team with ease. Can it do Google scale? Absolutely not! But do you have <strong>tens of thousands</strong> of engineers pushing to production every day? The plateau at which these tools stop performing is so high, it’s not worth thinking about until you’re close to it. And chances are, you’ll know when you’re getting close.</p>
<p>And finally, there’s cost. You’ll need at least one dedicated team to pull this off. Because the amount of work is certainly not trivial, and it demands passion and focus. This team will need to, and I’m just summarizing here, build and maintain in perpetuity what is essentially a platform that stores code, assets, build artifacts, reusable development infrastructure for running tests or static analysis, and a CI system able to withstand large workloads and traffic. If this sounds scary, it’s because it is. But you’ll have no problems convincing developers to join such a team, it’s the type of experience that’s hard to accumulate by doing side-projects at home.</p>
<h4 id="heading-in-closing">In closing</h4>
<p>I’ve talked about the many advantages of working in a monorepo, the drawbacks, and touched upon the costs. This setup is not for everyone. I wouldn’t encourage you to try it out without first evaluating exactly what your problems and your business requirements look like. And of course, do go through all of the possible alternatives before deciding.</p>
 ]]>
                </content:encoded>
            </item>
        
    </channel>
</rss>
