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.
- 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
ghCLI Compatible -- works as aGH_HOSTtarget 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
make up
# or:
docker compose up -dThe 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.
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# 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 --reloadAll 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_HOSTNAME |
ghemu.local |
Hostname for Caddy TLS certificate |
GITHUB_EMULATOR_SSH_ENABLED |
true |
Enable/disable the SSH transport |
GITHUB_EMULATOR_SSH_PORT |
2222 |
SSH server listen port |
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 -1Fresh instances seed a default admin token:
curl -s http://localhost:8000/api/v3/user \
-H "Authorization: token ghp_admin_default_token" \
| python3 -m json.toolOverride 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.toolTOKEN="<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.toolgit 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 maincurl -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.toolThe 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.
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-runnerBy default the runner watches admin/test-repo. Override it with:
RUNNER_REPO=admin/my-repo make actions-runner-env
docker compose up -d actions-runnerThe 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-runnerThis 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| 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 |
| 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 |
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
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.