GitHub Actions中actions/checkout与git checkout的区别及避坑指南
2026/8/30 6:36:00 网站建设 项目流程

先问一个问题:你有没有在 GitHub Actions 里写过这样的流水线——明明 workflow 第一步就用了actions/checkout,日志也显示 “Checkout complete”,但下一步脚本一执行,却告诉你ls: cannot access 'src/': No such file or directory。或者更常见的是,你辛辛苦苦在 runner 上改了文件,下一步运行却发现改动全部消失,白干一场。

如果你遇到过,或者正在排查这个问题,那么这篇文章就是写给你的。

这篇博客会聚焦一个看似简单、实则非常容易踩坑的官方 Action:actions/checkout。我会讲清楚它和本地命令git checkout到底有什么区别,它的默认行为有哪些坑,怎么用with参数控制拉取方式,以及当出现“目录不存在”“文件被清空”“子模块拉不下来”时,应该按什么顺序排查。读完你至少能把 GitHub Actions 里的代码检出环节彻底搞明白,从“能跑”升级到“知道为什么能跑”。

1. 这篇文章真正要解决的问题

先说判断:actions/checkout是 GitHub Actions 里被使用频率最高、同时也被误解最深的官方 Action。很多 CI/CD 问题,看似发生在后续的构建或测试步骤,根子其实在第一步代码检出时就埋下了。

为什么这么说?因为它的名字里带着 “checkout”,和 Git 本地命令git checkout太像了。很多有 Git 基础的开发者,第一次看到这个 Action 时,会下意识把它理解成“切换 Git 分支”的操作。但实际上,在 CI 环境里,runner 是一个临时的、全新的虚拟机或容器,它一开始根本没有你的代码。actions/checkout做的是“把仓库代码拉取到 runner 上”,而不是在已有仓库里“切换分支”。

这两种理解,导致的问题完全是两个方向:

  • 如果按“拉取代码”来理解,你会关注fetch-depthtokenpathsubmodules这些参数。
  • 如果按“切换分支”来理解,你会纠结于ref的写法,却忽略了 runner 上其实没有仓库这个前提。

所以这篇文章真正要解决的是三类问题:

第一,概念混淆问题。把actions/checkoutgit checkout放在一起对比,给你一个清晰的边界,以后不会再在脑子里打架。

第二,默认行为不透明的问题。很多人不知道actions/checkout默认只拉取单个提交的代码(fetch-depth: 1),也不知道它默认会使用GITHUB_TOKEN进行鉴权,更不知道它默认会执行clean操作把工作区里残留的文件清掉。这些默认行为,单独看都合理,组合在一起就是无数“灵异事件”的源头。

第三,生产环境配置问题。什么时候需要fetch-depth: 0?什么时候必须配submodules: recursive?私有仓库的依赖仓库怎么拉?persist-credentials要不要关?这些经验,不踩几次坑是积累不下来的,这篇文章一次性给你理清。

如果你是 GitHub Actions 的中级使用者,已经跑通了一些简单 workflow,但对 checkout 的细节缺乏完整认知,这篇文章尤其适合你。如果你是完全的新手,建议先照着第 5 章的示例跑一遍,再回头看原理,效果更好。

2. 从 git checkout 到 actions/checkout:名字相近,功能完全不同

要理解actions/checkout,必须先把它从git checkout这个“同名长辈”的阴影里拉出来。

2.1 git checkout:在已有仓库里切换状态

git checkout是 Git 自带的一个本地命令,核心作用是切换分支或恢复工作区文件。比如:

git checkout main git checkout -b feature/login git checkout -- src/main/java/App.java

这三个命令分别做了不同的事:切换分支、新建并切换到新分支、放弃某个文件的本地修改。它们的共同前提是:你当前已经在一个完整的 Git 仓库里,并且本地有完整的对象数据库git checkout本质上是把你当前 HEAD 指针移动到一个新的位置,并让工作区文件跟着变化。

这个过程不涉及网络拉取(除非配置了特殊的自动 fetch),也不涉及克隆仓库。它就是本地状态切换。

2.2 actions/checkout:在全新环境里克隆仓库

actions/checkout则是 GitHub 官方发布的一个 Action,它运行在 GitHub 托管的 runner(或者你自托管的 runner)上。它的任务是在一个全新的、通常没有任何项目代码的环境里,把指定仓库的指定版本代码拉到工作目录。

我们直接看它的源码逻辑(这是理解这个概念最直接的方式)。它的核心执行过程大致是:

  1. 在 runner 上创建或进入一个空的工作目录。
  2. 执行git init初始化一个空的 Git 仓库。
  3. 添加远程仓库地址,即origin
  4. 执行git fetch拉取指定ref对应的提交。
  5. 执行git checkout --detach <commit_id>git switch检出对应提交。
  6. 根据参数决定是否配置token.git/config、是否拉取子模块、是否清理工作区。

看到第 5 步你会发现,它内部确实也用了git checkout,但这里的前提是:它先完成了一个类似git clone的过程,然后在临时仓库里做了一次孤儿检出来匹配指定 commit。这一切对用户是封装好的。

2.3 为什么容易混淆

混淆的根本原因有三个:

  • 名字里都带checkout,搜索引擎和 AI 工具都会把它们混在一起。
  • 很多教程在解释actions/checkout时,会简单说“它相当于执行了git clonegit checkout”,但这句解释其实省略了关键细节。
  • 在 Runner 上执行命令时,你确实可以在 shell 里看到它调用了git checkout,于是不熟悉的人会以为“这个 Action 就是封装了一个 checkout 命令”。

为了彻底理清,我用一个表格对比:

对比维度git checkoutactions/checkout
运行位置本地已有仓库中GitHub Actions runner 的空白环境中
核心功能切换分支、恢复文件拉取仓库代码并检出指定提交
是否依赖已有仓库是,必须在仓库内执行否,自动完成 init/fetch/checkout
网络行为通常不拉取新对象必须从远端 fetch 代码
常见失败场景分支不存在、本地冲突token 无权限、ref 不匹配、fetch-depth 不足
使用场景日常开发、切换需求分支CI/CD 流水线第一步,为后续步骤准备代码

一句话总结:git checkout是“在已有代码里换一种状态”,actions/checkout是“把代码先弄到 runner 上,然后再进入指定状态”。

3. actions/checkout 的工作原理与默认行为

现在进入正题。我们要理解的不是“它怎么做”,而是“它默认做了什么,以及这些默认行为带来了什么后果”。

3.1 最小用法做了什么

一个最简单的 workflow:

name: checkout-demo on: push jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: List files run: ls -la

这个uses: actions/checkout@v4执行时,runner 上会发生这些事:

  1. 创建$GITHUB_WORKSPACE工作目录。
  2. 在该目录中初始化一个临时 Git 仓库。
  3. github.repository(即你当前的仓库)配置为origin
  4. GITHUB_TOKEN为凭据,从中获取当前分支/提交的最新代码。
  5. 检出代码到工作区,并切换到 detached HEAD 状态。
  6. 不配置任何额外用户信息,因为后续 Action 可以靠各自的 token 访问仓库。

看起来一切正常,但注意第 5 步末尾的 “detached HEAD”。这意味着,即使你在后续步骤里执行git commit,也不会自动提交到某个分支上。这是新手经常忽略的一个点——CI 环境里直接改代码再想提交回仓库,不能只靠 checkout。

3.2 fetch-depth 默认值的影响

输入材料里常见的fetch-depth默认值是1。也就是说,actions/checkout默认只拉取当前 ref 对应的那一条 commit 及其关联文件。这带来两个影响:

  • 优点是快。流水线不需要拉取完整 Git 历史,尤其是对于历史很长、提交量很大的仓库,省时省流量。
  • 缺点是,如果你想在后续步骤里做依赖版本对比、生成变更集(diff)、git log 分析,或者需要读取历史 commit 的信息,就会失败。因为这些数据根本没有被 fetch 下来。

所以当你看到下面这种报错:

fatal: ambiguous argument 'HEAD^': unknown revision or path not in the working tree.

大概率就是 fetch-depth 太浅,无法访问父提交。

3.3 默认 token 的权限边界

actions/checkout不配置 token 时,会使用 Actions 自动生成的GITHUB_TOKEN。这个 token 的权限由仓库 Settings -> Actions -> General -> Workflow permissions 控制,默认是Read and write permissions,但不同组织可能有不同的策略。

这个默认 token 的特点是:

  • 只在当前 workflow 运行期间有效。
  • 只能访问当前仓库(或者是触发 workflow 的那个仓库)。
  • 如果 workflow 是pull_request事件触发的,token 权限会被限制为只读,且无法修改 PR 来源分支。
  • 如果你要 checkout 的是另一个私有仓库,用默认 token 是拉不下来的。

第二个场景非常常见:你的主仓库引用了一个私有的模板仓库或依赖仓库,想通过actions/checkout把它一起拉下来。这时默认 token 没有权限,会报错:

remote: Repository not found. fatal: repository 'https://github.com/your-org/private-repo.git/' not found

解决办法是使用一个具有目标仓库读取权限的 PAT(Personal Access Token),或者配置secrets。这个后面在示例章节会详细演示。

3.4 clean 与后续步骤的交互

actions/checkoutclean参数默认值为true。它会在拉取代码之前,清空工作目录中与 Git 无关的所有文件。这个设计的初衷是保证 runner 环境纯净,防止上一次构建的残留文件干扰本次构建。

但它也是一个“坑”的来源:如果你在 workflow 的某一步用脚本生成了构建产物或者修改了文件,另一步又再次执行了actions/checkout(有些人会在多个 job 中重复 checkout),那么上一次的修改可能会被清除。更准确地说,clean: true会删除工作区里未跟踪的文件,但对已被 Git 跟踪的文件的修改,会被下一个步骤的 checkout 覆盖。

也就是说:

  • 如果你希望“先在 runner 上缓存/生成一些文件,然后再 checkout 代码”,需要设置clean: false
  • 如果你使用多个 job,并且希望把构建产物从一个 job 传给下一个 job,通常不能用重复 checkout,而应该用actions/upload-artifact

4. 核心配置项详解

actions/checkout的配置全部集中在with字段里。我按使用频率从高到低逐一说明。

4.1 repository

指定要检出哪个仓库。默认值是${{ github.repository }},也就是当前触发 workflow 的仓库。大多数情况下不需要改。

但如果你需要在 workflow 中同时检出另一个仓库(比如文档站要引用多个仓库的内容),可以这样写:

- name: Checkout docs repo uses: actions/checkout@v4 with: repository: your-org/docs token: ${{ secrets.DOCS_REPO_TOKEN }} path: docs

这个组合非常常见:repository指定目标仓库,token提供跨仓库权限,path指定检出到工作区下的哪个子目录。

4.2 ref

指定要检出的分支、标签或提交 SHA。默认值是${{ github.ref }},即触发 workflow 的分支或标签。

一个典型场景:当 workflow 由pull_request事件触发时,github.refrefs/pull/<number>/merge(合并后的 ref),而不是源分支。如果你希望检出源分支本身,可以覆盖:

- name: Checkout source branch uses: actions/checkout@v4 with: ref: ${{ github.head_ref }}

head_ref在 PR 事件中代表源分支名。注意,head_ref在 push 事件中为空,所以如果需要兼容两种事件,建议用上下文判断。

4.3 token

用于远程 git 操作的鉴权。默认是${{ github.token }}。如果需要跨仓库访问,或者默认 token 权限不够,需要传入一个有权限的 PAT 或者配置好的 secret。

- name: Checkout with PAT uses: actions/checkout@v4 with: token: ${{ secrets.MY_PAT }}

这里需要提醒一下安全边界:不要把 PAT 硬编码在 workflow 文件里,一律放到仓库或组织级的 Secrets 中,并在 Secrets 中配置最小权限(只读某个仓库的权限、不勾选不必要的 scope)。

4.4 fetch-depth

控制拉取 Git 历史的深度。默认是1

  • fetch-depth: 0表示获取全部历史。
  • fetch-depth: 1只获取最新一条 commit 及其包含的文件快照。
  • fetch-depth: N获取最近 N 条 commit。

需要知道的是:fetch-depth: 0并不只是“多拉点历史”这么简单。它会影响后续所有依赖 Git 历史的步骤,例如:

  • git diff HEAD^ HEAD能跑通。
  • 某些语义化版本计算工具(如 semantic-release)能正常工作。
  • 子模块遍历时能获得更准确的状态。

代价是拉取时间变长、仓库变大。建议按需设置,不要无条件全部拉取。

- name: Fetch all history uses: actions/checkout@v4 with: fetch-depth: 0

4.5 path

指定代码检出到工作区的哪个子目录。默认是仓库根目录。

- name: Checkout to subdirectory uses: actions/checkout@v4 with: path: my-project

之后你在后续步骤中访问代码时,路径就是$GITHUB_WORKSPACE/my-project。如果你要在同一个 job 中检出多个仓库并相互引用,这个参数是必须的。

4.6 clean

默认true,表示在检出前清空工作区中未被 Git 跟踪的文件。如果你知道自己后续要在 runner 上生成一些文件且不希望被 checkout 清理,可以设置为false

- name: Checkout without clean uses: actions/checkout@v4 with: clean: false

4.7 submodules

是否检出子模块。可选值有truerecursivefalse。默认false

如果你的仓库使用 Git Submodule,且构建过程需要子模块代码,必须设置:

- name: Checkout with submodules uses: actions/checkout@v4 with: submodules: recursive

recursive会递归拉取嵌套子模块。对于私有子模块仓库,如果子模块 URL 是 HTTP 形式,还需要配置token,否则拉取权限不足。

4.8 persist-credentials

默认true,会把 token 保存到.git/config中的http.https://github.com/.extraheader,这样后续git pushgit fetch可以使用同样凭据。

在安全要求严格的场景(比如不希望后续步骤中的任意脚本利用该 token 访问仓库),可以设置:

- name: Checkout without persisting credentials uses: actions/checkout@v4 with: persist-credentials: false

设置后,如果要 push 回仓库,需要手动配置凭据,否则会失败。这是一个安全与便利的取舍。

4.9 sparse-checkout

启用稀疏检出,只拉取指定路径下的文件。适合大型 monorepo 中只需要构建某个子目录的场景。

- name: Sparse checkout uses: actions/checkout@v4 with: sparse-checkout: | apps/api packages/shared

注意:sparse-checkout需要fetch-depth与 Git 版本的支持,GitHub 托管的 runner 上通常没有问题。

4.10 lfs

是否下载 Git LFS 对象。默认false

- name: Checkout with LFS uses: actions/checkout@v4 with: lfs: true

如果项目使用大文件存储且影响构建,必须开启。

5. 环境准备与最小示例

我们不需要本地安装任何 GitHub Actions 相关工具,只需要一个 GitHub 仓库和可用的网络。

5.1 准备仓库

第一步,在 GitHub 上创建一个新的测试仓库,例如命名为checkout-demo,并提交一个简单的文件:

mkdir checkout-demo cd checkout-demo git init -b main echo "# Checkout Demo" > README.md mkdir -p src echo "console.log('hello from checkout demo');" > src/index.js git add . git commit -m "Initial commit" git remote add origin https://github.com/<your-username>/checkout-demo.git git push -u origin main

5.2 创建 workflow

在仓库根目录创建.github/workflows/checkout-demo.yml

name: checkout-demo on: [push] jobs: demo: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Show workspace files run: pwd && ls -la - name: Show git status run: git status - name: Show current branch run: git branch -a

提交并推送这个 workflow 文件后,前往仓库的 Actions 页面,可以看到一次新的运行。

5.3 验证运行结果

运行结束后,点开Show git status这一步,你大概率会看到类似输出:

HEAD detached at <commit-sha> nothing to commit, working tree clean

这说明actions/checkout确实是在一个 detached HEAD 状态下检出了代码。你看到的这个 commit SHA,就是触发 workflow 的那次 push 的最新 commit。

再点开Show current branch,你会发现问题:git branch -a的输出只包含:

* (HEAD detached at <sha>)

根本看不到mainremotes/origin/main。这是fetch-depth: 1的典型表现:本地没有完整 refs,只有当前 commit 数据。

如果你想看到分支信息,需要在 checkout 中设置fetch-depth: 0

- name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0

改完后重新运行,git branch -a就能看到远程分支列表了。这个差异非常直观地展示了fetch-depth的意义。

6. 完整示例:从拉取代码到构建验证

下面我们用一套更接近生产环境的例子,把actions/checkout放在一个完整的 Java 项目流水线中演示。

假设项目是一个 Maven 工程,目标是通过 CI 构建并运行测试。我们设计三个场景:

  • 场景 A:普通主分支构建,浅检出就够了。
  • 场景 B:需要生成两次提交之间的 diff 报告,必须完整历史。
  • 场景 C:需要同时拉取文档仓库,路径隔离。

6.1 场景 A:浅检出 + 构建测试

name: java-build on: push: branches: [ main ] pull_request: jobs: build: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' - name: Build and test run: mvn -B clean verify

在这个例子中,actions/checkout默认的fetch-depth: 1就足够了,因为 Maven 构建不需要 Git 历史。这样做速度最快。

6.2 场景 B:完整历史 + 生成 diff

如果你想在 PR 中展示文件变更列表,或者自动判读是否需要更新文档,需要完整历史:

name: diff-report on: pull_request: jobs: diff: runs-on: ubuntu-latest steps: - name: Checkout with full history uses: actions/checkout@v4 with: fetch-depth: 0 - name: Generate diff run: | git diff origin/${{ github.event.pull_request.base.ref }}...${{ github.sha }} --name-only

这里的关键是,fetch-depth: 0确保我们能访问 base 分支的 commit。没有它,git diff会报错。

6.3 场景 C:多仓库检出

如果你的 CI 需要在本次构建中使用另一个文档仓库的内容:

name: multi-repo on: [push] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout main repo uses: actions/checkout@v4 - name: Checkout docs repo uses: actions/checkout@v4 with: repository: your-org/docs token: ${{ secrets.DOCS_REPO_TOKEN }} path: docs - name: Verify both repos run: | ls -la ls -la docs

这里DOCS_REPO_TOKEN必须是一个有docs仓库读取权限的 secret。如果你没有配置,第二步就会因为权限不足而失败。需要提醒的是:actions/checkout在检出第二个仓库时,会把当前 job 的GITHUB_WORKSPACE作为父目录,然后在其下创建docs子目录,所以不会污染主仓库的检出内容。

6.4 如何判断成功

每个场景成功与否,可以直接看 workflow 的绿色对勾。但更重要的判断方式是观察日志里是否有这些关键行:

Checkout complete

以及后续命令是否输出了预期内容。如果docs目录不存在,ls -la docs会直接非零退出,导致 job 失败,从而暴露问题。

7. 常用组合场景与进阶写法

7.1 子模块仓库

现代项目越来越普遍地使用子模块管理共享库。这时 checkout 配置要复杂一些:

- name: Checkout with submodules uses: actions/checkout@v4 with: submodules: recursive token: ${{ secrets.SUBMODULE_TOKEN }}

为什么还需要传 token?因为子模块的 URL 如果写的是https://github.com/org/private-repo.git,runner 上的 git 会尝试匿名访问,私有仓库直接 404。传入 token 后,actions/checkout会将其注入到 git 请求头中,才能通过鉴权。

这里有一个工程建议:子模块的 URL 尽量使用相对路径写法,例如../../org/private-repo.git,这样 GitHub 会自动基于当前仓库的 base URL 解析,可以在多个 fork 仓库间通用。但如果你需要自托管 Git 实例,这个方案不完全适用,还是需要 token。

7.2 分支策略与 ref 选择

当 workflow 需要同时处理多个分支或 tag 时,ref参数需要设计好。

举例:你希望在release/1.0分支上触发构建时,能拉取main分支上最新的配置文件。可以:

- name: Checkout release branch uses: actions/checkout@v4 with: ref: ${{ github.ref }} - name: Checkout config from main uses: actions/checkout@v4 with: repository: ${{ github.repository }} ref: main path: .config-from-main clean: false

但这个写法的隐患是:同一 job 中两次 checkout 同一个仓库,第二次可能因为第一次 checkout 产生的.git目录冲突。所以如果是同一仓库的不同分支,建议直接用后续步骤的git fetchgit show操作,而不是多次 checkout。

7.3 与 actions/upload-artifact 配合

一个常见的误区是:把actions/checkout写在了多个 job 中,并期望构建产物跨 job 保留。这是不成立的。每个 job 都是全新的 runner 环境。

正确做法:只在构建 job 中 checkout 并构建,然后使用actions/upload-artifact上传产物;后续部署 job 中下载产物,而不是再次 checkout。

jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: make build - uses: actions/upload-artifact@v4 with: name: dist path: dist/ deploy: needs: build runs-on: ubuntu-latest steps: - uses: actions/download-artifact@v4 with: name: dist

这个设计更贴近生产环境,也避免了“重复 checkout 导致文件被覆盖”的隐患。

8. 常见问题与排查方法

这里把最常见的报错和异常汇总成表,方便你遇到问题时快速定位。

问题现象可能原因排查方式解决方案
checkout 成功但目录中没有代码workflow 使用了path参数,代码在子目录中检查path配置,执行ls -la在后续步骤中使用正确路径,或去掉path参数
fatal: ambiguous argument 'HEAD^'fetch-depth 太小,缺少父 commit查看 checkout 日志中的Fetching...深度设置fetch-depth: 0
remote: Repository not found目标仓库是私有仓库,token 无权限检查 token 权限和仓库可见性使用有权限的 PAT secret 并配置token
构建后文件被修改但下一次运行时改动丢失clean: true清除了未跟踪文件/覆盖工作区检查是否在 checkout 前修改了文件在自定义文件生成步骤之前避免重复 checkout,或设置clean: false
子模块目录为空未设置submodules检查子模块目录是否为空、日志是否有子模块报错设置submodules: recursive,私有仓库配置 token
在 pull_request 中无法 push 回源分支GITHUB_TOKEN 在 PR 中只读查看 push 报错,检查 workflow 权限使用 PAT 配置 token,或调整分支保护策略
checkout 这一步很慢fetch-depth: 0 且仓库历史很大查看日志耗时评估是否必须完整历史,按需设置fetch-depth
后续 job 中找不到上一个 job 生成的文件不同 job 是独立环境查看文件路径和 artifact 配置使用actions/upload-artifact/download-artifact

下面单独展开两个值得细说的问题。

8.1 checkout 成功但代码“不在”目录

这个问题的核心原因,几乎都是path参数。当你在with中配置了path: my-app后,代码会放在$GITHUB_WORKSPACE/my-app,而不是 workspace 根目录。很多初学者在后续步骤里执行ls -la,看到根目录没有代码,就会误以为 checkout 失败了。

排查时,先看日志里有没有这行:

Checking out the ref

再看日志中是否有Path: my-app之类的工作目录信息。如果确实使用了子目录,后续所有引用路径都要加前缀。这个坑在配置了多个仓库的场景里尤其明显。

8.2 同一仓库二次 checkout 的问题

有些人会在一个 job 里连续写两次actions/checkout,以为这样可以分别在主目录和子目录各拿一份代码。但实际操作时,第二次 checkout 清洗了第一次的内容,导致第一个目录里的文件不完整。

最稳妥的做法是:一次 checkout 主仓库,再用git fetchactions/checkout的其他参数处理额外需求;不要对同一个仓库做无必要的重复 checkout。

9. 最佳实践与工程建议

这部分是整篇文章最有复用价值的内容,来自对大量真实项目经验的总结。

9.1 版本策略

uses中尽量使用带主版本的引用,如actions/checkout@v4。不要使用@main@master,因为上游更新可能引入不兼容变更。也不要锁定到某个具体的 patch 版本,例如@v4.1.1,除非你有非常强的可复现需求。锁定主版本,既能在一定周期内获得 bugfix,又能避免破坏性变更。

GitHub 官方每次发布新的主版本都会迁移文档,建议在升级主版本前先阅读 release notes。

9.2 权限最小化

关于token的一条核心建议:

  • 默认情况下,优先使用GITHUB_TOKEN。不需要为了“能跑”而总是传 PAT。
  • 如果确实需要跨仓库读取,为 PAT 配置尽可能少的 scope。比如只需要读取repo内容,就不要给repo的写权限。
  • 将 PAT 存入 Organization Secrets 而不是仓库 Secrets,方便统一控制和轮换。

安全底线再次提醒:任何形式的 token 都要通过 GitHub Secrets 注入,绝对不要直接写在 workflow 文件里。

9.3 fetch-depth 按需设置

不要默认全仓fetch-depth: 0,也不要默认浅检出。合理的判断标准是:

  • 只做编译、测试、打包:浅检出即可。
  • 需要生成 diff、分析历史提交、语义化版本计算:完整历史。
  • 仓库非常大,且只构建某个目录:考虑sparse-checkout

性能是 CI 体验的一部分。一个 5 分钟的流水线里,如果两分钟花在 checkout 完整历史上,而业务只需要一次构建,这个成本是不值得的。

9.4 注意 PR 事件的 token 限制

当 workflow 由pull_request触发时,GITHUB_TOKEN分支保护机制是只读的,它不能 write 到源分支。如果 CI 需要自动修复代码并 push 回 PR,必须使用 PAT,或者调整触发策略(比如pull_request_target,但这有安全风险,不是默认推荐方案)。

处理 PR 时,还有一点容易踩坑:github.ref是 PR 合入后的 ref,而不是源分支。如果你希望 checkout 源分支的代码进行更真实的测试,需要设置ref: ${{ github.head_ref }},但这也会带来一些安全性顾虑(比如依赖 PR 中的恶意 workflow 修改)。建议只在信任的贡献者范围内使用。

9.5 日志与可观测性

actions/checkout的日志默认包含很多有用信息,包括:

  • Remote URL / 目标仓库
  • 检出的 ref
  • fetch 深度
  • 是否启用 submodules
  • 是否使用 LFS

排错时第一步永远是打开 checkout 步骤的完整日志,查看这些配置项是否符合预期,而不是直接去看后续步骤的报错。有时候后续步骤的报错只是因为入参没传对。

9.6 自托管 runner 的差异

如果你在自托管 runner 上使用actions/checkout,注意:

  • runner 上的git版本必须支持 Actions 所需的功能(比如sparse-checkout需要新版 Git)。
  • 自托管 runner 不清空工作区,clean: true会负责清理,但如果多个 workflow 共用同一个 runner,仍可能出现缓存污染。
  • 自托管 runner 的GITHUB_WORKSPACE可能被多个 job 复用,建议在关键步骤前后都输出pwd确认路径。

10. runner 内部发生了什么:一次完整的时间线

为了把整个流程讲透,我用一个不涉及图表的时间线来概括一次 checkout 的执行过程。

假设你在main分支上推送了一次提交。workflow 开始后,runner 上的actions/checkout@v4执行:

  1. 环境准备阶段。runner 创建GITHUB_WORKSPACE目录,并确保 shell 环境(sh/bash)可用。
  2. 设置 Git 全局配置。actions/checkout会设置user.nameuser.email为临时的 GitHub Actions 用户,避免后续 git 操作因缺少身份而失败。
  3. 初始化临时仓库。在 workspace 下执行git init,并配置remote.origin.url为仓库地址。
  4. 认证配置。根据传入的token值,向 Git 请求头中写入Authorization: token <token>,或者设置为不持久化。
  5. 拉取代码。按照fetch-depth参数执行git fetch。这一步会从远程仓库拉取指定 ref 的 commit 数据。如果fetch-depth: 1,只会拉取最新的一个提交快照;如果是 0,拉取全部历史。
  6. 检出代码。执行类似git checkout --detach <commit>的操作,把工作区内容更新到目标 commit。注意是 detached HEAD,没有在本地创建对应的分支。
  7. 处理子模块和 LFS。如果开启submoduleslfs,在这一步执行相应的拉取。
  8. 清理工作区。如果clean: true,删除工作区中不被 Git 跟踪的文件,确保构建环境干净。
  9. 持久化凭据。如果persist-credentials: true,在.git/config中写入 token,供后续步骤使用。

整个过程对外只体现为日志里的一小段输出,但每一个参数都在悄悄影响最后的结果。这也是为什么我们排查问题时,第一步要回到 checkout 的日志上去看。

11. 总结与后续学习方向

到这里,actions/checkout的核心内容已经讲透了。我整理一下今天的要点:

  • actions/checkout是 GitHub Actions 的官方代码检出 Action,主要负责在 runner 上拉取仓库代码并检出指定提交,和本地命令git checkout是两个完全不同的概念。
  • GitHub Actions 的 checkout 默认执行浅检出(fetch-depth: 1),只会拉取当前提交;使用fetch-depth: 0才能拿到完整历史。
  • 跨仓库检出必须显式传入有权限的 token,否则非常容易遇到 “Repository not found” 类报错。
  • clean默认清理工作区,submodules默认关闭,这两个参数分别对应两种常见的构建污染和子模块缺失问题。
  • 排查问题时,先看 checkout 步骤的日志,确认 repository、ref、token、fetch-depth 等参数是否符合预期,再往下游找原因。

如果你的项目已经足够复杂,下一步可以继续研究这些方向:actions/checkoutactions/cache配合如何优化依赖安装耗时;pull_request事件下的细粒度权限控制和分支保护策略;自托管 runner 上的容器化配置;以及和workflow_call结合的可复用 workflow 设计。

最后给你一个可直接执行的建议:如果今天什么都记不住,那就先记住一句话——在 GitHub Actions 里遇到“明明检出成功但后续步骤看不到文件、看不到历史、拉不到子模块、push 不回去”这一类问题,十有八九是 checkout 的配置参数没对齐。把这篇文章的常见问题表复制到你的团队文档里,至少能少花半天排查时间。

如果你是刚开始接触 GitHub Actions,建议先不要追求把所有参数都配齐,而是从fetch-depth: 1的浅检出开始,跑通一个最小构建,然后再逐步引入完整历史、子模块、多仓库等高级特性。这样既能控制 CI 成本,也更容易定位问题出现的环节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询