条件与表达式
前几章的 workflow 大多是"事件一触发,所有 job、所有 step 无脑全跑"。但真实 CI/CD 需要更聪明的调度:只在推到 main 时部署、只在 PR 事件里跑检查、某步失败后还要发钉钉/Slack 通知、提交信息带 [skip ci] 就跳过。这些"要不要跑"的判断,靠的就是 if: 条件与 ${{ }} 表达式。
1. if: 控制 job 或 step 是否执行
if: 可以写在 job 级(控制整个 job 跑不跑)或 step 级(控制单个 step 跑不跑)。条件为"真"才执行,为假就跳过(在界面上显示为灰色 "skipped")。
jobs:
test:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request' # job 级:只有 PR 事件才跑测试
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # job 级:只有推到 main 才部署
steps:
- uses: actions/checkout@v4
- run: ./deploy.shif: 的值是一个表达式(写在 ${{ }} 里,不过 if: 比较特殊,可以省略外层 ${{ }},直接写表达式体)。两种写法等价:
if: github.ref == 'refs/heads/main'
if: ${{ github.ref == 'refs/heads/main' }}if:、env:、with: 等本身就是"表达式上下文",GitHub 会自动按表达式解析。所以 if: github.ref == 'refs/heads/main' 已经够了,再包一层 ${{ }} 反而冗余。但正文里提到 github.ref 这类写法时,仍要用反引号包成行内代码,避免被当标签解析。
2. 上下文对象:从运行环境里读信息
条件判断需要"读取当前运行的信息",这些信息的来源就是上下文对象(context)。常用几个:
| 上下文 | 关键字段 | 含义 |
|---|---|---|
github | github.ref、github.sha、github.event_name、github.actor、github.repository | 触发本次运行的仓库/事件信息 |
inputs | inputs.xxx | workflow_dispatch 等手动触发时传入的参数 |
needs | needs.<job_id>.outputs.xxx | 被依赖 job 的输出(配合 needs: 依赖) |
env | env.FOO | 当前可用的环境变量 |
matrix | matrix.os 等 | 矩阵策略里当前这次迭代的取值 |
steps | steps.<id>.outputs.xxx | 同 job 内某 step 的输出(呼应第 11 章 GITHUB_OUTPUT) |
重点看 github 的几个字段:
github.ref:触发事件对应的引用。推到main时是refs/heads/main,打 tagv1.0时是refs/tags/v1.0,PR 事件里通常是refs/pull/123/merge。github.sha:触发提交的完整 commit SHA。github.event_name:事件名,如push、pull_request、issues、workflow_dispatch。github.actor:触发本次运行的人(用户名)。github.repository:仓库名,形如owner/repo。
示例:根据事件名和分支组合判断。
steps:
- name: 仅 main 上的 push 才执行
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: echo "这是 main 的 push"3. 状态检查函数:success() / failure() / always() / cancelled()
默认情况下,一个 step 只有当它前面的 step 都成功时才执行(等价于隐式 if: success())。但有时你需要打破这个默认:失败后还要清理、无论成败都要发通知。这就需要状态函数。
| 函数 | 含义 | 典型用途 |
|---|---|---|
success() | 前面所有 step 都成功 | 默认行为,通常不用显式写 |
failure() | 前面有任意 step 失败 | 失败时才跑:发告警、上传错误日志 |
always() | 无论前面成功失败都跑 | 清理资源、归档产物 |
cancelled() | 工作流被取消时 | 取消时的善后 |
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 构建
run: ./build.sh
- name: 失败通知
if: failure() # 只有构建失败才发
run: curl -X POST ${{ secrets.DINGTALK_WEBHOOK }} -d '{"msg":"构建挂了"}'
- name: 清理(无论成败)
if: always() # 成功失败都跑
run: ./cleanup.sh常见组合拳:正常步骤之后,用 if: failure() 发"失败告警",用 if: always() 做"无论结果都清理"。这样哪怕中间某步崩了,你既能收到通知,临时文件/容器/云资源也被回收,不会留下"半截"的脏状态。注意 always() 不等于"忽略错误继续执行后面的 step"——它只是决定"这个 step 本是否运行",step 内部该失败还是失败。
4. 表达式语法 ${{ }} 与常用函数
在 if: 之外的很多地方(如 with:、name:、组合表达式),也要用 ${{ }} 包裹表达式。常用函数:
| 函数 | 作用 | 例子 |
|---|---|---|
contains(needle, haystack) | 是否包含子串 | contains(github.event.head_commit.message, '[skip ci]') |
startsWith(str, prefix) | 是否以某前缀开头 | startsWith(github.ref, 'refs/tags/') |
format(str, args...) | 格式化字符串 | format('{0}-{1}', github.run_id, github.sha) |
toJson(value) | 序列化成 JSON(调试上下文很有用) | toJson(github.event) |
比较运算 ==、!=、以及逻辑 &&、||、! 都支持。字符串字面量用单引号。
- name: 打印整个 event 调试
run: echo "${{ toJson(github.event) }}"当你不确定某个上下文里到底有什么字段时,临时加一个 run: echo "${{ toJson(github.event) }}",把整个事件对象打印成 JSON,肉眼就能看清有哪些可用字段。调试完记得删掉,避免把敏感信息(虽然 GitHub 会打码 secret,但 event 里可能有别的敏感内容)留在日志里。
5. 实例一:只在 push 到 main 时部署
这是最常见的"保护主干"模式——feature 分支的 push 只跑测试,只有合进 main 才部署。
name: CI/CD
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
deploy:
needs: test # 先测试通过
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # 仅 main
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh注意 github.ref 是完整引用名 refs/heads/main,不要写成 main 去比较,否则永远不相等。
6. 实例二:只有 PR 事件才跑检查
有些检查(比如"要求 PR 描述非空")只该在 PR 时跑,push 到分支时没必要。
jobs:
pr-check:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- name: 校验 PR 描述
run: |
if [ -z "${{ github.event.pull_request.body }}" ]; then
echo "PR 描述不能为空" && exit 1
fi7. 实例三:某 step 失败仍跑通知
配合 failure(),构建/测试挂了也要第一时间通知到人。
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: 测试
run: npm test
- name: 飞书/钉钉告警
if: failure()
env:
WEBHOOK: ${{ secrets.NOTIFY_WEBHOOK }}
run: |
curl -X POST "$WEBHOOK" \
-H 'Content-Type: application/json' \
-d '{"text":"⚠️ 测试失败,请查看运行日志"}'failure() 看的是"当前 job 里位于它之前的 step"是否有失败。如果你在中间插了一个 if: always() 的 step,该 step 本身的成功失败也会纳入后续 failure() 的判定。通知 step 一般放在最后,且自身不要失败(否则可能连环告警),必要时给它加 continue-on-error: true。
8. 实例四:用 contains 跳过 [skip ci]
团队协作时常约定:提交信息里带 [skip ci] 就不跑 CI(比如只改了文档)。
jobs:
ci:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, '[skip ci]')"
steps:
- uses: actions/checkout@v4
- run: npm test注意这里表达式前加了 ! 取反,且整个 if 用引号包起来(因为字符串里含 [] 等字符,加引号更稳妥)。含义:提交信息不包含 [skip ci] 时才跑。
也可以用 startsWith 判断分支前缀——比如只对 release/ 开头的分支做发布准备:
if: startsWith(github.ref, 'refs/heads/release/')9. 小结
if:写在 job 级或 step 级控制是否执行;if:是表达式上下文,可省略外层${{ }}。- 条件判断的数据来自上下文对象:
github(ref/sha/event_name/actor/repository)、inputs、needs、env、matrix、steps。 - 状态函数
success()(默认)、failure()、always()、cancelled()用来打破"前面成功才跑"的默认,常见于失败通知与清理善后。 - 表达式支持
==、!=、&&、||、!与函数contains()、startsWith()、format()、toJson();字符串用单引号。 failure()+always()组合是"失败告警 + 必清理"的标准套路。github.ref是完整引用名(如refs/heads/main),比较时要写全,别只写main。- 下一章:job 之间传数据 → 我们将用
needs、outputs和 artifact,把上游 job 的计算结果/产物可靠地交给下游 job。
- 写一个 job 级
if:,要求"仅在 push 事件且推到main分支"时执行。给出完整的条件表达式。 - 下面这个通知 step 有什么问题?应该怎么改?
steps:
- name: 构建
run: ./build.sh
- name: 不论成败都通知
if: failure()
run: echo "done"- 你希望"提交信息以
[docs]开头时跳过测试"。用startsWith写出该if:条件(取反形式)。 - 有一个 step 会打印整个 event 用于调试:
- run: echo "${{ toJson(github.event) }}"它有什么潜在风险?调试完应如何处理?