Skip to content

GitHub Emulator Specification — @deployment-dashboard/github-emulator

Status: Draft · Date: 2026-05-31

Standalone service that emulates the GitHub REST API surface the fetcher consumes, backed by an in-memory store. Enables end-to-end demo and integration-test scenarios without touching api.github.com.

Sources of truth

Source Owns
docs/FETCHER_SPECIFICATION.md §5 GitHub REST endpoint contracts, field mapping, status mapping, parent-derivation, version/artifact, backfill, rate-limit — authoritative spec for the surface this service emulates.
demo/data/github/ Curated demo fixture files (workflow YAML, deployment seeds).
docs/diagrams/github-emulation.md Visual reference for demo-mode topology and seed→backfill→poll sequence.

1. Stack

Aspect Value
Runtime Node.js / TypeScript
Framework NestJS 10 + Express
Default port 3100 (override via PORT env var)
State In-memory only — resets on restart
Location demo/github-emulator/
Auth None — internal-only service; no gateway route

2. Solution layout

demo/github-emulator/
  src/
    main.ts                       bootstrap (port, config)
    app.module.ts                 NestJS root module
    github-rest.controller.ts     emulated GitHub REST surface at root:
                                  GET /repos/{owner}/{repo}/...
                                  GET /rate_limit
    control.controller.ts         control surface: GET|POST /_github/*
    github-store.ts               in-memory store singleton (§3)
    github-store.service.ts       seed/clear/emit orchestration + store summary
    github-fixture-loader.ts      load curated demo set from demo/data/github/
    github-random-generator.ts    random repos/deployments/statuses/workflow-YAML factory
    rate-limit-headers.ts         X-RateLimit-* + Link header helper
  test/
    github-rest.controller.spec.ts
    control.controller.spec.ts
    github-store.spec.ts
    github-random-generator.spec.ts
    github-fixture-loader.spec.ts
  Dockerfile                      multi-stage: build → node:lts-alpine runtime
  package.json
  tsconfig.json

3. In-memory store

Store is process-local and independent of the dashboard API's data — it survives an API data-reset, enabling the fetcher to re-backfill from it post-reset.

Per-repo shape:

Key Contents
deployments List of deployment objects (id, sha, ref, environment, payload, creator.login, created_at)
statuses (per deployment) Status lifecycle (id, state, target_url, creator.login, created_at)
runs (keyed by run_id) Workflow run metadata (id, name, path, head_sha)
workflow YAML (keyed by path + ref) Raw YAML string
environments List of { name } objects
artifacts (per run_id) Artifact metadata + content (id, name, expired, version string)

4. Configuration

Env var Default Purpose
PORT 3100 HTTP listen port
GITHUB_SIM_RATE_LIMIT 5000 Simulated hourly request quota; reflected in X-RateLimit-* headers + GET /rate_limit response. Drives the fetcher's F16 budget logic.
SEED_ON_STARTUP true When true, seeds the demo set from demo/data/github/ on startup so the fetcher has data immediately (§9).
SCENARIOS_DIR ../../demo/data Base path for fixture resolution.
EMIT_INTERVAL_MS 8000 Interval between appended deployments while periodic emission is enabled (§6.3).

5. Emulated GitHub REST surface — /

Paths are served at the service root (no prefix). The fetcher points GITHUB_BASE_URL=http://github-emulator:3100; its root-relative paths resolve directly.

Method Path Response shape FETCHER_SPEC ref
GET /repos/{owner}/{repo}/deployments?environment=&per_page=&page= Array of deployment objects (id, sha, ref, environment, payload, creator.login, created_at); newest-first; filtered by environment when present; paginated (Link rel=next when more pages remain). §5.1, §5.8.2
GET /repos/{owner}/{repo}/deployments/{id}/statuses Array of status objects (id, state, target_url, creator.login, created_at); newest-first. §5.1
GET /repos/{owner}/{repo}/deployments/{id}/reviews Array of review objects (state, user.login, submitted_at); empty array when no reviews. Used by ResolveFailureStatusAsync to detect rejected. §5.3
GET /repos/{owner}/{repo}/actions/runs/{run_id} Run metadata (id, name, path, head_sha, conclusion). conclusion is null while in-progress; "cancelled" "success"
GET /repos/{owner}/{repo}/contents/{path}?ref={sha} { content: "<base64 workflow YAML>", encoding: "base64" }. §5.6.2
GET /repos/{owner}/{repo}/actions/workflows?per_page=100 { total_count, workflows: [{ id, name, path, state }] }. §5.8.2
GET /repos/{owner}/{repo}/environments { total_count, environments: [{ name }] }. §5.8.2
GET /repos/{owner}/{repo}/actions/runs/{run_id}/artifacts { total_count, artifacts: [{ id, name, expired }] }. §5.7.2
GET /repos/{owner}/{repo}/actions/artifacts/{artifact_id}/zip application/zip containing a single text file whose content is the version string. §5.7.2
GET /rate_limit { resources: { core: { limit, remaining, used, reset } } }. §5.9

Cross-cutting.

  • Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, and X-RateLimit-Reset from a simulated per-process budget that decrements per request and rolls over hourly (budget = GITHUB_SIM_RATE_LIMIT). Drives the fetcher's F16 budget path.
  • List endpoints carry a Link: <...>; rel="next" header while more pages remain. Drives the fetcher's pagination loop.
  • Unknown repo / deployment / run / path → GitHub-shaped 404 (NOT RFC 9457 — this surface emulates GitHub, not the dashboard API).
  • Auth headers (Authorization: Bearer, Accept, X-GitHub-Api-Version) are accepted but NOT validated (internal demo service).

6. Control surface — /_github/

Intended for the demo driver proxy, test harnesses, and manual operation. No auth required.

6.1 Status

Method · Path Response
GET /_github/status GithubStoreStatus

GET /_github/status is also consumed by the demo driver's GET /demo/health aggregator (DEMO_DRIVER_SPEC §4.10) as the emulator liveness probe — a 2xx response marks the emulator as "up".

GithubStoreStatus:

{
  "dataset":      "demo",
  "repos":        2,
  "deployments":  18,
  "statuses":     54,
  "workflows":    4,
  "environments": 3,
  "emitting":     false,
  "seeded_at":    "2026-05-31T10:00:00Z"
}

6.2 Seed + clear

Method · Path Request Response Effect
POST /_github/seed { dataset: "demo"\|"random", count?: number, reset?: boolean } GithubStoreStatus reset:true clears the store first. "demo" loads the curated fixture (§7); "random" generates synthetic data (§8). count = number of promotion chains for random (ignored for demo); repos always stay at 10.
POST /_github/clear GithubStoreStatus Empties the store.

6.3 Emission control

Method · Path Request Response Effect
GET /_github/emit { emitting: boolean } Read-only status.
POST /_github/emit { enabled?: boolean } — omit to toggle { emitting: boolean } When enabled, every EMIT_INTERVAL_MS appends a new deployment + in-progress→success/failure status lifecycle (created_at=now) so the fetcher's next poll picks it up.

7. Curated demo set (demo/data/github/)

The fixture files loaded when dataset: "demo" is requested. They MUST cover:

  • ≥2 repos whose workflow name: fields map to demo service identities coherent with demo/data/events.json (same service and environment names).
  • At least one workflow with a dev→staging→prod deployment-job needs chain, so parent_deployments resolves a real chain per FETCHER_SPEC §5.6 (F10).
  • At least one service whose version comes from a version.txt artifact to exercise artifact: resolution (FETCHER_SPEC §5.7, F15).
  • A spread of success, failure, and in-progress status lifecycles across deployments.
  • All 5 new statuses from issue #268 (one fixture deployment each):
Contract status Repo Deployment Mechanism
pending payments-api id 4840005, run 4840, env prod single pending status; run 4830 success is the effective slot
queued search-indexer id 1420005, run 1420, env prod single queued status
waiting billing-webhook id 826001, run 826, env prod single waiting status
cancelled ledger-projector id 1831001, run 1831, env prod failure status + run_conclusion: "cancelled"ResolveFailureStatusAsync reads run.conclusion
rejected catalog-edge id 5161001, run 5161, env prod failure status + reviews: [{state:"rejected"}]ResolveFailureStatusAsync reads /deployments/{id}/reviews

8. Random set + periodic emit

Repos. Always 10 synthetic repos (unaffected by count).

Promotion chains. count = number of promotion chains generated (default 20). Each chain walks a 5-stage ladder — dev → staging → qa → preprod → prod — with per-stage attrition: every chain reaches dev; each subsequent stage is reached with decreasing probability (≈ 85 / 75 / 70 / 55 %), so per-stage deployment counts decrease realistically. One chain produces 1–5 deployments depending on attrition.

Timestamps. Each chain starts at a random point in the trailing 14-day window (≥ 1 h before now). Subsequent stages in the chain advance the timestamp forward. This ensures multi-day analytics windows are populated and the day-truncated window (which excludes today) contains data.

DORA signals. ≈ 15 % of terminal deployments are set to failure, producing non-zero change-failure-rate and MTTR incidents. Actors, durations, and times of day are varied across chains.

parent_deployments chains. The generated workflow YAML includes a needs chain spanning all five stages, so parent_deployments resolves across the full ladder (F10 exercised on random data).

Periodic emit. Appends new deployments (created_at = now) over time; the fetcher's incremental poll picks them up on the next interval.


9. Startup defaults

State Default
Store Seeded from demo/data/github/ demo set when SEED_ON_STARTUP=true (default)
Emission Disabled
Rate-limit budget GITHUB_SIM_RATE_LIMIT (default 5 000), rolling hourly

When SEED_ON_STARTUP=true the emulator is immediately ready for the fetcher without a manual POST /_github/seed call — the fetcher starts backfilling as soon as it connects.


10. Testing

Layer File Scope
Unit github-store.spec.ts Store CRUD (seed / clear / emit); per-request rate-limit decrement + hourly rollover; store independent of API data
Unit github-rest.controller.spec.ts Each emulated endpoint returns the correct shape; X-RateLimit-* headers on every response; Link: rel="next" when more pages; unknown repo/deployment/run/path returns GitHub-shaped 404; GET .../reviews returns review objects (empty array when none); GET .../runs/:id emits conclusion field
Unit control.controller.spec.ts seed / clear / emit / status endpoints correct; seed + clear mutate the store; emit toggle works
Unit github-random-generator.spec.ts Generated workflow YAML includes a 5-stage needs chain (dev→staging→qa→preprod→prod); per-stage attrition produces fewer deployments at later stages; timestamps fall within the trailing 14-day window; ≈15 % terminal failures present; all required fields present; count controls chain count, repos always 10
Unit github-fixture-loader.spec.ts Curated demo fixture loads without error; covers F10 (dev→staging→prod needs chain) and F15 (artifact-sourced version); loaded dataset matches GithubStoreStatus counters; all 5 new-status fixtures present (pending/queued/waiting/cancelled/rejected) with correct run conclusions and review records
Integration fetcher-emulation.e2e.spec.ts Start emulator + seed demo set (POST /_github/seed {dataset:"demo"}); run the real fetcher-host against http://github-emulator:3100; real Dashboard.Api + Postgres; assert the dashboard shows the expected services, a non-trivial parent_deployments chain, and an artifact-sourced version. Realizes FETCHER_SPEC §7.2.

11. Running

cd demo/github-emulator
npm install          # first time only
npm run start:dev    # ts-node, hot-reload

Override port or rate-limit quota:

$env:PORT                 = '3100'
$env:GITHUB_SIM_RATE_LIMIT = '1000'
npm run start:dev

12. Deployment

Aspect Spec
Image Multi-stage Dockerfile in demo/github-emulator/. Stage 1: node:lts-alpine builds TypeScript. Stage 2: node:lts-alpine runs the compiled output.
Compose service github-emulator in the demo compose profile. Internal-only — no gateway route.
Network Fetcher reaches the emulator directly on the internal compose network at http://github-emulator:3100. The demo driver proxy reaches it at http://github-emulator:3100 (or GITHUB_EMULATOR_URL).
Port Container listens on PORT (default 3100). Not exposed to the host by default.

13. Out of scope

  • ETag/304 conditional-request emulation (the fetcher tolerates its absence — FETCHER_SPEC §5.5).
  • Dynamic editing of individual seeded fixtures beyond /_github/seed, /_github/clear, and /_github/emit.
  • Emulating GitHub APIs the fetcher does not consume (e.g. issues, pull requests, checks).
  • Authentication or access control (internal-only service).