Skip to content

GitHub Actions 全解析:核心概念、工作流优化与实战最佳实践

归类:DevOps / CI/CD / GitHub Actions
日期:2026-04-07
状态:✅ 已落地


一、核心概念速查

1.1 工作流文件结构

yaml
name: 工作流名称

on:                          # 触发条件
  push:
    branches: [main]
  schedule:
    - cron: '0 22 * * *'    # UTC 时间
  workflow_dispatch:         # 手动触发

concurrency:                 # 并发控制
  group: my-workflow
  cancel-in-progress: false

permissions:                 # 最小权限原则
  contents: write

jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      MY_VAR: value
    steps:
      - uses: actions/checkout@v4
      - run: echo "hello"

1.2 关键字层级

层级关键字作用
工作流级on, name, concurrency, permissions触发、名称、并发策略、权限
Job 级runs-on, needs, env, timeout-minutes运行环境、依赖、变量、超时
Step 级uses, run, with, if, id动作引用、命令、参数、条件、标识

1.3 触发事件类型

yaml
on:
  push:                      # 代码推送
  pull_request:              # PR 事件
  schedule:                  # 定时触发(UTC Cron)
  workflow_dispatch:         # 手动触发(UI / API)
  workflow_call:             # 被其他工作流调用
  repository_dispatch:       # 外部 API 触发

二、免费 Runner 排队延迟问题

2.1 现象

GitHub 免费计划共享 Runner(ubuntu-latest)在高峰时段存在 2~3 小时队列延迟

实测数据(news-base 项目):

触发约定时间(北京)实际执行时间(北京)延迟
09:1012:13~3h
09:1012:08~3h
09:1011:41~2.5h
09:1011:52~2.5h

2.2 策略:提前触发时间

核心思路:以预期实际执行时间为目标,在此基础上减去预估排队时间来设置 cron。

yaml
# 目标:北京时间 09:00 实际执行
# 队列延迟:2~3 小时
# cron 设定:北京 06:00 = UTC 22:00

schedule:
  - cron: '0 22 * * *'

⚠️ GitHub Actions cron 只能使用 UTC 时间。北京时间(UTC+8)换算:北京时间 - 8 = UTC 时间。

2.3 Cron 时区换算

想要北京时间cron(UTC)
06:000 22 * * *(前一天 UTC)
09:000 1 * * *
12:000 4 * * *
18:000 10 * * *

三、工作流稳定性优化

3.1 pnpm Store 缓存

缓存 pnpm store 后,pnpm install 命中缓存时可从 30s+ 降至 2~3s。

yaml
- name: Get pnpm store directory
  id: pnpm-cache
  run: echo "store=$(pnpm store path --silent)" >> $GITHUB_OUTPUT

- name: Cache pnpm store
  uses: actions/cache@v4
  with:
    path: ${{ steps.pnpm-cache.outputs.store }}
    # lock 文件内容变化时自动失效旧缓存
    key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
    restore-keys: |
      ${{ runner.os }}-pnpm-

3.2 步骤级失败重试

使用 nick-fields/retry 对网络密集型步骤自动重试,应对新闻源限流或 DNS 抖动。

yaml
- name: Crawl news (with retry)
  uses: nick-fields/retry@v3
  with:
    timeout_minutes: 10
    max_attempts: 3
    retry_wait_seconds: 60
    command: pnpm run crawl

3.3 并发控制

yaml
concurrency:
  group: daily-news-refresh
  cancel-in-progress: false  # 不取消当前运行,排队等待

cancel-in-progress: true 适合 PR 预览场景(新 commit 触发时取消旧任务)。
cancel-in-progress: false 适合定时任务(保证每次都执行完整)。

3.4 设置超时

yaml
jobs:
  refresh-news:
    timeout-minutes: 30   # 防止网络卡住导致 job 消耗免费配额

免费计划每月有 2000 分钟配额,不设超时时单次卡死会消耗大量配额。


四、失败通知方案

4.1 无第三方依赖:创建 GitHub Issue

无需配置额外 Token 或 Webhook,直接用 actions/github-script

yaml
- name: Create failure issue
  if: failure()
  uses: actions/github-script@v7
  with:
    script: |
      const { data: issues } = await github.rest.issues.listForRepo({
        owner: context.repo.owner,
        repo: context.repo.repo,
        labels: ['ci-failure'],
        state: 'open',
      });
      const today = new Date().toISOString().slice(0, 10);
      const exists = issues.some(i => i.title.includes(today));
      if (exists) return;

      await github.rest.issues.create({
        owner: context.repo.owner,
        repo: context.repo.repo,
        title: `[CI 失败] 工作流异常 ${today}`,
        body: `Run: [#${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n触发:${context.eventName}`,
        labels: ['ci-failure'],
      });

同一天只创建一条 Issue,避免重复通知噪声。

4.2 Slack / 企业微信 Webhook(需配置 Secret)

yaml
- name: Notify on failure
  if: failure()
  run: |
    curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
      -H 'Content-type: application/json' \
      --data '{"text":"❌ CI 失败:${{ github.workflow }} #${{ github.run_number }}"}'

五、Node.js 版本弃用警告处理

5.1 警告内容

Node.js 20 actions are deprecated.
actions/checkout@v4, actions/setup-node@v4 will be forced to Node.js 24
starting June 2nd, 2026.

5.2 根因

Actions 的 JavaScript 运行时(不是项目的 Node.js 版本)还在 Node 20。两者是独立的:

  • Actions 运行时:运行 actions/checkoutactions/cache 等官方 action 本身的 Node 环境
  • 项目 Node.js:由 setup-node + .nvmrc 决定,运行 pnpm installpnpm run build 等命令

5.3 修复方案

yaml
jobs:
  my-job:
    runs-on: ubuntu-latest
    env:
      # 提前迁移至 Node.js 24 运行时,消除弃用警告
      # GitHub 计划 2026-06-02 强制切换,2026-09-16 移除 Node.js 20
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

同步修改项目自身的版本声明:

bash
# .nvmrc
22.22.0

# package.json
"engines": { "node": "22.x" }

六、权限与安全最佳实践

yaml
# 最小权限原则:只授予必要权限
permissions:
  contents: write    # 允许 git push
  issues: write      # 允许创建 Issue
  # pull-requests: read  # 按需添加

# 不要在代码中硬编码 Token,使用 Secrets
run: |
  echo "${{ secrets.MY_TOKEN }}" | gh auth login --with-token

敏感信息不要提交到仓库

  • 绝对路径(如 /Users/yourname/...)不要出现在 README、workflow 文件中
  • 域名、用户名等可识别信息提取到 .env 或置入 .gitignore
  • 本地暂存用 local-secrets.md(加入 .gitignore

七、完整工作流模板(pnpm 项目)

yaml
name: Daily Task

on:
  workflow_dispatch:
  schedule:
    - cron: '0 22 * * *'  # UTC 22:00 = 北京 06:00

concurrency:
  group: daily-task
  cancel-in-progress: false

permissions:
  contents: write
  issues: write

jobs:
  run:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - uses: actions/setup-node@v4
        with:
          node-version-file: '.nvmrc'

      - name: Enable Corepack
        run: |
          corepack enable
          corepack prepare pnpm@9.15.9 --activate

      - name: Get pnpm store
        id: pnpm-cache
        run: echo "store=$(pnpm store path --silent)" >> $GITHUB_OUTPUT

      - name: Cache pnpm store
        uses: actions/cache@v4
        with:
          path: ${{ steps.pnpm-cache.outputs.store }}
          key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
          restore-keys: ${{ runner.os }}-pnpm-

      - run: pnpm install --frozen-lockfile

      - name: Run task (with retry)
        uses: nick-fields/retry@v3
        with:
          timeout_minutes: 10
          max_attempts: 3
          retry_wait_seconds: 60
          command: pnpm run your-task

      - name: Fail notification
        if: failure()
        uses: actions/github-script@v7
        with:
          script: |
            const today = new Date().toISOString().slice(0, 10);
            await github.rest.issues.create({
              ...context.repo,
              title: `[CI 失败] ${today}`,
              body: `Run [#${context.runNumber}](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})`,
              labels: ['ci-failure'],
            });