在实际的 GitHub Actions 工作流里,几乎没有一个项目能绕开actions/checkout。它是 GitHub 官方提供的 action,职责是在 runner 上把仓库代码拉取到工作目录,让后续的安装依赖、执行测试、构建镜像等步骤有代码可用。不过很多刚开始写 workflow 的开发者会把actions/checkout和 Git 命令git checkout混在一起,以为在 workflow 里写一个run: git checkout ...就能“检出仓库”,结果发现工作区里根本没有代码,或者检出的分支不符合预期。
这里需要先分清两件事:actions/checkout是 GitHub Actions 中的一个动作(action),它解决的是“自动化任务从哪里拿到代码”的问题;git checkout是 Git 的一个子命令,它解决的是“在当前仓库里切换到哪个分支或提交”的问题。二者名字接近,但所在层次和使用方式完全不同。这篇文章围绕actions/checkout展开,讲清楚它的工作原理、核心参数、典型用法、常见报错和排查方式,并且会专门讨论什么时候应该用actions/checkout的ref参数,什么时候需要手动执行git checkout,希望帮助你在写 CI 流程时少走弯路。
1. actions/checkout 为什么是 GitHub Actions 的第一步
1.1 一次 CI 运行里的代码来源
一个 workflow job 启动后,runner 会先分配一个工作目录,这个目录通常以$GITHUB_WORKSPACE表示。在默认情况下,runner 不会自动把代码放进去。也就是说,如果你的 workflow 没有使用actions/checkout,后续步骤里执行ls看到的是一个近乎空的工作目录,npm ci、mvn package、docker build都会因为没有源码而报错。
actions/checkout的职责,就是把指定仓库、指定 ref 的代码下载到这个工作目录。它拿到代码之后,后续步骤才能基于源码继续操作。
1.2 actions/checkout 和 git checkout 的区别
虽然名称里都有 “checkout”,两者的作用对象和使用层级完全不同:
| 对比项 | actions/checkout | git checkout |
|---|---|---|
| 使用层级 | GitHub Actions 中的 step 动作 | Git 命令行子命令 |
| 主要功能 | 将远程仓库克隆/检出到 runner 工作目录 | 切换当前仓库的工作区到指定分支、标签或提交 |
| 是否自动处理认证 | 是,默认使用GITHUB_TOKEN | 需要用户在运行环境中提前配置好凭据 |
| 是否自动解析触发事件 | 是,可以根据 push、pull_request 等事件自动选择 ref | 否,必须手动指定分支或提交 |
| 典型写法 | uses: actions/checkout@v4 | run: git checkout main |
| 使用场景 | workflow 一开始获取仓库代码 | 在已有代码中切换分支、标签或回退提交 |
在 GitHub Actions 中,不能把uses: actions/checkout@v4简单替换成run: git checkout。因为后者假设仓库已经存在于当前目录中,而实际上 runner 刚开始并没有仓库。不过,当actions/checkout完成之后,后续步骤确实是在一个 Git 仓库里工作,所以确实可以在需要切换分支时手动执行git checkout。这是很多开发者产生混淆的根源:不是同一层操作,却在同一个 job 里先后出现。
1.3 actions/checkout 到底做了什么
actions/checkout不是简单地执行一条git clone,它内部会按顺序处理多件事:
- 确定目标仓库,默认是当前 workflow 所在的仓库,也可以通过
repository参数指定其他仓库。 - 确定要检出的 ref,默认是触发 workflow 的事件对应的 ref。
- 使用 GITHUB_TOKEN 或自定义 token 配置远端地址,避免 Git 在拉取过程中出现交互式输入密码。
- 执行等效于
git fetch的操作,把目标 ref 对应的提交拉取到 runner 本地。 - 在目标 ref 上创建 detached HEAD,或切到对应分支。
- 默认执行清理操作,删除工作区里的残留文件。
- 根据
submodules、lfs等参数决定是否同步子模块或 Git LFS 对象。
其中 detached HEAD 这一点经常被忽略。actions/checkout检出的提交通常不是处于一个普通分支上,而是 detached HEAD 状态。这对后续要执行git push或基于分支名操作的流程有影响,需要在用到时主动处理。
注意:
actions/checkout检出后通常处于 detached HEAD 状态。如果后续步骤需要执行git push,要确保 push 的 ref 正确,必要时要先创建或切换分支。
2. 先跑通最小示例:把代码检出再验证
2.1 准备一个 workflow 文件
在仓库中创建.github/workflows/checkout-demo.yml。这个文件本身也是仓库的一部分,GitHub 会自动识别并执行里面的 workflow。
2.2 一个最基础的检出示例
name: checkout-demo on: push: branches: - main jobs: show-code: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Show workspace content run: | pwd ls -la git log --oneline -1这里的uses: actions/checkout@v4会在main分支收到 push 后,把最新提交的代码检出到 runner 工作目录。第二个 step 中的pwd会打印当前工作目录,ls -la会显示仓库根目录内容,git log --oneline -1会显示当前检出的提交。
2.3 检出后目录里有什么
在 GitHub 托管的 runner 上,工作目录默认是/home/runner/work/{仓库名}/{仓库名}。检出完成后,目录里会有.git目录、源码文件、README、.github目录等。由于默认使用浅克隆,.git目录会比较小,只保留最新一次提交和相关对象。
如果这个 job 里没有actions/checkout,那么后续的git log会直接报错,因为当前目录根本不是 Git 仓库。这也是判断是否成功检出的最简单方式。
2.4 如果不加 actions/checkout 会看到什么
jobs: no-checkout: runs-on: ubuntu-latest steps: - name: List current directory run: ls -la这个 job 没有使用actions/checkout,运行时当前目录里只有 runner 自己生成的极少文件,不会有仓库代码。实际开发中,很多新手遇到的 “找不到 package.json”“找不到 pom.xml” 正是这个原因。
3. 核心参数详解:按场景控制检出行为
3.1 参数总览
actions/checkout的常用参数如下表所示。不同版本之间参数会有差异,落地前先看对应版本 README。
| 参数 | 默认值 | 作用 |
|---|---|---|
| repository | 当前仓库 | 指定要检出的仓库,支持owner/repo格式 |
| ref | 触发工作流的事件 ref | 指定要检出的分支、标签或提交 SHA |
| token | GITHUB_TOKEN | 用于访问仓库的认证令牌 |
| fetch-depth | 1 | 拉取的提交深度,0表示拉取全部历史 |
| persist-credentials | true | 是否把 token 持久化到 git config |
| path | $GITHUB_WORKSPACE | 检出到工作目录下的哪个子目录 |
| clean | true | 检出前是否清理工作区残留文件 |
| submodules | false | 是否一并检出子模块,可设置为recursive |
| lfs | false | 是否下载 Git LFS 文件 |
| sparse-checkout | 不启用 | 只检出仓库的部分目录 |
| set-safe-directory | true | 是否配置 Git safe.directory,解决 owner 不一致问题 |
3.2 fetch-depth:浅克隆和完整历史
fetch-depth是使用频率最高的参数之一。默认值是1,也就是只拉取最新一次提交。这种浅克隆在大多数构建任务里已经足够,因为 CI 通常只需要最新代码。
但是,当后续步骤需要做这些事时,默认值就不够了:
- 执行
git diff HEAD^ HEAD比较提交历史。 - 根据
git describe生成版本号。 - 分析 PR 中所有变更文件。
- 统计两个版本之间的提交数量。
这时可以设置为fetch-depth: 0拉取全部历史,或者设置为一个足够大的数字。也需要注意,拉取全部历史在大型仓库里会明显增加耗时和磁盘占用,不能无脑使用。
- uses: actions/checkout@v4 with: fetch-depth: 03.3 ref:检出的分支、标签、SHA 如何选择
ref参数决定最终检出哪个提交。如果你不填,默认由触发事件决定:
push事件:检出被推送的分支。pull_request事件:检出 PR 的合并提交,而不是源分支的最新提交。workflow_dispatch事件:默认检出默认分支。schedule事件:默认检出默认分支。
这里很容易踩坑。比如在 PR 检查工作流中,你希望检出 PR 源分支上的最后提交,但默认行为是检出源分支与目标分支合并后的提交。如果后续要基于 PR 源码做独立分析,需要显式指定:
- uses: actions/checkout@v4 with: ref: ${{ github.head_ref }}如果要检出某个标签:
- uses: actions/checkout@v4 with: ref: refs/tags/v1.0.0如果要检出某个具体提交:
- uses: actions/checkout@v4 with: ref: ${{ github.sha }}github.sha是触发工作流的提交 SHA,在workflow_dispatch或push场景下很有用。
3.4 token 和 persist-credentials:认证与后续 git push 权限
actions/checkout默认使用GITHUB_TOKEN。GITHUB_TOKEN是 GitHub Actions 自动生成的临时令牌,作用范围通常限定在当前仓库。它是否具有写权限,取决于仓库 Settings 里的 Workflow permissions 配置。对于当前仓库的普通读写操作,默认令牌一般够用。
但是,默认令牌无法访问其他私有仓库。如果你需要checkout另一个私有仓库,必须通过token参数传入一个有权限的 personal access token(PAT)或其他 GitHub App token。
- uses: actions/checkout@v4 with: repository: owner/private-repo token: ${{ secrets.PRIVATE_REPO_TOKEN }}persist-credentials默认是true,表示会把 token 写入 Git 配置,使后续步骤中的git push、git submodule update等命令继续使用同一个凭据。如果 job 中明确不会有写回仓库的操作,可以设置persist-credentials: false,减少凭据在 runner 上的暴露时间。
3.5 submodules 和 LFS:包含外部引用
如果仓库包含 Git 子模块,只写一个普通actions/checkout不会拉取子模块内容。你需要设置:
- uses: actions/checkout@v4 with: submodules: recursiverecursive表示嵌套子模块也一并处理。如果子模块本身是私有仓库,还需要额外传入有读取权限的 token,例如:
- uses: actions/checkout@v4 with: submodules: recursive token: ${{ secrets.SUBMODULE_TOKEN }}如果仓库使用 Git LFS 管理大文件,需要设置lfs: true。一个常见问题是,在浅克隆状态下 LFS 文件可能只检出指针文件,而不是真实内容。遇到这种问题时,可以尝试将fetch-depth: 0与lfs: true同时使用:
- uses: actions/checkout@v4 with: fetch-depth: 0 lfs: true3.6 path、clean 和 sparse-checkout:控制代码位置与清理策略
path参数可以指定检出到工作目录下的子目录。当你需要在一个 job 中检出多个仓库时,这个参数很有用:
- uses: actions/checkout@v4 with: path: frontend - uses: actions/checkout@v4 with: repository: owner/backend path: backend使用path之后要注意,后续步骤的默认工作目录仍然是整个 job 的工作区根目录,而不是你指定的子目录。需要执行命令时,要么使用working-directory,要么先cd进对应目录。
clean默认是true,会在检出前删除工作区中的未跟踪文件,这适合自托管 runner 复用同一目录的场景。如果希望利用上一次构建留下的文件做增量构建,可以考虑设为false,但要额外处理残留文件带来的不确定性。
sparse-checkout适用于 monorepo 中只需要某个子目录的场景。例如只想检出apps/admin:
- uses: actions/checkout@v4 with: sparse-checkout: apps/admin这样可以减少下载内容,但需要确保你的构建逻辑不依赖仓库其他目录。
注意:设置
sparse-checkout后,Git 工作区默认只包含指定目录。如果后续命令需要访问其他路径,会提示文件不存在。
4. 实际项目中的组合用法
4.1 先拉代码,再判断变更范围
很多项目会先checkout,然后根据变更内容决定是否继续构建。例如前端 monorepo 中,只有前端目录发生变化时才执行构建。示例:
- uses: actions/checkout@v4 with: fetch-depth: 0 - name: Check frontend changes id: check run: | git diff --name-only HEAD~1 HEAD | grep -E '^(frontend/|package.json|yarn.lock)' || true这里的git diff需要足够的历史记录,所以fetch-depth不能是默认的1。如果只取最新提交,HEAD~1会报错。这也是“先想清楚后续命令需要什么,再选fetch-depth”的典型例子。
4.2 多仓库检出
在集成测试场景中,经常要同时检出应用代码和测试配置仓库。前面已经介绍过,用两次actions/checkout配合repository和path即可:
- uses: actions/checkout@v4 with: path: app - uses: actions/checkout@v4 with: repository: owner/ci-config token: ${{ secrets.CI_CONFIG_TOKEN }} path: ci-config随后在构建步骤中,使用working-directory指定在哪个目录中执行命令。需要注意,两次检出的仓库彼此独立,不要在子目录里假设另一个仓库已经存在。
4.3 配合缓存和制品上传
actions/checkout通常放在 workflow 开头,后面紧跟着actions/cache和构建命令。顺序不要乱:
steps: - uses: actions/checkout@v4 - uses: actions/cache@v4 with: path: ~/.npm key: npm-${{ runner.os }}-${{ hashFiles('package-lock.json') }} - run: npm ci这里checkout先把package-lock.json拉下来,actions/cache才能计算缓存 key。如果顺序反了,缓存步骤拿不到锁文件,key 会不稳定。
4.4 自托管 runner 上的差异
在 GitHub 托管 runner 上,每次 job 都是全新环境,所以clean: true的影响不大。但在自托管 runner 上,同一个工作目录会被多个 job 复用,可能出现:
- 上次构建生成的未跟踪文件污染本次构建。
- Git 仓库 owner 与当前运行用户不一致,触发 “dubious ownership” 错误。
- runner 上残留的全局 Git 配置影响 token 使用。
对于自托管 runner,建议保留默认的clean: true,并在 runner 环境中统一用户权限。如果仓库目录由 root 创建,而 job 以普通用户运行,可以依赖set-safe-directory参数或手动执行git config --global --add safe.directory /workspace来解决。
5. 常见问题排查:从日志倒推原因
5.1 先看日志里的三个关键字
拿到 GitHub Actions 失败任务后,先不要急着改参数。展开Checkout这个 step 的原始日志,优先搜索三处内容:
remote url:确认检出的仓库地址是否正确。fetch或receive关键字:确认拉取的是哪个 ref。fatal或Error:确认报错的具体原因。
大多数检出问题都能从这三类信息里找到线索。
5.2 Repository not found 或 403
现象:
remote: Repository not found. fatal: repository 'https://github.com/owner/private-repo.git/' not found可能原因:
- 检出的仓库不存在或路径写错。
- 仓库是私有的,但
GITHUB_TOKEN没有权限。 - 使用了过期的 PAT。
检查方式:
- 确认
repository参数写的是owner/repo。 - 确认用到的 PAT 是否过期。
- 确认 PAT 是否勾选了
repo或contents: read权限。 - 如果目标是私有仓库,确认当前账号是否有访问权限。
解决方案:
- uses: actions/checkout@v4 with: repository: owner/private-repo token: ${{ secrets.PRIVATE_REPO_TOKEN }}预防建议:为不同仓库创建独立 secret,不要把所有 PAT 混在一起使用。
5.3 fetch-depth 引起的 git diff 报错
现象:
fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree.原因:默认fetch-depth: 1只有最新一条提交记录,HEAD~1指向的父提交不存在。
检查方式:在报错 step 前添加一行临时命令:
git rev-parse HEAD git rev-list --count HEAD如果--count输出1,说明只有一条提交历史。
解决方案:把fetch-depth改为0,或者改成足够大的值。若只是需要比较最近一次提交,也可以改用git diff HEAD^ HEAD并确保有父提交。
5.4 自托管 runner 上的 dubious ownership 错误
现象:
fatal: detected dubious ownership in repository at '/workspace'原因:Git 检测到仓库目录的 owner 与当前 Git 进程用户不一致。常见于容器内以不同 UID 运行,或者 runner 工作目录由其他用户创建。
检查方式:执行ls -ld /workspace查看目录 owner,再执行id查看当前用户。
解决方案:
- 使用
actions/checkout内置的set-safe-directory参数(多数新版本默认开启)。 - 在 job 开头加入:
- run: git config --global --add safe.directory /workspace- 更根本的方式是统一容器和 runner 的用户 UID。
5.5 子模块或 LFS 文件缺失
现象:
- 子模块目录为空。
- LFS 文件内容变成类似
version https://git-lfs.github.com/spec/v1的文本指针。
原因:
- 没有设置
submodules。 - 没有设置
lfs。 - 子模块是私有仓库,但没有给 token 授权。
- 浅克隆状态下 LFS 对象没有被拉取。
检查方式:
- 查看仓库根目录是否有
.gitmodules。 - 执行
git submodule status。 - 打开 LFS 文件,看是否是指针文本。
解决方案:
- uses: actions/checkout@v4 with: submodules: recursive lfs: true fetch-depth: 0 token: ${{ secrets.MY_TOKEN }}如果子模块 URL 是https://github.com/owner/private-module.git,对应 token 必须有该仓库的读取权限。
5.6 同一个 job 里切换分支时怎么选
有些流程需要在同一个 job 里先后处理多个分支或标签。比如先检出 main 安装依赖,再切换到发布标签执行发布脚本。
推荐做法是,在一开始就能确定目标 ref 的场景下,直接用ref参数:
- uses: actions/checkout@v4 with: ref: refs/tags/v1.0.0只有当同一个 job 内确实需要多次切换工作区时,才在后续步骤中使用git checkout。此时要特别注意浅克隆的问题:
git fetch --tags git checkout v1.0.0不先git fetch --tags,本地很可能没有该标签对应的提交。这就是“git checkout problem 如何选择”的典型答案:获取代码这一步交给actions/checkout,仓库内部切换分支再考虑git checkout;能通过ref参数提前确定的,不要拖到后面手动切换。
6. 最佳实践清单与工程化建议
6.1 参数选型清单
| 场景 | 推荐配置 |
|---|---|
| 常规 CI 构建 | 使用默认参数即可 |
| 需要比较分支差异 | fetch-depth: 0 |
| 需要 PR 源分支代码 | ref: ${{ github.head_ref }} |
| 需要检出多个仓库 | 配置repository和path |
| 仓库包含子模块 | submodules: recursive并提供对应 token |
| 仓库使用 Git LFS | lfs: true,必要时fetch-depth: 0 |
| 后续不需要写回仓库 | persist-credentials: false |
| monorepo 只需部分目录 | 使用sparse-checkout |
| 自托管 runner 跨用户复用 | 保留默认clean并确认set-safe-directory |
6.2 安全建议
- 优先使用
GITHUB_TOKEN,少用长期有效的 PAT。 - 在仓库的 Settings 中,按最小权限原则配置 Workflow permissions。
- token 必须通过 secrets 注入,不要直接写到 YAML 文件里。
- 如果使用 PAT,限制权限范围、设置有效期,并定期轮换。
- 不要把自托管 runner 暴露给不可信仓库,因为 checkout 会执行仓库中的脚本和 Git 操作,风险较高。
6.3 性能建议
- 默认浅克隆已经够用时,不要随意改成
fetch-depth: 0。 - 只需要部分目录时,优先使用
sparse-checkout,减少拉取数据量。 - 同一个 job 内不要重复 checkout 同一仓库。需要子目录时用一次 checkout 加 path,或者后续用
working-directory定位。 - 在自托管 runner 上,
clean: true和缓存机制需要一起设计。清理工作区会防止残留污染,但也会减少增量构建的收益。
6.4 版本锁定与维护
生产环境不要直接使用actions/checkout@main这类不稳定引用,应该固定到 major 版本,例如actions/checkout@v4。
如果对供应链安全要求很高,可以把 action 固定到完整 commit SHA:
- uses: actions/checkout@e2b0e1a6c0f0e2c2a1e6d4e0c1a3e5d6f7a8b9c0这里只是示例格式,实际要从官方 releases 页面复制对应 SHA。锁定 SHA 可以防止 action 仓库被恶意维护者植入变更,但需要自己跟踪上游更新。
6.5 把“如何选择”变成一条判断规则
回到开头的混淆点:在 workflow 中获取代码,选择actions/checkout;在已有仓库里切换分支,选择git checkout。需要检出的目标 ref,尽量在actions/checkout的ref参数中声明,而不是在后续 step 里手动git checkout。原因在于actions/checkout会同时处理认证、工作目录、HEAD 状态和凭据持久化,手动git checkout反而容易因为浅克隆、未 fetch、凭据丢失等问题翻车。
写 workflow 时,建议先跑一个最小示例,把actions/checkout这一步的日志展开读一遍,重点关注 remote url、fetch 的 ref 和最终 HEAD。这三项清楚了,大部分检出问题就都有方向了。下一步可以继续学习actions/cache、actions/upload-artifact,结合本文的参数选型,把 CI 的缓存、产物和检出流程串起来。