CI/CD
Reference for the continuous-integration endpoints under
/api/v1/repos/{owner}/{repo}: pipelines, the jobs they run, the artifacts and
coverage those jobs produce, and the per-branch pipeline variables that feed
them.
Resource overview
A pipeline is one run of a repository's CI configuration against a single commit.
Every pipeline belongs to a repository and is addressed by its UUID. Repo-scoped
operations hang off /api/v1/repos/{owner}/{repo}/pipelines/...; one
cross-repository convenience listing lives at /api/v1/pipelines. This page
covers:
- pipelines — list, fetch (with jobs), trigger, cancel, retry, and delete;
- jobs — the per-stage units a pipeline runs, returned inside the pipeline detail, plus a Server-Sent Events stream of a job's logs;
- artifacts — files a job uploads: list, fetch metadata, download the raw bytes, read the durable coverage report, and attach, list, or download SBOM attestations;
- CI/CD variables — per-branch pipeline variables (secret values sealed at rest) and their log-disclosure flag.
The pipeline definition is read from the first file found on the search path
.ci/pipeline.yml → .vetrix/pipeline.yml → vetrix-ci.yml at the triggered
ref.
Two related surfaces are documented elsewhere. The job-side endpoints the runner itself calls (artifact push, build-cache URLs) authenticate with a job token rather than a user credential and are not part of the user-facing surface here. Fleet administration — CI runners, runner hosts, cluster CI defaults, and CI usage — lives in admin.md. The deployments and environments feature — environments and reviewers, blue/green traffic split, deployments and their statuses, and deployment approvals — is a separate, feature-gated endpoint family covered in Deployments, environments & approvals at the end of this page.
Pipeline and job lifecycle
A pipeline's state and each job's state are drawn from the same closed set:
| State | Meaning |
|---|---|
pending |
Created and queued; no job has started, or jobs are awaiting a runner. |
running |
At least one job is executing. |
success |
Every job that gates the pipeline finished successfully. |
failed |
A gating job failed. |
cancelled |
The pipeline was cancelled before completing. |
success, failed, and cancelled are terminal. A pipeline's trigger field
records how it started — push, merge, or manual (a trigger or retry through
the API).
A finished job carries an exit_code (the process exit status; absent while the
job is still pending or running). A job whose allow_failure flag is set stays in
state failed when it fails but does not fail the pipeline — the pipeline can
still roll up to success. When a job ends for a specific reason beyond the
coarse state, status_reason names it (for example timed_out for a job the
runner killed for overrunning its resolved timeout).
Auth & scopes
See conventions.md for the accepted credential types and how
scopes are enforced. Every pipeline, job, artifact, and variable endpoint on this
page, and the commit-status endpoints, require a bearer credential — there is no
anonymous CI read, even on a public repository. Authorization is then per
repository: reads require CI read access on the repo, and mutations require CI
write access.
| Operation | OAuth2 scope | PAT scope |
|---|---|---|
| Read (list/fetch pipelines, jobs, logs, artifacts, coverage, variables, attestations, commit statuses) | read:pipeline |
ci:read |
| Write (trigger/cancel/retry/delete pipelines, manage variables, attach attestations, publish commit statuses) | write:pipeline |
ci:write |
Scope is enforced additively with the repository access check: the caller must
hold both the scope and CI access on the repository. A read against a private
repository the caller may not read returns 404 Not Found, never 403, so the
repository's existence is not disclosed (see the 404-not-403 rule in
errors.md). A pipeline, job, or artifact UUID that belongs to a
different repository than the one named in the path is likewise an
enumeration-resistant 404.
The deployments and environments family at the end of this page authorizes differently — on the caller's repository role rather than a CI scope. See its Authorization note for that model.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET |
/api/v1/repos/{owner}/{repo}/pipelines |
List a repository's pipelines |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/{id} |
Fetch a pipeline with its jobs |
POST |
/api/v1/repos/{owner}/{repo}/pipelines |
Trigger a pipeline on a ref |
POST |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/cancel |
Cancel a running pipeline |
POST |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/retry |
Retry a failed or cancelled pipeline |
DELETE |
/api/v1/repos/{owner}/{repo}/pipelines/{id} |
Delete a pipeline and its data |
GET |
/api/v1/pipelines |
List the caller's recent pipelines across owned repos |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/jobs/{jid}/logs |
Stream a job's logs (SSE) |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts |
List a pipeline's artifacts |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts/{name} |
Fetch one artifact's metadata |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts/{artifactId}/content |
Download an artifact's bytes |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/{id}/coverage |
Read the durable coverage report |
GET |
/api/v1/repos/{owner}/{repo}/artifacts/{id}/attestations |
List an artifact's attestations |
POST |
/api/v1/repos/{owner}/{repo}/artifacts/{id}/attestations |
Attach an attestation to an artifact |
GET |
/api/v1/repos/{owner}/{repo}/artifacts/{id}/attestations/{attestation_id}/payload |
Download an attestation's stored payload bytes |
GET |
/api/v1/repos/{owner}/{repo}/commits/{sha}/statuses |
List a commit's statuses |
POST |
/api/v1/repos/{owner}/{repo}/commits/{sha}/statuses |
Publish a commit status |
GET |
/api/v1/repos/{owner}/{repo}/pipelines/variables |
List a branch's pipeline variables |
POST |
/api/v1/repos/{owner}/{repo}/pipelines/variables |
Create a pipeline variable |
PUT |
/api/v1/repos/{owner}/{repo}/pipelines/variables/{id} |
Rotate a variable's value |
PATCH |
/api/v1/repos/{owner}/{repo}/pipelines/variables/{id}/disclose_in_logs |
Set a variable's log-disclosure flag |
DELETE |
/api/v1/repos/{owner}/{repo}/pipelines/variables/{id} |
Delete a pipeline variable |
Pipelines
GET /api/v1/repos/{owner}/{repo}/pipelines
List the repository's pipelines, most recent first.
Path parameters
| Name | Type | Description |
|---|---|---|
owner |
string | Owner username. |
repo |
string | Repository name. |
Query parameters
page and per_page follow the Pagination
convention (page defaults to 1; per_page defaults to 25, and a value
outside [1, 100] falls back to that default). This endpoint returns a bare
JSON array of pipeline objects, not the
{ items, total, ... } page envelope — request the next page when a full page is
returned. The optional filters below narrow the list:
| Name | Type | Description |
|---|---|---|
state |
string | One of pending, running, success, failed, cancelled. A value outside this set yields an empty list. |
trigger |
string | One of push, merge, manual. A value outside this set yields an empty list. |
ref |
string | Match pipelines for a specific branch or tag ref. Matched verbatim; an unknown ref yields an empty list. |
Response
[
{
"id": "9f1c2e7a-...",
"repo_id": "3b8e...",
"commit_sha": "1a2b3c4d...",
"ref": "refs/heads/main",
"trigger": "push",
"state": "success",
"created_at": "2026-02-19T09:31:00Z",
"finished_at": "2026-02-19T09:36:12Z",
"has_coverage": true,
"config_path": "vetrix-ci.yml",
"config_source_sha": "1a2b3c4d..."
}
]
has_coverage reports whether a durable coverage record exists for the pipeline.
config_path and config_source_sha record which configuration file the jobs
were parsed from and the commit it was read at; both are omitted on older rows
that predate the fields. finished_at is omitted while the pipeline is not
terminal.
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
401 Unauthorized |
No valid credential. |
404 Not Found |
Repository missing, or private and not readable. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -H "Authorization: Bearer <token>" \
"https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines?state=failed&per_page=50"
GET /api/v1/repos/{owner}/{repo}/pipelines/{id}
Fetch one pipeline together with its constituent jobs.
Path parameters
| Name | Type | Description |
|---|---|---|
owner |
string | Owner username. |
repo |
string | Repository name. |
id |
string (uuid) | Pipeline UUID. |
Response
The pipeline fields above, plus a jobs array:
{
"id": "9f1c2e7a-...",
"repo_id": "3b8e...",
"commit_sha": "1a2b3c4d...",
"ref": "refs/heads/main",
"trigger": "push",
"state": "failed",
"created_at": "2026-02-19T09:31:00Z",
"finished_at": "2026-02-19T09:36:12Z",
"has_coverage": false,
"jobs": [
{
"id": "c0ffee00-...",
"pipeline_id": "9f1c2e7a-...",
"name": "test",
"stage": "test",
"image": "node:22-bookworm",
"state": "failed",
"runner_id": "runner-1",
"exit_code": 1,
"started_at": "2026-02-19T09:31:40Z",
"finished_at": "2026-02-19T09:35:02Z",
"allow_failure": false,
"status_reason": "timed_out"
}
]
}
Each job carries state, and once terminal an exit_code. allow_failure,
status_reason, and resolved_timeout_seconds appear when set. A job may also
carry runner-produced execution metadata when the runner records it —
host_id, outer_image, cgroup_limits, network_mode, cache_hit,
cache_key, and cold_start; each is omitted when the runner did not set it.
Status codes
| Status | When |
|---|---|
200 OK |
Pipeline returned. |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
404 Not Found |
Pipeline or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-...
POST /api/v1/repos/{owner}/{repo}/pipelines
Trigger a new pipeline on a ref. Requires CI write access. The handler resolves the ref to its tip commit, reads the pipeline configuration from the search path, and schedules the jobs the ref selects.
Request body
{
"ref": "main"
}
| Field | Type | Required | Description |
|---|---|---|---|
ref |
string | yes | Branch name, tag name, or fully-qualified ref to schedule against. |
Response
201 Created with the scheduled pipeline (the same shape as a list item).
Status codes
| Status | When |
|---|---|
201 Created |
Pipeline scheduled. |
400 Bad Request |
Malformed body, or ref missing. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Repository missing or not readable. |
422 Unprocessable Entity |
No configuration found, invalid pipeline YAML, the ref cannot be resolved to a commit, or every job is gated out for the ref. |
429 Too Many Requests |
The per-repository / per-user trigger rate limit was exceeded. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"ref":"main"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines
POST /api/v1/repos/{owner}/{repo}/pipelines/{id}/cancel
Cancel a pipeline that has not yet reached a terminal state. Requires CI write access.
Status codes
| Status | When |
|---|---|
204 No Content |
Cancellation accepted. |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Pipeline or repository missing, not readable, or belonging to another repository. |
409 Conflict |
The pipeline is already in a terminal state. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-.../cancel
POST /api/v1/repos/{owner}/{repo}/pipelines/{id}/retry
Re-run a finished pipeline. Only failed and cancelled pipelines are
retryable. The retry re-reads the configuration at the original pipeline's
commit and schedules a new pipeline against the same ref. Requires CI write
access.
Response
201 Created with the new pipeline.
Status codes
| Status | When |
|---|---|
201 Created |
A new pipeline was scheduled as a retry. |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Pipeline or repository missing or not readable. |
409 Conflict |
The pipeline is not in a retryable (failed or cancelled) state. |
422 Unprocessable Entity |
The configuration no longer parses, or every job is gated out for the ref. |
429 Too Many Requests |
The per-repository / per-user trigger rate limit was exceeded. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-.../retry
DELETE /api/v1/repos/{owner}/{repo}/pipelines/{id}
Delete a pipeline run and all of its dependent data (jobs, artifacts, commit statuses, logs, and stored artifact bytes). Requires CI write access.
Status codes
| Status | When |
|---|---|
204 No Content |
Pipeline deleted. |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Pipeline or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -X DELETE -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-...
GET /api/v1/pipelines
List the 50 most recent pipelines across every repository the caller owns,
newest first. This convenience listing is not paginated and is scoped to the
caller's own repositories. Returns a bare JSON array; each item is a pipeline
object plus owner and repo_name.
Response
[
{
"id": "9f1c2e7a-...",
"repo_id": "3b8e...",
"commit_sha": "1a2b3c4d...",
"ref": "refs/heads/main",
"trigger": "push",
"state": "success",
"created_at": "2026-02-19T09:31:00Z",
"finished_at": "2026-02-19T09:36:12Z",
"has_coverage": false,
"owner": "alice",
"repo_name": "widgets"
}
]
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
401 Unauthorized |
No valid credential. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/pipelines
Jobs
Jobs are returned inside the pipeline detail (see
GET .../pipelines/{id} above); there is no standalone job-fetch endpoint. The
one job-specific endpoint streams a job's logs.
GET /api/v1/repos/{owner}/{repo}/pipelines/{id}/jobs/{jid}/logs
Stream a job's logs as Server-Sent Events (text/event-stream). Historical
frames are replayed first, then live frames follow while the job runs; the stream
ends when the job is terminal. This endpoint also accepts the browser session
cookie so the in-app log console can open it with EventSource.
Path parameters
| Name | Type | Description |
|---|---|---|
owner |
string | Owner username. |
repo |
string | Repository name. |
id |
string (uuid) | Pipeline UUID. |
jid |
string (uuid) | Job UUID; must belong to the named pipeline and repository. |
Request headers
| Header | Description |
|---|---|
Last-Event-ID |
Optional. Resume the stream after the last event the client received, so a reconnect does not replay already-seen frames. |
Status codes
| Status | When |
|---|---|
200 OK |
SSE stream opened. |
400 Bad Request |
Malformed pipeline or job UUID. |
401 Unauthorized |
No valid credential. |
404 Not Found |
Pipeline, job, or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
The log broker or CI engine is not wired on this deployment. |
Example
curl -N -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-.../jobs/c0ffee00-.../logs
Artifacts
Artifacts are files a job uploads. List and metadata reads return the artifact record; a separate content route streams the raw bytes. The coverage report is a durable summary that survives artifact-byte pruning.
GET /api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts
List all artifacts produced by a pipeline's jobs. Returns a bare JSON array.
Response
[
{
"id": "a17fac70-...",
"pipeline_id": "9f1c2e7a-...",
"job_id": "c0ffee00-...",
"name": "web/coverage/coverage-summary.json",
"size_bytes": 4096,
"sha256": "e3b0c44298fc1c14...",
"created_at": "2026-02-19T09:35:30Z",
"retention_expires_at": "2026-03-21T09:35:30Z",
"paths": ["web/coverage/"],
"download_url": "/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-.../artifacts/a17fac70-.../content"
}
]
download_url is the relative path to the content route for the artifact's
bytes, addressed by id (artifact names routinely contain /, which is not
routable by name). retention_expires_at and paths are omitted on older
artifacts that lack them.
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
404 Not Found |
Pipeline or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
GET /api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts/{name}
Fetch one artifact's metadata by its name. {name} is a single path segment, so
this route resolves artifacts whose name has no /; use download_url from the
list for the bytes of any artifact.
Response
A single artifact object, the same shape as a list item.
Status codes
| Status | When |
|---|---|
200 OK |
Artifact returned. |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
404 Not Found |
Artifact, pipeline, or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
GET /api/v1/repos/{owner}/{repo}/pipelines/{id}/artifacts/{artifactId}/content
Stream an artifact's raw bytes, addressed by artifact id. The response
Content-Type is application/json for a .json artifact and
application/octet-stream otherwise.
Path parameters
| Name | Type | Description |
|---|---|---|
id |
string (uuid) | Pipeline UUID. |
artifactId |
string (uuid) | Artifact UUID; must belong to the named pipeline and repository. |
Status codes
| Status | When |
|---|---|
200 OK |
Bytes streamed. |
400 Bad Request |
Malformed pipeline or artifact UUID. |
401 Unauthorized |
No valid credential. |
404 Not Found |
Artifact or pipeline missing, not readable, belonging to another repository, or its bytes are no longer stored. |
503 Service Unavailable |
The CI engine, or artifact-byte storage, is not wired on this deployment. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-.../artifacts/a17fac70-.../content
GET /api/v1/repos/{owner}/{repo}/pipelines/{id}/coverage
Read the durable coverage report for a pipeline. This reads the persisted coverage record (the top-line totals collected at the end of the run), so it keeps returning after the underlying coverage artifact bytes have been pruned.
Response
{
"pipeline_id": "9f1c2e7a-...",
"commit_sha": "1a2b3c4d...",
"total_pct": 94.86,
"total_covered_stmts": 8123,
"total_stmts": 8563,
"package_count": 42,
"generated_at": "2026-02-19T09:35:25Z",
"collected_at": "2026-02-19T09:35:31Z"
}
generated_at is omitted when the source summary did not record it.
Status codes
| Status | When |
|---|---|
200 OK |
Coverage report returned. |
400 Bad Request |
Malformed pipeline UUID. |
401 Unauthorized |
No valid credential. |
404 Not Found |
No coverage record for the pipeline, or the pipeline / repository is missing, not readable, or belonging to another repository. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/9f1c2e7a-.../coverage
GET /api/v1/repos/{owner}/{repo}/artifacts/{id}/attestations
List the attestations attached to an artifact. Note the base path: attestations
hang off /artifacts/{id} directly, where {id} is the artifact UUID — not under
/pipelines/.
The response is a bare top-level JSON array, not the
{ items, total, page, per_page } envelope described under
Pagination. This endpoint takes no page or
per_page parameter and returns every attestation on the artifact in one
response; an artifact with no attestations returns []. Read the array
directly — indexing into an items key yields nothing.
Response
[
{
"id": "a77e57ed-...",
"artifact_id": "a17fac70-...",
"kind": "sbom",
"format": "cyclonedx-json",
"predicate_type": "https://cyclonedx.org/bom",
"payload_cid": "bafy...",
"payload_sha256": "9f86d081884c7d65...",
"payload_meta": { "component_count": "212" },
"signature_required": true,
"signer_subject": "ci@alice/widgets",
"created_at": "2026-02-19T09:36:00Z"
}
]
format, payload_meta, and signer_subject are omitted when not set.
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
400 Bad Request |
Malformed artifact UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
The repository is public or internal and the caller lacks CI read access on it. |
404 Not Found |
Artifact or repository missing; the repository is private and not readable by the caller; or the artifact belongs to another repository. |
503 Service Unavailable |
The attestation service is not configured on this deployment. |
POST /api/v1/repos/{owner}/{repo}/artifacts/{id}/attestations
Attach an attestation to an artifact. Requires CI write access. This endpoint
accepts SBOM attestations (kind is sbom).
{id} must be an artifact that is already registered against a pipeline in this
repository — attach never creates one. A running job registers the artifacts it
produced through the job-side endpoint
POST /api/v1/jobs/{id}/artifacts, specified in
../openapi/openapi-cicd-v2.yaml; its
201 Created body is the artifact record, and the id in that record is the
artifact UUID you attach against. That endpoint authenticates with the per-job
token the runner is issued rather than a user credential, and the token must
carry the registry:write permission and name the same job, pipeline, and
repository as the row it is pushing to. The per-job token is not accepted on
this attach endpoint or on any other endpoint on this page: they sit behind the
user-credential gate, which rejects a job token as an invalid credential and
answers 401. Attach with a user bearer credential holding CI write access on
the repository.
The usual order of operations is therefore: register the artifact (job-side, job token) → attach the attestation (this endpoint, user credential) → list the artifact's attestations → download an attestation's payload bytes.
Request body
{
"kind": "sbom",
"format": "cyclonedx-json",
"payload": "<base64-encoded bytes>",
"unsigned": false
}
| Field | Type | Required | Description |
|---|---|---|---|
kind |
string | yes | Attestation kind. Only sbom is accepted here. |
format |
string | no | SBOM format; defaults to cyclonedx-json. |
payload |
string | yes | Base64-encoded attestation bytes. |
unsigned |
boolean | no | When true, store the attestation without requiring a signature. |
Response
201 Created with the attestation record (the same shape as a list item).
Status codes
| Status | When |
|---|---|
201 Created |
Attestation recorded. |
400 Bad Request |
Malformed body, payload not valid base64, the SBOM payload is empty, or kind is missing. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Artifact or repository missing, not readable, or belonging to another repository. |
422 Unprocessable Entity |
An unsupported kind, or an unsupported SBOM format. The body lists the supported set. |
503 Service Unavailable |
The attestation service is not configured on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"kind":"sbom","payload":"'"$(base64 -w0 sbom.json)"'"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/artifacts/a17fac70-.../attestations
GET /api/v1/repos/{owner}/{repo}/artifacts/{id}/attestations/{attestation_id}/payload
Download the stored payload bytes of one attestation. Requires CI read access on
the repository, the same gate the attestation listing uses. The response body is
the exact byte sequence that was stored at attach time — for an SBOM
attestation, the SBOM document itself, byte-identical to the base64-decoded
payload that was attached. Nothing is re-serialised, re-signed, or wrapped.
Take {attestation_id} from the id of an entry in the attestation listing for
the same artifact.
Path parameters
| Name | Type | Description |
|---|---|---|
id |
string (uuid) | Artifact UUID; must belong to a pipeline in the named repository. |
attestation_id |
string (uuid) | Attestation UUID; must belong to that artifact. |
Response
The raw stored bytes, served as a download rather than an inline document. Payload bytes are supplied by the caller who attached them, so the response is always forced to an attachment and is never rendered inline by a browser.
| Header | Value |
|---|---|
Content-Type |
application/json, always — including for an attestation stored in a non-JSON SBOM format such as spdx-tag-value. Determine the real format from the format field of the attestation record, not from this header. |
Content-Disposition |
attachment; filename="attestation-<attestation_id>.json" |
X-Content-Type-Options |
nosniff |
X-Payload-SHA256 |
Hex SHA-256 of the returned bytes; identical to the record's payload_sha256. Hash what you received and compare to verify the download end to end. |
Content-Length |
Byte length of the payload. |
Status codes
| Status | When |
|---|---|
200 OK |
Payload streamed. |
400 Bad Request |
Malformed artifact UUID or attestation UUID. |
401 Unauthorized |
No valid credential. A per-job token is not a valid credential here. |
403 Forbidden |
The repository is public or internal and the caller lacks CI read access on it. |
404 Not Found |
Repository, artifact, or attestation missing; the repository is private and not readable by the caller; the artifact belongs to another repository; the attestation belongs to another artifact; or the record exists but its stored bytes are gone. |
503 Service Unavailable |
The attestation service is not configured on this deployment, or its blob store cannot serve payload reads. |
A private repository the caller may not read, a cross-repository artifact
UUID, and a cross-artifact attestation UUID are all indistinguishable from a
genuinely missing record: each answers 404.
Example
curl -H "Authorization: Bearer <token>" \
-D headers.txt -o sbom.json \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/artifacts/a17fac70-.../attestations/a77e57ed-.../payload
Verify the download against the digest the server echoed:
grep -i '^x-payload-sha256:' headers.txt
sha256sum sbom.json
Commit statuses
A commit status is a per-context verdict recorded against a commit SHA — a
(context, state) pair with an optional description and target URL. The
merge-request UI reads them to render the per-context status pills on a commit.
Statuses come from two sources: the CI engine writes one per pipeline as it runs,
and an external integration can publish its own against any commit in a
repository it has CI write access to. The upsert is keyed on the
(repository, commit SHA, context) triple, so re-publishing the same context
overwrites the prior status rather than appending a second row.
GET /api/v1/repos/{owner}/{repo}/commits/{sha}/statuses
List the statuses recorded against a commit SHA. Requires CI read access. Returns
a bare JSON array, most-recent state per context; an empty array when the commit
has no statuses (the SHA is not validated against the object store, so an unknown
SHA returns [] rather than 404).
Path parameters
| Name | Type | Description |
|---|---|---|
owner |
string | Owner username. |
repo |
string | Repository name. |
sha |
string | Commit SHA the statuses were written against. Matched verbatim. |
Response
[
{
"id": "7c0b9d21-...",
"commit_sha": "1a2b3c4d...",
"context": "vetrix/ci/pipeline:9f1c2e7a-...",
"state": "success",
"description": "All jobs passed",
"target_url": "https://ci.example.com/runs/42",
"updated_at": "2026-02-19T09:36:12Z",
"soft_failed": true
}
]
| Field | Type | Description |
|---|---|---|
id |
string (uuid) | Status row identifier. |
commit_sha |
string | The commit the status applies to. |
context |
string | The status context (a label namespacing one source of verdict, e.g. vetrix/ci/pipeline:<uuid> for an engine-written status). |
state |
string | One of pending, running, success, failure, cancelled. |
description |
string | Optional human-readable summary. Omitted when empty. |
target_url |
string | Optional link to the producing run. Omitted when empty. |
updated_at |
string (date-time) | When the status was last written. |
soft_failed |
boolean | Derived warning flag. true only for an engine-written status whose pipeline rolled up to success because its only failing jobs were marked allow_failure; lets the UI show a soft-warning pill instead of a clean one. Omitted when false, and always false for an externally published status (it has no originating pipeline). |
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks CI read access on it. |
404 Not Found |
Repository missing, or private and not readable. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/commits/1a2b3c4d.../statuses
POST /api/v1/repos/{owner}/{repo}/commits/{sha}/statuses
Publish a commit status against a SHA. Requires CI write access. This is the
surface an external CI system uses to render its own verdict on a commit; the
status carries no originating Vetrix pipeline, so its soft_failed is always
false. Re-posting the same context against the same SHA overwrites the
existing status.
Path parameters
| Name | Type | Description |
|---|---|---|
owner |
string | Owner username. |
repo |
string | Repository name. |
sha |
string | Commit SHA to record the status against. Stored verbatim. |
Request body
{
"state": "success",
"context": "ci/external-build",
"description": "Build #128 passed",
"target_url": "https://ci.example.com/runs/128"
}
| Field | Type | Required | Description |
|---|---|---|---|
state |
string | yes | One of pending, running, success, failure, cancelled. |
context |
string | yes | Label that namespaces this verdict. Required — the upsert key includes it, so an empty context is rejected rather than collapsed onto a default. |
description |
string | no | Human-readable summary. |
target_url |
string | no | Link to the producing run. |
Response
201 Created with the created (or updated) status, the same shape as a list
item.
Status codes
| Status | When |
|---|---|
201 Created |
Status published. |
400 Bad Request |
Malformed body, state missing or not one of the allowed values, or context missing. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. This also covers a private repository the caller cannot read at all: unlike the GET above (which 404s an unreadable private repo for enumeration resistance), the POST resolves the repository without visibility filtering and then gates on CI write, so an existing-but-inaccessible repo returns 403, not 404. |
404 Not Found |
Repository (owner/repo) not found. |
503 Service Unavailable |
The CI engine is not wired on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"state":"success","context":"ci/external-build","target_url":"https://ci.example.com/runs/128"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/commits/1a2b3c4d.../statuses
CI/CD variables
Pipeline variables are per-branch secret values the runner injects into a job's
environment. Values are sealed at rest and are never returned by any endpoint —
list, create, rotate, and flag responses carry metadata only. By default a
variable's value is masked in pipeline logs; the disclose_in_logs flag opts a
single variable out of that masking.
GET /api/v1/repos/{owner}/{repo}/pipelines/variables
List the variables defined for one branch. Returns metadata only. Requires CI read access.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
branch |
string | yes | The branch whose variables to list. |
Response
[
{
"id": "5ec00000-...",
"scope": "branch",
"scope_id": "3b8e...",
"branch_name": "main",
"name": "DEPLOY_TOKEN",
"created_by": "7f000000-...",
"created_at": "2026-02-10T12:00:00Z",
"last_used_at": "2026-02-19T09:31:40Z",
"disclose_in_logs": false
}
]
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
400 Bad Request |
branch query parameter missing. |
401 Unauthorized |
No valid credential. |
404 Not Found |
Repository missing or not readable. |
503 Service Unavailable |
Branch pipeline variables are not wired on this deployment. |
Example
curl -H "Authorization: Bearer <token>" \
"https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/variables?branch=main"
POST /api/v1/repos/{owner}/{repo}/pipelines/variables
Create a variable on a branch. Requires CI write access. The value is sealed before storage and is not echoed back.
Request body
{
"branch": "main",
"name": "DEPLOY_TOKEN",
"value": "s3cr3t",
"disclose_in_logs": false
}
| Field | Type | Required | Description |
|---|---|---|---|
branch |
string | yes | Target branch. Non-empty, at most 255 bytes. |
name |
string | yes | Environment-variable identifier: a letter or underscore followed by letters, digits, or underscores. |
value |
string | yes | The secret value. At most 64 KiB. |
disclose_in_logs |
boolean | no | When true, the runner does not mask this value in pipeline logs. Defaults to false. |
Response
201 Created with the variable's metadata:
{
"id": "5ec00000-...",
"branch": "main",
"name": "DEPLOY_TOKEN",
"created_by": "7f000000-...",
"disclose_in_logs": false
}
Status codes
| Status | When |
|---|---|
201 Created |
Variable created. |
400 Bad Request |
Malformed body, missing/oversized branch, invalid name, or oversized value. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Repository missing or not readable. |
503 Service Unavailable |
Branch pipeline variables are not wired on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"branch":"main","name":"DEPLOY_TOKEN","value":"s3cr3t"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/variables
PUT /api/v1/repos/{owner}/{repo}/pipelines/variables/{id}
Rotate a variable's value in place, keeping its branch, name, and
disclose_in_logs flag. Requires CI write access.
Request body
{
"value": "n3w-s3cr3t"
}
| Field | Type | Required | Description |
|---|---|---|---|
value |
string | yes | The replacement value. At most 64 KiB. |
Response
200 OK with the variable's metadata (same shape as the create response).
Status codes
| Status | When |
|---|---|
200 OK |
Value rotated. |
400 Bad Request |
Malformed body, malformed variable UUID, or oversized value. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Variable or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
Branch pipeline variables are not wired on this deployment. |
PATCH /api/v1/repos/{owner}/{repo}/pipelines/variables/{id}/disclose_in_logs
Set a variable's log-disclosure flag without changing its value. Requires CI write access.
Request body
{
"disclose_in_logs": true
}
| Field | Type | Required | Description |
|---|---|---|---|
disclose_in_logs |
boolean | yes | The target flag value. When true, the runner stops masking this variable's value in pipeline logs. |
Response
200 OK with the variable's metadata, reflecting the new flag.
Status codes
| Status | When |
|---|---|
200 OK |
Flag updated. |
400 Bad Request |
Malformed body, malformed variable UUID, or disclose_in_logs omitted. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Variable or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
Branch pipeline variables are not wired on this deployment. |
DELETE /api/v1/repos/{owner}/{repo}/pipelines/variables/{id}
Delete a variable. Requires CI write access.
Status codes
| Status | When |
|---|---|
204 No Content |
Variable deleted. |
400 Bad Request |
Malformed variable UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks CI write access. |
404 Not Found |
Variable or repository missing, not readable, or belonging to another repository. |
503 Service Unavailable |
Branch pipeline variables are not wired on this deployment. |
Example
curl -X DELETE -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/pipelines/variables/5ec00000-...
Deployments, environments & approvals
The deployments and environments feature is a separate endpoint family from the
CI engine above. It models where a repository's builds are released and the
controls around those releases: named environments (with optional protection
rules and reviewers), deployments to an environment and their status
history, reviewer approvals, and an optional blue/green traffic split per
environment. Every path is repo-scoped under
/api/v1/repos/{owner}/{repo}/....
Feature availability
These endpoints are served from the repository's Postgres-backed stores, which a
standard server constructs at boot (the database connection is mandatory), so on
a standard deployment the whole family is present. Two gating behaviors differ
from the CI engine's uniform 503:
- Environments, deployments, and approvals routes are mounted only when their
backing store is wired. A deployment that does not wire them never registers
the routes, so a request returns
404 Not Found(the path is unknown), not503. The deployments routes additionally need the environments store; if a deployment wires the deployments service without the environments store,POST .../deploymentsanswers503 Service Unavailable(environments store not configured). - Blue/green routes are always mounted and answer
503 Service Unavailable(bluegreen not configured) when the blue/green service or the environments store is not wired. The three mutations (split,flip,rollback) also answer503when no traffic-router driver is registered for the environment's router kind — the driver set is an operator choice (nginxandshellare available;kubernetesis not).
Authorization
Unlike the pipeline endpoints above, this family does not gate on a CI OAuth2 or PAT scope. Authorization is the caller's effective repository role (see conventions.md for credential types). A valid bearer credential is required on every endpoint; the role then carries the permission the operation needs:
| Operation | Permission | Lowest role that carries it |
|---|---|---|
| Read environments, deployments, approvals, blue/green state | environment:read |
repo read |
| Trigger a deployment | deployment:trigger |
repo write |
| Create an environment | environment:create |
repo admin |
| Update / delete an environment, manage reviewers, drive blue/green | environment:update / environment:delete |
repo admin |
| Add a deployment status, approve / reject a deployment | deployment:approve |
repo admin |
The repository owner and instance admins hold every permission; collaborator and team-group grants contribute their role's permissions.
Read and write paths differ in how they treat a repository the caller cannot see:
- Reads apply the repository's three-tier visibility model. A
publicorinternalrepository is readable by any authenticated caller that holdsenvironment:read; aprivaterepository is readable only by a member (owner, instance admin, or a collaborator / team-group grant). A non-member's read of a private repository's environments or deployments returns404 Not Found, never403, so the repository's existence is not disclosed. An authenticated caller who can see apublic/internalrepository but lacksenvironment:readgets403 Forbidden. - Writes resolve the repository and then check the permission directly: a
missing repository is
404, and a caller that lacks the required permission is403 Forbidden(requires <permission>).
A deployment id is global, not repo-scoped; every deployment endpoint binds the
id to the {owner}/{repo} in the path through the deployment's environment. A
deployment whose environment belongs to a different repository is an
enumeration-resistant 404.
Endpoints
| Method | Path | Summary |
|---|---|---|
GET |
/api/v1/repos/{owner}/{repo}/environments |
List environments |
POST |
/api/v1/repos/{owner}/{repo}/environments |
Create an environment |
GET |
/api/v1/repos/{owner}/{repo}/environments/{name} |
Fetch one environment |
PATCH |
/api/v1/repos/{owner}/{repo}/environments/{name} |
Update an environment |
DELETE |
/api/v1/repos/{owner}/{repo}/environments/{name} |
Delete an environment |
POST |
/api/v1/repos/{owner}/{repo}/environments/{name}/reviewers |
Add a reviewer |
DELETE |
/api/v1/repos/{owner}/{repo}/environments/{name}/reviewers/{reviewer_id} |
Remove a reviewer |
GET |
/api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen |
Fetch blue/green state |
PATCH |
/api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen/split |
Set the traffic split |
POST |
/api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen/flip |
Flip blue/green |
POST |
/api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen/rollback |
Roll back the split |
POST |
/api/v1/repos/{owner}/{repo}/deployments |
Create a deployment |
GET |
/api/v1/repos/{owner}/{repo}/deployments |
List deployments |
GET |
/api/v1/repos/{owner}/{repo}/deployments/{id} |
Fetch a deployment with its statuses |
POST |
/api/v1/repos/{owner}/{repo}/deployments/{id}/statuses |
Append a deployment status |
POST |
/api/v1/repos/{owner}/{repo}/deployments/{id}/approve |
Approve a deployment |
POST |
/api/v1/repos/{owner}/{repo}/deployments/{id}/reject |
Reject a deployment |
GET |
/api/v1/repos/{owner}/{repo}/deployments/{id}/approvals |
List a deployment's approvals |
Environments
An environment is a named release target (production, staging, a per-PR
preview, …) scoped to one repository. A name is unique per repository and must
match [A-Za-z0-9][A-Za-z0-9._/-]{0,63}. An environment carries an optional URL
template, a deployment tier, a set of protection rules (enforced when a
deployment is approved), an allow-list of deploy branches, and an optional
auto-stop interval for transient environments.
The environment object is:
{
"id": "e1a2b3c4-...",
"repo_id": "3b8e...",
"name": "production",
"url_template": "https://{branch}.example.com",
"deployment_tier": "production",
"protection_rules": {
"required_reviewers": ["7f000000-..."],
"required_review_count": 1,
"wait_minutes": 5,
"deployment_branches": ["main", "release/*"],
"prevent_self_review": true
},
"deploy_branches": ["main"],
"auto_stop_after_interval": 3600000000000,
"transient": false,
"created_at": "2026-02-19T09:31:00Z",
"updated_at": "2026-02-19T09:31:00Z"
}
deployment_tier is one of production, staging, development, preview.
protection_rules is always present (an empty object when no rules are set);
its sub-fields are omitted when zero. Note the asymmetry on
auto_stop_after_interval: a request supplies it as a duration string
(5m, 2h, 1d), while the response returns it as an integer of
nanoseconds (and omits it when unset). url_template is omitted when unset.
GET /api/v1/repos/{owner}/{repo}/environments
List every environment defined on the repository. Requires environment:read.
Returns a bare JSON array of environment objects (not the page envelope); the
list is not paginated.
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks environment:read. |
404 Not Found |
Repository missing, or private and not readable. |
Example
curl -H "Authorization: Bearer <token>" \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/environments
POST /api/v1/repos/{owner}/{repo}/environments
Create an environment. Requires environment:create.
Request body
{
"name": "production",
"url_template": "https://{branch}.example.com",
"deployment_tier": "production",
"protection_rules": {
"required_review_count": 1,
"deployment_branches": ["main"],
"prevent_self_review": true
},
"deploy_branches": ["main"],
"auto_stop_after_interval": "2h",
"transient": false
}
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Unique per repository; must match [A-Za-z0-9][A-Za-z0-9._/-]{0,63}. |
url_template |
string | no | Display URL template for the deployed environment. |
deployment_tier |
string | no | One of production, staging, development, preview. |
protection_rules |
object | no | Approval / branch / wait rules; see Deployment approvals. |
deploy_branches |
array of string | no | Branch allow-list for this environment. |
auto_stop_after_interval |
string | no | Duration form 5m, 2h, 1d. Required when transient is true. |
transient |
boolean | no | Marks a short-lived environment. Requires auto_stop_after_interval. |
Response
201 Created with the environment object.
Status codes
| Status | When |
|---|---|
201 Created |
Environment created. |
400 Bad Request |
Malformed body. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:create. |
404 Not Found |
Repository missing or not readable. |
409 Conflict |
An environment with that name already exists for the repository. |
422 Unprocessable Entity |
Invalid name, unknown deployment_tier, an unparseable auto_stop_after_interval, or transient: true without auto_stop_after_interval. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name":"production","deployment_tier":"production"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/environments
GET /api/v1/repos/{owner}/{repo}/environments/{name}
Fetch one environment by name. Requires environment:read.
Status codes
| Status | When |
|---|---|
200 OK |
Environment returned. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks environment:read. |
404 Not Found |
Environment or repository missing, or repository private and not readable. |
PATCH /api/v1/repos/{owner}/{repo}/environments/{name}
Update an environment in place. Requires environment:update. Every field is
optional; an omitted field is left unchanged. Sending
"auto_stop_after_interval": "" clears the interval.
Request body
{
"url_template": "https://{branch}.staging.example.com",
"deployment_tier": "staging",
"protection_rules": { "required_review_count": 2 },
"deploy_branches": ["main", "release/*"],
"auto_stop_after_interval": "1d",
"transient": true
}
| Field | Type | Description |
|---|---|---|
url_template |
string | Replace the URL template. |
deployment_tier |
string | Replace the tier (production / staging / development / preview). |
protection_rules |
object | Replace the protection rules. |
deploy_branches |
array of string | Replace the deploy-branch allow-list. |
auto_stop_after_interval |
string | Duration form; empty string clears it. |
transient |
boolean | Replace the transient flag. |
Response
200 OK with the updated environment object.
Status codes
| Status | When |
|---|---|
200 OK |
Environment updated. |
400 Bad Request |
Malformed body. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:update. |
404 Not Found |
Environment or repository missing or not readable. |
422 Unprocessable Entity |
Unknown deployment_tier or an unparseable auto_stop_after_interval. |
DELETE /api/v1/repos/{owner}/{repo}/environments/{name}
Delete an environment. Requires environment:delete.
Status codes
| Status | When |
|---|---|
204 No Content |
Environment deleted. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:delete. |
404 Not Found |
Environment or repository missing or not readable. |
Environment reviewers
Reviewers gate a deployment to an environment whose protection rules require approval. A reviewer entry names either a single user or a group, never both. There is no list-reviewers endpoint; manage reviewers by their returned id.
The reviewer object is:
{
"id": "5e1ec70r-...",
"environment_id": "e1a2b3c4-...",
"user_id": "7f000000-...",
"prevent_self_review": true,
"created_at": "2026-02-19T09:31:00Z"
}
Exactly one of user_id / group_id is present.
POST /api/v1/repos/{owner}/{repo}/environments/{name}/reviewers
Add a reviewer to an environment. Requires environment:update. Supply exactly
one of user_id or group_id.
Request body
{
"user_id": "7f000000-...",
"prevent_self_review": true
}
| Field | Type | Required | Description |
|---|---|---|---|
user_id |
string (uuid) | one of | A single reviewer user. |
group_id |
string (uuid) | one of | A reviewer group; its members may approve. |
prevent_self_review |
boolean | no | Whether this reviewer may approve a deployment they created. Defaults to true. The 403 self_review_blocked gate on approve / reject is driven by the environment's protection_rules.prevent_self_review. |
Response
201 Created with the reviewer object.
Status codes
| Status | When |
|---|---|
201 Created |
Reviewer added. |
400 Bad Request |
Malformed body. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:update. |
404 Not Found |
Environment or repository missing or not readable. |
422 Unprocessable Entity |
Neither or both of user_id / group_id supplied. |
DELETE /api/v1/repos/{owner}/{repo}/environments/{name}/reviewers/{reviewer_id}
Remove a reviewer. Requires environment:update.
Status codes
| Status | When |
|---|---|
204 No Content |
Reviewer removed. |
400 Bad Request |
Malformed reviewer UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:update. |
404 Not Found |
Reviewer, environment, or repository missing or not readable. |
Blue/green traffic split
Each environment may carry a blue/green traffic-split state: a traffic_split
percentage routed to the green (candidate) side, a drain timeout, the blue and
green deployment ids, and the router kind that fronts them. split adjusts the
percentage, flip promotes the green candidate to blue (routing 100% to it,
then swapping the pointers and persisting traffic_split: 0), and rollback
returns the split to 0.
The blue/green state object is:
{
"environment_id": "e1a2b3c4-...",
"blue_deployment_id": "d1...",
"green_deployment_id": "d2...",
"traffic_split": 25,
"drain_timeout_sec": 30,
"last_flipped_at": "2026-02-19T09:40:00Z",
"router_kind": "nginx",
"router_config": {},
"updated_at": "2026-02-19T09:41:00Z"
}
traffic_split is the percentage routed to green, in [0, 100]. router_kind
is one of nginx, kubernetes, shell. router_config is always present and
defaults to {}. blue_deployment_id, green_deployment_id, and
last_flipped_at are omitted when unset. An environment that has never had
blue/green configured has no state row — the read returns 404 and callers
treat that as "blue/green not configured" for the environment.
GET /api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen
Fetch the blue/green state for an environment. Requires environment:read.
Status codes
| Status | When |
|---|---|
200 OK |
State returned. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks environment:read. |
404 Not Found |
Environment or repository missing or not readable, or no blue/green state is configured for the environment. |
503 Service Unavailable |
Blue/green or the environments store is not wired on this deployment. |
PATCH /api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen/split
Set the percentage of traffic routed to the green side. Requires
environment:update. traffic_split is required; drain_timeout_sec is
optional and keeps the persisted value when omitted.
Request body
{
"traffic_split": 25,
"drain_timeout_sec": 30
}
| Field | Type | Required | Description |
|---|---|---|---|
traffic_split |
integer | yes | Percentage routed to green, 0–100. |
drain_timeout_sec |
integer | no | Seconds to drain the previous side; must be >= 0. Defaults to the persisted value. |
Response
200 OK with the updated blue/green state object.
Status codes
| Status | When |
|---|---|
200 OK |
Split applied. |
400 Bad Request |
Malformed body. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:update. |
404 Not Found |
Environment or repository missing or not readable, or no blue/green state is configured. |
422 Unprocessable Entity |
traffic_split missing or outside [0, 100], or a negative drain_timeout_sec. |
503 Service Unavailable |
Blue/green or the environments store is not wired, or no driver is registered for the environment's router kind. |
POST /api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen/flip
Promote the green candidate to blue: route 100% to green, drain the previous
side, swap the blue/green pointers, and persist traffic_split: 0 with a fresh
last_flipped_at. Requires environment:update.
Response
200 OK with the updated blue/green state object.
Status codes
| Status | When |
|---|---|
200 OK |
Flip applied. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:update. |
404 Not Found |
Environment or repository missing or not readable, or no blue/green state is configured. |
503 Service Unavailable |
Blue/green or the environments store is not wired, or no driver is registered for the environment's router kind. |
POST /api/v1/repos/{owner}/{repo}/environments/{name}/bluegreen/rollback
Return the split to 0 (back to the blue side) using an immediate drain. The
pointers are not swapped, so a subsequent flip can promote green again. Requires
environment:update.
Response
200 OK with the updated blue/green state object.
Status codes
| Status | When |
|---|---|
200 OK |
Rollback applied. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks environment:update. |
404 Not Found |
Environment or repository missing or not readable, or no blue/green state is configured. |
503 Service Unavailable |
Blue/green or the environments store is not wired, or no driver is registered for the environment's router kind. |
Deployments
A deployment records a release of one commit to one environment, plus the
ordered history of its status transitions. A deployment's latest_state is
drawn from this closed set:
| State | Meaning |
|---|---|
queued |
Created and awaiting promotion (e.g. protection checks). |
pending |
Accepted; not yet executing. |
in_progress |
Executing. |
success |
Released; marked the environment's current deployment. |
failure |
The release failed. |
error |
The release errored (including a protection block). |
inactive |
Superseded or stopped. |
Status transitions are validated against this graph; an illegal move is 409:
queued → pending | in_progress | failure | error | inactive
pending → in_progress | failure | error | inactive
in_progress → success | failure | error | inactive
success → inactive
failure → inactive
error → inactive
inactive → (terminal)
A transition to success marks the deployment as the environment's current
deployment and demotes any prior current row. Self-transitions are not legal.
The deployment object is:
{
"id": "d1a2b3c4-...",
"environment_id": "e1a2b3c4-...",
"workflow_run_id": "00000000-0000-0000-0000-000000000000",
"ref": "refs/heads/main",
"sha": "1a2b3c4d...",
"artifact_id": "a17fac70-...",
"creator_id": "7f000000-...",
"created_at": "2026-02-19T09:31:00Z",
"current": false,
"latest_state": "queued",
"statuses": [
{
"id": "57a70000-...",
"deployment_id": "d1a2b3c4-...",
"state": "queued",
"created_at": "2026-02-19T09:31:00Z",
"creator_id": "7f000000-..."
}
]
}
statuses carries the full transition history and is returned only by the
single-deployment fetch — the list endpoint omits it. workflow_run_id is
always present and is the nil UUID when the deployment was not tied to a workflow
run. artifact_id is omitted when unset; each status's description, log_url,
environment_url, and creator_id are omitted when unset.
POST /api/v1/repos/{owner}/{repo}/deployments
Create a deployment against an existing environment. Requires
deployment:trigger. The environment named in the body must already exist. A new
deployment starts in queued with a seeded queued status.
Request body
{
"environment": "production",
"ref": "refs/heads/main",
"sha": "1a2b3c4d...",
"workflow_run_id": "9f1c2e7a-...",
"artifact_id": "a17fac70-..."
}
| Field | Type | Required | Description |
|---|---|---|---|
environment |
string | yes | Name of an existing environment. |
ref |
string | yes | The ref being deployed. |
sha |
string | yes | The commit SHA being deployed. |
workflow_run_id |
string (uuid) | no | Associate the deployment with a workflow run. |
artifact_id |
string (uuid) | no | Associate a built artifact. |
Response
201 Created with the deployment object (latest_state: "queued").
Status codes
| Status | When |
|---|---|
201 Created |
Deployment created. |
400 Bad Request |
Malformed body, or environment / ref / sha missing. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks deployment:trigger. |
404 Not Found |
Repository not readable, or the named environment does not exist. |
503 Service Unavailable |
The environments store is not wired on this deployment. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"environment":"production","ref":"refs/heads/main","sha":"1a2b3c4d"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/deployments
GET /api/v1/repos/{owner}/{repo}/deployments
List a repository's deployments. Requires environment:read. Returns a bare JSON
array of deployment objects (without statuses).
Query parameters
| Name | Type | Description |
|---|---|---|
environment |
string | Filter by environment name. An unknown name is ignored (the filter is dropped, so all deployments are returned). |
state |
string | Filter by deployment state. An unknown value is 400. |
current |
string | true returns only each environment's current deployment. |
limit |
integer | Maximum rows. Defaults to 50; a non-integer value falls back to the default. |
offset |
integer | Zero-based offset. Defaults to 0; a non-integer value falls back to the default. |
This endpoint uses the limit / offset form (see conventions.md) but returns a bare array rather than a count envelope.
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
400 Bad Request |
Unknown state filter value. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks environment:read. |
404 Not Found |
Repository missing, or private and not readable. |
GET /api/v1/repos/{owner}/{repo}/deployments/{id}
Fetch one deployment together with its full status history. Requires
environment:read.
Status codes
| Status | When |
|---|---|
200 OK |
Deployment returned (with statuses). |
400 Bad Request |
Malformed deployment UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks environment:read. |
404 Not Found |
Deployment or repository missing or not readable, or the deployment's environment belongs to another repository. |
POST /api/v1/repos/{owner}/{repo}/deployments/{id}/statuses
Append a status transition to a deployment. Requires deployment:approve (this
is the operator surface for driving a deployment forward; the engine writes
statuses internally). The new state must be a legal transition from the
deployment's current latest_state.
Request body
{
"state": "in_progress",
"description": "rolling out",
"log_url": "https://logs.example.com/abc",
"environment_url": "https://app.example.com"
}
| Field | Type | Required | Description |
|---|---|---|---|
state |
string | yes | Target state; must be a legal transition. |
description |
string | no | Human-readable note. |
log_url |
string | no | Link to the release log. |
environment_url |
string | no | Link to the live environment. |
Response
201 Created with the created status object.
Status codes
| Status | When |
|---|---|
201 Created |
Status appended. |
400 Bad Request |
Malformed deployment UUID, malformed body, or an unknown state value. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks deployment:approve. |
404 Not Found |
Deployment or repository missing or not readable. |
409 Conflict |
The transition from the current state to state is not legal. |
Deployment approvals
When an environment's protection rules require review, reviewers approve or
reject a queued deployment. Each vote is upserted, so a reviewer may switch
between approve and reject until the deployment is promoted or blocked. After a
vote is recorded the protection rules are re-evaluated against the full vote set,
which can move the deployment: a passing evaluation drives it to in_progress,
and a blocking evaluation drives it to failure.
Protection rules are evaluated in priority order: any rejection blocks; a deployment branch outside the allow-list blocks; an unexpired wait timer waits; an unmet required-review count waits; otherwise the deployment is promoted.
The approve / reject response carries the recorded vote and the resulting decision. These two objects use Go field-name casing (the fields are capitalized), unlike the snake_case used elsewhere on this page:
{
"approval": {
"ReviewerID": "7f000000-...",
"Action": "approved",
"Comment": "ship it",
"CreatedAt": "2026-02-19T09:45:00Z"
},
"decision": {
"State": "promote",
"Reason": "",
"WaitUntil": null,
"MissingReviewers": 0,
"OutstandingActors": null
}
}
decision.State is one of promote, wait, block. decision.Reason is a
stable discriminator — rejected_by_reviewer, branch_not_allowed,
waiting_timer, or awaiting_review — and is empty on promote. WaitUntil is
set when the decision is a timer wait; MissingReviewers is set when the
decision is awaiting more approvals.
POST /api/v1/repos/{owner}/{repo}/deployments/{id}/approve
Record the caller's approval of a deployment, then re-evaluate protection rules.
Requires deployment:approve.
Request body
{
"comment": "ship it"
}
| Field | Type | Required | Description |
|---|---|---|---|
comment |
string | no | Optional note recorded with the vote. |
Response
201 Created with the { "approval": ..., "decision": ... } object above.
Status codes
| Status | When |
|---|---|
201 Created |
Vote recorded and rules re-evaluated. |
400 Bad Request |
Malformed deployment UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller lacks deployment:approve, or self-review is blocked (self_review_blocked) because the caller created the deployment and the environment prevents self-review. |
404 Not Found |
Deployment or repository missing or not readable, or the deployment's environment belongs to another repository. |
Example
curl -X POST -H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"comment":"ship it"}' \
https://api.gitvetrix.com/api/v1/repos/alice/widgets/deployments/d1a2b3c4-.../approve
POST /api/v1/repos/{owner}/{repo}/deployments/{id}/reject
Record the caller's rejection of a deployment, then re-evaluate. A rejection
blocks the deployment (driving it to failure). Requires deployment:approve.
Same request body, response shape, and status codes as approve.
GET /api/v1/repos/{owner}/{repo}/deployments/{id}/approvals
List the votes recorded for a deployment, ordered oldest-first. Requires
environment:read. Returns a bare JSON array of approval objects (the same
Go-cased fields as the approval object above):
[
{
"ReviewerID": "7f000000-...",
"Action": "approved",
"Comment": "ship it",
"CreatedAt": "2026-02-19T09:45:00Z"
}
]
Status codes
| Status | When |
|---|---|
200 OK |
Listing returned (possibly empty). |
400 Bad Request |
Malformed deployment UUID. |
401 Unauthorized |
No valid credential. |
403 Forbidden |
Caller can see the repository but lacks environment:read. |
404 Not Found |
Deployment or repository missing or not readable, or the deployment's environment belongs to another repository. |
Errors
These endpoints use the shared error envelope and status-code conventions in
errors.md. A 503 Service Unavailable on this page means the
named feature — the CI engine, artifact-byte storage, branch pipeline variables,
the attestation service, or (for the deployments and environments family) the
blue/green service, its environments store, or a traffic-router driver — is
staged but not wired on this deployment, not that the request was wrong; the
response body names the missing component. For the deployments and environments
family specifically, an unwired environments / deployments / approvals route is
absent rather than 503, so its requests return 404.
Rate limits
These endpoints are metered under the standard scopes described in
rate-limits.md: reads against api.read and mutations
against api.write. Triggering and retrying a pipeline are additionally subject
to a per-repository / per-user trigger limit that answers 429 Too Many Requests
with Retry-After when exceeded.