Skip to content
On this page

GitHub Actions自定义动作

自定义动作(Custom Actions)是GitHub Actions生态系统的核心组件,允许您创建可重用的自动化任务。通过自定义动作,可以封装复杂的逻辑并在多个工作流中重复使用。

自定义动作基础

动作类型

GitHub Actions支持三种类型的动作:

  1. Docker容器动作:在Docker容器中运行
  2. JavaScript动作:在runner环境中运行JavaScript
  3. 复合动作:运行多个步骤组成的动作

动作结构

每个动作都需要一个action.ymlaction.yaml文件来定义元数据:

yaml
# action.yml - 基本动作定义
name: 'My Custom Action'
description: 'A sample custom action'
author: 'Your Name'

inputs:
  name:
    description: 'Input name'
    required: true
    default: 'World'
  greeting:
    description: 'Greeting message'
    required: false
    default: 'Hello'

outputs:
  time:
    description: 'The time the action was executed'
  message:
    description: 'The greeting message'

runs:
  using: 'composite'  # 或 'node16', 'docker'
  steps:
    - run: echo "Hello ${{ inputs.name }}!"
      shell: bash

Docker容器动作

创建Docker容器动作

# Dockerfile
FROM alpine:latest

# 安装必要的工具
RUN apk add --no-cache curl jq bash

# 复制脚本
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
bash
#!/bin/bash
# entrypoint.sh
set -e

# 从环境变量获取输入
INPUT_NAME="${INPUT_NAME:-World}"
INPUT_GREETING="${INPUT_GREETING:-Hello}"

# 执行主要逻辑
echo "${INPUT_GREETING}, ${INPUT_NAME}!"

# 设置输出
echo "time=$(date)" >> $GITHUB_OUTPUT
echo "message=${INPUT_GREETING}, ${INPUT_NAME}!" >> $GITHUB_OUTPUT

# 设置环境变量(可选)
echo "CUSTOM_GREETING=${INPUT_GREETING}, ${INPUT_NAME}!" >> $GITHUB_ENV
yaml
# action.yml for Docker action
name: 'Docker Greeting Action'
description: 'A greeting action using Docker'
author: 'Your Name'

inputs:
  name:
    description: 'Name to greet'
    required: true
    default: 'World'
  greeting:
    description: 'Greeting message'
    required: false
    default: 'Hello'

outputs:
  time:
    description: 'Execution time'
  message:
    description: 'Greeting message'

runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ inputs.name }}
    - ${{ inputs.greeting }}

Docker动作使用示例

yaml
# .github/workflows/use-docker-action.yml
name: Use Docker Action

on: [push]

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Use Docker Greeting Action
        id: greeting
        uses: ./path/to/docker-action  # 本地动作
        # 或者使用远程动作
        # uses: username/repo-name@v1
        with:
          name: 'GitHub Actions'
          greeting: 'Welcome'
      
      - name: Show output
        run: |
          echo "Time: ${{ steps.greeting.outputs.time }}"
          echo "Message: ${{ steps.greeting.outputs.message }}"

JavaScript动作

创建JavaScript动作

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

try {
  // 获取输入
  const name = core.getInput('name');
  const greeting = core.getInput('greeting');
  
  // 执行主要逻辑
  const message = `${greeting}, ${name}!`;
  console.log(message);
  
  // 设置输出
  core.setOutput('time', new Date().toISOString());
  core.setOutput('message', message);
  
  // 设置环境变量
  core.exportVariable('CUSTOM_MESSAGE', message);
  
  // 设置状态
  core.info('Action completed successfully');
} catch (error) {
  core.setFailed(error.message);
}
json
// package.json
{
  "name": "greeting-action",
  "version": "1.0.0",
  "description": "A greeting action",
  "main": "index.js",
  "scripts": {
    "build": "ncc build index.js -o dist --source-map --minify"
  },
  "dependencies": {
    "@actions/core": "^1.10.0",
    "@actions/github": "^5.1.1"
  },
  "devDependencies": {
    "@vercel/ncc": "^0.36.1"
  }
}
yaml
# action.yml for JavaScript action
name: 'JavaScript Greeting Action'
description: 'A greeting action using JavaScript'
author: 'Your Name'
branding:
  icon: 'smile'
  color: 'green'

inputs:
  name:
    description: 'Name to greet'
    required: true
    default: 'World'
  greeting:
    description: 'Greeting message'
    required: false
    default: 'Hello'

outputs:
  time:
    description: 'Execution time'
  message:
    description: 'Greeting message'

runs:
  using: 'node16'
  main: 'dist/index.js'

构建JavaScript动作

bash
# 安装依赖
npm install

# 构建(使用ncc打包)
npm run build

复合动作

创建复合动作

yaml
# action.yml for composite action
name: 'Composite Greeting Action'
description: 'A greeting action using composite steps'
author: 'Your Name'
branding:
  icon: 'star'
  color: 'blue'

inputs:
  name:
    description: 'Name to greet'
    required: true
    default: 'World'
  greeting:
    description: 'Greeting message'
    required: false
    default: 'Hello'
  language:
    description: 'Language for greeting'
    required: false
    default: 'en'

outputs:
  time:
    description: 'Execution time'
  message:
    description: 'Greeting message'
  translated:
    description: 'Translated greeting'

runs:
  using: 'composite'
  steps:
    - name: Set greeting time
      shell: bash
      run: echo "time=$(date -u)" >> $GITHUB_OUTPUT
    
    - name: Generate greeting
      id: greet
      shell: bash
      run: |
        NAME="${{ inputs.name }}"
        GREETING="${{ inputs.greeting }}"
        LANG="${{ inputs.language }}"
        
        case $LANG in
          en) MSG="$GREETING, $NAME!" ;;
          es) MSG="¡$GREETING, $NAME!" ;;
          fr) MSG="$GREETING, $NAME !" ;;
          *) MSG="$GREETING, $NAME!" ;;
        esac
        
        echo "message=$MSG" >> $GITHUB_OUTPUT
        echo "Generated greeting: $MSG"
    
    - name: Translate greeting
      id: translate
      shell: bash
      run: |
        ORIGINAL="${{ steps.greet.outputs.message }}"
        # 简单的翻译逻辑(实际项目中应使用翻译API)
        case "${{ inputs.language }}" in
          en) TRANS="Hello in English: $ORIGINAL" ;;
          es) TRANS="Hola en español: $ORIGINAL" ;;
          fr) TRANS="Bonjour en français: $ORIGINAL" ;;
          *) TRANS="Translation: $ORIGINAL" ;;
        esac
        
        echo "translated=$TRANS" >> $GITHUB_OUTPUT
        echo "$TRANS"
    
    - name: Export environment variables
      shell: bash
      run: |
        echo "GREETING_MESSAGE=${{ steps.greet.outputs.message }}" >> $GITHUB_ENV
        echo "TRANSLATED_MESSAGE=${{ steps.translate.outputs.translated }}" >> $GITHUB_ENV

复合动作使用示例

yaml
name: Use Composite Action

on: [push]

jobs:
  greet:
    runs-on: ubuntu-latest
    steps:
      - name: Use Composite Greeting Action
        id: greeting
        uses: ./path/to/composite-action
        with:
          name: 'GitHub'
          greeting: 'Welcome to'
          language: 'en'
      
      - name: Display outputs
        run: |
          echo "Time: ${{ steps.greeting.outputs.time }}"
          echo "Message: ${{ steps.greeting.outputs.message }}"
          echo "Translated: ${{ steps.greeting.outputs.translated }}"
      
      - name: Use environment variables
        run: |
          echo "Env message: $GREETING_MESSAGE"
          echo "Env translated: $TRANSLATED_MESSAGE"

高级动作功能

条件执行

yaml
# conditional-action/action.yml
name: 'Conditional Action'
description: 'An action with conditional steps'

inputs:
  run_step1:
    description: 'Whether to run step 1'
    required: false
    default: 'true'
  run_step2:
    description: 'Whether to run step 2'
    required: false
    default: 'false'
  environment:
    description: 'Target environment'
    required: true
    default: 'development'

runs:
  using: 'composite'
  steps:
    - name: Step 1
      if: ${{ inputs.run_step1 == 'true' }}
      shell: bash
      run: echo "Running step 1 in ${{ inputs.environment }}"
    
    - name: Step 2
      if: ${{ inputs.run_step2 == 'true' }}
      shell: bash
      run: echo "Running step 2 in ${{ inputs.environment }}"
    
    - name: Environment-specific step
      if: ${{ inputs.environment == 'production' }}
      shell: bash
      run: echo "Running production-specific step"

错误处理

javascript
// error-handling-action/index.js
const core = require('@actions/core');
const fs = require('fs');

async function run() {
  try {
    const inputFile = core.getInput('input-file');
    const outputFile = core.getInput('output-file');
    
    // 验证输入
    if (!inputFile) {
      throw new Error('Input file is required');
    }
    
    // 读取文件
    let content;
    try {
      content = fs.readFileSync(inputFile, 'utf8');
    } catch (error) {
      core.setFailed(`Failed to read file ${inputFile}: ${error.message}`);
      return;
    }
    
    // 处理内容
    const processedContent = content.toUpperCase();
    
    // 写入文件
    try {
      fs.writeFileSync(outputFile, processedContent);
    } catch (error) {
      core.setFailed(`Failed to write file ${outputFile}: ${error.message}`);
      return;
    }
    
    // 设置输出
    core.setOutput('processed-content', processedContent);
    core.setOutput('file-size', processedContent.length);
    
  } catch (error) {
    // 记录错误但不立即失败,允许后续处理
    core.error(`Processing error: ${error.message}`);
    core.setFailed(error.message);
  }
}

run();
yaml
# error-handling-action/action.yml
name: 'Error Handling Action'
description: 'An action with proper error handling'

inputs:
  input-file:
    description: 'Input file path'
    required: true
  output-file:
    description: 'Output file path'
    required: true

outputs:
  processed-content:
    description: 'Processed file content'
  file-size:
    description: 'Size of processed content'

runs:
  using: 'node16'
  main: 'dist/index.js'

动作测试

本地测试动作

yaml
# .github/workflows/test-action.yml
name: Test Custom Action

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

jobs:
  test-docker-action:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test Docker Action
        id: docker-test
        uses: ./
        with:
          name: 'Docker Test'
          greeting: 'Hello'
      - name: Verify Docker Output
        run: |
          echo "Docker action output: ${{ steps.docker-test.outputs.message }}"
          if [[ "${{ steps.docker-test.outputs.message }}" == *"Docker Test"* ]]; then
            echo "✅ Docker action test passed"
          else
            echo "❌ Docker action test failed"
            exit 1
          fi

  test-javascript-action:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test JavaScript Action
        id: js-test
        uses: ./
        with:
          name: 'JavaScript Test'
          greeting: 'Hi'
      - name: Verify JavaScript Output
        run: |
          echo "JavaScript action output: ${{ steps.js-test.outputs.message }}"
          if [[ "${{ steps.js-test.outputs.message }}" == *"JavaScript Test"* ]]; then
            echo "✅ JavaScript action test passed"
          else
            echo "❌ JavaScript action test failed"
            exit 1
          fi

单元测试(JavaScript动作)

javascript
// __tests__/action.test.js
const action = require('../index.js');

describe('Action Tests', () => {
  beforeEach(() => {
    jest.resetModules();
  });

  test('should handle inputs correctly', () => {
    // Mock core module
    const core = require('@actions/core');
    const setInput = jest.spyOn(core, 'getInput');
    
    setInput.mockImplementation((name) => {
      switch(name) {
        case 'name': return 'Test';
        case 'greeting': return 'Hello';
        default: return '';
      }
    });

    // Run action
    require('../index.js');

    expect(setInput).toHaveBeenCalledWith('name');
    expect(setInput).toHaveBeenCalledWith('greeting');
  });
});

实际应用示例

代码质量检查动作

yaml
# code-quality-action/action.yml
name: 'Code Quality Action'
description: 'Run code quality checks'

inputs:
  working-directory:
    description: 'Working directory'
    required: false
    default: '.'
  run-lint:
    description: 'Run linting'
    required: false
    default: 'true'
  run-format:
    description: 'Run formatting check'
    required: false
    default: 'true'
  run-tests:
    description: 'Run tests'
    required: false
    default: 'true'

outputs:
  lint-status:
    description: 'Lint status'
  format-status:
    description: 'Format status'
  test-status:
    description: 'Test status'

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: '18'
        cache: 'npm'
    
    - name: Install dependencies
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: npm ci
    
    - name: Run linting
      id: lint
      if: ${{ inputs.run-lint == 'true' }}
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: |
        if npm run lint; then
          echo "status=passed" >> $GITHUB_OUTPUT
        else
          echo "status=failed" >> $GITHUB_OUTPUT
          exit 1
        fi
    
    - name: Check formatting
      id: format
      if: ${{ inputs.run-format == 'true' }}
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: |
        if npm run format:check; then
          echo "status=passed" >> $GITHUB_OUTPUT
        else
          echo "status=failed" >> $GITHUB_OUTPUT
          exit 1
        fi
    
    - name: Run tests
      id: tests
      if: ${{ inputs.run-tests == 'true' }}
      shell: bash
      working-directory: ${{ inputs.working-directory }}
      run: |
        if npm run test; then
          echo "status=passed" >> $GITHUB_OUTPUT
        else
          echo "status=failed" >> $GITHUB_OUTPUT
          exit 1
        fi

部署动作

javascript
// deploy-action/index.js
const core = require('@actions/core');
const github = require('@actions/github');

async function run() {
  try {
    const environment = core.getInput('environment');
    const service = core.getInput('service');
    const imageTag = core.getInput('image-tag');
    const deployToken = core.getInput('deploy-token');
    
    // 验证输入
    if (!environment || !service || !imageTag || !deployToken) {
      throw new Error('Missing required inputs');
    }
    
    // 部署逻辑
    console.log(`Deploying service ${service} to ${environment} with image ${imageTag}`);
    
    // 这里可以集成实际的部署API调用
    // 例如:Kubernetes, AWS, Azure等
    
    // 设置输出
    const deploymentId = `dep_${Math.random().toString(36).substr(2, 9)}`;
    core.setOutput('deployment-id', deploymentId);
    core.setOutput('status', 'deployed');
    core.setOutput('url', `https://${service}.${environment}.example.com`);
    
    console.log(`✅ Deployment completed: ${deploymentId}`);
    
  } catch (error) {
    core.setFailed(`Deployment failed: ${error.message}`);
  }
}

run();
yaml
# deploy-action/action.yml
name: 'Deployment Action'
description: 'Deploy application to environment'

inputs:
  environment:
    description: 'Target environment (dev, staging, prod)'
    required: true
  service:
    description: 'Service name to deploy'
    required: true
  image-tag:
    description: 'Docker image tag to deploy'
    required: true
  deploy-token:
    description: 'Deployment authentication token'
    required: true

outputs:
  deployment-id:
    description: 'Unique deployment identifier'
  status:
    description: 'Deployment status'
  url:
    description: 'Deployed service URL'

runs:
  using: 'node16'
  main: 'dist/index.js'

发布和版本管理

语义化版本

bash
# 创建标签和发布
git tag -a v1.0.0 -m "Release version 1.0.0"
git push origin v1.0.0

# 或者使用GitHub CLI
gh release create v1.0.0 --title "v1.0.0" --notes "Initial release"

动作市场发布

  1. 确保README.md文件包含清晰的文档
  2. 创建版本标签(v1, v1.0, v1.0.0)
  3. 在GitHub Marketplace发布
markdown
<!-- README.md for action -->
# My Custom Action

A brief description of what the action does.

## Inputs

### `name`
**Required** Name to greet. Default `"World"`.

### `greeting`
Optional greeting message. Default `"Hello"`.

## Outputs

### `message`
The generated greeting message.

### `time`
The time when the action was executed.

## Example Usage

```yaml
- uses: username/repo-name@v1
  with:
    name: 'GitHub'
    greeting: 'Hello'