.gitbay/wiki/API.org

v1.38.0
gitbay/.gitbay/wiki/API.org rendered · source · history · blame · raw

221 lines · 9207 bytes

gitbay API and webhooks

The JSON API

One endpoint fronts the entire command registry — everything the SSH control plane can do, current and future, with identical semantics. Disabled by default; the instance must set:

[api]
enabled = true

Tokens

Tokens are minted wherever the registry is reached: over SSH, on the API, anywhere. token create makes a read token unless --scope full is given; a read token runs only commands marked read-only. A full-scope token can mint another, but a token or SSH key with a --ttl cannot run any command that creates a credential — token create, keys add, repo deploy-key add, repo runner add, web login, admin invite, admin user create, email verify, admin email verify — since what it made would outlive it. Give a token the narrowest scope and shortest TTL that does its job, and revoke it when the job is over.

gitbay auth token create --name ci [--scope read|full] [--ttl 30d]
gitbay auth token list
gitbay auth token revoke ci [--created]

Tokens and keys record the token they were created through. token revoke prints what the token created, at any depth; with --created it revokes those too, and their SSH connections close. Without it they stay and the link is dropped.

The token (prefix gb_, shown exactly once) is presented as Authorization: Bearer gb_.... Only a hash is stored server-side. Scope read permits list/show/log/diff-style commands and refuses anything that modifies state. --ttl takes Go durations or a day suffix (30d); expired, revoked, and unknown tokens all answer the same 401.

POST /api/v1/cmd

Request body:

{"argv": ["issue", "create", "you/project", "--title", "from CI"],
 "stdin": "optional body for --file - style input"}

argv is real argv — no shell, no quoting rules. The response is the command's own JSON envelope with exit_code added, plus stderr when the command wrote diagnostics:

{"protocol_version": 1, "exit_code": 0, "data": {"number": 7}}

HTTP status maps the exit code: 0→200, 2→400, 3→404, 4→403, else 500. Commands that emit raw text rather than an envelope (help, mr diff) come wrapped as {"output": "..."}. Git transport commands are refused by name; the token commands are not — a full-scope token can mint, list and revoke tokens the same way it can run anything else.

curl -s -H "Authorization: Bearer $TOKEN" \
     -d '{"argv":["whoami"]}' https://gitbay.org/api/v1/cmd

printf '{"argv":["issue","create","you/project","--title","t","--file","-"],"stdin":"body\n"}' |
curl -s -H "Authorization: Bearer $TOKEN" -d @- https://gitbay.org/api/v1/cmd

GET /api/v1/read

The conditional half: the same registry over GET, admitting only commands the registry marks read-only, so a GET structurally cannot mutate. Responses carry an ETag; If-None-Match answers 304.

curl -s -H "Authorization: Bearer $TOKEN" \
     "https://gitbay.org/api/v1/read?argv=repo&argv=show&argv=you/project"

The dashboard read

dashboard returns the account aggregate — pinned repositories, open merge requests, assigned issues, recent builds — in one call:

curl -s -H "Authorization: Bearer $TOKEN" \
     "https://gitbay.org/api/v1/read?argv=dashboard"

For an instance admin the response also carries {"server": {"commit": "<sha>"}}, the build the daemon is running, so the deployed commit is readable without the journal. The key is absent for everyone else: the exact build a host runs narrows down which known issues apply to it.

Cursor pagination

issue list, mr list, repo list, and feed accept --limit <n> (1–200) and --cursor <c>. With either flag present the data envelope becomes {"items": [...], "next": "..."}; next is an opaque cursor for the following page, absent on the last one. Without the flags the response stays the complete bare array; a bare feed returns the newest 50 events.

Commit statuses (CI reporting)

CI reports results through the same command surface (over SSH or the JSON API with a full-scope token; reporting requires write access):

gitbay status set <owner/name> <sha> --context ext/build --state pending
gitbay status set <owner/name> <sha> --context ext/build --state success --url https://ci.example/run/1
gitbay status list <owner/name> <sha> --json    # {"combined": "...", "statuses": [...]}

One row per (commit, context): re-reporting updates in place. States: pending, success, failure, error; the combined state is the worst of them. Statuses appear on commit pages, MR pages, and mr show, each with updated_at; a ci/<job> status also carries duration, read from the build behind it, once that build has finished. With repo settings require-checks <repo> on, every status on the MR head must be green, and a head something was going to report on must carry some: a .gitbay/ci.yml with a job a push runs, or a repository that has recorded a status before, which is what reporting from outside through status set looks like. A repository where nothing has ever reported merges. Each report also emits a status event to webhooks.

Contexts starting with ci/ are the instance's own: its builds queue, reuse, skip and finish them, and status set refuses them with exit 4, so a writer cannot mark ci/test green on a head the build has not passed. Report under another prefix, such as ext/.

repo settings require-contexts <repo> ext/deploy ci/test names statuses the checks gate waits for whether or not they have reported: one that has not is pending, and mr show lists it as ext/deploy=missing. Naming any context turns require-checks on; with no contexts the command clears the list and leaves require-checks as it was. require-checks off keeps the list, which waits for nothing until the gate is on again. repo settings show prints both.

Webhooks

Per-repository outbound POSTs for repository events. Managed by repo admins:

printf %s "$SECRET" | gitbay webhook add <url> --secret - [--events push,issue.created]  # default *
gitbay webhook list
gitbay webhook deliveries [--limit 50]     # status, attempts, last error
gitbay webhook redeliver <delivery-id>     # requeue, including dead letters
gitbay webhook remove <id>

The signing secret is read from stdin with --secret -; a value on the command line is refused, since argv shows in process listings and shell history. Over the JSON API it goes in the request's stdin field.

Events

Every event this forge records, and so every name --events may take. The server holds the same list as control.EventKinds, and a test fails if the code emits something not listed or lists something it never emits — so this is the whole set, not a sample. webhook add refuses a name that is not one of them, since a subscription to a typo would silently never fire.

  • repository: push (ref, old, new, forced, deleted), repo.archived, repo.unarchived, repo.imported (from)
  • issues: issue.created, issue.edited, issue.commented, issue.closed, issue.open, issue.labeled (labels), issue.assigned (assignees), issue.milestoned (milestone)
  • merge requests: mr.created, mr.edited, mr.commented, mr.reviewed (verdict), mr.draft (draft), mr.retargeted (from, to), mr.labeled (labels), mr.milestoned (milestone), mr.merged (number, sha), mr.closed
  • releases: release.created (tag), release.deleted (tag)
  • CI: status, build.success, build.failure, build.cancelled

Every payload carries number where it names an issue or merge request.

There is deliberately no repo.deleted. Both events.repo_id and webhooks.repo_id cascade from repos, so recording one would delete it — and every webhook that could have received it — in the same statement. A repository's deletion is visible in the audit log.

Delivery

Each event POSTs one JSON body:

{"event": "push",
 "repo": "you/project",
 "actor": "alice",
 "created_at": "2026-08-24T01:00:00.000Z",
 "data": {"ref": "refs/heads/main", "old": "...", "new": "...",
          "forced": false, "deleted": false}}

Headers: X-Gitbay-Event, X-Gitbay-Delivery (id), and — when the hook has a secret — X-Gitbay-Signature-256: sha256=<hex hmac> over the raw body. Verify before trusting:

import hmac, hashlib
expected = "sha256=" + hmac.new(secret, raw_body, hashlib.sha256).hexdigest()
ok = hmac.compare_digest(expected, request.headers["X-Gitbay-Signature-256"])

A 2xx within 10 seconds is success. Anything else retries with exponential backoff (30s base, doubling) and dead-letters after five attempts; webhook deliveries shows the trail and redeliver revives a dead letter. Redirects are never followed, and targets resolving to loopback, private, shared (100.64.0.0/10), link-local or multicast addresses are refused both at registration and again at connect time, unless the instance sets [webhooks] allow_local.