GitHub Actions

Workflow Basics

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

Workflow files live in `.github/workflows/`. Each file defines one workflow with triggers, jobs, and steps.

Triggers (on)

# push and pull request
on:
  push:
    branches: [main, develop]
    tags: ["v*"]
  pull_request:
    branches: [main]

# schedule (cron)
on:
  schedule:
    - cron: "0 2 * * *"   # daily at 02:00 UTC

# manual trigger
on:
  workflow_dispatch:
    inputs:
      environment:
        description: "Deploy target"
        default: staging
        type: string
# trigger on issue or discussion
on:
  issues:
    types: [opened, labeled]
  discussion:
    types: [created]

# trigger on release
on:
  release:
    types: [published]

# trigger on other workflows
on:
  workflow_call:
    inputs:
      ref:
        type: string
        default: main

Jobs & Steps

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    defaults:
      run:
        working-directory: ./app
    steps:
      - uses: actions/checkout@v4
      - name: Install deps
        run: npm ci
      - name: Run tests
        run: npm test
      - name: Run lint
        run: npm run lint

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

`needs` defines job dependencies. Jobs without `needs` run in parallel.

Runners & Matrix

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci && npm test
# self-hosted runner
runs-on: [self-hosted, linux, x64]

# include/exclude specific combinations
strategy:
  matrix:
    python: ["3.10", "3.11", "3.12"]
    include:
      - python: "3.12"
        experimental: true
    exclude:
      - python: "3.10"

Environment Variables & Secrets

env:
  NODE_ENV: ci
  API_URL: https://api.example.com

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    env:
      DEPLOY_TARGET: production
    steps:
      - name: Deploy
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_URL: ${{ secrets.DATABASE_URL }}
        run: |
          echo "Deploying to $DEPLOY_TARGET"
          ./deploy.sh
# define secrets in repo Settings → Secrets and variables → Actions
# access in workflow:
${{ secrets.MY_SECRET }}
${{ vars.MY_VARIABLE }}

# GitHub-provided env vars (no secrets prefix needed)
# $GITHUB_SHA, $GITHUB_REF, $GITHUB_REPOSITORY
# $GITHUB_RUN_ID, $GITHUB_RUN_NUMBER
# $GITHUB_EVENT_NAME, $GITHUB_WORKSPACE

Actions (uses)

steps:
  # official actions
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: npm

  # community actions
  - uses: docker/login-action@v3
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

  # local action
  - uses: ./.github/actions/my-action
    with:
      param: value
# reference by commit SHA (most secure)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

# reference by tag
- uses: actions/checkout@v4

# reference by branch
- uses: actions/checkout@main

Checkout & Setup

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0          # full history for versioning
      ref: develop            # checkout specific branch/tag
      submodules: true        # include submodules
      token: ${{ secrets.PAT }}

  - uses: actions/setup-go@v5
    with:
      go-version: "1.22"

  - uses: actions/setup-python@v5
    with:
      python-version: "3.12"
      cache: pip

  - uses: actions/setup-java@v4
    with:
      distribution: temurin
      java-version: "21"
# cache multiple dependency managers
- uses: actions/setup-node@v4
  with:
    node-version: 20
    cache: npm

# use service containers (like docker-compose)
services:
  postgres:
    image: postgres:16
    env:
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

Build & Test

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    services:
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Docker Actions

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Caching

# built-in cache action
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

# tool-specific cache via setup actions
- uses: actions/setup-go@v5
  with:
    go-version: "1.22"
    cache: true
# cache Docker layers
- uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

Artifacts

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build

      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 5

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh

Deploy

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://app.example.com
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - name: Deploy to server
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          rsync -avz -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
            dist/ deploy@server:/var/www/app/

Conditional Steps

steps:
  - name: Run only on main
    if: github.ref == 'refs/heads/main'
    run: echo "on main branch"

  - name: Run only on PRs
    if: github.event_name == 'pull_request'
    run: echo "this is a PR"

  - name: Run on success
    if: success()
    run: echo "previous steps passed"

  - name: Run on failure
    if: failure()
    run: echo "something failed"

  - name: Always run
    if: always()
    run: echo "runs no matter what"
# job-level conditions
jobs:
  nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    steps:
      - run: echo "scheduled job"

  deploy:
    needs: test
    if: ${{ needs.test.result == 'success' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "tests passed, deploying"

Reusable Workflows

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
    secrets:
      api-key:
        required: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci && npm test
        env:
          API_KEY: ${{ secrets.api-key }}
# .github/workflows/ci.yml — caller
jobs:
  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: "20"
    secrets:
      api-key: ${{ secrets.API_KEY }}

Workflow 基础

# .github/workflows/ci.yml
name: CI
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test

工作流文件位于 `.github/workflows/`。每个文件定义一个工作流,包含触发器、任务和步骤。

触发器 (on)

# 推送和拉取请求
on:
  push:
    branches: [main, develop]
    tags: ["v*"]
  pull_request:
    branches: [main]

# 定时任务 (cron)
on:
  schedule:
    - cron: "0 2 * * *"   # 每天 02:00 UTC

# 手动触发
on:
  workflow_dispatch:
    inputs:
      environment:
        description: "部署目标"
        default: staging
        type: string
# Issue 或 Discussion 触发
on:
  issues:
    types: [opened, labeled]
  discussion:
    types: [created]

# Release 触发
on:
  release:
    types: [published]

# 被其他工作流调用
on:
  workflow_call:
    inputs:
      ref:
        type: string
        default: main

Jobs 与 Steps

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    defaults:
      run:
        working-directory: ./app
    steps:
      - uses: actions/checkout@v4
      - name: 安装依赖
        run: npm ci
      - name: 运行测试
        run: npm test
      - name: 代码检查
        run: npm run lint

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run build

`needs` 定义任务依赖。没有 `needs` 的任务并行执行。

Runners 与 Matrix

jobs:
  test:
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, macos-latest]
      fail-fast: false
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci && npm test
# 自托管 Runner
runs-on: [self-hosted, linux, x64]

# 包含/排除特定组合
strategy:
  matrix:
    python: ["3.10", "3.11", "3.12"]
    include:
      - python: "3.12"
        experimental: true
    exclude:
      - python: "3.10"

环境变量与 Secrets

env:
  NODE_ENV: ci
  API_URL: https://api.example.com

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    env:
      DEPLOY_TARGET: production
    steps:
      - name: 部署
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DB_URL: ${{ secrets.DATABASE_URL }}
        run: |
          echo "Deploying to $DEPLOY_TARGET"
          ./deploy.sh
# 在仓库 Settings → Secrets and variables → Actions 中定义
# 在工作流中访问:
${{ secrets.MY_SECRET }}
${{ vars.MY_VARIABLE }}

# GitHub 内置环境变量(无需 secrets 前缀)
# $GITHUB_SHA, $GITHUB_REF, $GITHUB_REPOSITORY
# $GITHUB_RUN_ID, $GITHUB_RUN_NUMBER
# $GITHUB_EVENT_NAME, $GITHUB_WORKSPACE

Actions (uses)

steps:
  # 官方 Actions
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
    with:
      node-version: 20
      cache: npm

  # 社区 Actions
  - uses: docker/login-action@v3
    with:
      registry: ghcr.io
      username: ${{ github.actor }}
      password: ${{ secrets.GITHUB_TOKEN }}

  # 本地 Action
  - uses: ./.github/actions/my-action
    with:
      param: value
# 使用 commit SHA 引用(最安全)
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11

# 使用标签引用
- uses: actions/checkout@v4

# 使用分支引用
- uses: actions/checkout@main

Checkout 与环境配置

steps:
  - uses: actions/checkout@v4
    with:
      fetch-depth: 0          # 完整历史(用于版本管理)
      ref: develop            # 检出指定分支/标签
      submodules: true        # 包含子模块
      token: ${{ secrets.PAT }}

  - uses: actions/setup-go@v5
    with:
      go-version: "1.22"

  - uses: actions/setup-python@v5
    with:
      python-version: "3.12"
      cache: pip

  - uses: actions/setup-java@v4
    with:
      distribution: temurin
      java-version: "21"
# 使用服务容器(类似 docker-compose)
services:
  postgres:
    image: postgres:16
    env:
      POSTGRES_PASSWORD: postgres
    ports:
      - 5432:5432
    options: >-
      --health-cmd pg_isready
      --health-interval 10s
      --health-timeout 5s
      --health-retries 5

构建与测试

jobs:
  build-and-test:
    runs-on: ubuntu-latest
    services:
      redis:
        image: redis:7-alpine
        ports:
          - 6379:6379
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test -- --coverage
      - uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Docker 操作

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: docker/setup-buildx-action@v3

      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            ghcr.io/${{ github.repository }}:latest
            ghcr.io/${{ github.repository }}:${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

缓存

# 内置缓存 Action
- uses: actions/cache@v4
  with:
    path: |
      ~/.npm
      ${{ github.workspace }}/.next/cache
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

# 通过 setup Actions 启用工具专属缓存
- uses: actions/setup-go@v5
  with:
    go-version: "1.22"
    cache: true
# 缓存 Docker 层
- uses: actions/cache@v4
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

Artifacts

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build

      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/
          retention-days: 5

  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist
          path: dist/
      - run: ./deploy.sh

部署

jobs:
  deploy:
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/main'
    environment:
      name: production
      url: https://app.example.com
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: dist
      - name: 部署到服务器
        env:
          SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          mkdir -p ~/.ssh
          echo "$SSH_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key
          rsync -avz -e "ssh -i ~/.ssh/deploy_key -o StrictHostKeyChecking=no" \
            dist/ deploy@server:/var/www/app/

条件步骤

steps:
  - name: 仅在 main 分支运行
    if: github.ref == 'refs/heads/main'
    run: echo "on main branch"

  - name: 仅在 PR 中运行
    if: github.event_name == 'pull_request'
    run: echo "this is a PR"

  - name: 前序步骤成功时运行
    if: success()
    run: echo "previous steps passed"

  - name: 前序步骤失败时运行
    if: failure()
    run: echo "something failed"

  - name: 始终运行
    if: always()
    run: echo "runs no matter what"
# 任务级别条件
jobs:
  nightly:
    if: github.event_name == 'schedule'
    runs-on: ubuntu-latest
    steps:
      - run: echo "scheduled job"

  deploy:
    needs: test
    if: ${{ needs.test.result == 'success' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "tests passed, deploying"

可复用工作流

# .github/workflows/reusable-test.yml
on:
  workflow_call:
    inputs:
      node-version:
        required: true
        type: string
    secrets:
      api-key:
        required: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
      - run: npm ci && npm test
        env:
          API_KEY: ${{ secrets.api-key }}
# .github/workflows/ci.yml — 调用方
jobs:
  test:
    uses: ./.github/workflows/reusable-test.yml
    with:
      node-version: "20"
    secrets:
      api-key: ${{ secrets.API_KEY }}