Local Development and Testing for Custom GitHub Actions¶
This guide shows you how to test your custom GitHub Actions locally without pushing to GitHub, using the local development simulator.
Why Use the Local Simulator?¶
When developing custom GitHub Actions, you typically need to:
Write your action code
Push to GitHub
Trigger the workflow
Wait for results
Repeat if there are bugs
The local simulator lets you skip steps 2-4 and test instantly on your machine!
Local Development Simulator¶
The local_simulator module simulates the GitHub Actions environment locally, allowing you to test your custom actions without pushing to GitHub.
Basic Usage¶
Here’s a simple example of testing a custom action that greets users:
from github_action_toolkit import simulate_github_action, SimulatorConfig
import github_action_toolkit as gat
# Your custom action code
def my_greeting_action():
name = gat.get_user_input("name") or "World"
greeting = gat.get_user_input("greeting") or "Hello"
message = f"{greeting}, {name}!"
gat.info(message)
gat.set_output("message", message)
gat.append_job_summary(f"# Greeting\n\nSaid: {message}")
# Test it locally
config = SimulatorConfig(
repository="myorg/myrepo",
inputs={"name": "Alice", "greeting": "Hi"}
)
with simulate_github_action(config) as sim:
my_greeting_action()
# Check results
print(sim.outputs) # {"message": "Hi, Alice!"}
print(sim.summary) # Job summary content
Configuration Options¶
The SimulatorConfig class allows you to customize the simulated environment to match your action’s needs:
config = SimulatorConfig(
repository="owner/repo", # Repository name
ref="refs/heads/main", # Git ref
sha="abc123...", # Commit SHA
actor="test-user", # Actor username
workflow="test-workflow", # Workflow name
action="test-action", # Action name
run_id="1", # Run ID
run_number="1", # Run number
job="test-job", # Job name
event_name="push", # Event type (push, pull_request, etc.)
inputs={"key": "value"}, # Action inputs
env_vars={"CUSTOM": "value"}, # Additional environment variables
)
Accessing Results¶
The simulator captures all outputs, summaries, and state from your action:
with simulate_github_action(config) as sim:
# Run your action code
gat.set_output("result", "success")
gat.append_job_summary("# Build Summary\n\nAll tests passed!")
gat.save_state("build_time", "2025-10-19")
# Access results within the context
print(sim.outputs) # {"result": "success"}
print(sim.summary) # Job summary markdown
print(sim.state) # {"build_time": "2025-10-19"}
print(sim.env_vars) # Environment variables set
print(sim.paths) # Paths added to PATH
Real-World Example: Docker Build Action¶
Here’s a realistic example of testing a custom action that builds and pushes Docker images:
from github_action_toolkit import simulate_github_action, SimulatorConfig
import github_action_toolkit as gat
def docker_build_action():
"""
Custom action that builds and pushes a Docker image.
Inputs:
- dockerfile: Path to Dockerfile (default: Dockerfile)
- image_name: Name of the Docker image
- image_tag: Tag for the Docker image (default: latest)
- push: Whether to push to registry (default: true)
"""
# Get inputs
dockerfile = gat.get_user_input("dockerfile") or "Dockerfile"
image_name = gat.get_user_input("image_name")
image_tag = gat.get_user_input("image_tag") or "latest"
should_push = gat.get_user_input_as("push", bool, default_value=True)
if not image_name:
gat.error("image_name input is required!")
return
# Build image
full_image = f"{image_name}:{image_tag}"
gat.start_group("Building Docker Image")
gat.info(f"Building {full_image} from {dockerfile}")
# Simulate docker build (in real action, you'd run docker commands)
gat.info("Step 1/5 : FROM python:3.11-slim")
gat.info("Step 2/5 : WORKDIR /app")
gat.info("Step 3/5 : COPY . .")
gat.info("Step 4/5 : RUN pip install -r requirements.txt")
gat.info("Step 5/5 : CMD ['python', 'app.py']")
gat.info("✓ Successfully built image")
gat.end_group()
# Push image (if enabled)
if should_push:
gat.start_group("Pushing Docker Image")
gat.info(f"Pushing {full_image} to registry")
gat.info("✓ Successfully pushed image")
gat.end_group()
else:
gat.info("Skipping push (push=false)")
# Set outputs
gat.set_output("image", full_image)
gat.set_output("image_name", image_name)
gat.set_output("image_tag", image_tag)
# Create job summary
gat.append_job_summary("# Docker Build Summary")
gat.append_job_summary(f"- **Image:** `{full_image}`")
gat.append_job_summary(f"- **Dockerfile:** `{dockerfile}`")
gat.append_job_summary(f"- **Pushed:** {'✅ Yes' if should_push else '❌ No'}")
gat.notice(f"Docker image {full_image} built successfully!")
# Test the action locally
config = SimulatorConfig(
repository="mycompany/myapp",
ref="refs/heads/main",
actor="developer",
inputs={
"dockerfile": "Dockerfile",
"image_name": "mycompany/myapp",
"image_tag": "v1.2.3",
"push": "true"
}
)
print("=" * 60)
print("Testing Docker Build Action Locally")
print("=" * 60)
with simulate_github_action(config) as sim:
docker_build_action()
# Verify outputs
print("\n" + "=" * 60)
print("Action Results:")
print("=" * 60)
print(f"Image: {sim.outputs.get('image')}")
print(f"Image Name: {sim.outputs.get('image_name')}")
print(f"Image Tag: {sim.outputs.get('image_tag')}")
print("\nJob Summary:")
print(sim.summary)
print("\n✓ Action tested successfully!")
Real-World Example: Release Notes Generator¶
Here’s another example of a custom action that generates release notes:
from github_action_toolkit import simulate_github_action, SimulatorConfig
import github_action_toolkit as gat
def release_notes_action():
"""
Custom action that generates release notes from commit messages.
Inputs:
- version: Release version (e.g., v1.2.3)
- previous_version: Previous version for comparison
- include_authors: Include commit authors (default: true)
"""
version = gat.get_user_input("version")
previous_version = gat.get_user_input("previous_version")
include_authors = gat.get_user_input_as("include_authors", bool, default_value=True)
if not version:
gat.error("version input is required!")
return
gat.info(f"Generating release notes for {version}")
# In a real action, you'd fetch commits from git/GitHub API
# Here we simulate with sample data
commits = [
{"message": "Add new authentication feature", "author": "alice"},
{"message": "Fix memory leak in processor", "author": "bob"},
{"message": "Update documentation", "author": "charlie"},
{"message": "Improve error handling", "author": "alice"},
]
# Generate release notes
notes = [f"# Release {version}\n"]
if previous_version:
notes.append(f"Changes since {previous_version}:\n")
notes.append("## Changes\n")
for commit in commits:
line = f"- {commit['message']}"
if include_authors:
line += f" (@{commit['author']})"
notes.append(line)
release_notes = "\n".join(notes)
# Set outputs
gat.set_output("release_notes", release_notes)
gat.set_output("version", version)
gat.set_output("commit_count", str(len(commits)))
# Add to job summary
gat.append_job_summary(release_notes)
gat.notice(f"Generated release notes for {version} with {len(commits)} commits")
# Test the action
config = SimulatorConfig(
repository="mycompany/myproject",
inputs={
"version": "v1.2.3",
"previous_version": "v1.2.2",
"include_authors": "true"
}
)
with simulate_github_action(config) as sim:
release_notes_action()
print("Release Notes Generated:")
print(sim.outputs.get("release_notes"))
print(f"\nVersion: {sim.outputs.get('version')}")
print(f"Commits: {sim.outputs.get('commit_count')}")
Testing Different Scenarios¶
You can easily test edge cases and different scenarios:
# Test with missing required inputs
config_missing = SimulatorConfig(
repository="myorg/myrepo",
inputs={} # No inputs provided
)
with simulate_github_action(config_missing) as sim:
my_action()
# Verify error handling works
# Test with different event types
config_pr = SimulatorConfig(
repository="myorg/myrepo",
event_name="pull_request",
inputs={"name": "PR Author"}
)
with simulate_github_action(config_pr) as sim:
my_action()
# Verify PR-specific behavior
# Test with custom environment variables
config_env = SimulatorConfig(
repository="myorg/myrepo",
inputs={"name": "World"},
env_vars={
"CUSTOM_API_URL": "https://api.example.com",
"DEBUG": "true"
}
)
with simulate_github_action(config_env) as sim:
my_action()
# Verify environment variable handling
Tips for Testing Custom Actions¶
Test Early and Often: Run your action locally before pushing to verify basic functionality
Test Edge Cases: Use the simulator to test error conditions, missing inputs, and edge cases
Verify Outputs: Always check that outputs, summaries, and state are set correctly
Test Different Events: If your action behaves differently for different GitHub events (push, pull_request, etc.), test each one
Use Realistic Data: Configure the simulator with realistic repository names, refs, and inputs that match your actual use case
Debugging Your Action¶
The simulator preserves all GitHub Actions logging, so you can see exactly what your action outputs:
with simulate_github_action(config) as sim:
# Your action code with debug logging
gat.debug("Starting action")
gat.info("Processing input")
gat.warning("This might take a while")
my_action()
gat.debug("Action complete")
# All logs are displayed in console as they would be in GitHub Actions
Next Steps¶
Once you’ve tested your action locally and verified it works:
Commit your action code
Push to GitHub
Create or update your action’s
action.ymlfileTest in a real workflow
Publish your action to the GitHub Marketplace (optional)
The local simulator helps you catch bugs early and iterate faster, making custom action development much more efficient!
Testing Actions¶
Local Testing with Simulator¶
from github_action_toolkit import simulate_github_action, SimulatorConfig
def test_greeting_action():
"""Test the greeting action locally."""
config = SimulatorConfig(
inputs={'name': 'Test User'},
repository='testorg/testrepo'
)
with simulate_github_action(config) as sim:
# Import and run your action
from action import main
main()
# Verify outputs
assert sim.outputs['greeting'] == 'Hello, Test User!'
# Verify summary was created
assert 'Greeting' in sim.summary
Integration Testing¶
import pytest
from github_action_toolkit import simulate_github_action, SimulatorConfig
@pytest.fixture
def github_env():
"""Provide simulated GitHub environment."""
config = SimulatorConfig(repository='test/repo')
with simulate_github_action(config) as sim:
yield sim
def test_action_with_inputs(github_env):
"""Test action with specific inputs."""
# Your action code runs here
from action import process_inputs
result = process_inputs()
assert result is not None
assert github_env.outputs['status'] == 'success'