GitHub Action
A comprehensive archetype for creating custom GitHub Actions with professional scaffolding, automated workflows, and release management capabilities.
Overview
This archetype generates a complete GitHub Action repository structure with best practices for action development, testing, documentation, and automated release management. It provides templates for both JavaScript and Docker-based actions with comprehensive CI/CD workflows.
Technology Stack
- GitHub Actions: Workflow automation platform
- Node.js: JavaScript action runtime
- Docker: Containerized action runtime
- TypeScript: Type-safe JavaScript development
- Jest: Testing framework
- ESLint: Code linting and formatting
- Semantic Release: Automated versioning and releases
Key Features
Action Types Support
- JavaScript Actions: Fast execution with Node.js runtime
- Docker Actions: Flexible environment with custom dependencies
- Composite Actions: Reusable workflow step combinations
- Reusable Workflows: Shareable workflow templates
Development Workflow
- TypeScript Support: Type-safe action development
- Local Testing: Test actions locally before deployment
- Unit Testing: Comprehensive test coverage
- Linting & Formatting: Code quality enforcement
Release Automation
- Semantic Versioning: Automated version management
- Release Notes: Auto-generated changelogs
- GitHub Releases: Automated release publishing
- Marketplace Publishing: Optional marketplace deployment
Project Structure
github-action/
├── .github/
│ ├── workflows/
│ │ ├── ci.yml # Continuous integration workflow
│ │ ├── release.yml # Release automation workflow
│ │ ├── codeql.yml # Security analysis
│ │ └── dependabot.yml # Dependency updates
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml # Bug report template
│ │ └── feature_request.yml # Feature request template
│ └── pull_request_template.md
├── src/
│ ├── main.ts # Main action entry point
│ ├── action.ts # Core action logic
│ ├── inputs.ts # Input validation and parsing
│ ├── outputs.ts # Output handling
│ └── utils/
│ ├── logger.ts # Logging utilities
│ ├── github.ts # GitHub API helpers
│ └── validation.ts # Input validation
├── tests/
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── fixtures/ # Test data and mocks
├── docs/
│ ├── README.md # Main documentation
│ ├── CONTRIBUTING.md # Contribution guidelines
│ ├── CHANGELOG.md # Version history
│ └── examples/ # Usage examples
├── action.yml # Action metadata and configuration
├── Dockerfile # Docker action (optional)
├── package.json # Node.js dependencies
├── tsconfig.json # TypeScript configuration
├── jest.config.js # Test configuration
├── .eslintrc.js # Linting rules
├── .gitignore # Git ignore patterns
├── LICENSE # License file
└── README.md # Repository README
Action Configuration
Action Metadata (action.yml)
name: 'Your Action Name'
description: 'A brief description of what your action does'
author: 'Your Name'
branding:
icon: 'activity'
color: 'blue'
inputs:
input-parameter:
description: 'Description of the input parameter'
required: true
default: 'default-value'
optional-parameter:
description: 'Optional parameter description'
required: false
outputs:
result:
description: 'Description of the output'
status:
description: 'Status of the action execution'
runs:
using: 'node20'
main: 'dist/index.js'
# For Docker actions:
# using: 'docker'
# image: 'Dockerfile'
Input Handling
import * as core from '@actions/core'
export interface ActionInputs {
inputParameter: string
optionalParameter?: string
numericInput?: number
booleanInput?: boolean
}
export function getInputs(): ActionInputs {
return {
inputParameter: core.getInput('input-parameter', { required: true }),
optionalParameter: core.getInput('optional-parameter') || undefined,
numericInput: parseInt(core.getInput('numeric-input')) || undefined,
booleanInput: core.getBooleanInput('boolean-input')
}
}
export function validateInputs(inputs: ActionInputs): void {
if (!inputs.inputParameter) {
throw new Error('input-parameter is required')
}
if (inputs.numericInput && inputs.numericInput < 0) {
throw new Error('numeric-input must be positive')
}
}
Core Action Implementation
import * as core from '@actions/core'
import * as github from '@actions/github'
import { getInputs, validateInputs } from './inputs'
import { setOutputs } from './outputs'
import { Logger } from './utils/logger'
export async function run(): Promise<void> {
const logger = new Logger()
try {
logger.info('Starting action execution')
// Get and validate inputs
const inputs = getInputs()
validateInputs(inputs)
logger.info('Inputs validated successfully', inputs)
// Execute main action logic
const result = await executeAction(inputs, logger)
// Set outputs
setOutputs({
result: result.data,
status: 'success'
})
logger.info('Action completed successfully')
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
logger.error('Action failed', { error: errorMessage })
core.setFailed(errorMessage)
setOutputs({
status: 'failed'
})
}
}
async function executeAction(inputs: ActionInputs, logger: Logger): Promise<any> {
// Implement your action logic here
logger.info('Executing main action logic')
// Example: GitHub API interaction
if (process.env.GITHUB_TOKEN) {
const octokit = github.getOctokit(process.env.GITHUB_TOKEN)
const context = github.context
const { data: repo } = await octokit.rest.repos.get({
owner: context.repo.owner,
repo: context.repo.repo
})
logger.info('Repository information retrieved', {
name: repo.name,
stars: repo.stargazers_count
})
}
// Simulate some processing
await new Promise(resolve => setTimeout(resolve, 1000))
return {
data: `Processed: ${inputs.inputParameter}`,
timestamp: new Date().toISOString()
}
}
Docker Action Implementation
FROM node:20-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY src/ ./src/
COPY tsconfig.json ./
# Build TypeScript
RUN npm run build
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S actionuser -u 1001 -G nodejs
USER actionuser
# Set entrypoint
ENTRYPOINT ["node", "/app/dist/index.js"]
Workflow Automation
CI/CD Workflow (.github/workflows/ci.yml)
name: Continuous Integration
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18, 20]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm test
- name: Build action
run: npm run build
- name: Test action
uses: ./
with:
input-parameter: 'test-value'
optional-parameter: 'optional-test'
security:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run security audit
run: npm audit --audit-level moderate
- name: Check for vulnerabilities
uses: github/codeql-action/analyze@v3
with:
languages: typescript
integration-test:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Test action in real workflow
uses: ./
with:
input-parameter: 'integration-test'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Release Workflow (.github/workflows/release.yml)
name: Release
on:
push:
branches: [ main ]
jobs:
release:
runs-on: ubuntu-latest
if: "!contains(github.event.head_commit.message, 'skip ci')"
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm test
- name: Build action
run: npm run build
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
with:
semantic_version: 22
extra_plugins: |
@semantic-release/changelog
@semantic-release/git
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
Testing Framework
Unit Tests
import { run } from '../src/action'
import * as core from '@actions/core'
// Mock GitHub Actions core
jest.mock('@actions/core')
const mockCore = core as jest.Mocked<typeof core>
describe('Action', () => {
beforeEach(() => {
jest.clearAllMocks()
// Setup default input mocks
mockCore.getInput.mockImplementation((name: string) => {
switch (name) {
case 'input-parameter':
return 'test-value'
case 'optional-parameter':
return 'optional-test'
default:
return ''
}
})
})
test('should execute successfully with valid inputs', async () => {
await run()
expect(mockCore.setFailed).not.toHaveBeenCalled()
expect(mockCore.setOutput).toHaveBeenCalledWith('status', 'success')
})
test('should fail with missing required input', async () => {
mockCore.getInput.mockImplementation((name: string) => {
if (name === 'input-parameter') return ''
return 'default'
})
await run()
expect(mockCore.setFailed).toHaveBeenCalledWith('input-parameter is required')
})
test('should handle GitHub API errors gracefully', async () => {
// Mock GitHub API failure
process.env.GITHUB_TOKEN = 'invalid-token'
await run()
// Action should still complete but handle the error
expect(mockCore.setOutput).toHaveBeenCalledWith('status', 'success')
})
})
Integration Tests
import { exec } from '@actions/exec'
import * as path from 'path'
describe('Integration Tests', () => {
test('should run action successfully in real environment', async () => {
const actionPath = path.join(__dirname, '..')
// Set environment variables
process.env['INPUT_INPUT-PARAMETER'] = 'integration-test'
process.env['INPUT_OPTIONAL-PARAMETER'] = 'optional-value'
// Execute the action
const exitCode = await exec('node', [path.join(actionPath, 'dist/index.js')])
expect(exitCode).toBe(0)
})
})
Usage Examples
Basic Usage
name: Use Custom Action
on:
push:
branches: [ main ]
jobs:
example:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run custom action
uses: your-org/your-action@v1
with:
input-parameter: 'example-value'
optional-parameter: 'optional-value'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Use action outputs
run: |
echo "Result: ${{ steps.custom-action.outputs.result }}"
echo "Status: ${{ steps.custom-action.outputs.status }}"
Advanced Configuration
name: Advanced Action Usage
on:
workflow_dispatch:
inputs:
environment:
description: 'Deployment environment'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run deployment action
uses: your-org/deployment-action@v2
with:
environment: ${{ github.event.inputs.environment }}
timeout: '30'
dry-run: ${{ github.event.inputs.environment == 'production' && 'false' || 'true' }}
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
- name: Notify on failure
if: failure()
uses: your-org/notification-action@v1
with:
message: 'Deployment failed for ${{ github.event.inputs.environment }}'
channel: '#deployments'
env:
SLACK_TOKEN: ${{ secrets.SLACK_TOKEN }}
Configuration Options
Package.json Scripts
{
"scripts": {
"build": "tsc && ncp src/action.yml dist/action.yml",
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage",
"lint": "eslint src/**/*.ts",
"lint:fix": "eslint src/**/*.ts --fix",
"format": "prettier --write src/**/*.ts",
"package": "ncc build src/main.ts -o dist --source-map --license licenses.txt",
"start": "node dist/index.js"
}
}
TypeScript Configuration
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "tests"]
}
Quick Start
-
Generate the action:
archetect render git@github.com:p6m-archetypes/github-action.archetype.git -
Install dependencies:
npm install -
Develop your action:
# Edit src/action.ts with your logic
npm run build
npm test -
Test locally:
export INPUT_INPUT-PARAMETER="test-value"
node dist/index.js -
Create repository and push:
git init
git add .
git commit -m "Initial action implementation"
git push origin main
Best Practices
Security
- Use least privilege principle for permissions
- Validate all inputs thoroughly
- Never log sensitive information
- Use secrets for authentication tokens
Performance
- Minimize action execution time
- Use appropriate runner types
- Cache dependencies when possible
- Optimize Docker images for size
Maintenance
- Keep dependencies updated
- Use semantic versioning
- Provide comprehensive documentation
- Include usage examples and troubleshooting guides