Chapter 05

自定义 Action 开发

创建可复用的 GitHub Actions——JavaScript、Docker、Composite 三种方式详解

Action 的三种类型

JavaScript Action
使用 Node.js 编写,直接在 Runner 上运行(无需容器),启动速度最快。适合需要调用 GitHub API、处理文件、复杂逻辑的 Action。依赖 @actions/core@actions/exec 等官方 SDK。
Docker Container Action
运行在自定义 Docker 容器中,可以使用任意语言和依赖。环境完全可控,适合需要特定系统依赖的工具(如特定版本的编译器、二进制工具)。只能在 Linux Runner 上运行。
Composite Action
将多个 Workflow Steps 组合成可复用单元,使用 YAML 定义(无需编程)。适合将常用步骤序列封装复用,如"安装依赖 + 缓存 + 运行测试"的标准流程。

action.yml 元数据文件

# action.yml(所有 Action 都需要这个文件)
name: 'Deploy to Server'
description: '通过 SSH 将构建产物部署到远程服务器'
author: 'YourName'

inputs:
  host:
    description: '服务器 IP 或域名'
    required: true
  username:
    description: 'SSH 用户名'
    required: true
    default: 'ubuntu'
  private-key:
    description: 'SSH 私钥内容'
    required: true
  source-path:
    description: '本地构建产物路径'
    required: false
    default: './dist'
  target-path:
    description: '远程目标路径'
    required: false
    default: '/var/www/html'

outputs:
  deployed-at:
    description: '部署完成的时间戳'
    value: ${{ steps.deploy.outputs.timestamp }}

runs:
  using: 'composite'
  steps:
    - id: deploy
      shell: bash
      run: |
        # 实现部署逻辑
        echo "timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT

JavaScript Action 实战

// src/index.js
const core = require('@actions/core');
const github = require('@actions/github');
const exec = require('@actions/exec');

async function run() {
  try {
    // 获取输入参数
    const token = core.getInput('github-token', { required: true });
    const message = core.getInput('comment-message');
    const severity = core.getInput('severity');

    // 使用 GitHub API
    const octokit = github.getOctokit(token);
    const context = github.context;

    // 只对 PR 添加评论
    if (context.eventName === 'pull_request') {
      const { data: comment } = await octokit.rest.issues.createComment({
        owner: context.repo.owner,
        repo: context.repo.repo,
        issue_number: context.payload.pull_request.number,
        body: `## 代码质量报告\n\n${message}\n\n严重程度:**${severity}**`
      });

      // 设置输出
      core.setOutput('comment-id', comment.id);
      core.info(`✅ 评论已创建:${comment.html_url}`);
    }

    // 执行 Shell 命令
    let output = '';
    await exec.exec('npm', ['run', 'lint'], {
      listeners: {
        stdout: (data) => { output += data.toString(); }
      }
    });

    core.setOutput('lint-output', output);

  } catch (error) {
    // 标记 Action 失败
    core.setFailed(error.message);
  }
}

run();

package.json 配置

{
  "name": "my-github-action",
  "version": "1.0.0",
  "main": "dist/index.js",
  "scripts": {
    "build": "ncc build src/index.js -o dist",
    "test": "jest"
  },
  "dependencies": {
    "@actions/core": "^1.10.0",
    "@actions/exec": "^1.1.1",
    "@actions/github": "^6.0.0"
  },
  "devDependencies": {
    "@vercel/ncc": "^0.38.0",
    "jest": "^29.0.0"
  }
}
必须打包 node_modules

JavaScript Action 需要将所有依赖打包进仓库(使用 @vercel/ncc 打包为单文件),或者在 action.yml 中指定 using: 'node20' 后将 node_modules 提交到仓库。不能依赖 Runner 上的全局安装,因为 GitHub 托管 Runner 只有标准系统工具。

Composite Action:最简单的复用方式

# .github/actions/setup-and-cache/action.yml
name: 'Setup Node and Cache'
description: '配置 Node.js 并缓存依赖(常用步骤复用)'

inputs:
  node-version:
    description: 'Node.js 版本'
    default: '20'
  package-manager:
    description: 'npm 或 yarn'
    default: 'npm'

outputs:
  cache-hit:
    description: '是否命中缓存'
    value: ${{ steps.cache.outputs.cache-hit }}

runs:
  using: 'composite'
  steps:
    - name: 配置 Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}

    - id: cache
      name: 缓存依赖
      uses: actions/cache@v4
      with:
        path: node_modules
        key: node-${{ inputs.node-version }}-${{ hashFiles('**/package-lock.json') }}
        restore-keys: |
          node-${{ inputs.node-version }}-

    - name: 安装依赖
      if: steps.cache.outputs.cache-hit != 'true'
      shell: bash
      run: ${{ inputs.package-manager }} ci

使用自定义 Composite Action

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

      # 使用本地 composite action(本仓库内)
      - uses: ./.github/actions/setup-and-cache
        with:
          node-version: '20'

      # 使用外部仓库的 Action(格式:owner/repo@ref)
      - uses: myorg/my-actions/setup-and-cache@v2

      - run: npm run build

发布 Action 到 GitHub Marketplace

# 发布 Action 的标准目录结构
my-action/
├── action.yml          # Action 元数据(必须在根目录)
├── src/
│   └── index.ts        # 源代码
├── dist/
│   └── index.js        # 打包后的文件(ncc 生成,必须提交到仓库)
├── package.json
└── README.md           # 使用文档

# CI/CD:自动打包并发布新版本
# .github/workflows/release.yml
name: Release Action

on:
  push:
    tags: ['v*']

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

      - name: 安装依赖
        run: npm ci

      - name: 打包(生成 dist/index.js)
        run: npm run build

      # 将打包后的代码提交到标签
      - name: 创建 Release
        uses: softprops/action-gh-release@v2
        with:
          files: dist/index.js
          generate_release_notes: true
三种 Action 类型选择建议

本章小结

本章核心要点