Skip to content

Latest commit

 

History

History
591 lines (487 loc) · 15.6 KB

API.md

File metadata and controls

591 lines (487 loc) · 15.6 KB

GitForge API Documentation

REST API for the GitForge self-hosted Git platform with CI/CD capabilities.

Base URL

http://localhost:42780

Interactive Documentation

Authentication

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.

Endpoints

Authenticated endpoints are mounted under the /api prefix; the examples below show the full paths.

Health Check

GET /health

Returns server health status including database connectivity.

Response:

{
  "status": "healthy",
  "timestamp": "2024-01-01T00:00:00Z",
  "database": "connected"
}

Repositories

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

Ref-update policy

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_checks names pipelines by name; a commit satisfies the gate when the latest run of each named pipeline on that commit is succeeded. An empty list disables the gate.
  • deny_non_fast_forward sets git's native receive.denyNonFastForwards on the repository, so git-receive-pack rejects 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.

User roles

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"
}

Pipelines

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"
}

Pipeline Runs

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.

CI trigger receipt (scheduler service)

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
}

Jobs

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!"
}

Scheduler queue status

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
}

Code Review Runs

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"
    }
  ]
}

Dashboard

GET /dashboard

Public aggregate dashboard data (no authentication required).

Runners

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"
}

Artifacts

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"
}

SSH Keys

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"
}

Webhook Trigger

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.

Metrics

GET /metrics

Prometheus-format metrics including:

  • http_requests_total - HTTP request counts
  • gitforge_job_duration_seconds - Job execution duration
  • gitforge_runners_online - Number of online runners
  • gitforge_pipeline_runs_total - Pipeline run counts by status
  • And more...

Status Codes

Code Description
200 Success
201 Created
204 No Content (deleted)
400 Bad Request
401 Unauthorized
404 Not Found
500 Internal Server Error

Example Usage

Create a Repository

curl -X POST http://localhost:42780/api/repos \
  -H "Content-Type: application/json" \
  -d '{"name": "my-project", "visibility": "public"}'

Check Pipeline Status

curl http://localhost:42780/api/pipeline-runs/your-run-id

Register a Runner

curl -X POST http://localhost:42780/api/runners \
  -H "Content-Type: application/json" \
  -d '{"name": "docker-runner-1", "type": "docker", "capacity": 4}'

Rate Limits

No rate limits currently enforced in MVP.