GitHub Actions 部署步骤挂到 6 小时 job 上限被强杀 —— 排查与修复

GitHub Actions 部署步骤一直挂着,到 360 分钟 job 上限被取消。最快的修法是用 shell timeout 包住命令再加非交互参数。完整诊断与修复见正文。

平时 4 分钟跑完的 GitHub Actions workflow 卡在部署步骤上 6 小时,最后被 The job running on runner X has exceeded the maximum execution time of 360 minutes. The job is canceled. 强杀。同样的 vercel deploy --prodfirebase deploy 在本地手动跑又一切正常。取消重跑有时成功,有时还卡。

原因几乎都是部署 CLI 在静默地等一个永远不会出现的东西:一个没有 stdin 可以回答的确认提示、一个因为 deployment ID 从没被设上而卡死的 wait-for-deployment 轮询、一个等首次 host key 确认的 SSH 部署,或一次停住的云上传。360 分钟是 job 上限,不是你的部署上限。

最快的修法:用 shell 的 timeout 工具把部署命令包起来,再加上 CLI 的非交互参数。这样卡住会在几分钟内带着明确报错失败,而不是白烧 6 小时 CI 分钟:

- name: Deploy to production
  timeout-minutes: 15        # job 级安全网
  run: timeout 600 vercel deploy --prod --yes
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

然后照下面的诊断修掉真正的卡点。

你属于哪一类?

对照症状,跳到对应修复。

卡住前日志最后一行可能原因跳到
Continue? (Y/n),或停在 Starting deploy... 之后CLI 在等提示,没有 stdin步骤 2
反复的 [poll N] / state=BUILDING,没有终止wait-for-deployment 永不收敛步骤 4
停在一行 ssh/scp/rsync 之后SSH 首连 host key 提示步骤 3
uploaded 47/120 assets 后归于沉寂出站限速 / 代理把传输卡住步骤 5
Downloading cache... 后没有完成行actions/cache 版本太旧、后端已变更步骤 7
hook 步骤是绿的,但目标面板没有构建部署 hook 收下了却没真正开始步骤 6
没有 timeout-minutes,步骤直接跑到 360 分钟没有步骤级上限,job 默认值吃掉了卡住步骤 1

开始排查前

  • 取出失败 job 的 workflow YAML(完整的部署步骤,含 with:env: 和命令)。
  • 顺着 job 日志确定卡在哪个步骤。gh run view <run-id> --log 比 Web UI 抓日志更干净。
  • 是必现还是偶发?
  • 打开目标部署平台自己的面板(Vercel deployments、Firebase function logs、Render/Railway 的 activity),对照他们端到底发生了什么。
  • 确认 runner 类型(ubuntu-latest、GitHub-hosted larger runner、还是 self-hosted):出站和 DNS 行为不一样,而 360 分钟上限只对 GitHub-hosted runner 生效;self-hosted runner 用的是 72 小时的 workflow 上限。

分步修复

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

步骤 1:给步骤设上限,让卡住几分钟就失败

每个部署和等待步骤都设 timeout-minutes。job 级默认是 360 分钟(截至 2026 年 6 月),而步骤级 timeout 永远不能超过 job timeout,只能往下压。

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

一个重要注意点:步骤级 timeout-minutes 会取消步骤,但对 uses: 类 action、以及某些 Docker / 子进程卡死的情况,runner 不一定能干净地中断那个卡住的子进程。凡是顽固的,就在命令外面再套一层 GNU timeout,在操作系统层面直接杀掉进程:

- name: Deploy to production
  timeout-minutes: 15            # 外层安全网
  run: timeout --signal=SIGTERM 600 ./deploy.sh

timeout 600 ... 在命令跑超过 600 秒时以退出码 124 退出,给你一个确定、快速的失败 —— 这是单靠步骤 timeout 不一定能保证的。

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

这是最常见的原因:CLI 检测到变更,正在等一个永远不会来的 Y/n

Vercel(用 env 传 token,不要用 --token--token 参数可能进日志,而且在 Vercel CLI 50.16.052.0.0 上会进入一个结构化 JSON 错误负载 —— CVE-2026-44479,2026 年 5 月;升级到 52.0.1+):

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

Firebase:

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

已知缺口(截至 2026 年 6 月):--non-interactive --forcefirebase-tools 里仍不能自动回答每一个提示。Firestore index 字段覆盖删除、以及 firebase deploy --only firestore--only functions 上某些 min-instances / min-bill 确认仍可能卡住。如果加了两个参数 Firebase 部署还卡,就把部署拆细(--only hosting--only functions:myFn),让那个会卡的资源不在同一条命令里,并到 firebase-tools issue 区查你这条提示。

npm publish:

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

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

全新的 runner 上跑 ssh user@host 会问 Are you sure you want to continue connecting (yes/no/[fingerprint])?,因为没有 known_hosts 条目。提前把它填好:

- 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

不要StrictHostKeyChecking=no 来”修”这个 —— 那是把中间人防护关掉了。ssh-keyscan 会把真实的 host key 钉上。

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

bobheadxi/deployments 或自己写的 gh api 轮询会等状态更新。如果部署根本没回报(部署者用了别的 SHA、状态 webhook 静默失败),轮询会无限继续。加上硬上限、心跳日志,和一个 ID 缺失的守卫:

- 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 — trigger step never set an output" >&2
      exit 1
    fi
    for i in $(seq 1 60); do
      STATE=$(vercel inspect "$DEPLOY_ID" | grep -E "^\s+status" | awk '{print $2}')
      echo "[poll $i] status=$STATE"
      [ "$STATE" = "Ready" ] && exit 0
      [ "$STATE" = "Error" ] && exit 1
      sleep 10
    done
    echo "ERROR: deploy did not reach Ready in 10m" >&2
    exit 1
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

deployment ID 缺失现在立即失败;真正卡住的轮询 10 分钟超时,而不是 6 小时。

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

云上传(S3、GCS、Cloudflare R2)可能在传输中途撞上出站限速或公司代理而停住。TCP 连接不报错,只是不再前进。日志一安静就把它杀掉:

- name: Upload artifacts
  timeout-minutes: 20
  run: |
    aws s3 sync ./dist s3://my-bucket --delete --no-progress | tee upload.log &
    UP_PID=$!
    touch /tmp/_lastsize
    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 upload progress in 30s — killing transfer" >&2
        kill $UP_PID
        exit 1
      fi
      touch /tmp/_lastsize
    done
    wait $UP_PID

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

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

有些卡住其实不是卡住 —— workflow 在正确地等,而目标服务静默拒绝了、或根本没开始部署。webhook 风格的部署(Render、Railway、带 hook URL 的 Fly)可能回 runner 一个 200 OK 却从不开始构建。

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

- name: Sanity-check the deploy exists
  run: vercel ls | head -5
  env:
    VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}

如果目标面板上看不到这次运行时间戳对应的部署,那触发步骤就是静默失败了 —— 把它的完整响应和退出码打到日志。相关静默部署模式见 firebase deploy permission denied

步骤 7:升级 actions/cache —— 旧的钉版本现在会直接失败,不只是卡住

这一节有实质变化。GitHub 在 2025 年 2 月 1 日关停了旧的缓存服务后端。任何还钉在 actions/cache@v1@v2,或 3.4.0 / 4.2.0 之前版本(很多仓库到处复制的那个老 v3.3.2 SHA)的 workflow,现在已经连不上一个能用的后端 —— 它们现在会直接失败或卡在 Downloading cache...,而不只是偶尔挂一下。

截至 2026 年 6 月,当前线是 actions/cache@v5(Node.js 24 运行时,需要 runner 2.327.1+)。升到 v5,或者如果不方便升运行时就钉 v4.2.0+ / v3.4.0+:

- uses: actions/cache@v5
  with:
    path: ~/.pnpm-store
    key: pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

如果出于供应链安全要钉 commit SHA,用 v5v4.2.0+v3.4.0+ 版本的 SHA —— 绝不要再用老的 v3.3.x SHA。浮动的主版本 tag 也可能带进回归,升级前先过一遍 release notes。

怎么确认修好了

  • 重新跑 workflow,总耗时回到上次绿色构建的约 1.5 倍以内。
  • 每个部署和等待步骤都设了 timeout-minutes(顽固的 uses:/脚本步骤还套了 timeout N ...)。
  • 故意制造的卡住现在 10–15 分钟带明确报错失败,而不是 6 小时被强杀。快速测试:临时把某个部署指向一个不存在的 host,确认它快速失败。
  • 每条部署 CLI 调用都带了非交互参数(--yes--non-interactive --forceNPM_CONFIG_YES)。
  • 等待步骤在 deployment 没出现时非零退出。
  • actions/cache 在 v5 / v4.2.0+ / v3.4.0+ 上;没有 Downloading cache... 这一行后面没有完成行的情况。

长期预防

  • 每个 CI workflow 强制步骤级 timeout-minutes,PR 里 lint 检测缺失。
  • 始终给部署 CLI 显式传非交互参数,哪怕默认看似 OK —— 默认值会随 CLI 版本变。
  • token 通过 env:VERCEL_TOKENFIREBASE_TOKEN / GOOGLE_APPLICATION_CREDENTIALS)传,不要用会进日志的命令行 --token 参数。
  • 部署成功要通过查目标服务的 API/CLI 确认,不只是看触发步骤的退出码。
  • 写一份 .github/workflows/CHECKS.md,列出每步最坏 timeout 和卡住时的 runbook。
  • self-hosted runner 上监控磁盘和内存;缓存恢复卡住常跟磁盘压力相关。

常见坑

  • timeout-minutes: 360 以为修好了卡住 —— 这本来就是默认值。要的是低得多的步骤级上限。
  • uses: 类 action 只靠步骤级 timeout-minutes —— 顽固的子进程卡死要再用 shell timeout 工具包住命令。
  • StrictHostKeyChecking=no “解决 SSH 提示” —— 那是把 host 校验关了,是安全漏洞。用 ssh-keyscan 替代。
  • continue-on-error: true,workflow 绿了但部署根本没发生。相关静默部署模式见 vercel build failed
  • if: always() 的 Slack 步骤在部署超时了还报 deploy succeeded。改成用 steps.deploy.outcome == 'success' 来 gate。
  • actions/cache 还钉在 2025 年之前的 SHA 上 —— 那些版本指向一个已经不存在的后端,现在会直接失败。

常见问答

job timeout 能不能直接超过 360 分钟?

GitHub-hosted runner 上不行 —— 每个 job 上限就是 360 分钟硬顶;一次 workflow run 整体最多能撑 72 小时。self-hosted runner 上可以把 timeout-minutes 设到那个 workflow 上限,但真要超过 6 小时,该做的是拆部署而不是延长。处理卡住该用的工具是步骤级 timeout。

我的步骤设了 timeout-minutes,可步骤还是跑满了 360 分钟。为什么?

步骤级 timeout-minutesuses: action 里或 Docker 步骤里卡死的子进程不一定能中断,于是 job 级上限把它吸收了。在 run: 步骤里用 timeout 600 ... 把真正的命令包住,让操作系统直接杀进程。

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

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

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

要。部署 job 把 URL 作为 output 输出、几分钟内结束保持干净状态;烟雾测试 job 消费那个 output,可以跑久一点也不堵部署日志。

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

可能 —— 镜像变更偶尔会改默认 ~/.ssh/config、装的 Node 版本,或某个预装 CLI 的版本。生产部署 workflow 把 runner 钉到具体镜像(ubuntu-22.04ubuntu-24.04),别用 ubuntu-latest。相关 runner 环境调试见 vercel build failed

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