Skip to content
 
 

Repository files navigation

GitHub Emulator

A lightweight, self-contained emulator of the GitHub API designed for integration testing. Run it locally or in CI to exercise client libraries, gh CLI workflows, and automation scripts without touching real GitHub.

Features

  • REST API -- compatible subset of GitHub REST API v3 (repositories, issues, pull requests, labels, milestones, comments, reviews, reactions, branches, commits, contents, releases, deploy keys, commit statuses, check runs, search, starring, notifications, gists, and more)
  • GraphQL API -- Strawberry-based implementation of common GitHub GraphQL queries and mutations
  • Git Smart HTTP -- clone, fetch, and push over HTTP/HTTPS against bare repositories
  • Git SSH Transport -- clone and push over SSH (port 2222 by default)
  • Web UI (/ui/) -- browse repositories, files, commits, issues, and pull requests in a GitHub-like interface
  • GitHub App Authentication -- register apps, generate RSA keypairs, JWT-based auth, installation access tokens (ghs_), and commit verification badges
  • Admin Panel (/admin/) -- manage users, tokens, organisations, repositories, GitHub Apps, and import repos from real GitHub
  • GitHub Import -- clone a single repo by URL or bulk-import all repos from a GitHub user/org via the admin panel
  • Webhooks -- event delivery with recorded payloads
  • gh CLI Compatible -- works as a GH_HOST target for the GitHub CLI
  • TLS via Caddy -- automatic HTTPS with a local CA for realistic gh/git testing
  • SQLite + aiosqlite -- zero-dependency storage; no external database server required

Quick Start

Docker / Podman Compose (recommended)

make up
# or:
docker compose up -d

To use Podman instead of Docker:

make CONTAINER_ENGINE=podman up

To use a different port (e.g. if 8000 is already in use):

make PORT=8001 up
# or for local dev:
uv run uvicorn app.main:app --reload --port 8001

The server will be available at:

Endpoint URL
REST API http://localhost:8000/api/v3
Web UI http://localhost:8000/ui/
Admin Panel http://localhost:8000/admin/
GraphQL http://localhost:8000/api/graphql

Default admin credentials: admin / admin. Fresh instances also seed a default admin personal access token, ghp_admin_default_token, so API clients can authenticate immediately.

Vagrant (two-VM setup with TLS)

For full gh CLI integration testing with TLS, a Vagrantfile provisions a server VM (Debian 12 + Docker, static IP 192.168.123.10) and a client VM for running tests:

# Add the hostname to /etc/hosts
echo "192.168.123.10  ghemu.local" | sudo tee -a /etc/hosts

# Boot both VMs, sync code, build, and start
make vm-deploy

# The server is now reachable at https://ghemu.local

Development Setup

# Create a virtual environment and install dependencies
uv venv
uv pip install -e ".[dev]"

# Run the test suite
uv run pytest tests/ -v

# Start the server locally (without Docker)
uv run uvicorn app.main:app --reload

Configuration

All settings are driven by environment variables with the GITHUB_EMULATOR_ prefix:

Variable Default Description
GITHUB_EMULATOR_BASE_URL http://localhost:8000 Base URL used in API response URLs
GITHUB_EMULATOR_DATA_DIR ./data Directory for bare git repos and the SQLite DB
GITHUB_EMULATOR_DATABASE_URL sqlite+aiosqlite:///{DATA_DIR}/github_emulator.db SQLAlchemy database URL
GITHUB_EMULATOR_SECRET_KEY change-me-in-production Secret for JWT/session signing
GITHUB_EMULATOR_SEED_DATA true Seed default admin user and PAT at startup
GITHUB_EMULATOR_ADMIN_USERNAME admin Admin user created on first startup
GITHUB_EMULATOR_ADMIN_PASSWORD admin Admin user password
GITHUB_EMULATOR_DEFAULT_ADMIN_TOKEN ghp_admin_default_token Default admin PAT seeded at startup
GITHUB_EMULATOR_HOST 0.0.0.0 Server bind address
GITHUB_EMULATOR_PORT 8000 Server listen port
GITHUB_EMULATOR_HOSTNAME ghemu.local Hostname for Caddy TLS certificate
GITHUB_EMULATOR_APP_JWT_PERMISSIVE true Skip JWT signature verification for GitHub App auth (accept any well-formed JWT)
GITHUB_EMULATOR_SSH_ENABLED true Enable/disable the SSH transport
GITHUB_EMULATOR_SSH_PORT 2222 SSH server listen port

Database Migrations (Alembic)

The project uses Alembic with async SQLAlchemy for schema migrations.

# Generate a new migration after changing models
uv run alembic revision --autogenerate -m "describe the change"

# Apply all pending migrations
uv run alembic upgrade head

# Downgrade one revision
uv run alembic downgrade -1

API Usage Examples

Create a personal access token

Fresh instances seed a default admin token:

curl -s http://localhost:8000/api/v3/user \
  -H "Authorization: token ghp_admin_default_token" \
  | python3 -m json.tool

Override it with GITHUB_EMULATOR_DEFAULT_ADMIN_TOKEN when starting the server.

To create an additional token:

curl -s -X POST http://localhost:8000/admin/tokens \
  -H "Content-Type: application/json" \
  -d '{"login":"admin","name":"my-token","scopes":["repo","user"]}' \
  | python3 -m json.tool

Create a repository

TOKEN="<token-from-above>"

curl -s -X POST http://localhost:8000/user/repos \
  -H "Authorization: token $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"my-repo","description":"Test repo"}' \
  | python3 -m json.tool

Clone and push

git clone http://localhost:8000/admin/my-repo.git /tmp/my-repo
cd /tmp/my-repo
echo "# Hello" > README.md
git add README.md && git commit -m "initial commit"
git push http://admin:$TOKEN@localhost:8000/admin/my-repo.git main

Create an issue

curl -s -X POST http://localhost:8000/repos/admin/my-repo/issues \
  -H "Authorization: token $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title":"Bug report","body":"Something is broken"}' \
  | python3 -m json.tool

Use with gh CLI

The emulator uses Caddy to generate a self-signed TLS certificate. By default gh and git will reject it. You need to tell both tools to skip certificate verification:

# Skip TLS verification for gh
export GH_HOST=ghemu.local
export GH_INSECURE=1

# Skip TLS verification for git
git config --global http.sslVerify false

# Authenticate
gh auth login --hostname ghemu.local --with-token <<< "$TOKEN"

# Use normally
gh repo list
gh issue create --repo admin/my-repo --title "Test" --body "Hello"

Note: If you prefer not to disable TLS verification globally, you can extract Caddy's root CA certificate from the container and add it to your system trust store instead. See the Caddy documentation for details.

GitHub App Authentication

Register a GitHub App, install it, and use installation tokens for API calls:

# 1. Register a new GitHub App (returns private key PEM)
curl -s -X POST http://localhost:8000/admin/api/apps \
  -H "Content-Type: application/json" \
  -d '{"name": "my-app", "owner": "admin"}' \
  | python3 -m json.tool

# 2. Install the app on an org
curl -s -X POST http://localhost:8000/admin/api/apps/1/installations \
  -H "Content-Type: application/json" \
  -d '{"owner": "admin"}' \
  | python3 -m json.tool

# 3. Generate a JWT and exchange it for an installation token
#    (use PyJWT or similar to sign with the private key from step 1)
JWT="<your-jwt-here>"
curl -s -X POST http://localhost:8000/api/v3/app/installations/1/access_tokens \
  -H "Authorization: Bearer $JWT" \
  | python3 -m json.tool

# 4. Use the installation token for API calls
INST_TOKEN="ghs_..."
curl -s http://localhost:8000/api/v3/user \
  -H "Authorization: token $INST_TOKEN" \
  | python3 -m json.tool

Commits created with an installation token return "verified": true in the verification payload, matching real GitHub's behavior for App-signed commits.

By default, JWT signature verification is skipped (permissive mode). Set GITHUB_EMULATOR_APP_JWT_PERMISSIVE=false for strict RS256 validation.

View GitHub Actions jobs in the Web UI

The repository Actions UI is available at:

http://localhost:8000/ui/<owner>/<repo>/actions

The Docker Compose stack includes an actions-runner service. Bootstrap its token and default repository, then start the runner:

make up
make actions-runner-env
docker compose up -d actions-runner

By default the runner watches admin/test-repo. Override it with:

RUNNER_REPO=admin/my-repo make actions-runner-env
docker compose up -d actions-runner

The project roadmap prefers real actions/runner compatibility for maximum fidelity, but the bundled Python runner is kept as the deterministic fallback. It executes local shell run: steps that the emulator stores in the job payload and uploads the captured logs.

An opt-in compose profile builds the upstream actions/runner binary for the compatibility spike:

make up
make actions-runner-env
make actions-real-runner
# equivalent:
docker compose --profile real-runner up --build actions-real-runner

This real-runner path is the intended compatibility target. The current emulator still needs protocol validation for real runner registration, session message polling, timeline updates, log upload, and completion before it can replace the Python fallback.

Desktop Playwright validation can be run against the compose-served UI after a workflow run exists:

python -m pip install playwright
python -m playwright install chromium
make actions-ui-smoke

Makefile Targets

Docker (local)

Target Description
build Build the Docker image
up Build and start the container
down Stop and remove containers and volumes
restart Rebuild and restart from scratch
logs Tail container logs
test Run pytest locally
smoke End-to-end smoke test against the running server
actions-runner-env Create .env values for the compose Actions runner
actions-real-runner Start the opt-in real actions/runner compose profile
actions-ui-smoke Run desktop Playwright smoke test against Actions UI
clean Remove containers, images, and build artifacts

Vagrant

Target Description
vm-up Boot the server and client VMs
vm-deploy Sync, build, and start containers in the server VM
vm-sync Rsync the codebase into the server VM
vm-build Build the container image inside the server VM
vm-start Start containers inside the server VM
vm-stop Stop containers inside the server VM
vm-logs Tail container logs inside the server VM
vm-destroy Destroy all VMs
vm-ssh SSH into the server VM
vm-client-ssh SSH into the client VM
vm-test Run gh CLI integration tests from the client VM
vm-git-test Run git CLI integration tests from the client VM
vm-gh Quick gh repo list from the client VM

Project Structure

app/
  api/            # REST API route handlers
  admin/          # Admin panel (Jinja2 templates, static assets, routes)
  git/            # Git Smart HTTP and SSH transport
  graphql/        # Strawberry GraphQL schema, queries, mutations, types
  middleware/     # FastAPI middleware (auth, rate limiting, ETag, error handling)
  models/         # SQLAlchemy ORM models
  schemas/        # Pydantic request/response schemas
  services/       # Business-logic layer (import, webhooks, search, etc.)
  web/            # Web UI (Jinja2 templates with Primer CSS)
  config.py       # Settings (env-driven via pydantic-settings)
  database.py     # Async engine, session factory, Base
  main.py         # Application entrypoint
alembic/          # Database migration scripts
tests/            # Pytest test suite
scripts/          # Integration test scripts for gh/git CLI
Dockerfile
docker-compose.yml
Caddyfile
supervisord.conf  # Runs Caddy + Uvicorn inside the container
Vagrantfile       # Two-VM dev environment (server + client)
Makefile
pyproject.toml

Important Note

This project is intended for integration testing only. It implements enough of the GitHub API surface to exercise client libraries, CI tooling, and automation scripts in isolated environments. It is not a production-grade GitHub replacement and should never be exposed to untrusted networks.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages