C

CULTIVA Platform — Turborepo Setup

Monorepo con cache local + remota · 4 apps · 3 packages compartidos

Turbo 2.x npm workspaces Vercel Remote Cache
Build sin cache 12 min CI anterior
Build con cache 47 seg hit rate ~94%
Ahorro CI/mes ~18h 150 PRs × 7 min
Paquetes en repo 7 4 apps + 3 packages
Cache backend Dual Vercel + self-hosted
{ }
turbo.json — Raíz del monorepo Pipeline principal
{
  "$schema": "https://turbo.build/schema.json",
  "globalDependencies": [
    ".env",
    ".env.local"
  ],
  "globalEnv": [
    "NODE_ENV",
    "VERCEL_URL"
  ],
  "remoteCache": {
    "signature": true
  },
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": [
        "dist/**",
        ".next/**",
        "!.next/cache/**"
      ],
      "inputs": [
        "$TURBO_DEFAULT$",
        "!**/*.md",
        "!**/*.test.*"
      ],
      "env": [
        "API_URL",
        "NEXT_PUBLIC_*"
      ]
    },
    "test": {
      "dependsOn": ["^build"],
      "outputs": ["coverage/**"],
      "inputs": [
        "src/**",
        "tests/**",
        "*.config.*"
      ],
      "env": ["CI", "NODE_ENV"]
    },
    "lint": {
      "outputs": [],
      "cache": true
    },
    "typecheck": {
      "dependsOn": ["^build"],
      "outputs": []
    },
    "dev": {
      "cache": false,
      "persistent": true
    },
    "deploy": {
      "dependsOn": [
        "build",
        "test",
        "lint"
      ],
      "outputs": [],
      "cache": false
    },
    "@cultiva/web#build": {
      "dependsOn": [
        "^build",
        "@cultiva/db#db:generate"
      ],
      "outputs": [".next/**"],
      "env": ["NEXT_PUBLIC_*"]
    },
    "db:generate": {
      "cache": false
    },
    "db:push": {
      "cache": false,
      "dependsOn": ["db:generate"]
    }
  }
}
{ }
package.json — Raíz workspaces npm workspaces
{
  "name": "cultiva-platform",
  "private": true,
  "workspaces": [
    "apps/*",
    "packages/*"
  ],
  "scripts": {
    "build":  "turbo build",
    "dev":    "turbo dev",
    "lint":   "turbo lint",
    "test":   "turbo test",
    "clean":  "turbo clean && rm -rf node_modules",
    "format": "prettier --write \"**/*.{ts,tsx,md}\"",
    "release":
      "turbo build --filter=./packages/* && changeset publish"
  },
  "devDependencies": {
    "turbo":             "^2.0.0",
    "prettier":          "^3.2.0",
    "@changesets/cli":   "^2.26.0"
  },
  "packageManager": "npm@10.5.0"
}


// apps/web/package.json
{
  "name": "@cultiva/web",
  "dependencies": {
    "@cultiva/ui":     "workspace:*",
    "@cultiva/config": "workspace:*",
    "@cultiva/db":     "workspace:*"
  }
}


// packages/ui/package.json
{
  "name": "@cultiva/ui",
  "main":   "./dist/index.js",
  "types":  "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.mjs",
      "require": "./dist/index.js"
    }
  }
}
.github/workflows/ci.yml Filtra paquetes afectados
name: CI — CULTIVA Platform

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

env:
  TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
  TURBO_TEAM:  ${{ vars.TURBO_TEAM }}
  TURBO_REMOTE_ONLY: true

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 2

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

      - name: Install dependencies
        run:  npm ci

      - name: Typecheck
        run: |
          npx turbo typecheck \
            --filter='...[origin/main]'

      - name: Lint
        run: |
          npx turbo lint \
            --filter='...[origin/main]'

      - name: Test
        run: |
          npx turbo test \
            --filter='...[origin/main]'

      - name: Build
        run: |
          npx turbo build \
            --filter='...[origin/main]'

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name:  coverage
          path:  "**/coverage/**"

  deploy-preview:
    needs: ci
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Vercel preview
        run: |
          npx vercel --token $VERCEL_TOKEN \
            --scope cultiva-ia
        env:
          VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
TS
cache-server/src/index.ts Railway · Express · S3-compat
import express from "express";
import { S3Client, GetObjectCommand,
         PutObjectCommand, HeadObjectCommand }
  from "@aws-sdk/client-s3";

const app = express();
const BUCKET = "cultiva-turbo-cache";

const s3 = new S3Client({
  endpoint: process.env.S3_ENDPOINT,
  region:   "auto",
  credentials: {
    accessKeyId:
      process.env.S3_ACCESS_KEY ?? "",
    secretAccessKey:
      process.env.S3_SECRET_KEY ?? ""
  }
});

/** Middleware: Bearer token auth */
app.use((req, res, next) => {
  const auth = req.headers["authorization"];
  if (auth !== `Bearer ${process.env.TURBO_TOKEN}`)
    return res.status(401).end();
  next();
});

/** GET artifact */
app.get("/v8/artifacts/:hash",
  async (req, res) => {
    const key =
      `${req.query.teamId ?? "default"}/${req.params.hash}`;
    try {
      const obj = await s3.send(
        new GetObjectCommand({
          Bucket: BUCKET, Key: key
        })
      );
      res.setHeader(
        "Content-Type",
        "application/octet-stream"
      );
      (obj.Body as NodeJS.ReadableStream).pipe(res);
    } catch {
      res.status(404).end();
    }
  }
);

/** PUT artifact */
app.put("/v8/artifacts/:hash",
  async (req, res) => {
    const key =
      `${req.query.teamId ?? "default"}/${req.params.hash}`;
    const chunks: Buffer[] = [];
    for await (const chunk of req)
      chunks.push(chunk);
    await s3.send(new PutObjectCommand({
      Bucket: BUCKET,
      Key:    key,
      Body:   Buffer.concat(chunks)
    }));
    res.json({ ok: true });
  }
);

/** HEAD artifact */
app.head("/v8/artifacts/:hash",
  async (req, res) => {
    const key =
      `${req.query.teamId ?? "default"}/${req.params.hash}`;
    try {
      await s3.send(
        new HeadObjectCommand({
          Bucket: BUCKET, Key: key
        })
      );
      res.status(200).end();
    } catch {
      res.status(404).end();
    }
  }
);

app.listen(3001, () =>
  console.log("CULTIVA cache-server :3001")
);
Estructura del monorepo + Pipeline de tareas + Comandos de filtrado
Estructura
cultiva-platform/ ├── apps/ │ ├── web/ Next.js · marketing │ ├── dashboard/ Next.js · panel IA │ ├── docs/ Astro · skills docs │ └── cache-server/ Express · Railway ├── packages/ │ ├── ui/ shadcn components │ ├── config/ ESLint · TS · Tailwind │ └── db/ Prisma schema + client ├── turbo.json ├── package.json └── .github/workflows/ci.yml
Tareas del pipeline
build
dependsOn^build
cachetrue
outputsdist/**, .next/**
test
dependsOn^build
cachetrue
outputscoverage/**
lint · typecheck
dependsOn^build
cachetrue
outputs[]
dev
cachefalse
persistenttrue
deploy
dependsOnbuild, test, lint
cachefalse
Filtros de scope
--filter=@cultiva/web Solo web
--filter=@cultiva/web... Web + sus dependencias
--filter=...@cultiva/ui Todos los que usan ui
--filter='...[origin/main]' Solo afectados por PR
--filter='./apps/*' Todas las apps
--filter='!@cultiva/docs' Excluir docs
Debug cache
turbo build --dry-run # qué correría
turbo build --summarize # estado cache
turbo build --verbosity=2 # hashes
turbo build --force # sin cache
turbo build --graph # grafo de tareas
TURBO_LOG_VERBOSITY=debug \
  turbo build --filter=@cultiva/web