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 }}
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