Appearance
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:10 | 12:13 | ~3h |
| 09:10 | 12:08 | ~3h |
| 09:10 | 11:41 | ~2.5h |
| 09:10 | 11: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:00 | 0 22 * * *(前一天 UTC) |
| 09:00 | 0 1 * * * |
| 12:00 | 0 4 * * * |
| 18:00 | 0 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 crawl3.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/checkout、actions/cache等官方 action 本身的 Node 环境 - 项目 Node.js:由
setup-node+.nvmrc决定,运行pnpm install、pnpm 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'],
});