GithubActions

GitHub Actions

GitHub Actions 是 GitHub 提供的 CI/CD 服务,支持自动化构建、测试、部署等工作流。

下面只介绍基础,常用的 Action 及其用法请参考官方文档。

Action 说明
actions/checkout 签出代码
actions/setup-node 设置 Node.js 环境
actions/setup-python 设置 Python 环境
actions/setup-java 设置 Java 环境
actions/cache 缓存依赖,加速构建
actions/upload-artifact 上传构建产物
actions/download-artifact 下载构建产物
softprops/action-gh-release 自动创建和发布 GitHub Release
stefanzweifel/git-auto-commit-action 自动提交并推送代码
ad-m/github-push-action 半自动推送代码
Mattraks/delete-workflow-runs 清理历史运行记录

1. 语法基础

1.1 Action 引用语法 (uses)

语法:uses: {owner}/{repo}@{ref}

@{ref} 决定了使用哪个版本的 Action,优先级如下:

  1. Tag (推荐): 比如 @v3, @v3.1.0
  • 特点:版本固定,稳定性高,推荐生产环境使用。
  1. Commit SHA (最安全): 比如 @f2a1d8...
  • 特点:不可篡改,供应链安全级别最高,但可读性差,无法自动获取更新。
  1. Branch (慎用): 比如 @main, @master, @latest
  • 特点:只是开发者维护的一个分支(Branch),代码随时变动。

1.2 npm vs npx

  • npm (Node Package Manager): 管理工具。侧重于安装、卸载、更新包,管理 package.json
  • npx (Node Package Execute): 执行工具
  • 特性:可以直接运行 node_modules/.bin 下的命令,或者运行未安装的包(临时下载,运行完即删)。
  • 场景npx prettier --write . (无需全局安装 prettier).

1.3 npm install vs npm ci

在 CI 环境中,必须使用 npm ci

特性 npm install npm ci (Clean Install)
依据文件 package.json package-lock.json
锁文件行为 如果不一致,会修改 lock 文件 严格遵守,不一致直接报错
前置动作 尝试增量安装 直接删除 node_modules
速度 较慢(需解析依赖树) 极快(跳过解析,直接下载)
适用场景 本地开发 CI/CD 环境

2. 多语言环境配置与缓存

2.1 Node.js (actions/setup-node)

不要手动使用 actions/cache,直接利用 setup-node 的内置缓存。

- name: Setup Node.js
  uses: actions/setup-node@v6
  with:
    node-version: '24'
    cache: 'npm' # 根据 package-lock.json 生成缓存 Key

2.2 Python (actions/setup-python)

支持 pip, pipenv, poetry 的缓存,支持 Version Range(如 3.x 会自动选最新的 3.x 版本)。

- name: Setup Python
  uses: actions/setup-python@v6
  with:
    python-version: '3.13'
    architecture: 'x64'        # (可选) x64 或 x86
    cache: 'pip' # 根据 requirements.txt 生成缓存 Key
- run: pip install -r requirements.txt

2.3 Java (actions/setup-java)

Java 有很多发行版,使用时 必须指定 distribution** 参数,推荐使用 Temurin

- name: Setup Java JDK
  uses: actions/setup-java@v5
  with:
    java-version: '17'
    distribution: 'temurin' # 必填 (temurin, zulu, corretto)
    cache: 'gradle'          # 支持 maven 或 gradle

3. 存储策略:Cache vs Artifacts

这两者经常被混淆,但用途完全不同。

特性 Cache (缓存) Artifacts (构建产物)
对应 Action actions/cache (或 setup-xx 内置) actions/upload-artifact & download-artifact
主要用途 存储依赖 (node_modules),加速下一次构建 存储结果 (.exe, .jar, dist/),用于部署或下载
持久性 临时。7天未命中或总量超10GB自动删除 (LRU)。 持久。默认保留 90 天 (可配置)。
跨 Job 访问 同一分支及子分支可读。 任何 Job 均可下载,支持 UI 界面手动下载。
计费 通常不计费 (GitHub 托管)。 占用存储配额 (私有仓库超额收费)。

构建产物传递示例: Job A 编译 -> Upload -> Job B Download -> 发布。

在 GitHub Actions 中,每个 Job 都是在一个全新的、隔离的虚拟机中运行的。 必须使用 uploaddownload 来“传递”这些文件。详见附录为什么要上传和下载构建产物(Artifact)?

3.1 actions/upload-artifact (上传)

作用:将当前 Job 生成的文件(编译结果、测试报告、日志)打包上传到 GitHub 服务器。

常用参数

  • name: 产物名称(后续下载要用)。
  • path: 要上传的文件或目录路径(支持通配符)。
  • retention-days: 保留几天(默认 90 天,建议设短一点省空间)。

示例

- name: Upload Build Artifact
  uses: actions/upload-artifact@v6
  with:
    name: my-build-files       # 给这个包起个名
    path: dist/                # 上传 dist 目录下的所有内容
    retention-days: 1          # 只保留1天,传给下一个Job用完就行

3.2 actions/download-artifact (下载)

作用:在另一个 Job 中,下载之前上传的产物,以便进行部署或分析。

常用参数

  • name: 对应上传时的名称。如果不填,会下载所有产物。
  • path: 下载到哪里(默认解压到当前目录)。

示例(配合上面的 Upload):

- name: Download Build Artifact
  uses: actions/download-artifact@v7
  with:
    name: my-build-files       # 必须和 upload 的名字一致
    path: build-files/         # 下载并解压到这个目录

4. Git 自动化操作 (Auto Push)

4.1 核心前提:权限

默认 Token 通常只有读权限。若要 Push,必须在 Workflow 顶部或 Job 级添加:

permissions:
  contents: write

或者去 Settings -> Actions -> General 修改为 Read and write permissions

4.2 方案对比:全自动 vs 半自动 vs 手动

需求 推荐 Action 说明
懒人/简单场景 stefanzweifel/git-auto-commit-action 全自动。自动检测变更、add、commit、push。
精细控制/动态消息 ad-m/github-push-action 半自动。只负责 Push,需要写 git commit 命令。
极简主义/复杂钩子 原生命令 (Shell) 手动。完全自定义 git 命令。

全自动配置示例

- name: Apply changes & Push
  uses: stefanzweifel/git-auto-commit-action@v7
  with:
    commit_message: "chore: update stats [skip ci]"
    branch: ${{ github.head_ref }}

半自动配置示例

- name: Commit changes
  run: |
    git config --global user.name "github-actions[bot]"
    git config --global user.email "github-actions[bot]@users.noreply.github.com"
    git add .
    # 可以使用 shell 变量拼接 commit message
    git commit -m "Update data: $(date) [skip ci]"

- name: Push changes
  uses: ad-m/github-push-action@v1
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}
    branch: ${{ github.ref }}

手动配置示例

- name: Commit and Push
  run: |
    git config --global user.name "github-actions[bot]"
    git config --global user.email "github-actions[bot]@users.noreply.github.com"
    git add .
    git commit -m "Auto update [skip ci]"
    git push

4.3 防止 CI 无限循环 (Infinite Loop)

当 Action 推送代码后,新的 Commit 可能会再次触发 on: push,导致死循环。 解决方法:在 Commit Message 中包含以下关键词之一:

  • [skip ci]
  • [ci skip]
  • [no ci]

5. 自动创建与发布 Release (Release Automation)

Release 通常包含构建产物(如 .exe, .zip, .jar)以及更新日志(Changelog)。

5.1 核心逻辑

  1. 触发时机:通常由 打标签 (Git Tag) 触发。例如,当推送一个 v1.0.0 的标签时,Action 自动运行。
  2. 执行动作:编译代码 -> 打包文件 -> 在 GitHub 创建 Release 页面 -> 上传文件。

5.2 推荐工具

  • Action: softprops/action-gh-release
  • 理由: 社区事实标准,配置简单,支持通配符文件上传,支持自动生成更新日志。

5.3 完整配置示例

这是一个生产环境可用的配置,包含了从“触发”到“构建”再到“发布”的全过程。

文件: .github/workflows/release.yml

name: Build and Release

on:
  push:
    tags:
      - "v*" # 仅当推送以 v 开头的标签时触发 (如 v1.0.0)

permissions:
  contents: write # 必须!否则无法创建 Release

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      # 模拟构建步骤 (需要替换为真实的构建命令)
      - name: Build Project
        run: |
          mkdir -p build
          echo "This is the binary content" > build/app-v1.0.0.exe
          zip -r build/source-code.zip . -x ".git/*"

      - name: Create Release
        uses: softprops/action-gh-release@v2
        if: startsWith(github.ref, 'refs/tags/')
        with:
          # 1. 自动根据 PR 标题生成更新日志 (强烈推荐)
          generate_release_notes: true
          
          # 2. 上传的文件 (支持通配符)
          files: |
            build/*.exe
            build/*.zip
          
          # 3. 如果标签包含 'beta',标记为预发布版本
          prerelease: ${{ contains(github.ref, 'beta') }}
          
          # 4. 自定义 Release 标题 (默认使用 Tag 名)
          # name: Release ${{ github.ref_name }}

5.4 关键参数详解

参数 说明 推荐配置
files 指定要上传到 Release页面的文件路径。支持多行和 glob 通配符。 务必确认构建产物的路径正确。
generate_release_notes 神器。让 GitHub 自动根据两次 Tag 之间的 Pull Request 标题生成 Changelog。 true (省去手动写日志的麻烦)
body 手动指定 Release 的描述文本。 如果开启了 generate_release_notes,通常不需要。
draft 是否存为草稿(不直接对外发布)。 生产环境建议 false,调试时 true
prerelease 是否标记为 “Pre-release”(黄色标签)。 配合 contains 函数动态判断。

6. 自动化维护与清理

6.1 Dependabot 配置

防止 PR 轰炸,合并更新。 文件:.github/dependabot.yml

version: 2
updates:
  # 🤖 更新 GitHub Actions 版本
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly" # 指定更新频率
      time: "10:00" # 指定更新时间
      timezone: "Asia/Shanghai" # 指定时区
    open-pull-requests-limit: 10 # 限制同时打开的PR数量
    labels: # 为Dependabot创建的PR添加标签
      - "dependencies" # 添加一个名为"dependencies"的标签
      - "automerge" # 添加一个名为"automerge"的标签,需要配合工作流
    # ignore:                       # 忽略的依赖
    # allow:                       # 允许的依赖
    groups:
      actions:
        patterns:
          - "*"

6.2 清理历史运行记录

方式 A:GitHub 原生设置 (基于时间)

  • 位置: Settings -> Actions -> General -> Workflow retention settings.
  • 功能: 设置保留天数(如 90 天)。
  • 局限: 无法按“数量”保留。

方式 B:使用 Action (基于数量/状态)

  • 推荐: Mattraks/delete-workflow-runs
  • 场景: 即使时间没到,也想删除那些 Failed 的记录,或者只保留最近 3 条记录以保持界面整洁。
  • 示例:
- uses: Mattraks/delete-workflow-runs@v2
  with:
    token: ${{ github.token }}
    repository: ${{ github.repository }}
    retain_days: 1  # 超过1天的都删
    keep_minimum_runs: 3 # 但至少保留最近3个

7. 进阶控制与技巧

7.1 并发控制 (Concurrency)

场景:连续 Push 了 3 次代码。

  • 默认行为:GitHub 会同时启动 3 个 Job 并行跑。
  • 后果
  1. 浪费构建分钟数。
  2. 如果涉及部署,可能会导致旧代码覆盖新代码(Race Condition)。
  3. 如果是 Auto-Push,可能会产生 Git Lock 报错。

解决方案:配置 concurrency,让新任务自动取消旧任务。

# 在 workflow 顶层配置
concurrency:
  # 组名:通常以 workflow 名 + 分支名 组合
  group: ${{ github.workflow }}-${{ github.ref }}
  # 核心:一旦有新提交,立即取消正在运行的旧任务
  cancel-in-progress: true

7.2 定时任务 (Cron) 的陷阱

on:
  schedule:
    - cron: '0 0 * * *'
  • 时区:GitHub Actions 永远使用 UTC 时间
  • 换算:北京时间 (UTC+8) 需要减去 8 小时。例如想在北京时间 早上 8 点 运行,Cron 应写为 0 0 * * * (00:00 UTC)。在 GitHub 资源紧张时,定时任务可能会延迟 10-30 分钟启动,不要用于秒级精度的任务

7.3 步骤间传参 (Outputs)

场景:步骤 A 生成了一个版本号(如 v1.2.3),步骤 B 需要用这个版本号来命名文件。不能直接用 Shell 变量,因为步骤间环境是隔离的。

steps:
  - name: Generate Version
    id: meta  # ⚠️ 必须给步骤起个 id
    run: |
      VERSION="v$(date +%Y%m%d)"
      # 写入环境变量文件
      echo "version=$VERSION" >> "$GITHUB_OUTPUT"

  - name: Use Version
    # 使用 ${{ steps.ID.outputs.KEY }} 获取
    run: echo "The generated version is ${{ steps.meta.outputs.version }}"

7.4 数据库服务 (Service Containers)

场景:测试代码(如 Node.js + MariaDB)需要连接真实的数据库,而不是 Mock。 方法:直接在 Job 里启动一个临时的 Docker 容器。

jobs:
  test:
    runs-on: ubuntu-latest
    # 定义服务
    services:
      mysql:
        image: mysql:8.0
        env:
          MYSQL_ROOT_PASSWORD: root
          MYSQL_DATABASE: test_db
        ports:
          - 3306:3306
        # 健康检查 (确保数据库启动后再跑步骤)
        options: >-
          --health-cmd="mysqladmin ping"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=3

    steps:
      - uses: actions/checkout@v6
      - name: Run Tests
        # 代码里直接连 localhost:3306 即可
        run: npm test
        env:
          DB_HOST: 127.0.0.1
          DB_USER: root
          DB_PASS: root

7.5 条件控制 (if 的妙用)

不仅仅是 if: github.ref == 'refs/heads/main',还有很多状态判断。

  • 即使前面报错也要运行 (比如发送报警通知):
if: failure() 
  • 无论成功失败都要运行 (比如清理环境):
if: always()
  • 仅在主仓库运行:
if: github.repository == 'my-name/my-repo'

7.6 万能脚本 (actions/github-script)

通常如果想调用 GitHub API(比如给 Issue 评论、合并 PR、打标签),需要写 curl 命令(很难读)或者专门写一个 Node.js 脚本文件并安装 octokit 依赖(很麻烦)。 Github Script 把这些都封装好了,只需要写核心逻辑。

三大内置对象

  • github: 预认证的 API 客户端 (Octokit)。
  • context: 当前运行的上下文(包含仓库信息、触发者、Issue 内容等)。
  • core: 用于设置 Action 的输出变量或标记失败。

使用场景

  • 简单的 API 交互(如自动欢迎新用户)。
  • 根据复杂的条件触发逻辑。

简单示例 (给当前的 Issue 自动评论 “Hello”)

- name: Comment on Issue
  uses: actions/github-script@v7
  with:
    script: |
      // 直接调用 GitHub API,无需配置 token
      github.rest.issues.createComment({
        issue_number: context.issue.number,
        owner: context.repo.owner,
        repo: context.repo.repo,
        body: '👋 Thanks for reporting!'
      })

8. 安全与调试

8.1 Secrets vs Variables

  • Secrets (${{ secrets.XXX }}): 加密存储。日志中显示为 ***。用于存 Token、密码。
  • Variables (${{ vars.XXX }}): 明文存储。用于存非敏感配置(如 API URL、构建标志)。

8.2 开启调试模式

当 Action 莫名失败时,不要瞎猜。

  1. 在仓库 Settings -> Secrets 中添加一个 Secret:ACTIONS_RUNNER_DEBUG 值为 true
  2. 重新运行 Job,日志会输出极详细的 Debug 信息(包括文件路径、具体命令参数等)。

8.3 矩阵构建 (Matrix)

如果需要测试代码在不同 Node 版本下的兼容性:

strategy:
  matrix:
    node-version: [18, 20, 22]
steps:
  - uses: actions/setup-node@v6
    with:
      node-version: ${{ matrix.node-version }}

这会并行启动 3 个 Job 运行


9. 总结:这些 Action 怎么串起来?

一个典型的多语言、多步骤工作流通常是这样组合的:

  1. checkout: 拿出代码。
  2. setup-node/python/java: 准备环境(顺便自动开启 cache 加速)。
  3. run build: 编译代码。
  4. upload-artifact: 把编译好的文件(如 dist/.jar)传上去。
  5. (另一个 Job) download-artifact: 把文件下载下来。
  6. action-gh-release: 把下载下来的文件发布到 Release 页面。
  7. delete-workflow-runs: 最后清理一下运行记录。

附录

为什么要上传和下载构建产物(Artifact)?

1. 核心原因:Job 之间的“失忆”机制

在 GitHub Actions 中,每一个 Job(作业)都是在不同的、全新的虚拟机(Runner)上运行的。

  • Job 1 (Build):系统启动一台虚拟机 A。下载代码,编译生成了 dist/app.exe当这个 Job 结束时,虚拟机 A 会被直接销毁,里面的所有文件(包括刚编译好的代码)都会消失。
  • Job 2 (Release):系统启动一台全新的虚拟机 B。这是个空房子,里面连代码都没有,更别提虚拟机 A 里生成的 app.exe 了。

Artifact(工件)的作用就像是一个“云端中转站”或“网盘”:

  1. Upload: 虚拟机 A 在销毁前,把 dist/ 上传到云端存起来。
  2. Download: 虚拟机 B 启动后,先从云端把这个文件下载下来,然后才能发布。

2. 为什么不把所有步骤写在一个 Job 里?

可能会问:“那不分 Job 不就行了?从头跑到尾,不就不需要上传下载了吗?”

确实可以(对于简单项目),但对于典型的、复杂的项目,拆分 Job 有巨大的优势:

  • A. 并行构建(Matrix Builds)——最常见的原因

假设需要同时发布 Windows、Linux 和 Mac 版软件。

  • 单 Job 模式:得先在 Windows 上跑,跑完换 Linux 跑……这太慢了。

  • 多 Job 模式:可以启动 3 个 Job 同时跑(并行)。

  • Job Win: 编译 -> Upload win.exe

  • Job Lin: 编译 -> Upload linux.binary

  • Job Mac: 编译 -> Upload mac.app

  • Release Job: 等上面 3 个都跑完 -> Download All -> 一次性发布所有文件。

  • B. 权限与安全

  • 构建 Job:通常只需要读取代码的权限,即使脚本被黑客篡改,也无法修改的 Release 页面。

  • 发布 Job:需要写入权限(发布 Release)。

  • 将它们分开,可以限制高权限 Token 仅在最后的发布环节使用,降低风险。

  • C. 容错与重试

如果“构建”花了 30 分钟成功了,但在“上传 Release”这一步网络抖动失败了:

  • 单 Job:必须重头开始,再花 30 分钟重新编译一遍。
  • 多 Job:只需要点击“Re-run”那个失败的 Release Job,它会直接去下载之前编译好的文件(Artifact),几秒钟就能重试完成。