GitHub Actions 部署步骤跑了 6 小时被强杀 —— 排查与修复

GitHub Actions 部署步骤挂在那里直到 6 小时 job 上限被取消,通常是等待部署的轮询、网络出站受阻,或部署 CLI 在等一个永远不会到的输入。

平时 4 分钟跑完的 GitHub Actions workflow 卡在部署步骤上 6 小时,最后被 The job running on runner X has exceeded the maximum execution time of 360 minutes 强杀。本地手动跑 vercel deploy --prodfirebase deploy 又一切正常。取消重跑有时成功有时还卡。原因几乎都是部署 CLI 在静默地等一个永远不会出现的东西:缺失的交互输入、需要公网回调的 webhook、卡死的 wait-for-deployment 轮询、SSH 部署等首次 host key 确认。6 小时是 job 上限不是部署上限 —— 你真正需要的是步骤级 timeout,再加一个真正的修复。

常见原因

按实际遇到频率排序。

1. 部署 CLI 在等交互提示

firebase deploy 检测到 config 变更会问 ? You're about to deploy a function in region X. Continue? (Y/n)。CI 里没有 stdin,它会无限静默等待。

如何识别:步骤日志在部署中途断尾、无报错。最后一行是 Detected target change. Continue?,或在 Starting deploy... 后无更新。

2. wait-for-deployment 轮询永不收敛

bobheadxi/deployments 或自定义 gh api 轮询会等部署状态更新。如果部署根本没回报(部署者用了别的 SHA、或状态 webhook 静默失败),轮询无限继续。

如何识别:YAML 里有 with: timeout: 600while gh api ...; sleep 30; done。日志里看到反复的轮询行没有终止。

3. SSH 部署卡在 host key 确认

ssh user@host 首次会问 Are you sure you want to continue connecting (yes/no)?。CI runner 没有 known_hosts 条目,它一直等。

如何识别:步骤里有 sshscprsync,或类似 appleboy/ssh-action。SSH 连接尝试后日志再无输出。

4. 部署中途网络出站被限速 / 拒绝

云上传(S3、GCS、Cloudflare R2)撞到 runner 出站限速或公司代理时,TCP 连接不报错就那么挂着不再前进。

如何识别:日志先有进度,然后停在比如”uploaded 47/120 assets”再无下文。

5. 步骤没设 timeout-minutes,job 默认 360 分钟

GitHub Actions job 默认 6 小时(360 分钟)。步骤级没设 timeout-minutes,一个挂住的步骤把整个 job 预算吃干。

如何识别:workflow YAML 的部署步骤没有 timeout-minutes:

6. 部署 hook 触发了但目标服务静默拒收

webhook 风格的部署(Render、Railway、带 hook URL 的 Fly)会回 200 OK,但根本不开构建。等部署完成的步骤永远等不到那个不存在的部署。

如何识别:hook 步骤成功;轮询步骤挂住;目标服务面板在那个时间根本没看到部署记录。

7. 缓存恢复卡在坏 blob 上

actions/cache@v3 恢复 5 GB 构建缓存时,缓存服务器可能返回一个卡死的流,不报错也不前进。

如何识别Run actions/cache 显示 Downloading cache... 后再无完成行。新版有所改善但偶尔还撞。

开始排查前

  • 取出失败 job 的 workflow YAML。
  • 顺着日志确定卡在哪个步骤。
  • 是必现还是偶发?
  • 拿到目标部署平台的面板权限,能对照他们端真实发生了什么。
  • 记录 runner 类型(ubuntu-latest、self-hosted、GitHub-hosted larger runner),出站 / DNS 行为不一样。

需要收集的信息

  • 完整的部署步骤 YAML,含 with:env:、命令。
  • 步骤日志超时前最后 50 行。
  • 同一时间窗口目标平台的面板日志(Vercel deployments、Firebase function logs)。
  • 用了哪些第三方部署 action 及版本。
  • 部署 CLI 期望的 secrets / env 在 workflow 里是否真的设了。
  • gh run view <run-id> --log 输出,比 Web UI 抓日志更干净。

分步修复

顺序:先止血、再修根因。

步骤 1:给步骤加 timeout-minutes

任何部署步骤立即设上限:

- name: Deploy to production
  timeout-minutes: 15
  run: vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }}

job 现在会快失败,不再烧 6 小时 CI 分钟。卡住几分钟就能发现。每个部署 / 等待步骤都加。

步骤 2:让部署 CLI 进非交互模式

Firebase:

- run: firebase deploy --non-interactive --force --project=prod

Vercel:

- run: vercel deploy --prod --yes --token=${{ secrets.VERCEL_TOKEN }}

npm publish:

- run: npm publish --provenance --access public
  env:
    NPM_CONFIG_YES: "true"

任何提示自动按默认接受,不再静默等待。

步骤 3:稳妥跳过 SSH host key 提示

- name: Add SSH known_hosts
  run: |
    mkdir -p ~/.ssh
    ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
    chmod 600 ~/.ssh/known_hosts

- name: Deploy via SSH
  timeout-minutes: 10
  uses: appleboy/ssh-action@v1
  with:
    host: ${{ secrets.DEPLOY_HOST }}
    username: deploy
    key: ${{ secrets.DEPLOY_KEY }}
    script: |
      cd /var/www && git pull && pnpm install --prod && systemctl reload app

预填 known_hosts 消掉首连提示。不要StrictHostKeyChecking=no,那等于关掉中间人防护。

步骤 4:让 wait-for-deployment 轮询快失败

加硬上限和心跳日志:

- name: Wait for Vercel deployment
  timeout-minutes: 10
  run: |
    DEPLOY_ID="${{ steps.deploy.outputs.id }}"
    if [ -z "$DEPLOY_ID" ]; then
      echo "ERROR: deployment id missing" >&2
      exit 1
    fi
    for i in $(seq 1 60); do
      STATE=$(vercel inspect "$DEPLOY_ID" --token=${{ secrets.VERCEL_TOKEN }} | grep -E "^\s+state" | awk '{print $2}')
      echo "[poll $i] state=$STATE"
      [ "$STATE" = "READY" ] && exit 0
      [ "$STATE" = "ERROR" ] && exit 1
      sleep 10
    done
    echo "ERROR: deploy did not reach READY in 10m" >&2
    exit 1

deployment id 缺失立即失败,轮询挂住 10 分钟超时而不是 6 小时。

步骤 5:给上传加进度看门狗

- name: Upload artifacts
  timeout-minutes: 20
  run: |
    aws s3 sync ./dist s3://my-bucket --delete --no-progress \
      | tee upload.log &
    UP_PID=$!
    while kill -0 $UP_PID 2>/dev/null; do
      sleep 30
      if [ -z "$(find upload.log -newer /tmp/_lastsize 2>/dev/null)" ]; then
        echo "no progress in 30s" >&2
        kill $UP_PID
        exit 1
      fi
      touch /tmp/_lastsize
    done
    wait $UP_PID

上传卡住 30 秒就死,不再挂到 job 超时。

步骤 6:交叉核对目标平台自己的日志

有时其实不是卡住 —— workflow 在正确地等,而目标服务静默拒绝了部署:

- name: Check Vercel deployment exists
  run: |
    vercel ls --token=${{ secrets.VERCEL_TOKEN }} | head -5

目标面板上看不到最近这次部署,触发步骤就是静默失败了。把响应打到日志:

- name: Trigger deploy
  run: |
    RESPONSE=$(curl -fsSL -X POST "$DEPLOY_HOOK_URL")
    echo "deploy hook response: $RESPONSE"

相关静默部署失败见 firebase deploy permission denied

步骤 7:第三方 action 钉版本,缓存恢复要审计

把 action 钉到 commit SHA 避免随机回归:

- uses: actions/cache@0c45773b623bea8c8e75f6c82b208c3cf94ea4f9 # v3.3.2
  with:
    path: ~/.pnpm-store
    key: pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
    enableCrossOsArchive: false

@v3 这类浮动 tag 会带进回归。部署路径上的 action 都用 SHA 钉。

验证

  • 重新跑 workflow,总耗时回到上次绿色构建的 1.5 倍以内。
  • 每个部署 / 等待步骤都设了步骤级 timeout-minutes
  • 现在卡住是 10-15 分钟报错,而不是 6 小时被强杀。
  • 部署 CLI 命令处处带非交互参数。
  • wait-for-deployment 步骤在 deployment 没出现时非零退出。

长期预防

  • 每个 CI workflow 强制步骤级 timeout-minutes,PR 里 lint 检测缺失。
  • CI 上的部署 CLI 始终显式传非交互参数,哪怕默认看似 OK。
  • 第三方 action 钉 commit SHA;升级前过一遍 CHANGELOG。
  • 部署成功要通过查目标服务 API/CLI 来确认,不只是看触发步骤的退出码。
  • 写一份 .github/workflows/CHECKS.md,列出每步最坏 timeout 和卡住时的 runbook。
  • self-hosted runner 监控磁盘 + 内存;缓存恢复卡住常跟磁盘压力相关。

常见坑

  • timeout-minutes: 360 以为修了卡住 —— 这本来就是默认值。要的是更低的步骤级上限。
  • StrictHostKeyChecking=no “解决 SSH 提示” —— 那是把 host 校验关了,是安全漏洞。用 ssh-keyscan 替代。
  • 给步骤加 continue-on-error: true —— workflow 绿了但部署根本没发生。相关静默部署模式见 vercel build failed
  • 通知步骤用 if: always() 而不检查 steps.deploy.outcome == 'success',部署超时了 Slack 还在报”成功”。
  • 部署 workflow 里随手升 actions/cache 主版本而不测试 —— cache action 的回归曾让数千仓库 CI 卡几小时。

常见问答

Q: job timeout 能不能超过 360 分钟?

可以,job 级 timeout-minutes: 1440(24 小时上限)。但真的需要这么长,多半应该拆部署、不是延长。该用的是步骤级 timeout。

Q: workflow 失败但部署其实成功了,怎么办?

最难处理 —— 回滚困难。先去目标平台面板确认;部署确实进去了的话,手动 promote / approve,再去查 workflow 退出码为什么错了(多半是后面的烟雾测试步骤)。

Q: 部署和烟雾测试要不要拆成两个 job?

要。部署 job 把 URL 作为 output 输出,烟雾测试 job 消费它。部署 job 几分钟内结束保持干净状态,烟雾测试随便跑久也不堵部署日志。

Q: ubuntu-22.04 上跑通,runner 镜像升级后 ubuntu-latest 上挂了?

可能 —— 镜像变更偶尔影响默认 ~/.ssh/config 或 Node 默认版本。生产部署 workflow 把 runner 钉到具体镜像(ubuntu-22.04)。相关 runner 环境调试见 vercel build failed

标签: #排查 #GitHub Actions #CI #deploy #timeout