多 Repo 微前端的 CI 统一:巧用 Shell + Turbo 实现“伪 Monorepo”构建流
在微前端架构的演进过程中,很多团队会陷入一个尴尬的境地:为了权限隔离和模块解耦,选择了 Multi-Repo(多仓库);但随着子应用数量增加,维护 N 套几乎相同的 CI/CD 流水线成了一场灾难。
你想用 Turborepo 来做增量构建和缓存优化,但 Turbo 的核心前提是 Workspace。面对散落在不同 Git 地址的独立 Repo,如何既保留多仓的灵活性,又能享受单仓的构建快感?
你尝试过用 Shell 脚本强行组合,却卡在了“工件传递”这一步。本文将分享一种“动态组装影子 Monorepo”的方案,并重点解决 Docker 多阶段构建中的产物同步难题。
一、 核心思路:动态工作区组装(Dynamic Workspace Assembly)
不要试图在每个子仓库里写复杂的构建逻辑,而是建立一个中央 CI 仓库(或者在 CI 运行环境中动态生成)。其核心流程如下:
- 环境初始化:CI 触发后,在一个干净的工作目录下执行 Shell 脚本。
- 散点拉取:根据配置清单,将所有相关的子 Repo 平铺拉取到
packages/目录下。 - 注入灵魂:在根目录动态生成
package.json(包含workspaces配置)和turbo.json。 - 执行构建:利用 Turborepo 的拓扑排序进行并行构建。
二、 实战 Shell 脚本:构建“影子 Monorepo”
以下是一个简化的构建引导脚本 orchestrator.sh:
#!/bin/bash
# 1. 定义子应用清单
APPS=("auth-v3" "dashboard-core" "user-profile")
ROOT_DIR=$(pwd)
# 2. 拉取所有子应用(假设已有 SSH 权限)
mkdir -p packages
for app in "${APPS[@]}"; do
echo "Fetching $app..."
git clone --depth 1 "git@github.com:your-org/${app}.git" "packages/${app}"
done
# 3. 动态注入根目录配置
cat <<EOF > package.json
{
"name": "shadow-monorepo",
"private": true,
"workspaces": ["packages/*"],
"devDependencies": { "turbo": "latest" },
"scripts": { "build": "turbo run build" }
}
EOF
cat <<EOF > turbo.json
{
"\$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
}
}
}
EOF
# 4. 安装依赖并构建
pnpm install
pnpm build
三、 突破“工件传递”:Docker 多阶段构建的避坑指南
你遇到的“工件传递”问题,通常是因为 Docker 每次 RUN 都在独立的层中,或者不同 Repo 的构建处于不同的 Docker 上下文。在“伪 Monorepo”模式下,我们有两种高级玩法:
方法 A:利用 Docker BuildKit 的 --output (推荐)
如果你不希望将最终产物封装在一个镜像里,而是想把各仓库构建出的 dist 拿出来交给 CDN,可以使用 BuildKit 的新特性:
# syntax=docker/dockerfile:1
FROM node:18-alpine AS builder
WORKDIR /app
COPY . .
RUN pnpm install && pnpm turbo run build
# 使用 scratch 镜像导出产物
FROM scratch AS exporter
COPY --from=builder /app/packages/auth-v3/dist /auth-v3
COPY --from=builder /app/packages/dashboard-core/dist /dashboard-core
构建命令:
docker buildx build --target exporter --output type=local,dest=./combined-artifacts .
这会直接将 Docker 内部生成的各个 Repo 产物同步到宿主机的 combined-artifacts 目录,完美解决工件拿不出来的问题。
方法 B:多仓库上下文共享
如果必须通过多个 docker build 解决,可以使用 Named Contexts (Docker 23.0+)。
在构建子应用 B 时,引用子应用 A 的构建结果:
# App B 的 Dockerfile
FROM node:18-alpine
WORKDIR /app
# 引用名为 app-a-context 的上下文(在构建时注入)
COPY --from=app-a-context /app/dist ./external/app-a
...
构建命令:
docker build --build-context app-a-context=../app-a/dist .
四、 性能优化:如何避免 CI 变成“性能黑洞”?
- 稀疏检出 (Sparse Checkout):如果子 Repo 非常大,只需拉取代码,不要拉取完整的 Git 历史。
- 远程缓存 (Remote Caching):Turbo 的强大之处在于
node_modules之外的缓存。配置TURBO_TOKEN和TURBO_TEAM使用 Vercel 或自建的turborepo-remote-cache服务,能让重复构建时间从分钟级降至秒级。 - 层缓存提升:在 Dockerfile 中,先
COPY **/package.json并执行pnpm install,再COPY源码。这样只要依赖没变,安装过程就会被缓存。
五、 总结
统一多 Repo 的 CI 并不一定要强行合并仓库。通过 Shell 脚本动态组装目录结构 + 注入 Workspace 配置 + Docker BuildKit 输出重定向,你完全可以复刻 Monorepo 的开发体验,同时保持多 Repo 的组织边界。
卡在工件传递时,多思考“上下文(Context)”的流动,而不是单纯的 cp 命令。希望这套方案能帮你清理掉那些冗余的 CI 配置文件。