GitHub Actions: 在 workflow 中更新 workflow 文件的正确方法
GitHub Actions, CI/CD, PAT, git, and devops
问题描述
GitHub Actions workflow 中,git push 修改 .github/workflows/*.yml 时报错,提示 “refusing to allow a GitHub App to create or update workflow”。即使 Fine-grained PAT 显式授予了 workflows: write 权限,仍然被拒。
! [remote rejected] main -> main (refusing to allow a GitHub App to create or update
workflow `.github/workflows/sync.yml` without `workflows` permission)
本文由 AI 助手 opencode 生成
根因分析
逐一排除可能原因:
- ❌ 权限不足?— PAT 有
workflows: write,排除。 - ❌ URL 前缀(
x-access-token:vsoauth2:)?— extraheader 优先级高于 URL 凭证,单纯改 URL 无效,排除。 - ❌ PAT 类型(classic vs fine-grained)?— 清除 extraheader 后两种 PAT 均可用,类型不是根源,排除。
- ✅ 真凶:extraheader 中的
x-access-token:包装。
查看 actions/checkout 源码 (src/git-auth-helper.ts):
const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`, // ← 任何 token 都包
'utf8'
).toString('base64')
this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}`
actions/checkout 将所有 token 包装为 x-access-token: 存入 git config 的 https.extraheader:
[http "https://github.com/"]
extraheader = AUTHORIZATION: basic <base64("x-access-token:<TOKEN>")>
git push 发送该 header 时,服务器看到 x-access-token: 前缀,判定为 GitHub App 安装 token,转而读取 workflow YAML 的 permissions: 块,忽略 PAT 自身的 scope。因此即使 PAT 有 workflows: write 权限,仍然被拒。
注意:
GITHUB_TOKEN不支持workflows: write权限,解析 workflow YAML 时即报错,无法用于更新 workflow 文件。必须使用 PAT(classic 或 fine-grained 均可)。
解决方案
根源是 x-access-token: 包装。以下方案按推荐优先级排列:
方案 C(推荐):persist=false + 手动写入裸 PAT extraheader
- uses: actions/checkout@v4
with:
token: $
persist-credentials: false
# ... 中间步骤 ...
- name: Update cron (schedule only)
if: github.event_name == 'schedule'
env:
NEW_N: $
WORKOUTS_WORKFLOW_PAT: $
run: |
# persist-credentials: false → no extraheader written.
# Write bare PAT extraheader (no x-access-token: prefix)
# so server routes as PAT, not GitHub App.
git config --add http.https://github.com/.extraheader \
"AUTHORIZATION: basic $(printf '%s:' "$WORKOUTS_WORKFLOW_PAT" | base64 -w0)"
git add .github/workflows/sync.yml
git diff --cached --quiet || \
git commit -m "chore: sync interval N=$NEW_N"
git pull --rebase
git push
原理: persist-credentials: false 阻止 checkout 写入 extraheader,然后手动写入一条不含 x-access-token: 前缀的 Authorization header。服务器看到裸 PAT 值,正确识别为用户 PAT。
优点: PAT 不出现在进程参数(ps aux)、不修改 remote URL、一行 bash 搞定。
方案 D(备选):persist=true + 清除 extraheader + URL 内嵌 PAT
git config --unset-all http.https://github.com/.extraheader || true
git remote set-url origin https://$WORKOUTS_WORKFLOW_PAT@github.com/Lax/workouts.git
缺点: URL 中的 PAT 在 ps aux 中可见(单租户 VM 风险极低,但可能过安全审查)。
四种方案对比
| # | 方案 | ps 安全 |
代码行数 | 已验证 |
|---|---|---|---|---|
| C | persist=false + 手动裸 PAT extraheader | ✅ | 2 行 | ✅ |
| A | persist=false + URL 嵌入 PAT | ⚠️ | 1 行 | ✅ |
| D | persist=true + 清除 extraheader + URL PAT | ⚠️ | 2 行 | ✅ |
| B | persist=true + mutate extraheader | ✅ | 5 行 | ❌ |
简要说明:
- 方案 A(
persist=false+ URL 嵌入 PAT)代码最简但 PAT 会暴露在ps aux中 - 方案 B(
persist=true+ 解码 extraheader 替换x-access-token:前缀)ps安全但步骤繁琐,未验证
关键点:
- 根源是
x-access-token:包装,不是 PAT 类型 — classic 和 fine-grained PAT 均可用 persist-credentials: false从源头阻止 — 最优雅- extraheader 优先级高于 URL 凭证 — 仅改 URL 不够
复盘
这个问题的排查过程本身值得记录:AI 在”token 类型 → permissions → 版本 → scope”等 参数上反复尝试,每次只改一个变量、运行、看结果,消耗了大量时间。真正突破来自人工介入 ——缩小验证范围,设计对比实验(几种 token × 是否有 extraheader 的交叉测试), 最终定位到 extraheader 而非 PAT 类型。
AI 擅长执行已知路径的试验,但在搜索空间过大时容易原地打转。 人类的贡献在于精确定义”什么值得测”,缩小搜索空间。 这是当前 AI 辅助调试的一个典型局限。