Skip to main content

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

  1. Generate the action:

    archetect render git@github.com:p6m-archetypes/github-action.archetype.git
  2. Install dependencies:

    npm install
  3. Develop your action:

    # Edit src/action.ts with your logic
    npm run build
    npm test
  4. Test locally:

    export INPUT_INPUT-PARAMETER="test-value"
    node dist/index.js
  5. 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