Creating and Publishing GitHub Actions

A guide to creating custom GitHub Actions and publishing them to the Marketplace

Featured image



Overview

Learn how to create custom GitHub Actions and publish them to the GitHub Marketplace. We’ll walk through creating a container-based action and the process of publishing it.

My Published Actions



Types of GitHub Actions

GitHub Actions can be implemented in three different ways, each with its own advantages and use cases:

  • JavaScript Actions
    Run directly on the runner, execute quickly with no container overhead, and can leverage the GitHub Actions Toolkit.
  • Docker Container Actions
    Run in an isolated container environment, can use any language, and can include specific dependencies.
  • Composite Actions
    Combine multiple workflow steps within one action, reuse shared steps, and can reference other actions.

Choosing the Right Action Type


When to use each action type:

JavaScript Actions: - When performance matters (faster execution)
- For lightweight operations that don't need external dependencies
- When you want to leverage GitHub's Actions Toolkit
- Cross-platform compatibility is required

Docker Container Actions: - When you need specific environments or dependencies
- For actions written in languages other than JavaScript
- When isolation between steps is important
- For more complex processing with system-level access

Composite Actions: - To combine and reuse multiple steps
- When you need to refactor repeated steps across workflows
- For simple sequences of shell commands and existing actions


Creating GitHub Action Template

1. Create Action Repository

Use the container-action template provided by GitHub as a starting point.

2. Configure Docker Components

Dockerfile



entrypoint.sh



3. Define Action Metadata

action.yml



4. Local Testing

Create a test environment file



# Build Docker image
docker build -f Dockerfile -t extract-commit-action .

Run test



Creating JavaScript Action

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

async function run() {
  try {
    // Get inputs
    const name = core.getInput('name');
    
    // Log the inputs
    console.log(`Hello ${name}!`);
    
    // Set outputs
    core.setOutput('greeting', `Hello ${name}!`);
  } catch (error) {
    core.setFailed(error.message);
  }
}

run();
# action.yml for JavaScript Action
name: 'Hello World JavaScript Action'
description: 'Say hello to the world or to a specific person'
inputs:
  name:
    description: 'Who to greet'
    required: true
    default: 'World'
outputs:
  greeting:
    description: 'The greeting message'
runs:
  using: 'node16'
  main: 'index.js'

Creating Composite Action

# action.yml for Composite Action
name: 'Setup and Test'
description: 'Setup environment and run tests'
inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '16'
  run-lint:
    description: 'Whether to run linting'
    required: false
    default: 'true'
runs:
  using: "composite"
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: $
        
    - name: Install dependencies
      shell: bash
      run: npm ci
      
    - name: Run linting
      if: inputs.run-lint == 'true'
      shell: bash
      run: npm run lint
      
    - name: Run tests
      shell: bash
      run: npm test


Advanced GitHub Actions Features

1. Using Inputs and Outputs

# In action.yml
inputs:
  config-path:
    required: true
    description: 'Path to configuration file'
outputs:
  result:
    description: 'The result of the operation'
// In JavaScript
const configPath = core.getInput('config-path');
// ... do something ...
core.setOutput('result', result);

2. Handling Secrets

# In workflow
jobs:
  my-job:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/my-action@v1
        with:
          token: $
// In JavaScript
const token = core.getInput('token');
const octokit = github.getOctokit(token);

3. Versioning Strategies

# Semantic versioning with tags
git tag -a v1.0.0 -m "Release v1.0.0"
git push origin v1.0.0

# Major version tag (always points to latest in major version)
git tag -f v1 v1.0.0
git push -f origin v1

# Using SHA for immutability
uses: actions/my-action@a1b2c3d



Publishing to GitHub Marketplace

1. Version Management

# Create new version
git tag v1.0.1
git push origin v1.0.1

# Update major version tag
git tag -f v1 v1.0.1
git push -f origin v1

2. Action Usage Example

3. Publishing Checklist


Before Publishing:

✅ Ensure your repository is public
✅ Create a valid action.yml in the root
✅ Include proper documentation in README.md
✅ Add LICENSE file (MIT or other open source)
✅ Use semantic versioning tags
✅ Add proper icon and color in action.yml
✅ Test the action thoroughly
✅ Create a release on GitHub


Testing Strategies for GitHub Actions

1. Unit Testing JavaScript Actions

// hello.test.js
const process = require('process');
const cp = require('child_process');
const path = require('path');

// Mock the GitHub Actions core library
jest.mock('@actions/core');
const core = require('@actions/core');

describe('Hello Action', () => {
  it('Outputs greeting with input name', () => {
    process.env.INPUT_NAME = 'Developer';
    const ip = path.join(__dirname, 'index.js');
    
    // Execute the action
    cp.execSync(`node ${ip}`, {env: process.env});
    
    // Verify output was set
    expect(core.setOutput).toHaveBeenCalledWith(
      'greeting', 
      'Hello Developer!'
    );
  });
});

2. Integration Testing

# .github/workflows/test.yml
name: Test Action

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Test local action
        uses: ./
        with:
          name: World
      
      - name: Verify action output
        run: |
          if [[ "$" != "Hello World!" ]]; then
            echo "Unexpected output"
            exit 1
          fi


Best Practices for GitHub Actions


Action Design Principles:

1. Follow Single Responsibility Principle
- Design actions to do one thing well
- Compose complex workflows from simple actions
- Keep the interface simple and focused

2. Optimize for Performance
- Use JavaScript actions for speed when possible
- Keep Docker images small and efficient
- Implement appropriate caching strategies
- Use alpine-based images for containers

3. Handle Errors Gracefully
- Implement proper error handling and reporting
- Set appropriate exit codes
- Provide clear error messages
- Include debugging information when failures occur

4. Document Thoroughly
- Document all inputs, outputs, and environment variables
- Include usage examples in README.md
- Provide real-world examples
- Document limitations and requirements

5. Version Properly
- Use semantic versioning
- Maintain a CHANGELOG.md
- Test thoroughly before publishing new versions
- Consider the impact of breaking changes


Common Use Cases and Examples

1. Environment Setup Action

# action.yml
name: 'Setup Development Environment'
description: 'Sets up consistent development environment with dependencies'
inputs:
  node-version:
    description: 'Node.js version'
    required: false
    default: '16'
  python-version:
    description: 'Python version'
    required: false
    default: '3.10'
  install-deps:
    description: 'Install dependencies'
    required: false
    default: 'true'
runs:
  using: "composite"
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v3
      with:
        node-version: $
        
    - name: Setup Python
      uses: actions/setup-python@v4
      with:
        python-version: $
        
    - name: Install dependencies
      if: inputs.install-deps == 'true'
      shell: bash
      run: |
        npm ci
        pip install -r requirements.txt

2. Release Management Action

# action.yml
name: 'Semver Release'
description: 'Creates semantic versioned releases based on commit messages'
inputs:
  github-token:
    description: 'GitHub token for creating releases'
    required: true
  release-type:
    description: 'Release type (auto, patch, minor, major)'
    required: false
    default: 'auto'
outputs:
  new-version:
    description: 'The new version created'
  changelog:
    description: 'Generated changelog for the release'
runs:
  using: 'node16'
  main: 'dist/index.js'



Reference