先问一个问题:你有没有在 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-depth、token、path、submodules这些参数。 - 如果按“切换分支”来理解,你会纠结于
ref的写法,却忽略了 runner 上其实没有仓库这个前提。
所以这篇文章真正要解决的是三类问题:
第一,概念混淆问题。把actions/checkout和git 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)上。它的任务是在一个全新的、通常没有任何项目代码的环境里,把指定仓库的指定版本代码拉到工作目录。
我们直接看它的源码逻辑(这是理解这个概念最直接的方式)。它的核心执行过程大致是:
- 在 runner 上创建或进入一个空的工作目录。
- 执行
git init初始化一个空的 Git 仓库。 - 添加远程仓库地址,即
origin。 - 执行
git fetch拉取指定ref对应的提交。 - 执行
git checkout --detach <commit_id>或git switch检出对应提交。 - 根据参数决定是否配置
token到.git/config、是否拉取子模块、是否清理工作区。
看到第 5 步你会发现,它内部确实也用了git checkout,但这里的前提是:它先完成了一个类似git clone的过程,然后在临时仓库里做了一次孤儿检出来匹配指定 commit。这一切对用户是封装好的。
2.3 为什么容易混淆
混淆的根本原因有三个:
- 名字里都带
checkout,搜索引擎和 AI 工具都会把它们混在一起。 - 很多教程在解释
actions/checkout时,会简单说“它相当于执行了git clone和git checkout”,但这句解释其实省略了关键细节。 - 在 Runner 上执行命令时,你确实可以在 shell 里看到它调用了
git checkout,于是不熟悉的人会以为“这个 Action 就是封装了一个 checkout 命令”。
为了彻底理清,我用一个表格对比:
| 对比维度 | git checkout | actions/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 上会发生这些事:
- 创建
$GITHUB_WORKSPACE工作目录。 - 在该目录中初始化一个临时 Git 仓库。
- 将
github.repository(即你当前的仓库)配置为origin。 - 以
GITHUB_TOKEN为凭据,从中获取当前分支/提交的最新代码。 - 检出代码到工作区,并切换到 detached HEAD 状态。
- 不配置任何额外用户信息,因为后续 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/checkout的clean参数默认值为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.ref是refs/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: 04.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: false4.7 submodules
是否检出子模块。可选值有true、recursive、false。默认false。
如果你的仓库使用 Git Submodule,且构建过程需要子模块代码,必须设置:
- name: Checkout with submodules uses: actions/checkout@v4 with: submodules: recursiverecursive会递归拉取嵌套子模块。对于私有子模块仓库,如果子模块 URL 是 HTTP 形式,还需要配置token,否则拉取权限不足。
4.8 persist-credentials
默认true,会把 token 保存到.git/config中的http.https://github.com/.extraheader,这样后续git push或git 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 main5.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>)根本看不到main或remotes/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 fetch和git 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 fetch或actions/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执行:
- 环境准备阶段。runner 创建
GITHUB_WORKSPACE目录,并确保 shell 环境(sh/bash)可用。 - 设置 Git 全局配置。
actions/checkout会设置user.name和user.email为临时的 GitHub Actions 用户,避免后续 git 操作因缺少身份而失败。 - 初始化临时仓库。在 workspace 下执行
git init,并配置remote.origin.url为仓库地址。 - 认证配置。根据传入的
token值,向 Git 请求头中写入Authorization: token <token>,或者设置为不持久化。 - 拉取代码。按照
fetch-depth参数执行git fetch。这一步会从远程仓库拉取指定 ref 的 commit 数据。如果fetch-depth: 1,只会拉取最新的一个提交快照;如果是 0,拉取全部历史。 - 检出代码。执行类似
git checkout --detach <commit>的操作,把工作区内容更新到目标 commit。注意是 detached HEAD,没有在本地创建对应的分支。 - 处理子模块和 LFS。如果开启
submodules或lfs,在这一步执行相应的拉取。 - 清理工作区。如果
clean: true,删除工作区中不被 Git 跟踪的文件,确保构建环境干净。 - 持久化凭据。如果
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/checkout与actions/cache配合如何优化依赖安装耗时;pull_request事件下的细粒度权限控制和分支保护策略;自托管 runner 上的容器化配置;以及和workflow_call结合的可复用 workflow 设计。
最后给你一个可直接执行的建议:如果今天什么都记不住,那就先记住一句话——在 GitHub Actions 里遇到“明明检出成功但后续步骤看不到文件、看不到历史、拉不到子模块、push 不回去”这一类问题,十有八九是 checkout 的配置参数没对齐。把这篇文章的常见问题表复制到你的团队文档里,至少能少花半天排查时间。
如果你是刚开始接触 GitHub Actions,建议先不要追求把所有参数都配齐,而是从fetch-depth: 1的浅检出开始,跑通一个最小构建,然后再逐步引入完整历史、子模块、多仓库等高级特性。这样既能控制 CI 成本,也更容易定位问题出现的环节。