Appearance
GitHub Actions自定义动作
自定义动作(Custom Actions)是GitHub Actions生态系统的核心组件,允许您创建可重用的自动化任务。通过自定义动作,可以封装复杂的逻辑并在多个工作流中重复使用。
自定义动作基础
动作类型
GitHub Actions支持三种类型的动作:
- Docker容器动作:在Docker容器中运行
- JavaScript动作:在runner环境中运行JavaScript
- 复合动作:运行多个步骤组成的动作
动作结构
每个动作都需要一个action.yml或action.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"
动作市场发布
- 确保README.md文件包含清晰的文档
- 创建版本标签(v1, v1.0, v1.0.0)
- 在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'