Git Submodule 踩坑实战:从 CI 拉取失败到完整解决方案

作者 lucy · 2026-04-17

⚠️ 声明:本文相关内容仅供参考,实际操作请以官方文档为准,谨慎参考。

使用 Git 子模块(submodule)管理依赖仓库是团队协作中的常见需求,但在实际项目中,很多人踩过 submodule 同步失败、版本不对、CI 跑不过等坑。本文结合一个真实踩坑案例,梳理 Git 子模块的正确使用姿势和常见错误排查方法。

一、问题背景

团队项目 A 引用了公共组件库 B(私有仓库),以 Git submodule 方式引入。在某次合并分支后,CI 流水线报错:

Gitsubmodules 'frontend/components' registered in .gitmodules could not be cloned.
fatal: clone of 'git@gitee.com:team/components.git' into submodule path
'/builds/frontend/components' failed
fatal: clone of 'git@gitee.com:team/components.git' into submodule path
'/builds/frontend/components' failed
Could not clone submodule 'frontend/components'

开发机器上 git submodule update --init 正常,但 CI 环境中始终无法拉取子模块。

二、根因分析

1. CI 机器没有部署 SSH Key

仓库地址使用的是 SSH 格式(git@gitee.com:...),CI runner 容器内没有配置对应的 deploy key,导致认证失败。

2. .gitmodules 路径配置问题

# .gitmodules 文件内容
[submodule "frontend/components"]
    path = frontend/components
    url = git@gitee.com:team/components.git   # SSH 格式

# 但 CI 环境用的是 HTTPS 地址
# 本地可以用 SSH,CI 必须用 HTTPS

3. 子模块引用的是旧版本

# 查看当前 submodule 指向的 commit
git submodule status

# 可能出现:
# abc1234... frontend/components (heads/main)
#                      ^^^^^^^^ ← 指向一个旧 commit

# 需要在主仓库里更新 submodule 引用
git checkout develop
git submodule update --remote --merge
# 或者手动切到目标 commit 后提交
cd frontend/components
git checkout main
git pull origin main
cd ../..
git add frontend/components
git commit -m "chore: 更新 components 到最新main"
git push

三、解决方案

方案一:CI 使用 HTTPS + Access Token

# 在 Gitee 仓库 → 设置 → Access Token 生成一个只读 Deploy Key
# 或者用项目级 Deploy Key

# 将 .gitmodules 中的 SSH 地址改为 HTTPS
[submodule "frontend/components"]
    path = frontend/components
    url = https://gitee.com/team/components.git

# 在 CI 中设置环境变量
# GIT_TOKEN = 你的 Gitee Access Token

# 修改 CI 拉取逻辑
git config --global url."https://gitee.com/".insteadOf "git@gitee.com:"
# 或者通过 GIT 配置注入认证
git clone https://username:$GIT_TOKEN@gitee.com/team/components.git

方案二:在 CI 中配置 SSH Key(推荐)

# 1. 生成 SSH 密钥对
ssh-keygen -t ed25519 -C "ci-runner@gitlab" -f ./deploy_key

# 2. 将公钥 deploy_key.pub 添加到 Gitee 仓库的「部署公钥」
# 3. 将私钥 deploy_key 添加到 CI 变量的 Secret 变量
# 4. CI 配置示例(GitLab CI)
#.gitlab-ci.yml:
before_script:
  - 'which ssh-agent || (apt-get update -qq && apt-get install -qq -y openssh-client)'
  - eval $(ssh-agent -s)
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  - mkdir -p ~/.ssh
  - chmod 700 ~/.ssh
  - 'ssh-keyscan gitee.com >> ~/.ssh/known_hosts 2>/dev/null'

variables:
  SSH_PRIVATE_KEY: $(cat $CI_DEPLOY_KEY)  # 从 CI 变量读取

方案三:切换到 Git subtree(不推荐,但无 CI 依赖)

# 将子模块替换为 subtree(一次性拉取,不依赖 submodule)
git subtree add --prefix=frontend/components https://gitee.com/team/components.git main --squash

# 之后同步更新
git subtree pull --prefix=frontend/components https://gitee.com/team/components.git main --squash

# 优点:不需要任何额外配置,clone 主仓库即可得到完整代码
# 缺点:更新流程比 submodule 更繁琐,团队需要适应

四、正确使用 submodule 的规范

  1. 始终使用 HTTPS 地址:避免 CI 环境中 SSH Key 配置不一致的问题

  2. 指定固定版本号:不要用 @branch,固定 commit hash 避免隐性更新

  3. 主仓库 CI 中强制初始化 submodule

# .gitlab-ci.yml
variables:
  GIT_SUBMODULE_STRATEGY: recursive
  # 等同于每次 clone 后自动执行:
  # git submodule update --init --recursive
  1. 禁止在子模块内直接修改代码:子模块只读,需要单独提 MR 合入

  2. 更新子模块前先 CI 通过:确认子模块本身的流水线是绿的

# 规范流程:
# 1. 在 components 仓库提 MR,合并到 main
# 2. 在主项目执行:
git checkout develop
git submodule update --remote --merge frontend/components
git add frontend/components
git commit -m "chore: 更新 components@$NEW_COMMIT"
git push
# 3. 等待主项目 CI 验证通过

五、踩坑检查清单

  • ☐ .gitmodules 中 submodule 的 URL 是否可公开访问(私有仓库需配置 CI Access Token)
  • ☐ CI 环境是否配置了对应的 SSH Key 或 HTTPS 认证
  • ☐ submodule 指向的 commit 是否在目标分支存在
  • ☐ .gitmodules 的 path 路径和实际目录是否一致
  • ☐ submodule 的 upstream 分支是否设置正确(git submodule add -b main ...

⚠️ 再声明:子模块管理方案需要结合团队 CI 环境和协作流程选择,HTTPS + Access Token 是最通用的方案,SSH 方式适合有安全管控要求的团队。

有问题欢迎留言交流 🚀

发表评论

苏ICP备18039580号-2