# OpenAPI 3.1 specification for the Vetrix CICDv2 (Vetrix Workflows) API.
# Published as part of the gitvetrix.com API reference.
openapi: 3.1.0
info:
title: Vetrix CICDv2 API
description: |
OpenAPI 3.1 specification for the Vetrix CICDv2 (Vetrix Workflows)
surface. Covers four endpoint families:
1. **Internal control-plane** -- controller <-> host-agent RPCs
under `/internal/host-pool/*` and `/api/v1/internal/host-pool/*`,
transport-authenticated by mTLS (CICDv2 private CA, see
`internal/cicdv2/mtls`).
2. **Public admin API** -- operator-only surface under
`/api/v1/admin/cicd-defaults`, `/api/v1/admin/hosts`,
`/api/v1/admin/runners`, `/api/v1/admin/audit:cicd`,
`/api/v1/admin/usage:cicd`. Bearer JWT auth with admin scopes
(`acl.AdminRunners`, `acl.AdminAuditRead`, `acl.AdminSystem`).
3. **Public tenant API** -- pipeline / job CRUD scoped to a
repository, plus the per-org CICDv2 *policy* overlay. Bearer
JWT auth with tenant scopes (`acl.PermCIRead`,
`acl.PermCIWrite`).
4. **Job-side API** -- endpoints called by the in-DinD runner with
a `VETRIX_JOB_TOKEN` (typ=job-token JWT minted at dispatch by
`auth.IssueJobTokenForJob`).
## Sunset: the `cicd_v2` dispatch controller and its feature flag
The `cicd_v2` dispatch controller was sunset before it ever shipped
a dispatch loop. The feature flag that selected between the v1
worker path and the v2 controller path, and every backend surface
that read it, were removed. Three operations that earlier revisions
of this document specified as live contract have been **deleted
from this specification**, because the routes are no longer
registered and the server answers them with 404:
- `GET /api/v1/orgs/{owner}/cicd` and
`PATCH /api/v1/orgs/{owner}/cicd` -- the per-tenant `cicd_v2`
flag overlay (`operationId` `getTenantCICDFlag` /
`patchTenantCICDFlag`).
- `POST /api/v1/runner-controller/jobs:next` -- the v1 worker
long-poll deprecation stub, whose only behaviour was gated on
the flag (`operationId` `runnerControllerJobsNext`).
They are recorded in this note rather than retained as `deprecated`
operations on purpose. This file is meant to drive client
generation, request validation and contract testing; an operation
left in `paths:` -- carrying `deprecated: true` or not -- still
produces callable client methods and "Try it out" affordances for
routes that cannot succeed. `deprecated` means "available but
discouraged", which is not what these are.
`GET` / `PATCH /api/v1/admin/cicd-defaults` survives the sunset and
is still live, but it is a **different** surface: it edits the
instance-wide CI/CD *policy* defaults persisted as `cicd.defaults.*`
rows, and its PATCH body is per-field (`max_job_timeout` /
`cpu_millis` / `memory_bytes` / `pids_max`). It has never accepted
the `{"value": ...}` flag payload earlier revisions of this document
attributed to it; that body is rejected with a 400, because the
handler decodes with unknown fields disallowed. The per-org and
per-repo `.../cicd-policy` endpoints belong to the same policy
family.
No endpoint in this document selects a dispatcher. There is exactly
one dispatch path -- the v1 queue-only path -- and nothing can be
switched onto or off it.
None of the above touches the CICDv2 *pipeline schema* (the
`version: 2` pipeline configuration format) or the CICDv2 host-pool
control plane under `/internal/host-pool/*` and
`/api/v1/admin/hosts`. Both are live and specified here as such.
version: v00.12.22
contact:
name: Vetrix
url: https://www.gitvetrix.com
license:
name: Proprietary
servers:
- url: https://api.gitvetrix.com/api/v1
description: Production API (read-only for external testing per repo CLAUDE.md)
- url: https://api.gitvetrix.test/api/v1
description: Local development stack
tags:
- name: internal-control-plane
description: Controller <-> host-agent RPCs. mTLS only.
- name: public-admin
description: Operator-only admin surface. Bearer JWT + admin scope.
- name: public-tenant
description: Tenant-scoped CICDv2 surface (pipelines, jobs, artifacts, policy overlay).
- name: job-side
description: Endpoints called by the in-DinD job runner with VETRIX_JOB_TOKEN.
security:
- bearerAuth: []
paths:
# =============================================================
# Internal control-plane (mTLS only)
# =============================================================
/internal/host-pool/register:
post:
tags: [internal-control-plane]
summary: Register a host-agent with the controller
description: |
Host-agent -> controller registration. Authenticates via mTLS
(CICDv2 private CA pinned at the listener) plus an in-band
single-use `host_attach_token` operator invite. On success the
controller persists a `runner_hosts` row and returns the
canonical `host_id` plus initial `state`.
operationId: hostPoolRegister
security:
- mtlsAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolRegisterRequest'
responses:
'200':
description: Registration accepted
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolRegisterResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/internal/host-pool/heartbeat:
post:
tags: [internal-control-plane]
summary: Host-agent liveness + capacity heartbeat
description: |
Periodic host-agent heartbeat (default 30s). Reports current
CPU + memory utilisation; the controller stamps
`last_heartbeat_at` and updates `in_use_cpu` / `in_use_mem_bytes`
for scheduler placement decisions. mTLS leaf CN must match the
registered FQDN for the body's `host_id`.
operationId: hostPoolHeartbeat
security:
- mtlsAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolHeartbeatRequest'
responses:
'204':
description: Heartbeat accepted (no response body)
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/internal/host-pool/jobs/{job_id}/dispatch:
post:
tags: [internal-control-plane]
summary: Dispatch a job spec to a host-agent
description: |
Controller-side endpoint a host-agent calls after being targeted
for a job. Validates the JobSpec shape, authenticates the host
identity from the mTLS peer cert subject (CN), and emits a
`job.dispatched` audit row scoped to the job id.
operationId: hostPoolDispatch
security:
- mtlsAuth: []
parameters:
- $ref: '#/components/parameters/JobIDPath'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolJobSpec'
responses:
'200':
description: Dispatch accepted
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolDispatchAck'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/internal/host-pool/jobs/{job_id}/complete:
post:
tags: [internal-control-plane]
summary: Mark a job as complete
description: |
Host-agent reports a terminal job state. Updates
`pipeline_jobs.state` (+ `exit_code` when supplied) and emits a
`job.completed` audit row. Accepts wire spellings
`succeeded` / `success` / `failed` / `cancelled`.
operationId: hostPoolCompleteJob
security:
- mtlsAuth: []
parameters:
- $ref: '#/components/parameters/JobIDPath'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolCompleteJobRequest'
responses:
'204':
description: Job marked complete (no response body)
'400': { $ref: '#/components/responses/BadRequest' }
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/internal/host-pool/jobs/{job_id}/logs:
post:
tags: [internal-control-plane]
summary: Stream host-agent job logs to the controller
description: |
Chunked log ingest. The body is a newline-delimited stream of
log frames (one line per frame). Each line is persisted via the
ChunkStore and tee'd to the live in-memory LogBroker so
existing SSE consumers see new bytes immediately.
Supports `Last-Event-ID` for resume: the header's decimal byte
offset tells the server which bytes the agent has already
delivered. Frames whose cumulative byte position is at or
below the offset are dropped server-side.
Caps: line size <= 4 MiB; total request body <= 256 MiB.
operationId: hostPoolLogsIngest
security:
- mtlsAuth: []
parameters:
- $ref: '#/components/parameters/JobIDPath'
- in: header
name: Last-Event-ID
required: false
schema:
type: string
description: Decimal byte offset to resume from.
requestBody:
required: true
content:
text/plain:
schema:
type: string
description: Newline-delimited UTF-8 log frames.
responses:
'200':
description: Ingest accepted; counts of accepted / dropped bytes returned.
content:
application/json:
schema:
$ref: '#/components/schemas/HostPoolLogsResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'413':
description: Request body exceeded `hostPoolLogsMaxBodyBytes` (256 MiB).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'500': { $ref: '#/components/responses/InternalError' }
# POST /api/v1/runner-controller/jobs:next (operationId
# runnerControllerJobsNext) was specified here. It was the v1 worker
# long-poll deprecation stub: 204 while the cluster-wide `cicd_v2`
# flag was off, 410 once it was flipped on. The flag is gone, so the
# 410 could never fire; the route was removed and is not registered on
# any current deployment. See the sunset note in `info.description`.
# Deliberately not retained as a `deprecated` operation -- see that
# note for why.
# =============================================================
# Public admin API (bearer JWT + admin scope)
# =============================================================
/api/v1/admin/cicd-defaults:
get:
tags: [public-admin]
summary: Read cluster-wide CICDv2 policy defaults
description: |
Returns the instance-wide CICDv2 *policy* defaults the policy
resolver consults when a tenant, org, repo or pipeline does not
override them: `max_job_timeout`, `cpu_millis`, `memory_bytes`
and `pids_max`. They are persisted as `cicd.defaults.*` rows in
`app_settings`; a key with no row yet reads back its registered
built-in default (`6h` / `2000` / `2147483648` / `1024`) rather
than a zero value.
One handler serves this path -- `AdminCICDDefaultsHandler` in
`internal/api/admin_cicd_defaults.go`. The response is always
the typed policy envelope below.
This is **not** a feature-flag surface. Earlier revisions of
this document described a second handler on this path returning
a `{scope, value, source}` flag envelope; that handler was
removed with the `cicd_v2` sunset (see the note in
`info.description`) and no deployment serves that shape here.
Authz: `acl.AdminSystem`, enforced inline by the route's
`requireAdminScope` check -- 401 without claims, 403 without the
scope. The route is mounted only where the app-settings store is
wired; where it is not, the path is absent from the router and
chi answers 404.
operationId: getCICDDefaults
security:
- bearerAuth: []
responses:
'200':
description: Typed cluster-wide policy-defaults envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/CICDDefaultsResponse'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
patch:
tags: [public-admin]
summary: Update cluster-wide CICDv2 policy defaults
description: |
PATCH the instance-wide CICDv2 policy defaults. The body is
**per-field**: any subset of `max_job_timeout`, `cpu_millis`,
`memory_bytes` and `pids_max`. An omitted field is a no-op; an
empty body (no recognised field supplied) is a 400.
There is exactly one accepted body shape. The decoder rejects
unknown fields, so a body carrying any other key -- including
the `{"value": ...}` flag payload earlier revisions of this
document listed on this path -- is a 400, not a flag write.
That flag surface no longer exists anywhere; see the sunset
note in `info.description`.
Validation runs against the bounded validators registered on
the `cicd.defaults.*` keys, whole-batch, before any write: an
out-of-bounds value is a 422 and nothing is persisted; an
unparseable value is a 400.
On success the handler emits one `admin.cicd.defaults.update`
audit row carrying `actor_user_id` and a `changed` sub-map of
`{old, new}` per key whose value actually moved (keys PATCHed
to their existing value are omitted from `changed`, but the row
is still emitted). Audit emission is best-effort and never
fails the request. The response body is the full re-read typed
envelope, not just the mutated fields.
Authz: `acl.AdminSystem`, enforced inline by the route's
`requireAdminScope` check -- 401 without claims, 403 without the
scope.
operationId: patchCICDDefaults
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CICDDefaultsPatchRequest'
responses:
'200':
description: Updated typed policy-defaults envelope (full re-read).
content:
application/json:
schema:
$ref: '#/components/schemas/CICDDefaultsResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'422': { $ref: '#/components/responses/UnprocessableEntity' }
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/admin/hosts:
get:
tags: [public-admin]
summary: List CICDv2 runner hosts
description: |
Operator-only paginated view of `runner_hosts`. Each row carries
capacity / in-use / state / heartbeat / derived `healthy` flag
for the host fleet. Auth: `acl.AdminRunners`.
operationId: listRunnerHosts
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/PageQuery'
- $ref: '#/components/parameters/PerPageQuery'
responses:
'200':
description: Paginated list of runner hosts.
content:
application/json:
schema:
$ref: '#/components/schemas/RunnerHostsListResponse'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/admin/hosts/{id}/drain:
post:
tags: [public-admin]
summary: Drain a runner host
description: |
Marks the runner host `draining` so the scheduler stops
dispatching new jobs to it while in-flight jobs complete.
Idempotent: a `409 host_already_drained` is returned when the
host is already in a terminal drain state.
Returns `202 Accepted` with a `Location` header pointing at the
GET progress endpoint. Auth: `acl.AdminRunners`.
operationId: drainRunnerHost
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/HostIDPath'
responses:
'202':
description: "Drain accepted; poll the progress URL until `drained: true`."
headers:
Location:
schema:
type: string
description: Relative URL of the drain-progress GET.
content:
application/json:
schema:
$ref: '#/components/schemas/HostDrainStatus'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: Host is already drained or in a non-drainable state.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503': { $ref: '#/components/responses/ServiceUnavailable' }
get:
tags: [public-admin]
summary: Poll runner-host drain progress
description: |
Read-only progress endpoint paired with POST .../drain. Reports
current `state`, `in_flight_jobs` remaining, and the boolean
`drained` flag (true once the host has no in-flight work).
operationId: getRunnerHostDrainProgress
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/HostIDPath'
responses:
'200':
description: Drain status.
content:
application/json:
schema:
$ref: '#/components/schemas/HostDrainStatus'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/admin/hosts/{id}/decommission:
post:
tags: [public-admin]
summary: Decommission a drained runner host
description: |
Permanently removes a runner host from the CICDv2 fleet.
Precondition: the host must already be drained
(`state == drained`). On success the `runner_hosts` row is
deleted atomically alongside teardown of the host's tenant
networks.
operationId: decommissionRunnerHost
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/HostIDPath'
responses:
'204':
description: Decommissioned.
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: Host not in `drained` state.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/admin/runners:
get:
tags: [public-admin]
summary: List v1 CI runners
description: |
Lists registered v1 CI runners. Carries v1 / CICDv2 coexistence
fields (`isolation_kind`, `ssh_capable`, `arch`, `os`,
`cpu_millis`, `mem_mib`) for capacity-aware scheduling.
Auth: `acl.AdminRunners`.
operationId: listAdminRunners
security:
- bearerAuth: []
responses:
'200':
description: Runner list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/AdminRunner'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
post:
tags: [public-admin]
summary: Register a new v1 CI runner
description: |
Creates a new runner record. Returns the raw token EXACTLY
ONCE in the response body (`token` / `raw_token` aliases) --
the operator must capture it; subsequent reads only see the
hashed value.
operationId: registerAdminRunner
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRunnerRequest'
responses:
'201':
description: Runner created.
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRunnerResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/admin/runners/{id}:
delete:
tags: [public-admin]
summary: Delete a v1 CI runner
description: Removes the runner row. The runner will fail its next heartbeat once the row is gone. Auth scope is `acl.AdminRunners`.
operationId: deleteAdminRunner
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/RunnerIDPath'
responses:
'204':
description: Runner deleted.
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/admin/runners/{id}/rotate-token:
post:
tags: [public-admin]
summary: Rotate a runner's authentication token
description: |
Returns a new raw token EXACTLY ONCE; the previous token hash
is invalidated atomically inside the UPDATE. The runner must
re-authenticate with the new value on its next heartbeat or
the controller will refuse it.
operationId: rotateAdminRunnerToken
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/RunnerIDPath'
responses:
'200':
description: New raw token (returned once).
content:
application/json:
schema:
$ref: '#/components/schemas/RotateRunnerTokenResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/admin/audit:cicd:
get:
tags: [public-admin]
summary: List CICDv2 audit events
description: |
Operator-only paginated, filterable view of the CICDv2 audit
trail (`cicd_audit_events`). Filter by `actor_id` / `scope_kind`
(`runner_host`, `runner_group`, `pipeline`, `job`, `secret`,
`policy`) / `scope_id` / `action`. Auth: `acl.AdminAuditRead`.
The colon `:cicd` is a literal path character (not a chi
wildcard); the route is mounted with the
`requireAdminSystemMiddleware` wrap inline so the leaf chi
registration carries the rate-scope marker.
operationId: listCICDAuditEvents
security:
- bearerAuth: []
parameters:
- in: query
name: actor_id
schema: { type: string, format: uuid }
- in: query
name: scope_kind
schema:
type: string
enum: [runner_host, runner_group, pipeline, job, secret, policy]
- in: query
name: scope_id
schema: { type: string, format: uuid }
- in: query
name: action
schema: { type: string }
- $ref: '#/components/parameters/PageQuery'
- $ref: '#/components/parameters/PerPageQuery'
responses:
'200':
description: Paginated audit events.
content:
application/json:
schema:
$ref: '#/components/schemas/CICDAuditEventsListResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/admin/usage:cicd:
get:
tags: [public-admin]
summary: Cross-tenant CICDv2 usage rollup
description: |
Aggregated CICDv2 usage by tenant and by hour. Source is the
`cicd_usage_summary` rollup read via the
`cicd.UsageSummarySource` seam. When the rollup table is
absent the seam returns an empty envelope rather than a 500.
Auth: `acl.AdminSystem` via `requireAdminSystemMiddleware`.
operationId: getCICDUsage
security:
- bearerAuth: []
parameters:
- in: query
name: from
schema: { type: string, format: date-time }
description: RFC3339 inclusive lower bound (default `to - 30d`).
- in: query
name: to
schema: { type: string, format: date-time }
description: RFC3339 exclusive upper bound (default now).
responses:
'200':
description: Per-tenant + grand-total usage envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/UsageCICDResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalError' }
# =============================================================
# Public tenant API
# =============================================================
# GET and PATCH /api/v1/orgs/{owner}/cicd (operationIds
# getTenantCICDFlag / patchTenantCICDFlag) were specified here as the
# per-tenant `cicd_v2` feature-flag overlay, backed by the
# `cicd_v2.tenant.<owner_uuid>` app-settings key. The overlay editor
# was removed with the `cicd_v2` sunset; neither route is registered
# on any current deployment and both answer 404. See the sunset note
# in `info.description`. Deliberately not retained as `deprecated`
# operations -- see that note for why.
#
# The similarly named `/api/v1/orgs/{owner}/cicd-policy` below is a
# DIFFERENT, live surface: the per-org numeric policy overlay.
/api/v1/orgs/{owner}/cicd-policy:
get:
tags: [public-tenant]
summary: Read per-org CICDv2 numeric policy overlay
description: |
Reads the per-owner numeric CICDv2 policy overlay (the four
cap fields `max_job_timeout` / `cpu_millis` / `memory_bytes` /
`pids_max`, mirroring the cluster-wide `cicd.defaults.*` keys
the admin surface installs). When no overlay row
exists the response carries empty / zero values with
`source: "default"`; when at least one row exists the
response carries every field (zero-valued for absent ones)
with `source: "explicit"`.
Path note: the `cicd-policy` leaf, rather than a bare `cicd`
leaf, is a leftover of routing history. The bare leaf was once
occupied by the boolean `cicd_v2` feature-flag overlay, and chi
cannot multiplex two handlers on the same (method, path) pair.
The flag overlay has since been removed (see the sunset note in
`info.description`), but this surface keeps the `cicd-policy`
path, which is also what its per-repo and per-user siblings
use. A bare `/api/v1/orgs/{owner}/cicd` is not routed and
answers 404.
Authz: instance admin holding `acl.AdminSystem` OR the
owning user themselves (mirrors the tenant overlay
posture; no `OrgAdmin` scope exists today).
operationId: getOrgCICDPolicy
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
responses:
'200':
description: Org-scoped CICDv2 policy envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/OrgCICDPolicyResponse'
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
patch:
tags: [public-tenant]
summary: Update per-org CICDv2 numeric policy overlay
description: |
Upserts one or more numeric policy fields for the named owner.
Each field is optional; an omitted field is a no-op. An empty
body returns 400. Unknown fields are rejected (the decoder
uses `DisallowUnknownFields`).
Validation pipeline (whole-batch, no DB writes until every
supplied field clears both gates):
1. Syntactic floor -- mirrors the
`cicd.defaults.*` lower bounds (`max_job_timeout` >= 1m,
`cpu_millis` >= 100, `memory_bytes` >= 134217728,
`pids_max` >= 16). Violations return 400.
2. App-cap ceiling -- reads the live cluster cap from
`admin.Settings` (with documented defaults as a
fallback) and rejects any value that exceeds the cap
with 422 + `error_code: org_policy_exceeds_app_cap`.
On success the handler emits a `cicd.policy.org.update`
audit row carrying `actor_user_id`, `org_id`, and a
`changed` sub-map of `{old, new}` per mutated key.
Authz: instance admin holding `acl.AdminSystem` OR the
owning user themselves.
operationId: patchOrgCICDPolicy
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OrgCICDPolicyPatchRequest'
responses:
'200':
description: Updated org-scoped CICDv2 policy envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/OrgCICDPolicyResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422':
description: |
Validation failure -- a supplied field value exceeds the
cluster-wide app cap. Body carries
`error_code: org_policy_exceeds_app_cap` alongside the
human-readable message.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelopeWithCode'
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/pipelines:
get:
tags: [public-tenant]
summary: List the caller's 50 most recent pipelines across all owned repos
description: Convenience cross-repo pipeline list scoped to repositories the caller owns. Bounded to the 50 most recent rows by created_at descending; pagination is not exposed today.
operationId: listAllPipelines
security:
- bearerAuth: []
responses:
'200':
description: Pipeline list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/GlobalPipeline'
'401': { $ref: '#/components/responses/Unauthorized' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines:
get:
tags: [public-tenant]
summary: List pipelines for a repository
description: Paginated pipeline list for the named repository. Uses the `page` / `per_page` pagination convention.
operationId: listRepoPipelines
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PageQuery'
- $ref: '#/components/parameters/PerPageQuery'
responses:
'200':
description: Repo pipelines list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Pipeline'
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
post:
tags: [public-tenant]
summary: Trigger a new pipeline
description: |
Schedules a new pipeline on the supplied ref. The handler
walks the documented config search path
(`.ci/pipeline.yml` -> `.vetrix/pipeline.yml` -> `vetrix-ci.yml`)
for the pipeline definition. Auth requires `acl.PermCIWrite`
on the repository.
operationId: triggerPipeline
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerPipelineRequest'
responses:
'201':
description: Pipeline scheduled.
content:
application/json:
schema:
$ref: '#/components/schemas/Pipeline'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'422': { $ref: '#/components/responses/UnprocessableEntity' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines/{id}:
get:
tags: [public-tenant]
summary: Get a pipeline with its jobs
description: Returns the pipeline plus its constituent pipeline_jobs. 404 when the pipeline or repo is not visible to the caller.
operationId: getPipeline
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PipelineIDPath'
responses:
'200':
description: Pipeline detail (with jobs).
content:
application/json:
schema:
$ref: '#/components/schemas/PipelineDetail'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines/{id}/cancel:
post:
tags: [public-tenant]
summary: Cancel a pipeline
description: 409 when the pipeline is already in a terminal state.
operationId: cancelPipeline
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PipelineIDPath'
responses:
'204':
description: Pipeline cancellation accepted.
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: Pipeline already in terminal state.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines/{id}/retry:
post:
tags: [public-tenant]
summary: Retry a failed or cancelled pipeline
description: Re-schedules the original commit SHA and ref through the discovered pipeline config. Only `failed` and `cancelled` source pipelines are retryable (409 otherwise). Auth requires `acl.PermCIWrite` on the repo.
operationId: retryPipeline
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PipelineIDPath'
responses:
'201':
description: New pipeline scheduled as a retry.
content:
application/json:
schema:
$ref: '#/components/schemas/Pipeline'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'409':
description: Pipeline is not in a retryable (failed/cancelled) state.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'422': { $ref: '#/components/responses/UnprocessableEntity' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines/{id}/jobs/{jid}/logs:
get:
tags: [public-tenant]
summary: Stream pipeline-job logs (SSE)
description: |
Server-Sent Events stream of historical + live log frames for
the named job. Mounted with the `noWriteTimeout` middleware so
the long-lived connection is not terminated by the global write
timeout. Supports `Last-Event-ID` for resume per RFC 6202.
operationId: streamPipelineJobLogs
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PipelineIDPath'
- $ref: '#/components/parameters/PipelineJobIDPath'
- in: header
name: Last-Event-ID
required: false
schema: { type: string }
responses:
'200':
description: SSE stream (`text/event-stream`).
content:
text/event-stream:
schema:
type: string
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
/api/v1/repos/{owner}/{repo}/commits/{sha}/statuses:
get:
tags: [public-tenant]
summary: List commit statuses for a commit SHA
description: Returns the commit_statuses rows associated with the supplied commit SHA in the repository. Used by the merge-request UI to render the per-context status pills.
operationId: listCommitStatuses
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- in: path
name: sha
required: true
schema: { type: string }
responses:
'200':
description: Commit statuses.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CommitStatus'
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts:
get:
tags: [public-tenant]
summary: List artifacts for a pipeline
description: Returns all pipeline_artifacts rows produced by jobs in the pipeline. Auth is the repo-visibility gate; no extra scope.
operationId: listPipelineArtifacts
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PipelineIDPath'
responses:
'200':
description: Artifact list.
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/Artifact'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
'500': { $ref: '#/components/responses/InternalError' }
/api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts/{name}:
get:
tags: [public-tenant]
summary: Get a specific artifact by name
description: Returns one artifact by its `(pipeline_id, name)` tuple. 404 when no matching artifact exists.
operationId: getPipelineArtifact
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/OwnerPath'
- $ref: '#/components/parameters/RepoPath'
- $ref: '#/components/parameters/PipelineIDPath'
- in: path
name: name
required: true
schema: { type: string }
responses:
'200':
description: Artifact metadata.
content:
application/json:
schema:
$ref: '#/components/schemas/Artifact'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'404': { $ref: '#/components/responses/NotFound' }
# Additional public-tenant CICDv2 paths under
# /api/v1/repos/{owner}/{repo}/cicd* (secrets, variables, triggers,
# runner-group selectors, etc.) are not yet part of this spec; those
# paths will be added when that surface lands.
# =============================================================
# Job-side API (VETRIX_JOB_TOKEN bearer)
# =============================================================
/api/v1/jobs/{id}/artifacts:
post:
tags: [job-side]
summary: Push a job artifact
description: |
The in-DinD runner pushes artifact metadata back to the control
plane. Auth uses `VETRIX_JOB_TOKEN` (typ=job-token JWT minted
by `auth.IssueJobTokenForJob`). The token's `job_id` claim
MUST equal the path `{id}`; the token's `pipeline_id` /
`repo_id` claims MUST match the persisted job row. Requires the
`registry:write` permission on the token.
v1 (legacy) payloads that omit the v2 fields (`retention_days`,
`paths`) are accepted unchanged -- v1 callers do not regress.
operationId: pushJobArtifact
security:
- jobTokenAuth: []
parameters:
- $ref: '#/components/parameters/JobIDPath2'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/JobArtifactPushRequest'
responses:
'201':
description: Artifact recorded.
content:
application/json:
schema:
$ref: '#/components/schemas/Artifact'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
/api/v1/jobs/{id}/cache:url:
get:
tags: [job-side]
summary: Presigned GET URL for restoring the job cache
description: |
Returns a presigned S3/MinIO GET URL the runner uses to restore
its build cache at job start. The object key is derived ONLY
from the token claims via `internal/cicd/cachekey`; the path
`{id}` is asserted equal to the token's `job_id`. Auth uses
`VETRIX_JOB_TOKEN`. URL expiry is clamped server-side to <= 1h.
operationId: getJobCacheURL
security:
- jobTokenAuth: []
parameters:
- $ref: '#/components/parameters/JobIDPath2'
responses:
'200':
description: Presigned GET URL envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/JobCacheRestoreURLResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'500': { $ref: '#/components/responses/InternalError' }
'503': { $ref: '#/components/responses/ServiceUnavailable' }
put:
tags: [job-side]
summary: Presigned PUT URL for saving the job cache
description: |
Returns a presigned S3/MinIO PUT URL the runner uses to upload
its build cache archive. The object key is derived ONLY from
the server-side `pipeline_jobs JOIN pipelines` row via
`internal/cicd/cachekey` (the consolidated
`cachekey.ObjectKey(repoID, pipelineID, jobID)` resolver) and
cross-checked against the token claims; the presigned PUT itself
is minted by `internal/cicd/cachestore`. Quota metered via
`ratelimit.RequestLimiter` on the `api.expensive` scope
(per-repo). Expiry is clamped to <= 1h.
operationId: putJobCacheURL
security:
- jobTokenAuth: []
parameters:
- $ref: '#/components/parameters/JobIDPath2'
responses:
'200':
description: Presigned PUT URL envelope.
content:
application/json:
schema:
$ref: '#/components/schemas/JobCacheSaveURLResponse'
'400': { $ref: '#/components/responses/BadRequest' }
'401': { $ref: '#/components/responses/Unauthorized' }
'403': { $ref: '#/components/responses/Forbidden' }
'404': { $ref: '#/components/responses/NotFound' }
'429':
description: Per-repo cache-URL quota exceeded.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
headers:
Retry-After:
schema: { type: integer }
'502':
description: Could not mint cache URL (presign failure).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
'503': { $ref: '#/components/responses/ServiceUnavailable' }
# =============================================================
# Components
# =============================================================
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
Standard Vetrix bearer JWT (session / access token).
`auth.Service.Verify` rejects tokens of `typ=job-token` --
those are accepted only by job-side endpoints
(`jobTokenAuth`).
jobTokenAuth:
type: http
scheme: bearer
bearerFormat: JWT
description: |
`VETRIX_JOB_TOKEN` -- the typ=job-token JWT minted at dispatch
time by `auth.IssueJobTokenForJob`. Carries
`repo_id` + `pipeline_id` + `job_id` claims plus a permissions
list (`registry:write` for artifact / cache push). The runner
passes this in `Authorization: Bearer <token>` (also accepted
as `Authorization: Basic base64(user:token)` mirroring the
docker convention the registry uses).
mtlsAuth:
type: mutualTLS
description: |
Mutual TLS using the CICDv2 private CA (see
`internal/cicdv2/mtls`). The /internal/* listener pins
`tls.RequireAndVerifyClientCert` and the application layer
cross-checks the peer cert `Subject.CommonName` against the
registered host FQDN. Documentation-only here -- no OpenAPI
request shape carries the TLS handshake.
parameters:
PageQuery:
in: query
name: page
schema: { type: integer, minimum: 1, default: 1 }
description: 1-based page number; values < 1 fall back to 1.
PerPageQuery:
in: query
name: per_page
schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
description: >-
Page size (valid range 1-100, default 25). Out-of-range handling is
endpoint-specific: the repository pipeline list falls back to the
default, while the admin CICDv2 lists clamp to the nearest bound.
LimitQuery:
in: query
name: limit
schema: { type: integer, minimum: 1, maximum: 100 }
description: Maximum number of results to return.
OffsetQuery:
in: query
name: offset
schema: { type: integer, minimum: 0 }
description: Zero-based offset into the result set.
OwnerPath:
in: path
name: owner
required: true
schema: { type: string }
description: Owning user or organisation username (URL slug).
RepoPath:
in: path
name: repo
required: true
schema: { type: string }
description: Repository name (URL slug).
HostIDPath:
in: path
name: id
required: true
schema: { type: string, format: uuid }
description: Runner host UUID.
RunnerIDPath:
in: path
name: id
required: true
schema: { type: string, format: uuid }
description: CI runner UUID.
PipelineIDPath:
in: path
name: id
required: true
schema: { type: string, format: uuid }
description: Pipeline UUID.
PipelineJobIDPath:
in: path
name: jid
required: true
schema: { type: string, format: uuid }
description: Pipeline-job UUID.
JobIDPath:
in: path
name: job_id
required: true
schema: { type: string, format: uuid }
description: Job UUID (host-pool internal routes use `job_id`).
JobIDPath2:
in: path
name: id
required: true
schema: { type: string, format: uuid }
description: Job UUID (public job-side routes use `id`).
responses:
BadRequest:
description: Malformed request (invalid JSON / missing required field / unknown field).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
Unauthorized:
description: Missing or invalid credentials (no claims; JWT expired; unknown token).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
Forbidden:
description: Authenticated but lacking the required scope / permission.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
NotFound:
description: Resource not found.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
UnprocessableEntity:
description: Validation failure (e.g. pipeline YAML invalid, value out of bounds).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
InternalError:
description: Internal server error.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
ServiceUnavailable:
description: |
Feature staged but the backing dependency is not wired on this
deployment (e.g. cache presigner / chunk store / host-pool
store unconfigured).
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorEnvelope'
schemas:
ErrorEnvelope:
type: object
description: |
Standard JSON error envelope written by `respondError` and
`respondErrorDetail`. `detail` is set only by handlers that
explicitly call `respondErrorDetail` (e.g. malformed JSON
body).
required: [error]
properties:
error:
type: string
description: Human-readable error summary.
detail:
type: string
description: Optional machine-or-operator-readable secondary detail.
# ---- Internal host-pool ------------------------------------
HostPoolRegisterRequest:
type: object
required:
- host_attach_token
- fqdn
- host_class
- capacity_cpu
- capacity_mem_bytes
properties:
host_attach_token:
type: string
description: Single-use plaintext invite minted by an operator.
fqdn:
type: string
description: Fully-qualified hostname (must match the peer-cert CN).
host_class:
type: string
example: linux-amd64
capacity_cpu:
type: integer
minimum: 1
description: Total CPU capacity in millicores (or as the host accounting layer defines).
capacity_mem_bytes:
type: integer
format: int64
minimum: 1
tags:
type: array
items: { type: string }
version:
type: string
description: Host-agent build version (e.g. `0.1.0`).
HostPoolRegisterResponse:
type: object
required: [host_id, state]
properties:
host_id:
type: string
format: uuid
state:
type: string
enum: [registering, online, draining, drained, decommissioned]
example: registering
tenant_pin_id:
type: string
format: uuid
nullable: true
description: Set when the host is pinned to a single tenant; null for the shared pool.
HostPoolHeartbeatRequest:
type: object
required: [host_id, cpu_used, mem_used]
properties:
host_id:
type: string
format: uuid
cpu_used:
type: integer
minimum: 0
description: In-use CPU (millicores).
mem_used:
type: integer
format: int64
minimum: 0
description: In-use memory (bytes).
HostPoolJobSpec:
type: object
description: |
JobSpec carried in the dispatch RPC body. Unknown fields are
tolerated for forward-compat with later JobSpec extensions.
required: [job_id, image, commands]
properties:
job_id:
type: string
format: uuid
description: Must equal the path `{job_id}`.
image:
type: string
description: Inner build image the executor runs.
outer_image:
type: string
description: Outer/host image the host-agent uses.
commands:
type: array
minItems: 1
items: { type: string, minLength: 1 }
tags:
type: array
items: { type: string }
HostPoolDispatchAck:
type: object
required: [job_id, host_id]
properties:
job_id: { type: string, format: uuid }
host_id:
type: string
description: Peer-cert CN the dispatch was recorded against.
HostPoolCompleteJobRequest:
type: object
required: [state]
properties:
state:
type: string
enum: [succeeded, success, failed, cancelled]
description: |
Terminal job state. `succeeded` is the host-agent
spelling and maps to the DB-side `success`; `success`
is also accepted verbatim.
exit_code:
type: integer
nullable: true
description: Process exit code (optional; leaves `exit_code` NULL when absent).
duration_ms:
type: integer
format: int64
nullable: true
description: Wall-clock duration in milliseconds (audit-only).
host_id:
type: string
format: uuid
description: Host-agent UUID (will be required once mTLS SAN binding is wired through).
HostPoolLogsResponse:
type: object
required: [job_id, persisted_bytes, received_lines, dropped_bytes]
properties:
job_id: { type: string, format: uuid }
persisted_bytes:
type: integer
format: int64
description: Cumulative bytes the controller has persisted (= the agent's new high-water mark).
received_lines:
type: integer
description: Number of log lines accepted in this request.
dropped_bytes:
type: integer
format: int64
description: Bytes silently dropped because Last-Event-ID indicated they were already persisted.
# ---- Admin: CICD policy defaults --------------------------
CICDDefaultsResponse:
type: object
description: Typed cluster-wide CICDv2 policy defaults.
required: [max_job_timeout, cpu_millis, memory_bytes, pids_max]
properties:
max_job_timeout:
type: string
example: 6h
description: Go-duration formatted maximum job runtime.
cpu_millis:
type: integer
example: 2000
memory_bytes:
type: integer
format: int64
example: 2147483648
pids_max:
type: integer
example: 1024
CICDDefaultsPatchRequest:
type: object
description: |
Per-field patch body for the cluster-wide CICDv2 policy
defaults. All four fields are optional; an omitted field is a
no-op. A body supplying none of them returns 400. Values are
checked against the bounded validators registered on the
`cicd.defaults.*` keys before anything is written -- out of
bounds is 422, unparseable is 400.
`additionalProperties: false` mirrors the handler, which
decodes with unknown fields disallowed. A key outside this set
-- notably `value` -- is a 400, not a silently ignored field
and not a feature-flag write.
additionalProperties: false
properties:
max_job_timeout:
type: string
example: 6h
description: >-
Go-duration formatted maximum job runtime, e.g. `6h`. Sent
as a string; the other three fields are integers.
cpu_millis: { type: integer }
memory_bytes: { type: integer, format: int64 }
pids_max: { type: integer }
# CICDFlagResponse and CICDFlagPatchRequest -- the `{scope, value,
# source}` envelope and the `{"value": <bool string>}` body of the
# `cicd_v2` feature flag -- were defined here. Both were removed
# with the operations that referenced them; no live endpoint accepts
# or returns either shape. See the sunset note in
# `info.description`.
# ---- Tenant: org-scoped CICDv2 policy -----------
OrgCICDPolicyResponse:
type: object
description: |
Org-scoped numeric CICDv2 policy envelope. Every
cap field is always present; absent overlay rows surface as
the field's zero value (`""` for `max_job_timeout`, `0` for
the integer fields). `source` is `explicit` when any overlay
row exists for the owner; `default` otherwise.
required:
- max_job_timeout
- cpu_millis
- memory_bytes
- pids_max
- source
properties:
max_job_timeout:
type: string
description: |
Go-duration formatted maximum job runtime (e.g. `6h`,
`30m`). Empty string when no overlay row exists.
example: 6h
cpu_millis:
type: integer
description: Per-job CPU budget in milli-CPUs.
example: 2000
memory_bytes:
type: integer
format: int64
description: Per-job memory budget in bytes.
example: 2147483648
pids_max:
type: integer
description: Per-job max PIDs.
example: 1024
source:
type: string
enum: [default, explicit]
description: |
`explicit` when at least one org overlay row exists;
`default` when no rows exist for this owner.
owner:
type: string
description: Owner username (URL slug); echoed on every response.
OrgCICDPolicyPatchRequest:
type: object
description: |
All fields optional; an omitted field is a no-op. An empty
body returns 400. Unknown fields are rejected
(`DisallowUnknownFields`). Each supplied value is validated
against the syntactic floor (e.g. `cpu_millis` >=
100, `max_job_timeout` >= 1m) and the live cluster app cap;
cap exceedances return 422 +
`error_code: org_policy_exceeds_app_cap`.
additionalProperties: false
properties:
max_job_timeout:
type: string
description: Go-duration formatted maximum job runtime.
example: 4h
cpu_millis:
type: integer
example: 1500
memory_bytes:
type: integer
format: int64
example: 1073741824
pids_max:
type: integer
example: 512
ErrorEnvelopeWithCode:
type: object
description: |
Extension of `ErrorEnvelope` emitted by `respondErrorCode`.
Carries a stable machine-readable `error_code` alongside the
human-readable `error` message. The frontend pattern-matches
on `error_code` rather than the message copy.
required: [error, error_code]
properties:
error:
type: string
description: Human-readable error summary.
error_code:
type: string
description: |
Stable machine-readable error identifier (e.g.
`org_policy_exceeds_app_cap`).
# ---- Admin: runner hosts (CICDv2) -------------------------
RunnerHost:
type: object
required:
- id
- fqdn
- host_class
- capacity_cpu
- capacity_mem_bytes
- in_use_cpu
- in_use_mem_bytes
- tags
- state
- version
- created_at
- healthy
properties:
id: { type: string, format: uuid }
fqdn: { type: string }
host_class: { type: string }
capacity_cpu: { type: integer }
capacity_mem_bytes: { type: integer, format: int64 }
in_use_cpu: { type: integer }
in_use_mem_bytes: { type: integer, format: int64 }
tags:
type: array
items: { type: string }
state:
type: string
enum: [registering, online, draining, drained, decommissioned]
version: { type: string }
last_heartbeat_at:
type: string
format: date-time
nullable: true
tenant_pin_id:
type: string
format: uuid
nullable: true
created_at: { type: string, format: date-time }
healthy:
type: boolean
description: Derived from `(state == online) AND (last_heartbeat_at within threshold)`.
RunnerHostsListResponse:
type: object
required: [items, total, page, per_page]
properties:
items:
type: array
items:
$ref: '#/components/schemas/RunnerHost'
total: { type: integer }
page: { type: integer }
per_page: { type: integer }
HostDrainStatus:
type: object
required: [host_id, state, in_flight_jobs, drained]
properties:
host_id: { type: string, format: uuid }
state:
type: string
enum: [online, draining, drained]
in_flight_jobs: { type: integer }
drained: { type: boolean }
# ---- Admin: v1 runners ------------------------------------
AdminRunner:
type: object
required: [id, name, tags, status, last_seen_at, registered_at, isolation_kind, ssh_capable]
properties:
id: { type: string, format: uuid }
name: { type: string }
tags:
type: array
items: { type: string }
status: { type: string }
last_seen_at:
type: string
format: date-time
nullable: true
registered_at: { type: string, format: date-time }
group_id: { type: string, format: uuid }
isolation_kind: { type: string }
ssh_capable: { type: boolean }
arch: { type: string }
os: { type: string }
cpu_millis: { type: integer }
mem_mib: { type: integer }
RegisterRunnerRequest:
type: object
required: [name]
properties:
name: { type: string }
tags:
type: array
items: { type: string }
RegisterRunnerResponse:
type: object
required: [token, raw_token, runner]
properties:
token:
type: string
description: Legacy alias of `raw_token`.
raw_token:
type: string
description: |
Bearer token the runner uses to authenticate.
**Returned exactly once** -- the response is the only
opportunity to capture it.
runner:
$ref: '#/components/schemas/AdminRunner'
RotateRunnerTokenResponse:
type: object
required: [raw_token]
properties:
raw_token:
type: string
description: |
New bearer token. The previous token's hash is invalidated
atomically; the runner must re-authenticate with the new
value on its next call.
# ---- Admin: audit ----------------------------------------
CICDAuditEvent:
type: object
required: [id, occurred_at, scope_kind, scope_id, action]
properties:
id: { type: string, format: uuid }
occurred_at: { type: string, format: date-time }
actor_id:
type: string
format: uuid
nullable: true
scope_kind:
type: string
enum: [runner_host, runner_group, pipeline, job, secret, policy]
scope_id: { type: string, format: uuid }
action: { type: string }
payload:
type: object
additionalProperties: true
description: Free-form action-specific JSON.
CICDAuditEventsListResponse:
type: object
required: [items, total, page, per_page]
properties:
items:
type: array
items:
$ref: '#/components/schemas/CICDAuditEvent'
total: { type: integer }
page: { type: integer }
per_page: { type: integer }
# ---- Admin: usage ----------------------------------------
UsageHourBucket:
type: object
required: [hour_bucket, pipeline_runs, job_runs, build_minutes]
properties:
hour_bucket:
type: string
format: date-time
description: RFC3339 timestamp of the hour-aligned bucket start.
pipeline_runs: { type: integer, format: int64 }
job_runs: { type: integer, format: int64 }
build_minutes: { type: integer, format: int64 }
UsageTenantGroup:
type: object
required: [tenant_id, pipeline_runs, job_runs, build_minutes, hours]
properties:
tenant_id: { type: string, format: uuid }
tenant_slug: { type: string }
pipeline_runs: { type: integer, format: int64 }
job_runs: { type: integer, format: int64 }
build_minutes: { type: integer, format: int64 }
hours:
type: array
items:
$ref: '#/components/schemas/UsageHourBucket'
UsageCICDResponse:
type: object
required: [window_from, window_to, tenants, totals]
properties:
window_from: { type: string, format: date-time }
window_to: { type: string, format: date-time }
tenants:
type: array
items:
$ref: '#/components/schemas/UsageTenantGroup'
totals:
type: object
required: [pipeline_runs, job_runs, build_minutes]
properties:
pipeline_runs: { type: integer, format: int64 }
job_runs: { type: integer, format: int64 }
build_minutes: { type: integer, format: int64 }
# ---- Pipelines (public tenant) ----------------------------
Pipeline:
type: object
required: [id, repo_id, commit_sha, ref, trigger, state, created_at]
properties:
id: { type: string, format: uuid }
repo_id: { type: string, format: uuid }
commit_sha: { type: string }
ref: { type: string }
trigger: { type: string, example: manual }
state:
type: string
enum: [pending, running, success, failed, cancelled]
created_at: { type: string, format: date-time }
finished_at:
type: string
format: date-time
nullable: true
GlobalPipeline:
allOf:
- $ref: '#/components/schemas/Pipeline'
- type: object
required: [owner, repo_name]
properties:
owner: { type: string }
repo_name: { type: string }
PipelineJob:
type: object
required: [id, pipeline_id, name, stage, image, state]
properties:
id: { type: string, format: uuid }
pipeline_id: { type: string, format: uuid }
name: { type: string }
stage: { type: string }
image: { type: string }
state:
type: string
enum: [pending, running, success, failed, cancelled]
runner_id: { type: string, format: uuid }
exit_code: { type: integer, nullable: true }
started_at: { type: string, format: date-time, nullable: true }
finished_at: { type: string, format: date-time, nullable: true }
allow_failure:
type: boolean
description: >-
When set, the job's failure is tolerated: it stays in state
`failed` but does not fail the pipeline. Absent when the job does
not set the flag.
status_reason:
type: string
description: >-
Why a terminal job ended, distinct from `state` (for example
`timed_out` for a job the runner killed for overrunning its
effective timeout). Absent when no reason was recorded.
resolved_timeout_seconds:
type: integer
nullable: true
description: >-
Resolved per-job timeout, in seconds, that the worker computed and
enforced as the execution deadline. Absent on jobs the worker has
not stamped.
host_id:
type: string
format: uuid
description: CICDv2 runner host that executed the job. Absent on v1-shaped jobs.
outer_image:
type: string
description: Outer/host image the runner used to run the job. Absent on v1-shaped jobs.
network_mode:
type: string
description: Container network mode the runner applied. Absent on v1-shaped jobs.
cache_hit:
type: boolean
nullable: true
description: >-
Whether the declared job cache was restored. Three-state: absent
(no cache declared), `true` (restored), or `false` (declared but
missed).
cache_key:
type: string
description: Cache key the job resolved. Absent on v1-shaped jobs.
cold_start:
type: boolean
nullable: true
description: >-
Whether the sandbox was cold-started rather than reused from a warm
pool. Absent on v1-shaped jobs.
cgroup_limits:
allOf:
- $ref: '#/components/schemas/CgroupLimits'
description: >-
CICDv2 cgroup resource caps the runner applied to the job sandbox.
Absent on v1-shaped jobs.
CgroupLimits:
type: object
description: >-
Cgroup resource caps applied to a CICDv2 job sandbox. Each field is
present only when the corresponding cap was set.
properties:
cpus: { type: number }
memory_bytes: { type: integer, format: int64 }
memory_swap_bytes: { type: integer, format: int64 }
pids: { type: integer, format: int64 }
blkio_weight: { type: integer }
PipelineDetail:
allOf:
- $ref: '#/components/schemas/Pipeline'
- type: object
required: [jobs]
properties:
jobs:
type: array
items:
$ref: '#/components/schemas/PipelineJob'
TriggerPipelineRequest:
type: object
required: [ref]
properties:
ref:
type: string
description: Git ref (branch / tag / commit) to schedule the pipeline against.
Artifact:
type: object
required: [id, pipeline_id, job_id, name, size_bytes, sha256, created_at]
properties:
id: { type: string, format: uuid }
pipeline_id: { type: string, format: uuid }
job_id: { type: string, format: uuid }
name: { type: string }
size_bytes: { type: integer, format: int64 }
sha256: { type: string }
created_at: { type: string, format: date-time }
retention_expires_at:
type: string
format: date-time
description: CICDv2 field; absent on v1 artifacts.
paths:
type: array
items: { type: string }
description: CICDv2 field; absent on v1 artifacts.
download_url:
type: string
description: >-
Relative path to the artifact content route
(`GET .../artifacts/{id}/content`) that streams the raw bytes.
Present on the repository-scoped list and detail responses; absent
on the job-side push response.
CommitStatus:
type: object
required: [id, commit_sha, context, state, updated_at]
properties:
id: { type: string, format: uuid }
commit_sha: { type: string }
context: { type: string }
state: { type: string }
description: { type: string }
target_url: { type: string, format: uri }
updated_at: { type: string, format: date-time }
# ---- Job-side --------------------------------------------
JobArtifactPushRequest:
type: object
required: [name, storage_key, size_bytes]
properties:
name: { type: string }
storage_key: { type: string }
size_bytes: { type: integer, format: int64, minimum: 0 }
sha256: { type: string }
retention_days:
type: integer
minimum: 0
description: Days to retain the artifact (CICDv2 field).
retention:
type: integer
minimum: 0
description: Back-compat alias for `retention_days`.
paths:
type: array
items: { type: string }
description: |
Source paths from the runner workspace that produced the
artifact (CICDv2 field, optional).
JobCacheRestoreURLResponse:
type: object
required: [url, expires_at, expires_in_seconds, object_key]
properties:
url:
type: string
format: uri
description: Presigned S3/MinIO GET URL.
expires_at:
type: string
format: date-time
expires_in_seconds: { type: integer }
object_key: { type: string }
JobCacheSaveURLResponse:
type: object
required: [method, url, expires_at, object_key]
properties:
method:
type: string
enum: [PUT]
url:
type: string
format: uri
description: Presigned S3/MinIO PUT URL.
expires_at:
type: string
format: date-time
object_key: { type: string }