Steps 与 Actions
这一课你将能做到
- 区分 step 的两种形态:
run(shell 命令)与 uses(复用 Marketplace / 仓库内 action),并说清各自的适用场景
- 写出「checkout → setup 工具链 → 在子目录执行命令」的标准三段,并用
working-directory 在 backend/ 或 platform/ 下跑命令
- 用
$GITHUB_OUTPUT 在同 job 的 step 之间传递一个简单输出(如生成的镜像 tag)
为什么要学
Job 只是容器;真正干活的是 step。几乎所有 CI 都长同一个骨架:先把代码拉到 runner,再装 JDK/Node,再执行项目自己的脚本。
不会用 uses 时,人会手写很长的安装脚本且难维护;乱用 @main 浮动引用时,又会踩供应链风险——上游一改,你的 CI 行为跟着变,而且你毫不知情。
本课把「最小执行单元」钉清楚,并为后面 Java/Node 实战课铺路。
先建立图像
job(一台 ubuntu runner)
step1 uses: actions/checkout@v4 ← 把仓库克隆进来
step2 uses: actions/setup-java@v4 ← 装 Temurin 21
step3 run: ./mvnw -B verify ← 你自己的命令
working-directory: backend
人话:uses 像请一位熟手工具人(checkout、setup-node);run 像你自己敲终端。先请人把工位摆好,再敲项目命令。
| 写法 | 做什么 | 典型用途 |
|---|
uses: owner/repo@ref | 运行一个 action | checkout、cache、docker login |
run: … | 在 shell 里执行 | npm ci、./mvnw verify |
with: | 传给 action 的输入 | java-version: "21" |
env: / working-directory | 管环境与目录 | monorepo 子项目 |
id: + outputs | 给 step 命名并暴露输出 | step 间传值 |
跟着做
步骤 1:checkout 后看目录
jobs:
inspect:
runs-on: ubuntu-latest
steps:
- name: 拉取代码
uses: actions/checkout@v4
- name: 列出仓库根目录
run: ls -la
日志里应能看到 backend、platform、.github 等。没有 checkout 就 ls,看到的是空工位——这是初学者最常见的「文件找不到」。
步骤 2:setup 工具链 + 子目录命令
以 Java 为例(Node 课会对称写 npm):
- name: 安装 JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: 查看 Maven Wrapper
working-directory: backend
run: |
chmod +x mvnw
./mvnw -v
应看到 Java / Maven 版本输出。working-directory 避免每条命令都写 cd backend &&。
步骤 3:对照 run 与 uses
# 方式 A:用官方 action 装 Node(推荐)
- uses: actions/setup-node@v4
with:
node-version: "20"
# 方式 B:自己 run 下载安装(脆弱、难维护,仅作对比)
# - run: curl -fsSL https://deb.nodesource.com/setup_20.x | sudo bash - && sudo apt-get install -y nodejs
步骤 4:Pin 版本,拒绝浮动 main
# 推荐:大版本 tag(生产可进一步收紧到 commit SHA)
- uses: actions/checkout@v4
# 不推荐:跟踪默认分支,行为可能突变
# - uses: actions/checkout@main
生产加固可读官方「Security hardening」:对关键 action 钉 commit SHA,并配合 Dependabot 更新。本课最低要求:不要 @main。
步骤 5:step 输出传递
- name: 生成临时 tag
id: meta
run: |
# 写入 GITHUB_OUTPUT,供后续 step 读取
echo "tag=1.0.${GITHUB_RUN_NUMBER}" >> "$GITHUB_OUTPUT"
- name: 打印 tag
run: echo "tag=${{ steps.meta.outputs.tag }}"
应看到带 run number 的 tag。要点:id 给 step 命名,输出写进 $GITHUB_OUTPUT,引用格式是 steps.<id>.outputs.<name>。跨 job 传递要用 job outputs 或 artifact(进阶);本课先掌握同 job 内传递。
想给后续所有 step 设置环境变量,用 $GITHUB_ENV:
- run: echo "BUILD_VERSION=1.0.${GITHUB_RUN_NUMBER}" >> "$GITHUB_ENV"
- run: echo "版本是 $BUILD_VERSION"
步骤 6:默认定目录(可选整理)
jobs:
platform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: platform
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: npm
cache-dependency-path: platform/package-lock.json
- run: npm ci
注意:defaults.run.working-directory 只影响 run,不影响 uses 的默认路径;cache-dependency-path 仍常写相对仓库根的路径。
讲清楚要点
- 因为 runner 初始没有你的代码,所以几乎每个构建 job 第一步都是 checkout——找不到文件时先查是否漏了这一步。
- 因为
uses 复用社区/官方逻辑,所以装 Node/Java/Docker login 优先用维护中的 action——少维护安装脚本,还自带跨平台兼容。
- 因为 action 也是代码,所以引用要 pin 版本(tag 或 SHA),避免
@main——降低供应链突发变更风险。
- 因为 monorepo 有多项目根,所以用
working-directory 或 defaults.run——命令与本地 cd 习惯对齐,日志也更可读。
- 因为 Linux runner 的
run 默认用 bash 执行,所以多行脚本注意失败即停与引号——复杂脚本可抽成 backend/scripts/ci.sh 再 run: ./scripts/ci.sh,可本地调试。
- 因为后续 step 常依赖前面的产物路径与输出,所以
name/id 要起得可读——排错时日志标题就是地图。
你可能会懵的地方
-
「uses 的代码在哪执行?信任谁?」
Action 来自 GitHub 上的仓库(或 Docker 镜像)。它以你 workflow 的权限运行,所以只引入你信任的发布方;组织可限制允许的 action 白名单。
-
「为什么 setup-java 之后还要 ./mvnw?」
setup 提供的是 JDK(及可选缓存);构建生命周期仍由项目的 Maven Wrapper / pom 管理,与本地一致。工具版本与构建配置是两回事。
-
「Windows 上 run 一样吗?」
runs-on: windows-latest 时默认 shell 是 PowerShell,命令不通用。本站主路径用 ubuntu-latest,与多数 Linux 生产环境一致。
-
「GITHUB_OUTPUT 和旧的 ::set-output?」
旧命令语法已废弃移除。统一用写入 $GITHUB_OUTPUT 文件的方式。
-
「一个 step 失败了,后面的 step 还跑吗?」
默认停止。想无论成败都跑(如清理),给该 step 加 if: always();想仅失败时跑(如通知),加 if: failure()。
错法对照
| 错法 | 问题 | 正解 |
|---|
不做 checkout 直接 npm ci | 工作区无 package.json | 先 actions/checkout |
uses: actions/setup-node@main | 行为不可预期 | @v4 或 commit SHA |
| 所有命令写在仓库根且路径混乱 | backend/pom.xml 找不到 | working-directory: backend |
| 依赖「上一步手工 cd」影响下一步 | 每个 run 都回到约定目录 | 显式 working-directory 或 defaults |
用旧 ::set-output 传值 | 静默失效 | echo "k=v" >> $GITHUB_OUTPUT |
卡点急救
- step 报
No such file or directory:先确认 checkout 是否存在且成功;再确认 working-directory 指向的目录在仓库里真实存在;monorepo 场景重点检查是「仓库根相对路径」还是「项目内相对路径」搞混了。
${{ steps.meta.outputs.tag }} 读出来是空:检查写入方 step 是否有 id: meta(名字必须一致)、是否真的写进了 $GITHUB_OUTPUT(注意 >> 追加且一行一条 key=value);写作失败常见于把输出写到了普通 stdout。
- action 版本浮动导致昨天绿今天红:排查是否引用了
@main 或浮动 tag;改为固定大版本 tag(@v4),生产流水线对关键 action 进一步钉 commit SHA,用 Dependabot 定期升级并走 PR 验证。
面试常问
-
run 和 uses 有什么区别?什么时候自己写 shell、什么时候用 action?
run 在 runner 的 shell 里执行任意命令,适合项目自身的构建命令(npm ci、./mvnw verify);uses 执行一个封装好的 action,适合跨项目通用的环境准备(checkout、装 JDK/Node、docker login)。判断标准:逻辑通用且社区有维护良好的实现就用 action;项目特定逻辑用 run,复杂脚本抽成仓库脚本文件。
-
step 之间如何传递数据?job 之间呢?
同 job 内:step 用 $GITHUB_OUTPUT 暴露输出,后续用 ${{ steps.id.outputs.name }} 读;环境变量用 $GITHUB_ENV。跨 job:磁盘不共享,小数据用 job 级 outputs(底层仍走 GITHUB_OUTPUT),文件用 upload-artifact/download-artifact。
-
如何降低使用第三方 action 的供应链风险?
三层做法:① 引用固定版本 tag 而非 @main;② 关键 action 钉 commit SHA,保证内容不可变;③ 用 Dependabot 自动开 PR 升级、经 CI 验证后再合。再配合仓库/组织层的 action 白名单与最小 permissions。
自测三问
run 与 uses 各适合什么?举本站一步例子。
- 为什么不建议
uses: …@main?
- monorepo 里如何让
./mvnw 在 backend/ 下执行?
小结一句话
checkout → setup 工具链 → 在子目录 run 项目命令,是 CI step 的最常见三步;输出用 GITHUB_OUTPUT 传。
延伸