WEBKT

多 Repo 微前端的 CI 统一:巧用 Shell + Turbo 实现“伪 Monorepo”构建流

2 0 0 0

在微前端架构的演进过程中,很多团队会陷入一个尴尬的境地:为了权限隔离和模块解耦,选择了 Multi-Repo(多仓库);但随着子应用数量增加,维护 N 套几乎相同的 CI/CD 流水线成了一场灾难。

你想用 Turborepo 来做增量构建和缓存优化,但 Turbo 的核心前提是 Workspace。面对散落在不同 Git 地址的独立 Repo,如何既保留多仓的灵活性,又能享受单仓的构建快感?

你尝试过用 Shell 脚本强行组合,却卡在了“工件传递”这一步。本文将分享一种“动态组装影子 Monorepo”的方案,并重点解决 Docker 多阶段构建中的产物同步难题。

一、 核心思路:动态工作区组装(Dynamic Workspace Assembly)

不要试图在每个子仓库里写复杂的构建逻辑,而是建立一个中央 CI 仓库(或者在 CI 运行环境中动态生成)。其核心流程如下:

  1. 环境初始化:CI 触发后,在一个干净的工作目录下执行 Shell 脚本。
  2. 散点拉取:根据配置清单,将所有相关的子 Repo 平铺拉取到 packages/ 目录下。
  3. 注入灵魂:在根目录动态生成 package.json(包含 workspaces 配置)和 turbo.json
  4. 执行构建:利用 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 变成“性能黑洞”?

  1. 稀疏检出 (Sparse Checkout):如果子 Repo 非常大,只需拉取代码,不要拉取完整的 Git 历史。
  2. 远程缓存 (Remote Caching):Turbo 的强大之处在于 node_modules 之外的缓存。配置 TURBO_TOKENTURBO_TEAM 使用 Vercel 或自建的 turborepo-remote-cache 服务,能让重复构建时间从分钟级降至秒级。
  3. 层缓存提升:在 Dockerfile 中,先 COPY **/package.json 并执行 pnpm install,再 COPY 源码。这样只要依赖没变,安装过程就会被缓存。

五、 总结

统一多 Repo 的 CI 并不一定要强行合并仓库。通过 Shell 脚本动态组装目录结构 + 注入 Workspace 配置 + Docker BuildKit 输出重定向,你完全可以复刻 Monorepo 的开发体验,同时保持多 Repo 的组织边界。

卡在工件传递时,多思考“上下文(Context)”的流动,而不是单纯的 cp 命令。希望这套方案能帮你清理掉那些冗余的 CI 配置文件。

码头架构师 微前端TurborepoCICD

评论点评