返回博客

GitHub Actions SSH 脚本里嵌套 Heredoc 会让 ${{ secrets }} 炸掉

在一次自动化部署改造中,我需要在 GitHub Actions 里通过 SSH 把若干密钥写入远程服务器的 .env.local。思路很直接:用 heredoc 批量写入,顺手加个 if ! grep -q 做幂等保护。写完本地测试没问题,推到仓库后 CI 直接报 syntax error: unexpected end of file,排查了近半小时才发现是一个隐蔽的展开顺序问题。

现象

CI runner 在执行 SSH action 的 script 字段时抛出以下错误(节选):

bash: line 12: syntax error: unexpected end of file
# 或者
OSSEOF: command not found
# 或者
unterminated quoted string

但在本地 shell 里手动跑同一段脚本完全正常,换任何人的机器也都正常,只有 GitHub Actions runner 上会报错

根因

问题出在两件事的执行顺序上:

  1. GitHub Actions 先展开 ${{ }} 表达式,再把结果字符串通过 SSH 发送给远端 shell 执行。
  2. Shell 再解析 heredoc,此时它收到的脚本已经是展开后的内容。

下面是触发问题的原始写法:

- name: Sync env
  uses: appleboy/ssh-action@v1.2.2
  with:
    script: |
      cat >> /app/.env.local << 'ENVEOF'
      if ! grep -q 'OSS_ACCESS_KEY_ID' .env.local 2>/dev/null; then
        cat >> .env.local << 'OSSEOF'
      OSS_ACCESS_KEY_ID=${{ secrets.OSS_ACCESS_KEY_ID }}
      OSS_ACCESS_KEY_SECRET=${{ secrets.OSS_ACCESS_KEY_SECRET }}
      OSS_BUCKET=my-bucket
      OSSEOF
      fi
      ENVEOF

Actions 展开 ${{ secrets.OSS_ACCESS_KEY_ID }} 时,如果 secret 的值包含换行、空格、或任何看起来像 heredoc 终止符的字符串,就会破坏外层 ENVEOF 或内层 OSSEOF 的定界符匹配。即便使用了带引号的 << 'OSSEOF'(本意是阻止 shell 变量展开),也无法阻止 Actions 在更早的阶段就完成了 ${{ }} 展开——引号对 Actions 模板引擎没有任何约束力。

坑:${{ }} 展开发生在 shell 看到脚本之前

<< 'EOF' 的单引号只能告诉 shell 不要展开 $var。但 ${{ secrets.X }}Actions 模板引擎在构建脚本字符串时处理的,此时 shell 还没有介入。结果是:shell 拿到的脚本已经是含有 secret 明文的字符串,若该字符串里恰好出现了 OSSEOFENVEOF,外层 heredoc 就会提前关闭,后续内容变成孤立命令,触发 syntax error

嵌套 heredoc 本身在 bash 里也是边缘场景,稍有不慎就会让 shell 混淆两层定界符的归属。

正解

彻底放弃嵌套 heredoc,改用逐行 echo 追加:

- name: Sync env
  uses: appleboy/ssh-action@v1.2.2
  with:
    script: |
      cd /app
      if ! grep -q 'OSS_ACCESS_KEY_ID' .env.local 2>/dev/null; then
        echo "OSS_ACCESS_KEY_ID=${{ secrets.OSS_ACCESS_KEY_ID }}" >> .env.local
        echo "OSS_ACCESS_KEY_SECRET=${{ secrets.OSS_ACCESS_KEY_SECRET }}" >> .env.local
        echo "OSS_BUCKET=my-bucket" >> .env.local
        echo "OSS_REGION=oss-cn-hangzhou" >> .env.local
      fi

每条 echo 是一条独立的 shell 语句。Actions 展开 ${{ secrets.X }} 后得到一个单行字符串,嵌入 echo "K=v" 里,shell 收到的是若干条普通的重定向命令,没有任何 heredoc 参与,定界符匹配问题彻底消失。

正解:用逐行 echo 替代嵌套 heredoc
  • 每个 ${{ secrets.X }} 只在单行 echo 里展开,最坏情况只破坏那一行,不会引发结构性 parse error。
  • 幂等保护(if ! grep -q)照样有效。
  • 如果 secret 可能含特殊 shell 字符(!$\),改用 printf '%s\n' 'KEY=${{ secrets.X }}' 或在运行时从 base64 解码,进一步隔离风险。

一句话外卖

在 GitHub Actions SSH 脚本里,${{ }} 的展开早于 shell 的 heredoc 解析——凡需要把 secret 写进远程文件,一律用逐行 echo,永远不要嵌套 heredoc。