环境变量与 Secrets
CI/CD 里大量配置并不适合写死在代码里:服务器地址、版本号前缀可以公开,但 API Token、密码、私钥绝对不能进代码仓库。GitHub Actions 把这两类东西分开处理——公开配置用 env,机密用 secrets。本章讲清楚它们的写法、作用范围,以及那条最重要的安全铁律:secret 的值绝不能以任何形式进日志。
1. env 的三个层级:小范围覆盖大范围
env 可以在三个层级定义,作用范围从大到小是:workflow 级 → job 级 → step 级。层级越小的定义会覆盖层级大的同名变量。
name: Env Demo
env: # workflow 级:所有 job、所有 step 都能用
LEVEL: workflow
REGION: ap-shanghai
on: [push]
jobs:
build:
env: # job 级:只在本 job 内有效,覆盖 workflow 级
LEVEL: job
runs-on: ubuntu-latest
steps:
- name: 打印 job 级变量
run: echo "$LEVEL / $REGION" # 输出:job / ap-shanghai
- name: step 级变量
env: # step 级:只在本 step 内有效,覆盖 job 级
LEVEL: step
run: echo "$LEVEL" # 输出:step规则总结:
- 就近优先:step 级覆盖 job 级,job 级覆盖 workflow 级。
- 同级不串门:job A 的
env不会自动出现在 job B 里;想跨 job 共享,要用第 13 章讲的 job output / artifact。 - 在
run脚本里用 shell 语法$VAR读取;在uses等表达式中用${{ env.VAR }}引用。
- 在
run:后面写的是 shell 脚本,用$VAR或${VAR}读取环境变量(由 runner 的 shell 注入)。 - 在 YAML 的表达式上下文(比如
if:、step 的with:参数里)要用${{ env.VAR }},这是在 YAML 被解析、交给 Actions 引擎求值前就展开的。 - 混用会出错:在
run里用${{ env.VAR }}也能工作(因为先展开成字符串再交给 shell),但在if:里用裸$VAR是无效的。
2. 在 run 与 uses 中引用 env
${{ env.FOO }} 在表达式上下文里处处可用。下面两个例子分别展示在 run 和 uses 中引用:
jobs:
demo:
env:
DEPLOY_ENV: staging
runs-on: ubuntu-latest
steps:
- name: 在 run 中引用
run: echo "当前部署环境是 ${{ env.DEPLOY_ENV }}"
- name: 在 uses 的参数中引用
uses: some/action@v1
with:
environment: ${{ env.DEPLOY_ENV }}注意上面 run 那行:这里用 ${{ env.DEPLOY_ENV }} 和用 $DEPLOY_ENV 效果相同,因为表达式先被展开成字符串 staging 再交给 shell。两种写法都合法,团队里统一一种即可。
3. Secrets:在仓库设置里配置,用 ${{ secrets.TOKEN }} 引用
Secret(机密)和 env 最大的区别:它的值不写进仓库文件,而是存放在 GitHub 的加密存储里,在 workflow 运行时才被注入。
配置位置有三个层级:
| 位置 | 入口 | 作用范围 |
|---|---|---|
| 仓库级 | 仓库 Settings > Secrets and variables > Actions > Repository secrets | 本仓库所有 workflow |
| 环境级(Environment) | Settings > Environments 里给某个环境配 secret | 仅在部署到该环境(environment)时可用,可设保护规则 |
| 组织级 | 组织 Settings > Secrets | 组织下多个仓库共享 |
引用方式永远统一:
steps:
- name: 调用外部 API
env:
TOKEN: ${{ secrets.API_TOKEN }}
run: curl -H "Authorization: Bearer $TOKEN" https://api.example.com/deploysecret 之所以安全,是因为它不经过你的文件、不经过 Git 历史。一旦你把 secret 写进 .yml 文件、写进代码、或者 echo 出来,它就不再是 secret 了。配置时只配名字和值,引用时只用 ${{ secrets.XXX }},切勿在文件里出现真实值。
4. 日志自动打码(MASK)与那条铁律
GitHub 会自动把所有 secret 的值在日志输出中打码(mask):即使某个命令不小心把 secret 的值打印出来了,日志里也只会显示 ***。这是 GitHub 的安全兜底。
但是——打码是"尽力而为"的,它挡不住你的主动泄露。下面这些行为会绕过打码:
# ❌ 危险:把 secret 原样 echo 到日志
echo "$TOKEN"
# ❌ 危险:写进文件再 cat / 上传,打码只对"日志流"生效,文件内容不在保护范围内
echo "$TOKEN" > token.txt
cat token.txt
# ❌ 危险:把 secret 当 artifact 上传,artifact 不在日志打码保护范围内- 不要
echo ${{ secrets.X }}到日志——即便你以为有用,也会把值暴露(而且打码只在"GitHub 识别出这是 secret 值"时生效,拼装、转义、部分打印都可能露出破绽)。 - 不要把 secret 写入文件后再
cat/ 打印——打码只保护日志流,文件内容不在保护范围内。 - 不要把含 secret 的文件作为 artifact 上传——artifact 可下载,等于把机密打包发出去。
记住:GitHub 的打码是安全网,不是你可以随意 echo 的理由。
5. GITHUB_ENV:把变量传给后续 step
有时候某个 step 计算出了一个值(比如版本号、构建 ID),想让同一个 job 里后面的 step也能用。办法是往一个特殊文件 $GITHUB_ENV 里追加 NAME=VALUE:
steps:
- name: 计算版本号
run: |
VERSION="v1.2.$(date +%Y%m%d)"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: 后续 step 使用
run: echo "本次构建版本是 $VERSION"原理:GitHub 在 runner 上维护一个 $GITHUB_ENV 文件,你往里追加的每一行 NAME=VALUE 会被注入到当前 job 后续 step 的环境变量中。注意:
- 只对"后面的 step"生效,当前 step 这一行之后也不能立即用,要等下一个 step。
- 适合传递简单的字符串;多行内容可用
>> $GITHUB_ENV配合<<EOF语法(进阶,用到再查)。
6. GITHUB_OUTPUT:把值传给后续 step 或作为 job output
与 GITHUB_ENV 类似,还有一个特殊文件 $GITHUB_OUTPUT,用来把值输出出去。它的两个用途:
- 传给同 job 后续 step(通过
steps.<id>.outputs.<name>读取)。 - 作为 job 级别的 output,供依赖它的其他 job 使用(呼应第 13 章"job 之间传数据")。
steps:
- name: 生成构建产物名
id: meta
run: echo "artifact_name=build-$(date +%s)" >> $GITHUB_OUTPUT
- name: 读取并使用
run: echo "产物名叫 ${{ steps.meta.outputs.artifact_name }}"这里 id: meta 给 step 起了名字,后续用 ${{ steps.meta.outputs.artifact_name }} 读取。GITHUB_OUTPUT 与 GITHUB_ENV 的区别在于:前者语义上是"这个 step 产出了一个结果",更适合配合 needs 跨 job 传递;后者语义上是"设置一个环境变量"。第 13 章会详细展开 job 之间的数据传递。
env(公开配置变量):值可以安全出现在日志、配置里,比如NODE_ENV=production、REGION=ap-shanghai。随便打印没问题。secrets(机密):值一旦泄露就有真实风险,比如数据库密码、云厂商 AK/SK、部署 token。绝不打印、绝不进文件、绝不传 artifact。- 一个简单判断:如果这个值出现在日志里你会紧张,那它就该是 secret,而不是 env。
7. 完整示例:设置 env、用 secrets 调外部 API、用 GITHUB_ENV 传变量
name: Deploy
on:
push:
branches: [main]
env:
DEPLOY_ENV: production
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # 可选:绑定受保护环境,使用环境级 secret
steps:
- uses: actions/checkout@v4
- name: 计算本次发布标签
run: |
TAG="release-$(date +%Y%m%d-%H%M%S)"
echo "TAG=$TAG" >> $GITHUB_ENV
- name: 调用部署 API(使用 secret)
env:
API_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: |
echo "开始部署到 ${{ env.DEPLOY_ENV }},标签 ${{ env.TAG }}"
curl -X POST https://api.example.com/deploy \
-H "Authorization: Bearer $API_TOKEN" \
-d "{\"tag\":\"${{ env.TAG }}\"}"注意:第 25 行的 echo 打印的是 ${{ env.TAG }} 和 ${{ env.DEPLOY_ENV }},都是公开变量,安全;真正的机密 API_TOKEN 只作为请求头发给 API,没有 echo 出来。
8. 小结
env有三个层级:workflow / job / step,小范围覆盖大范围;run中用$VAR,表达式上下文用${{ env.FOO }}。- Secrets 配置在仓库 / 环境 / 组织三级设置里,引用统一为
${{ secrets.TOKEN }},值不进仓库文件。 - GitHub 会对 secret 值在日志中自动打码(MASK),但这只是兜底,不能主动
echo、不能写文件cat、不能当 artifact 上传。 $GITHUB_ENV把变量追加进去,供同 job 后续 step 使用。$GITHUB_OUTPUT把值输出,供同 job 后续 step(steps.<id>.outputs.<name>)或跨 job(job output)使用,第 13 章详述。- 公开配置用
env,真实机密用secrets;判断标准:出现在日志里你会紧张吗? - 下一章:条件与表达式 → 我们将学习用
if:配合github/needs等上下文和success()/failure()等函数,精确控制 job 与 step 是否执行。
- 下面这段 YAML 中,
echo实际会输出什么?为什么?
env:
NAME: global
jobs:
a:
env:
NAME: joblevel
runs-on: ubuntu-latest
steps:
- name: s1
env:
NAME: steplevel
run: echo "$NAME"
- name: s2
run: echo "$NAME"- 你想在 workflow 里调用一个需要
AWS_SECRET_ACCESS_KEY的外部 CLI。请写出正确引用 secret 的env配置,并说明为什么不能写echo "$AWS_SECRET_ACCESS_KEY"调试。 - step A 算出了一个提交哈希
SHA=abc123,想让 step B(同 job)打印它。请用$GITHUB_ENV写出 step A 的代码,以及 step B 读取并打印的代码。 - 判断:把数据库密码放在
env而不是secrets里,只要不在日志里echo就安全。这个说法对吗?为什么?