REST API for the GitForge self-hosted Git platform with CI/CD capabilities.
http://localhost:42780
- Swagger UI: http://localhost:42780/swagger-ui
- OpenAPI Spec: http://localhost:42780/api-docs/openapi.json
Protected /api routes pass through a shared JWT authentication boundary and
receive an AuthenticatedUser context. Resource authorization is still
enforced by the individual route. The one documented bootstrap exception is
POST /api/runners, which is intentionally unauthenticated so a new runner
can register before receiving credentials; runner listing and detail routes
remain protected.
Repository routes expose only repositories owned by the authenticated user,
with admin and maintainer role overrides. Artifact routes require the
shared authenticated context and resolve artifact access through the owning
job, pipeline run, and repository; unauthorized artifacts are returned as
not-found to avoid leaking private resource existence.
Administrators may change a user's persisted role with
PATCH /api/users/{id}/role. Supported roles are admin, maintainer,
developer, and read_only; non-administrators receive 403, invalid roles
receive 400, and the last administrator cannot be demoted. Protected
requests resolve the current persisted role, so demotion takes effect
immediately even when an older JWT is presented.
GitForge uses JWT tokens for API authentication. Include the token in the Authorization header:
Authorization: Bearer <your-token>
The public authentication endpoints are mounted at the server root, not under
the protected /api prefix:
POST /auth/login
GET /auth/status
POST /auth/refresh
POST /auth/logout
POST /auth/login accepts username and password and returns token,
token_type, and expires_in. GET /auth/status returns an
authenticated boolean and, for a valid bearer token, the user identity and
role.
Login also returns a long-lived refresh credential (stored server-side as a
SHA-256 digest only, never the plaintext). POST /auth/refresh accepts
{"refresh_token": "..."} and returns a new bearer token plus a rotated
refresh credential; presenting an already-rotated credential is rejected
(401), so a captured response cannot be replayed. POST /auth/logout takes
the same body and revokes the credential server-side, idempotently —
revoking an already-revoked credential still returns 200.
User registration is not exposed by the GitForge API; users must be
provisioned through the supported local CLI bootstrap path on a fresh
database. Run gitforge admin --bootstrap --username <name> --email <email> --confirm; the password is read from the terminal and the command refuses to
run after an administrator exists.
Authenticated endpoints are mounted under the /api prefix; the
examples below show the full paths.
GET /health
Returns server health status including database connectivity.
Response:
{
"status": "healthy",
"timestamp": "2024-01-01T00:00:00Z",
"database": "connected"
}GET /api/repos
POST /api/repos
GET /api/repos/{id}
DELETE /api/repos/{id}
GET /api/repos/{owner}/{repo}/policy
PATCH /api/repos/{owner}/{repo}/policy
GET /api/repos/{owner}/{repo}/commits/{sha}/status
A repository can require its pipelines to succeed before refs/heads/*
advances to a commit, and can reject non-fast-forward branch updates
(#240). Enforcement happens in the git server on both Smart HTTP and SSH:
a push that violates the policy is declined with a standard receive-pack
report and git shows ! [remote rejected] <ref> (reason). Branch
deletions, tag pushes, and other ref namespaces are never gated.
GET /api/repos/{owner}/{repo}/policy
PATCH /api/repos/{owner}/{repo}/policy
GET /api/repos/{owner}/{repo}/commits/{sha}/status
Update Policy Request (omitted fields keep their current values):
{
"required_checks": ["ci", "gates-and-release"],
"deny_non_fast_forward": true
}required_checksnames pipelines by name; a commit satisfies the gate when the latest run of each named pipeline on that commit issucceeded. An empty list disables the gate.deny_non_fast_forwardsets git's nativereceive.denyNonFastForwardson the repository, sogit-receive-packrejects rewrites of branch history on both transports.
Setting the policy requires the repository owner, an administrator, or a maintainer. Reads are limited to the same callers and 404 for everyone else so repository existence is not leaked.
Commit status reports the same evaluation the push path performs:
{
"commit": "abcdef1234…",
"required_checks": [
{"check": "ci", "status": "succeeded"},
{"check": "gates-and-release", "status": null}
],
"satisfied": false
}A null status means the pipeline has never run on the commit; a commit
with no runs never satisfies a configured gate.
PATCH /api/users/{id}/role
Request:
{"role": "maintainer"}Create Repository Request:
{
"name": "my-repo",
"visibility": "private"
}Response:
{
"id": "uuid",
"name": "my-repo",
"owner_id": "user-uuid",
"visibility": "private",
"git_path": "/git/my-repo",
"created_at": "2024-01-01T00:00:00Z",
"updated_at": "2024-01-01T00:00:00Z"
}GET /api/pipelines
POST /api/pipelines
GET /api/pipelines/{id}
DELETE /api/pipelines/{id}
POST /api/pipelines/{id}/runs
POST /api/pipelines registers a pipeline definition for a repository —
the request carries the repository in owner/name form and the pipeline
definition document (the committed .gitforge.yml contents) as config.
The document must parse with a single active version; the caller must own
the repository (admin/maintainer override applies).
POST /api/pipelines/{id}/runs triggers a run of the stored definition,
resolving the optional ref (default: repository HEAD) to a commit and
rejecting ambiguous or hostile refs.
DELETE /api/pipelines/{id} deletes a runless pipeline outright and
deactivates one with run history (the history stays queryable).
Register Pipeline Request:
{
"repo": "owner/name",
"config": "name: ci\nversion: \"1.0\"\n..."
}Trigger Run Request:
{
"ref": "main"
}GET /api/pipeline-runs
GET /api/pipeline-runs/{id}
GET /api/pipeline-runs/{id}/jobs
POST /api/pipeline-runs/{id}/rerun
POST /api/pipeline-runs/{id}/rerun triggers a new run pinned to the
original run's own commit hash — the reason to rerun is usually doubt
about the original verdict, and refs move. A commit that no longer
resolves is refused up front (400 unknown_revision) instead of minting
a run doomed at clone time. Delegates to the CI orchestrator the same way
a push trigger does (202 with the new run id, riding its durable
duplicate protection); 404 if the run does not exist, 503 when the CI
trigger is not configured, 502 when the orchestrator rejects the trigger.
The scheduler service on port 42781 exposes a narrow read-only receipt for
the durable request created by POST /pipelines/trigger:
GET /pipeline-trigger-requests/{request_id}
Send the same Authorization: Bearer <GITFORGE_TRIGGER_TOKEN> credential
(or x-gitforge-trigger-token) used for enqueue. This route returns only the
trigger lifecycle, correlated pipeline run ID, and pipeline status; it does
not expose the stored error text or general pipeline listings. A missing
request returns 404, a missing database returns 503, and a database read
failure returns 500.
status is pending, processing, completed, or failed. Until a run is
materialized, pipeline_run_id and pipeline_status are null. Once present,
pipeline_status is pending, running, succeeded, failed, or
cancelled. CI clients should continue polling while the trigger is
pending/processing or the pipeline is pending/running, and stop only at
a terminal pipeline status.
Pipeline Run Response:
{
"id": "uuid",
"pipeline_id": "uuid",
"status": "running",
"commit_hash": "abc123",
"triggered_by": "push",
"started_at": "2024-01-01T00:00:00Z",
"finished_at": null
}POST /api/jobs
GET /api/jobs/{id}
GET /api/jobs/{id}/logs
POST /api/jobs/{id}/cancel
Job submission requires a pipeline run owned by the authenticated user (or an
admin/maintainer role), bounded commands, and a stable idempotency_key.
Retries return the original job ID; reusing a key for different parameters is
rejected. Cancellation is persisted in the shared database so the separate CI
scheduler and runner observe it safely.
Submit Job Request:
{
"pipeline_run_id": "run-uuid",
"name": "manual-check",
"commands": ["cargo test"],
"working_dir": null,
"idempotency_key": "attempt-uuid"
}Job Response:
{
"id": "uuid",
"name": "build",
"status": "running",
"runner_id": "runner-uuid",
"started_at": "2024-01-01T00:00:00Z",
"finished_at": null
}Job Logs Response:
{
"job_id": "uuid",
"logs": "Building...\nRunning tests...\nAll tests passed!"
}GET /queue/status *(CI orchestrator, port 42781 — not the API gateway)*
The authenticated scheduler/runner boundary exposes read-only admission
telemetry. durable_pending is the number of pending or queued database
rows, in_memory_queued is the scheduler's current queue depth,
assigned_jobs is the number of active assignments, and online_runners is
the in-memory online-runner count. durable_pending is null for an
in-memory scheduler without a database.
{
"contract_version": "scheduler.queue.v1",
"durable_pending": 156,
"in_memory_queued": 156,
"assigned_jobs": 1,
"online_runners": 1
}POST /api/review-runs
GET /api/review-runs/{id}
GET /api/review-runs/{id}/findings
Review run submission persists an AI code-review run for a repository head
(the review provider itself executes in a separate worker; runs stay pending
until dispatch). Submission requires the repository to be owned by the
authenticated user (or an admin/maintainer role) and validates bounded
base_sha, head_sha, idempotency_key, and a positive attempt. The
repository is identified by repo_id, or by owner plus name (exactly one
form must be supplied); unknown repositories return 404 before any run is
created.
Idempotency follows the review contract: the first submission creates the run
(201), a retry with the same key against the same head SHA returns the
existing run (200), and reuse of the same key against a different head SHA
is a 409 conflict that never silently re-points the run.
Findings are read back in deterministic order (path, then line with NULL lines
first, then fingerprint) and bounded pagination (limit 1-500, default 100;
offset >= 0). Runs the caller is not authorized to see are reported as
404, never 403, to avoid leaking private review activity.
Submit Review Run Request:
{
"repo_id": "repo-uuid",
"base_sha": "base-commit-sha",
"head_sha": "head-commit-sha",
"idempotency_key": "client-generated-key",
"attempt": 1
}Submission Response (201 created / 200 idempotent retry):
{
"status": "created",
"run": {
"id": "run-uuid",
"repo_id": "repo-uuid",
"base_sha": "base-commit-sha",
"head_sha": "head-commit-sha",
"idempotency_key": "client-generated-key",
"status": "pending",
"attempt": 1,
"receipt_id": null,
"created_at": "2026-09-05T00:00:00+00:00",
"updated_at": "2026-09-05T00:00:00+00:00"
}
}Findings Response:
{
"run_id": "run-uuid",
"run_status": "succeeded",
"total": 4,
"limit": 100,
"offset": 0,
"findings": [
{
"id": "finding-uuid",
"run_id": "run-uuid",
"source": "static-analysis",
"fingerprint": "16-hex-digest",
"path": "src/main.rs",
"line": 42,
"severity": "warning",
"category": "logic",
"title": "Off-by-one",
"message": "Loop excludes the final element.",
"evidence": null,
"confidence": "high",
"position_status": "line",
"disposition": "pending",
"created_at": "2026-09-05T00:00:00+00:00",
"updated_at": "2026-09-05T00:00:00+00:00"
}
]
}GET /dashboard
Public aggregate dashboard data (no authentication required).
GET /api/runners
POST /api/runners
GET /api/runners/{id}
DELETE /api/runners/{id}
POST /api/runners is a public bootstrap endpoint used before a runner
has credentials. DELETE /api/runners/{id} retires an idle runner while
preserving its record for audit; it requires an administrator or
maintainer and refuses runners with active jobs (409 runner_busy).
Register Runner Request:
{
"name": "runner-1",
"type": "docker",
"capacity": 4
}Response:
{
"id": "uuid",
"name": "runner-1",
"type": "docker",
"status": "online",
"capacity": 4,
"last_heartbeat": "2024-01-01T00:00:00Z"
}GET /api/artifacts
GET /api/artifacts/{id}
GET /api/artifacts/{id}/content
DELETE /api/artifacts/{id}
GET /api/jobs/{job_id}/artifacts
Artifact Response:
{
"id": "uuid",
"job_id": "job-uuid",
"name": "test-results.zip",
"path": "/artifacts/test-results.zip",
"checksum": "abc123",
"size_bytes": 1024,
"created_at": "2024-01-01T00:00:00Z"
}GET /api/ssh-keys
POST /api/ssh-keys
DELETE /api/ssh-keys/{id}
The per-user public key registry backing Git-over-SSH authentication.
Fingerprints are computed server-side and globally unique; registering a
duplicate key — even under a different comment or account — returns 409
duplicate_key.
Register Key Request:
{
"name": "laptop",
"public_key": "ssh-ed25519 AAAA... user@host"
}POST /api/webhook/trigger/{pipeline_id}
Authenticated webhook entry point. When the API gateway is deployed with
a CI trigger client it delegates execution to the CI orchestrator and
returns 202; otherwise it queues the pipeline's first runnable job
directly into the durable queue (200). Redeliveries for the same commit
are idempotent — one webhook key maps to exactly one job, and a replay's
run row is recorded as cancelled. If the pipeline's stored definition
is unusable the endpoint returns 422, and CI delegation failures return
502 without persisting a phantom job.
GET /metrics
Prometheus-format metrics including:
http_requests_total- HTTP request countsgitforge_job_duration_seconds- Job execution durationgitforge_runners_online- Number of online runnersgitforge_pipeline_runs_total- Pipeline run counts by status- And more...
| Code | Description |
|---|---|
| 200 | Success |
| 201 | Created |
| 204 | No Content (deleted) |
| 400 | Bad Request |
| 401 | Unauthorized |
| 404 | Not Found |
| 500 | Internal Server Error |
curl -X POST http://localhost:42780/api/repos \
-H "Content-Type: application/json" \
-d '{"name": "my-project", "visibility": "public"}'curl http://localhost:42780/api/pipeline-runs/your-run-idcurl -X POST http://localhost:42780/api/runners \
-H "Content-Type: application/json" \
-d '{"name": "docker-runner-1", "type": "docker", "capacity": 4}'No rate limits currently enforced in MVP.