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
  • Admin Panel (/admin/) -- manage users, tokens, organisations, repositories, 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 Compose (recommended)

make up
# or:
docker compose up -d

The server will be available at:

EndpointURL
REST APIhttp://localhost:8000/api/v3
Web UIhttp://localhost:8000/ui/
Admin Panelhttp://localhost:8000/admin/
GraphQLhttp://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/hostsecho"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:

VariableDefaultDescription
GITHUB_EMULATOR_BASE_URLhttp://localhost:8000Base URL used in API response URLs
GITHUB_EMULATOR_DATA_DIR./dataDirectory for bare git repos and the SQLite DB
GITHUB_EMULATOR_DATABASE_URLsqlite+aiosqlite:///{DATA_DIR}/github_emulator.dbSQLAlchemy database URL
GITHUB_EMULATOR_SECRET_KEYchange-me-in-productionSecret for JWT/session signing
GITHUB_EMULATOR_SEED_DATAtrueSeed default admin user and PAT at startup
GITHUB_EMULATOR_ADMIN_USERNAMEadminAdmin user created on first startup
GITHUB_EMULATOR_ADMIN_PASSWORDadminAdmin user password
GITHUB_EMULATOR_DEFAULT_ADMIN_TOKENghp_admin_default_tokenDefault admin PAT seeded at startup
GITHUB_EMULATOR_HOSTNAMEghemu.localHostname for Caddy TLS certificate
GITHUB_EMULATOR_SSH_ENABLEDtrueEnable/disable the SSH transport
GITHUB_EMULATOR_SSH_PORT2222SSH 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 ghexport 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.

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)

TargetDescription
buildBuild the Docker image
upBuild and start the container
downStop and remove containers and volumes
restartRebuild and restart from scratch
logsTail container logs
testRun pytest locally
smokeEnd-to-end smoke test against the running server
actions-runner-envCreate .env values for the compose Actions runner
actions-real-runnerStart the opt-in real actions/runner compose profile
actions-ui-smokeRun desktop Playwright smoke test against Actions UI
cleanRemove containers, images, and build artifacts

Vagrant

TargetDescription
vm-upBoot the server and client VMs
vm-deploySync, build, and start containers in the server VM
vm-syncRsync the codebase into the server VM
vm-buildBuild the container image inside the server VM
vm-startStart containers inside the server VM
vm-stopStop containers inside the server VM
vm-logsTail container logs inside the server VM
vm-destroyDestroy all VMs
vm-sshSSH into the server VM
vm-client-sshSSH into the client VM
vm-testRun gh CLI integration tests from the client VM
vm-git-testRun git CLI integration tests from the client VM
vm-ghQuick 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 (219 tests)
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

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages