Skip to content

GitHub Actions CI/CD

大白话解释: GitHub Actions 就像"自动化的流水线"。当你推送代码到 GitHub 时,它会自动帮你做一系列事情:安装依赖、运行测试、构建项目、部署到服务器。

为什么要用 CI/CD?

  • 自动化:不用手动执行 npm run buildnpm run deploy 等命令
  • 一致性:每次构建环境都一样,不会出现"在我电脑上能跑"的问题
  • 快速反馈:代码有问题,马上就能知道,不用等到部署后才发现
  • 节省时间:推完代码就不用管了,自动部署到服务器

核心概念:

  • Workflow:整个自动化流程(如"部署到生产环境")
  • Job:流程中的一个任务(如"构建"、"测试"、"部署")
  • Step:任务中的一个步骤(如"安装依赖"、"运行测试")
  • Runner:执行任务的服务器(GitHub 提供免费的)

GitHub Actions 是 GitHub 内置的 CI/CD 平台,用于自动化构建、测试和部署。前端项目最常用的自动化工具之一。


核心概念

概念说明
Workflow一个自动化流程,定义在 .github/workflows/*.yml
Event触发 Workflow 的事件(push、PR、定时等)
JobWorkflow 中的一个任务,包含多个 Step
StepJob 中的一个步骤,执行一条命令或一个 Action
Action可复用的步骤单元(社区有大量现成的)
Runner执行 Job 的服务器(GitHub 提供,也可自托管)

免费额度与计费

Runner免费分钟数存储超出费用
Ubuntu / Windows2000 分钟500 MB$0.008/分钟
macOS250 分钟500 MB$0.08/分钟
自托管 Runner无限不限免费

💡 私有仓库才受免费额度限制,公开仓库不限分钟数。存储指 Artifacts 和日志的总占用量。


基本结构

yaml
# .github/workflows/deploy.yml

name: Deploy          # Workflow 名称

on:                   # 触发条件
  push:
    branches: [main]  # push 到 main 分支时触发

jobs:                 # 任务列表
  build-and-deploy:   # 任务名称
    runs-on: ubuntu-latest  # 运行环境

    steps:            # 步骤列表
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install dependencies
        run: yarn install

      - name: Build
        run: yarn build

      - name: Deploy
        run: echo "Deploying..."

前端项目完整示例

Vue/React 项目部署到 GitHub Pages

yaml
name: Deploy to GitHub Pages

on:
  push:
    branches: [main]

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: yarn

      - name: Install dependencies
        run: yarn install --frozen-lockfile

      - name: Build
        run: yarn build

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3
        with:
          path: dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

部署到 Vercel

yaml
name: Deploy to Vercel

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: yarn

      - name: Install dependencies
        run: yarn install

      - name: Build
        run: yarn build
        env:
          VITE_API_URL: ${{ secrets.VITE_API_URL }}

      - name: Deploy to Vercel
        uses: amondnet/vercel-action@v25
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          vercel-args: '--prod'

超时设置

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    timeout-minutes: 30          # 单个 Job 最大运行时间,默认 360 分钟
    steps:
      - uses: actions/checkout@v4
      - run: yarn install
      - run: yarn build

  deploy:
    runs-on: ubuntu-latest
    timeout-minutes: 10          # 部署任务设置更短超时
    steps:
      - run: echo "Deploy"

💡 timeout-minutes 放在 Job 级别,Step 级别不支持此字段。超时后 Job 被强制取消。


容器服务(services)

services 字段为 Job 启动依赖的容器,常用于集成测试需要数据库、缓存等场景。

yaml
jobs:
  test:
    runs-on: ubuntu-latest

    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: testdb
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping -h localhost"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

      redis:
        image: redis:7
        ports:
          - 6379:6379
        options: >-
          --health-cmd="redis-cli ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5

    steps:
      - uses: actions/checkout@v4

      - name: Run tests
        env:
          DB_HOST: localhost
          DB_PORT: 3306
          DB_USER: root
          DB_PASSWORD: root
          DB_NAME: testdb
          REDIS_HOST: localhost
          REDIS_PORT: 6379
        run: yarn test

💡 services 仅在 ubuntu-latest 等 Linux Runner 上支持,macOS 和 Windows 不支持。容器通过 localhost + 映射端口访问。


Job 容器运行(container)

让整个 Job 在指定容器内执行,而非直接在 Runner 上运行。

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: node:20-alpine           # Job 的所有步骤都在此容器内执行
      options: --user root

    steps:
      - uses: actions/checkout@v4
      - run: node -v
      - run: yarn install
      - run: yarn build

💡 使用 container 时,actions/checkout 等 Action 会在容器内执行。可指定 volumes 挂载宿主机目录。


GITHUB_TOKEN 详解

GITHUB_TOKEN 是 GitHub Actions 自动创建的临时令牌,每个 Job 独立生成,工作流结束后失效。

基本用法

yaml
steps:
  - name: Checkout
    uses: actions/checkout@v4
    # token 参数默认值就是 github.token,通常无需显式指定
    # 仅在需要使用其他 PAT(如跨仓库访问)时才需要设置

  - name: Create release
    uses: actions/github-script@v7
    with:
      script: |
        // github.rest 是 Octokit REST API 客户端
        // 详见:https://octokit.github.io/rest.js
        await github.rest.repos.createRelease({
          owner: context.repo.owner,
          repo: context.repo.repo,
          tag_name: 'v1.0.0'
        });

权限配置

yaml
# 方式1:顶层声明(推荐)
permissions:
  contents: read
  issues: write
  pull-requests: write

# 方式2:只读模式
permissions: read-all

# 方式3:读写模式
permissions: write-all

常用权限范围

权限说明
contents读写仓库代码和文件
issues读写 Issue
pull-requests读写 PR
packages读写 GitHub Packages
actions管理 Actions(取消工作流等)
deployments读写部署状态
statuses读写提交状态
pages部署 GitHub Pages
id-token获取 OIDC token(用于云服务联合认证)

💡 默认权限可在仓库 Settings → Actions → Workflow permissions 中设置为"Read and write permissions"或"Read repository contents and packages permissions"。


路径过滤器(paths / paths-ignore)

根据变更文件路径决定是否触发 Workflow。

yaml
on:
  push:
    branches: [main]
    paths:
      - 'src/**'              # src 目录下任意文件变更触发
      - 'package.json'
      - '!docs/**'            # 排除 docs 目录

  pull_request:
    branches: [main]
    paths-ignore:
      - '*.md'                # 仅修改 Markdown 文件时不触发
      - 'docs/**'
      - '.github/FUNDING.yml'

分支过滤器通配符

通配符说明示例
*匹配单层级任意字符release/* 匹配 release/v1,不匹配 release/v1/fix
**匹配多层级任意字符release/** 匹配 release/v1release/v1/fix
!排除模式!main 排除 main 分支
[abc]字符集[ab] 匹配 ab
yaml
on:
  push:
    branches:
      - 'main'
      - 'release/**'          # 匹配 release/v1、release/v2.0.1 等
      - 'feature/**'
      - '!experimental/**'    # 排除 experimental 分支

workflow_dispatch 输入类型

手动触发支持 5 种输入类型:

类型说明特点
string文本输入(默认)支持 default
boolean布尔选择值为 true/false
choice下拉菜单必须提供 options 列表
environment环境选择自动列出仓库已配置的环境
number数字输入支持 default,值为数字字符串
yaml
on:
  workflow_dispatch:
    inputs:
      name:
        type: string
        description: '部署名称'
        required: true
        default: 'my-app'

      dry-run:
        type: boolean
        description: '是否为模拟运行'
        required: false
        default: false

      environment:
        type: choice
        description: '部署环境'
        required: true
        options:
          - staging
          - production

      target-env:
        type: environment
        description: '选择已配置的环境'
        required: true

      replicas:
        type: number
        description: '副本数量'
        required: false
        default: '1'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Show inputs
        run: |
          echo "Name: ${{ inputs.name }}"
          echo "Dry run: ${{ inputs.dry-run }}"
          echo "Environment: ${{ inputs.environment }}"
          echo "Target: ${{ inputs.target-env }}"
          echo "Replicas: ${{ inputs.replicas }}"

环境变量作用域

环境变量可在 3 个级别定义,作用范围逐级缩小:

workflow 级别

yaml
env:
  NODE_ENV: production
  CI: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo $NODE_ENV          # production

job 级别

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    env:
      BUILD_ENV: staging
    steps:
      - run: echo $BUILD_ENV         # staging
      - run: echo $NODE_ENV          # production(继承 workflow 级别环境变量)

  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo $BUILD_ENV         # 空(job 之间不共享)

step 级别

yaml
steps:
  - name: Build
    run: yarn build
    env:
      VITE_API_URL: ${{ secrets.API_URL }}

  - name: Test
    run: yarn test
    # 这里 VITE_API_URL 不可用

优先级

step 级别 > job 级别 > workflow 级别。同名变量高优先级覆盖低优先级。


常用触发事件

yaml
on:
  # push 到指定分支
  push:
    branches: [main, develop]
    tags:
      - 'v*'              # tag 推送(发布版本)

  # Pull Request
  pull_request:
    branches: [main]

  # 定时任务(每天 UTC 0 点)
  schedule:
    - cron: '0 0 * * *'

  # 手动触发
  workflow_dispatch:
    inputs:
      environment:
        description: '部署环境'
        required: true
        default: 'staging'
        type: choice
        options:
          - staging
          - production

常用 Actions

缓存依赖(加速构建)

yaml
- name: Setup Node.js
  uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: yarn  # 自动缓存 yarn 依赖

# 或手动缓存
- name: Cache dependencies
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}
    restore-keys: |
      ${{ runner.os }}-node-

矩阵测试(多版本)

yaml
jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: yarn install
      - run: yarn test

发送通知

yaml
- name: Send notification
  if: failure()
  uses: appleboy/telegram-action@master
  with:
    to: ${{ secrets.TELEGRAM_CHAT_ID }}
    token: ${{ secrets.TELEGRAM_BOT_TOKEN }}
    message: "部署失败:${{ github.repository }}"

Secrets 管理

设置 Secrets

操作步骤:

  1. 进入仓库 → SettingsSecrets and variablesActions
  2. 点击 "New repository secret"
  3. 输入 Secret 名称(如 VERCEL_TOKEN)和对应的值
  4. 点击 "Add secret" 保存

Secret 类型:

类型说明适用场景
Repository secrets仓库级别,所有 workflow 可用通用密钥(API Token、SSH Key)
Environment secrets环境级别,需指定 environment 才能访问多环境隔离(生产/测试用不同密钥)
Organization secrets组织级别,可在多个仓库共享团队共享的通用密钥

⚠️ Secret 在日志中自动显示为 ***,无法被 echo 输出。不要在代码中硬编码密钥。

使用 Secrets

yaml
steps:
  - name: Build
    run: yarn build
    env:
      # 通过 ${{ secrets.SECRET_NAME }} 引用仓库中配置的 Secret
      VITE_API_URL: ${{ secrets.VITE_API_URL }}        # API 地址
      VITE_APP_TITLE: ${{ secrets.VITE_APP_TITLE }}    # 应用标题

  - name: Deploy
    run: echo "Deploying..."
    env:
      # SSH 部署所需的密钥(在仓库 Settings → Secrets 中添加)
      SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}  # SSH 私钥内容
      DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}        # 部署令牌

多环境部署

yaml
name: Deploy

on:
  push:
    branches: [main, develop]

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
    steps:
      - uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: yarn

      - name: Install
        run: yarn install

      - name: Build
        run: yarn build
        env:
          VITE_API_URL: ${{ github.ref == 'refs/heads/main' && secrets.PROD_API_URL || secrets.STAGING_API_URL }}

      - name: Deploy
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo "Deploy to production"
          else
            echo "Deploy to staging"
          fi

常见问题

Workflow 不触发

bash
# 检查 YAML 语法
# 确认分支名正确
# 确认 Actions 已启用(仓库 Settings → Actions)

构建失败

yaml
# 添加调试信息
- name: Debug
  run: |
    node -v
    npm -v
    yarn -v
    ls -la

缓存失效

yaml
# 使用 lock 文件的 hash 作为缓存 key
key: ${{ runner.os }}-node-${{ hashFiles('yarn.lock') }}

YAML 语法速查

常用数据类型

类型示例说明
字符串'hello'hello单引号可避免特殊字符解析
多行字符串|>| 保留换行,> 折叠换行
数字423.14整数和浮点数
布尔truefalse注意不要加引号
数组[a, b, c]- a行内和多行两种写法
对象key: value键值对

多行字符串

yaml
# | 保留换行符
run: |
  echo "第一行"
  echo "第二行"

# > 折叠换行符(合并为一行)
run: >
  echo "这会被合并为一行"

变量引用

yaml
# 上下文表达式
${{ github.ref }}
${{ secrets.MY_SECRET }}
${{ vars.MY_VAR }}

# 环境变量
env:
  MY_VAR: hello
steps:
  - run: echo $MY_VAR        # shell 语法
  - run: echo ${{ env.MY_VAR }}  # GitHub 表达式语法

锚点与别名(复用配置)

yaml
# 定义锚点
common-steps: &common-steps
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20

# 引用锚点
jobs:
  build:
    steps:
      - *common-steps
      - run: yarn build
  test:
    steps:
      - *common-steps
      - run: yarn test

条件执行

if 条件表达式

yaml
steps:
  # 仅在 main 分支执行
  - name: Deploy
    if: github.ref == 'refs/heads/main'
    run: echo "Deploy to production"

  # 仅在 PR 时执行
  - name: Lint
    if: github.event_name == 'pull_request'
    run: yarn lint

  # 多条件组合
  - name: Deploy production
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    run: echo "Deploy"

常用条件函数

函数说明使用场景
success()所有前置步骤成功(默认)可省略
failure()前置步骤有失败发送失败通知
always()无论成功失败都执行清理资源、上传日志
cancelled()工作流被取消时取消通知
yaml
steps:
  - name: Run tests
    run: yarn test

  - name: Notify on failure
    if: failure()
    run: echo "Tests failed!"

  - name: Cleanup
    if: always()
    run: echo "Always runs"

  - name: On cancel
    if: cancelled()
    run: echo "Workflow was cancelled"

表达式中的运算符

运算符示例说明
==github.ref == 'refs/heads/main'等于
!=github.event_name != 'push'不等于
&&a && b逻辑与
||a || b逻辑或
!!success()逻辑非
contains()contains(github.event.head_commit.message, '[skip ci]')字符串包含
startsWith()startsWith(github.ref, 'refs/tags/')字符串前缀
endsWith()endsWith(github.ref, '-release')字符串后缀

Job 间依赖与数据传递

needs 依赖

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Building..."

  test:
    needs: build          # 等 build 完成后执行
    runs-on: ubuntu-latest
    steps:
      - run: echo "Testing..."

  deploy:
    needs: [build, test]  # 等 build 和 test 都完成后执行
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying..."

outputs 数据传递

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-version.outputs.version }}
    steps:
      - uses: actions/checkout@v4

      - name: Get version
        id: get-version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Show version
        run: echo "Deploying version ${{ needs.build.outputs.version }}"

Artifacts 上传下载

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: yarn install && yarn build

      - name: Upload build artifacts
        uses: actions/upload-artifact@v4
        with:
          name: dist-files
          path: dist/
          retention-days: 7        # 保留天数
          compression-level: 6     # 压缩级别 0-9

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          name: dist-files
          path: dist/

      - name: Deploy
        run: ls dist/

多 Artifact 并行上传

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        platform: [web, mobile]
    steps:
      - uses: actions/checkout@v4
      - run: yarn build:${{ matrix.platform }}

      - name: Upload
        uses: actions/upload-artifact@v4
        with:
          name: build-${{ matrix.platform }}
          path: dist/${{ matrix.platform }}

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: all-builds
      - run: ls all-builds/

环境与部署保护规则

environment 配置

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - run: echo "Deploying to production"

使用环境变量

yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Deploy
        run: echo "Deploying to ${{ vars.DEPLOY_URL }}"
        env:
          API_KEY: ${{ secrets.API_KEY }}

部署保护规则

在仓库 Settings → Environments 中配置:

保护规则说明
Required reviewers指定审批人,部署前需手动批准
Wait timer部署前等待指定分钟数
Deployment branches限制可部署的分支(如仅 main

多环境流水线

yaml
jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    environment:
      name: staging
      url: https://staging.example.com
    steps:
      - run: echo "Deploy to staging"

  deploy-production:
    needs: deploy-staging
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com
    steps:
      - run: echo "Deploy to production (requires approval)"

复合 Action(Composite Action)

复合 Action 将多个步骤打包为一个可复用的 Action。

创建复合 Action

yaml
# .github/actions/setup-project/action.yml
name: 'Setup Project'
description: 'Checkout, setup Node.js and install dependencies'

inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '20'

runs:
  using: 'composite'
  steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: yarn

    - name: Install dependencies
      shell: bash
      run: yarn install --frozen-lockfile

使用复合 Action

yaml
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Setup project
        uses: ./.github/actions/setup-project
        with:
          node-version: '20'

      - name: Build
        run: yarn build

  test:
    runs-on: ubuntu-latest
    steps:
      - name: Setup project
        uses: ./.github/actions/setup-project

      - name: Test
        run: yarn test

复合 Action 输出

yaml
# .github/actions/get-version/action.yml
name: 'Get Version'
outputs:
  version:
    description: 'Package version'
    value: ${{ steps.get-version.outputs.version }}

runs:
  using: 'composite'
  steps:
    - name: Get version
      id: get-version
      shell: bash
      run: echo "version=$(node -p 'require(\"./package.json\").version')" >> "$GITHUB_OUTPUT"

可重用 Workflow(Reusable Workflow)

定义可重用 Workflow

yaml
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy

on:
  workflow_call:                 # 声明为可重用
    inputs:
      environment:
        required: true
        type: string
      node-version:
        required: false
        type: string
        default: '20'
    secrets:
      DEPLOY_KEY:
        required: true
    outputs:
      deploy-url:
        description: 'Deployed URL'
        value: ${{ jobs.deploy.outputs.url }}

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    outputs:
      url: ${{ steps.deploy.outputs.url }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: yarn

      - run: yarn install --frozen-lockfile
      - run: yarn build

      - name: Deploy
        id: deploy
        run: |
          echo "Deploying to ${{ inputs.environment }}"
          echo "url=https://${{ inputs.environment }}.example.com" >> "$GITHUB_OUTPUT"
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}

调用可重用 Workflow

yaml
# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]

jobs:
  deploy-staging:
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: staging
      node-version: '20'
    secrets:
      DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}

  deploy-production:
    needs: deploy-staging
    uses: ./.github/workflows/reusable-deploy.yml
    with:
      environment: production
    secrets:
      DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}

调用远程可重用 Workflow

yaml
jobs:
  call-remote:
    uses: org/repo/.github/workflows/shared.yml@main
    with:
      config: production
    secrets: inherit       # 传递所有 secrets

常用场景模板

PR 自动 Code Review

yaml
name: Auto Code Review

on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get changed files
        id: changed-files
        uses: tj-actions/changed-files@v44
        with:
          files: |
            src/**/*.{ts,tsx,js,jsx}

      - name: Run ESLint on changed files
        if: steps.changed-files.outputs.any_changed == 'true'
        run: |
          yarn eslint ${{ steps.changed-files.outputs.all_changed_files }} \
            --format json --output-file eslint-report.json
        continue-on-error: true

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            let report = 'No ESLint issues found.';
            try {
              const results = JSON.parse(fs.readFileSync('eslint-report.json', 'utf8'));
              const errors = results.reduce((sum, f) => sum + f.errorCount, 0);
              const warnings = results.reduce((sum, f) => sum + f.warningCount, 0);
              report = `ESLint: ${errors} errors, ${warnings} warnings`;
            } catch (e) {}
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `### Code Review\n${report}`
            });

自动创建 Tag 和 Release

yaml
name: Auto Release

on:
  push:
    branches: [main]

permissions:
  contents: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get version
        id: version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      - name: Check if tag exists
        id: check-tag
        run: |
          if git rev-parse "v${{ steps.version.outputs.version }}" >/dev/null 2>&1; then
            echo "exists=true" >> "$GITHUB_OUTPUT"
          else
            echo "exists=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Create tag and release
        if: steps.check-tag.outputs.exists == 'false'
        uses: actions/github-script@v7
        with:
          script: |
            const version = '${{ steps.version.outputs.version }}';
            await github.rest.git.createRef({
              owner: context.repo.owner,
              repo: context.repo.repo,
              ref: `refs/tags/v${version}`,
              sha: context.sha
            });
            await github.rest.repos.createRelease({
              owner: context.repo.owner,
              repo: context.repo.repo,
              tag_name: `v${version}`,
              name: `v${version}`,
              generate_release_notes: true
            });

构建 Docker 镜像并推送到 Registry

💡 此示例需要项目根目录有 Dockerfile。以下是一个配套的多阶段构建 Dockerfile:

dockerfile
# Dockerfile — 配套的多阶段构建(放在项目根目录)
FROM node:20-alpine AS builder       # 构建阶段:使用 Node.js 20 Alpine
WORKDIR /app                         # 设置工作目录
COPY package.json yarn.lock ./       # 复制依赖文件
RUN yarn install --frozen-lockfile   # 安装依赖
COPY . .                             # 复制源代码
RUN yarn build                       # 执行构建

FROM nginx:alpine                    # 运行阶段:使用 nginx
COPY --from=builder /app/dist /usr/share/nginx/html  # 复制构建产物
EXPOSE 80                            # 声明端口
CMD ["nginx", "-g", "daemon off;"]   # 启动 nginx
yaml
name: Build and Push Docker Image

on:
  push:
    tags: ['v*']                     # 推送 v 开头的 tag 时触发(如 v1.0.0)

env:
  REGISTRY: ghcr.io                  # GitHub Container Registry 地址
  IMAGE_NAME: ${{ github.repository }}  # 镜像名 = 仓库名

jobs:
  build-and-push:
    runs-on: ubuntu-latest           # 运行环境
    permissions:
      contents: read                 # 读取仓库代码
      packages: write                # 推送镜像到 GHCR

    steps:
      - uses: actions/checkout@v4    # 拉取代码

      - name: Log in to Container Registry
        uses: docker/login-action@v3  # 登录 GitHub Container Registry
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}             # 当前触发者用户名
          password: ${{ secrets.GITHUB_TOKEN }}     # GitHub 自动生成的临时令牌

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5  # 自动提取 tag 信息生成镜像标签
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=semver,pattern={{version}}          # 如 v1.2.3
            type=semver,pattern={{major}}.{{minor}}  # 如 1.2
            type=sha                                 # Git commit SHA

      - name: Build and push
        uses: docker/build-push-action@v6  # 构建并推送镜像
        with:
          context: .                         # 构建上下文为当前目录
          push: true                         # 构建后自动推送
          tags: ${{ steps.meta.outputs.tags }}     # 使用 metadata 生成的标签
          labels: ${{ steps.meta.outputs.labels }} # 使用 metadata 生成的标签
          cache-from: type=gha               # 从 GitHub Actions 缓存读取构建层
          cache-to: type=gha,mode=max        # 将构建层写入 GitHub Actions 缓存

Monorepo 按需构建(path filter)

yaml
name: Monorepo CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  changes:
    runs-on: ubuntu-latest
    outputs:
      frontend: ${{ steps.filter.outputs.frontend }}
      backend: ${{ steps.filter.outputs.backend }}
    steps:
      - uses: actions/checkout@v4

      - uses: dorny/paths-filter@v3
        id: filter
        with:
          filters: |
            frontend:
              - 'packages/frontend/**'
            backend:
              - 'packages/backend/**'

  build-frontend:
    needs: changes
    if: needs.changes.outputs.frontend == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: yarn

      - run: yarn install --frozen-lockfile
      - run: yarn workspace frontend build

  build-backend:
    needs: changes
    if: needs.changes.outputs.backend == 'true'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: yarn

      - run: yarn install --frozen-lockfile
      - run: yarn workspace backend build

调试技巧

启用调试日志

方法设置方式说明
Actions Step DebugSettings → Secrets → ACTIONS_STEP_DEBUG = true显示每个 Step 的详细日志
Actions Runner DebugSettings → Secrets → ACTIONS_RUNNER_DEBUG = true显示 Runner 级别日志

也可以在触发 Workflow 时通过 URL 参数启用:

https://github.com/org/repo/actions/runs/123456?debug=true

act 本地测试工具

act 可在本地运行 GitHub Actions Workflow,无需推送到远程仓库。

bash
# 安装(macOS)
brew install act

# 安装(Windows)
winget install nektos.act

# 运行默认事件(push)
act

# 指定事件类型
act pull_request

# 指定 Job
act -j build

# 使用中型镜像(包含更多工具)
act -P ubuntu-latest=catthehacker/ubuntu:act-latest

# 模拟 secrets
act -s MY_SECRET=value

# 列出所有 Workflow 和 Job
act -l

# 详细输出
act -v

调试 Workflow 语法

yaml
# 输出上下文信息(排查变量问题)
- name: Debug context
  run: |
    echo "Event: ${{ github.event_name }}"
    echo "Ref: ${{ github.ref }}"
    echo "SHA: ${{ github.sha }}"
    echo "Actor: ${{ github.actor }}"
    echo "Runner OS: ${{ runner.os }}"
    echo "Workspace: ${{ github.workspace }}"

# 输出完整事件 payload
- name: Dump event
  run: cat ${{ github.event_path }}

常见调试场景

问题排查方法
环境变量为空echo 输出变量值,检查 Secrets 配置
条件不满足查看 Step 是否被跳过(灰色)
权限不足检查 permissions 配置和 Token 权限
缓存未命中查看 Cache 命中日志,对比 key
YAML 语法错误使用 actionlint 本地检查

参考

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准