Deployments Endpoints¶
Manage deployments, view the deploy queue, and access deployment history.
How a deploy travels¶
A deploy request is a single PostgreSQL write:
- A
deploymentsrow is created with statusqueued. - A queue action is written to the same database — the agent polls it.
The agent updates both as it progresses. Cancel and undeploy write to the same place; there is no second store to keep in sync.
Environment
None of these routes take an environment parameter — the API process is
bound to a single environment and resolves it server-side.
Group scoping
Every route is caller-scoped. Actions and deployments belonging to another group return 404, never 403. Id lookups accept a prefix, but it must be at least 8 hex-or-hyphen characters — a shorter or wildcard-shaped value is rejected rather than matched loosely.
Endpoints¶
| Method | Path | Scope | Description |
|---|---|---|---|
GET |
/api/deployments?app_name= |
read | List deployments |
GET |
/api/deployments/history?app_name= |
read | Deployment history |
GET |
/api/deployments/queue?app_name=&all=&exclude_types=&limit= |
read | View queue actions |
GET |
/api/deployments/queue/{action_id} |
read | Get any queue action by id |
GET |
/api/deployments/queue/{action_id}/diagnose |
read | Why is this action stuck? |
GET |
/api/deployments/{deployment_id} |
read | Get deployment details |
GET |
/api/deployments/{deployment_id}/logs |
read | Deployment logs |
POST |
/api/deployments |
deploy | Queue a new deployment → 202 |
POST |
/api/deployments/queue/prioritize |
deploy | Push queued actions to the head of the queue |
POST |
/api/deployments/queue/{action_id}/admit-force |
admin | Single-use admission override |
POST |
/api/deployments/{deployment_id}/cancel |
deploy | Cancel a deployment |
POST |
/api/deployments/{app_name}/undeploy |
deploy | Undeploy an app → 202 |
List Deployments¶
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
app_name |
string | — | Filter by app. Omit to see every app the caller can reach. |
limit |
integer | 50 |
Max results (1-200) |
offset |
integer | 0 |
Pagination offset |
Response: 200 OK
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"deployment_id": "abc12345",
"app_name": "myapp",
"branch": "main",
"image_tag": "main-abc12345",
"status": "running",
"phase": "completed",
"previous_tag": "main-def67890",
"created_by": "alice",
"created_at": "2026-03-15T09:10:00Z",
"updated_at": "2026-03-15T09:15:00Z",
"completed_at": "2026-03-15T09:15:00Z"
}
],
"total": 1
}
Operator-only fields
placement_plan, instance_status, leader_instance and
infrastructure describe which host ran what. They are nulled for
tenant callers — the fields are always present in the schema, but only
a platform operator sees values in them.
Get Deployment¶
Response: 200 OK — single DeploymentResponse.
Errors: 404 Not Found — unknown deployment, or it belongs to another group.
Deployment Logs¶
Returns the per-phase output the agent stamped for that action, plus its error message when it failed.
Errors:
404 Not Found— unknown action, or it belongs to another group.422 Unprocessable Entity— the id prefix is too short or malformed.
Queue Deployment¶
Scope: deploy or higher.
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
app_name |
string | yes | — | App to deploy |
branch |
string | no | main |
Branch |
image_tag |
string | no | — | Specific image tag to deploy |
services |
array | no | — | Service definitions |
placement_plan |
object | no | — | Host placement plan |
There is no environment field, and no replicas field — a
deploy-time replica override was removed as dead code (nothing ever read it;
ARCHIVED-BUGS #1196). The declared count lives in appmecca.yaml and is
changed with
PATCH /api/apps/{app_name}/services/{service_name}/replicas.
A type: static app may additionally supply
six fields, all ignored for container apps. They fall into two classes, and the
difference is not cosmetic:
| Field | Behaviour |
|---|---|
aliases, record_fqdn, cert_arn_us_east_1 |
Authoritative. Declared nowhere in the app's own config, so they always apply. |
spa_fallback, index_document, error_document |
Advisory. Applied only when the app declares no value for them. A static_site: block in the app's appmecca.yaml — per-service or top-level — wins. |
The advisory three used to appear authoritative on the no-build path only, because the deploy carried a truncated copy of the app's config and so had nothing to lose to. Since BUGS #907 the agent resolves the app's own effective config, and both deploy paths agree: a value the tenant committed to their repository is not overridden by a one-off API call.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
skip_preflight |
boolean | false |
Bypass the git-repo preflight gate |
Behavior:
- Creates a deployment record with status
queued. - Queues the action for the agent.
- Returns immediately — the agent picks up the action and executes.
Response: 202 Accepted
{
"deployment_id": "abc12345",
"action_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"pg_id": "660e8400-e29b-41d4-a716-446655440001"
}
Errors:
400 Bad Request— validation failure (e.g. no successful build for the requested tag).404 Not Found— app does not exist in the caller's scope.409 Conflict— the app is quarantined by the git-repo preflight, or the group is over its storage entitlement.409 Conflict— the app name and branch exist in more than one group, so the deploy cannot be attributed to one of them. Nothing is queued. The detail names the groups. Re-send the request with?group=<slug>for the one you mean.
Only a caller that sees every group, such as a platform admin or the legacy admin api-key, can get the second 409. A caller scoped to one group resolves the name inside that group, where it is unique.
Cancel Deployment¶
Scope: deploy or higher.
Request body (optional):
Behavior:
- A queued action is cancelled outright.
- An in-flight action is flagged for cancellation; the agent checks between phases and stops cleanly.
- Cancelling a build cascades to the deploy that depends on it — except when the parent is refused (below), in which case nothing is cascaded.
- ⚠ A DEPLOY past its blue/green swap commit is REFUSED (BUGS #885). The swap commits at the tail of the health-check phase; from there the new containers are the live ones, so aborting would kill them. The row is left untouched, the deploy rolls forward to
completed, and the response says so.
Response: 200 OK — always inspect status; a 200 is not a
cancellation.
{
"deployment_id": "abc12345",
"status": "cancelled",
"cancelled": true,
"cancel_requested": false,
"queue_status": "cancelled",
"dependents_cancelled": 1,
"app_name": "shop"
}
status values:
| Value | Meaning |
|---|---|
cancelled |
A pre-work row was flipped to the cancelled terminal. |
cancel_requested |
An in-flight row was flagged; the agent aborts between phases. |
refused_past_point_of_no_return |
The blue/green swap has committed — the deploy will finish. Roll back with mecca app deploy <app> --image-tag <prev> --no-build. |
not_found |
No queue action matches the id. |
not_cancellable |
The row is already terminal. |
⚠ Clients must branch on the VALUE. An unknown value must render as a non-success — the refusal was added after the first four, and treating any 200 as a cancellation is what let both CLIs and the dashboard report success over an action that was about to take an environment down.
Errors:
404 Not Found— unknown action, or it belongs to another group.422 Unprocessable Entity— the id prefix is too short or malformed.
Undeploy¶
Scope: deploy or higher.
Request body:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
branch |
string | no | resolved | Branch. Omit and the API resolves the app's live branch. |
purge |
boolean | no | false |
Also delete registry data (images, builds) |
Omitting branch is the safe default: exactly one running branch resolves to
that branch; several running branches is a 409 telling you to name one;
none running falls back to the registry.
Response: 202 Accepted
{
"action_id": "550e8400-e29b-41d4-a716-446655440000",
"app_name": "myapp",
"status": "queued",
"purge": false
}
View Deploy Queue¶
Returns queue actions — by default only the non-terminal ones, i.e. what the agent has not finished yet.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
app_name |
string | — | Filter by app. Omit to see every app the caller can reach. |
all |
boolean | false |
Include completed / failed / cancelled actions |
exclude_types |
string | — | Comma-separated action types to exclude, e.g. BACKUP_QUERY,RECONCILE_ROUTING |
limit |
integer | repository default (100) | Max rows, 1–500 |
Filtering out agent plumbing (exclude_types)¶
The result set is capped before any client-side filtering, so a caller that
discards agent-internal rows after the fact spends its whole page budget on rows
it throws away. Measured on a live environment, one app-scoped page of 100 was
96 BACKUP_QUERY + 2 DEPLOY + 2 BUILD — the app's real deploy history
was pushed off the end of the page.
exclude_types moves that filter server-side:
GET /api/deployments/queue?all=true&app_name=myapp
&exclude_types=BACKUP_QUERY,RECONCILE_ROUTING,GIT_PREFLIGHT&limit=101
- Optional and caller-supplied — there is no server-side default. Surfaces
like
mecca queue listmust keep seeing every action type, so the API stays presentation-neutral. Omit the parameter and the response is unchanged. - It excludes, it never includes. An action type this API has never heard of stays VISIBLE. Naming an unknown type is a harmless no-op, so a newer client can talk to an older API.
- It can only narrow within what the caller may already see — group scoping is applied independently and is unaffected.
Detecting truncation exactly¶
Ask for one more row than you intend to render. Receiving limit rows is
ambiguous at the boundary; receiving limit + 1 proves more existed:
⚠ Terminal queue rows are pruned after 30 days, so no page size reaches further back than that. Surface that horizon rather than presenting a bounded list as a complete history.
Response: 200 OK — a bare array of actions.
[
{
"action_id": "550e8400-e29b-41d4-a716-446655440000",
"app_name": "myapp",
"branch": "main",
"action_type": "DEPLOY",
"status": "queued",
"image_tag": "main-abc12345",
"commit_message": "Fix the checkout total rounding",
"commit_author": "Alice Smith",
"depends_on": null,
"auto_deploy": true,
"admission_check_reason": null,
"last_admission_check_at": null,
"stuck_class": null,
"deploy_warnings": [],
"created_at": "2026-03-15T09:10:00Z",
"updated_at": "2026-03-15T09:10:00Z"
}
]
| Field | Meaning |
|---|---|
depends_on |
The action this one waits for — a deploy queued alongside its build points at the build |
admission_check_reason |
Why the agent last refused to claim this action (e.g. it would push the fleet past its database-connection budget). Cleared on a successful claim. |
last_admission_check_at |
When that decision was last evaluated — distinguishes "refused two seconds ago" from "nothing has ever looked at this row" |
stuck_class |
A classification computed from the timestamps: never_claimed, admission_refused, in_progress, build_in_progress, or null when the row is simply fresh |
deploy_warnings |
Non-default decisions the agent applied, e.g. raising a service's memory limit above the platform default because it detected a JVM runtime |
service_name |
For a SERVICE_ACTION row, the service it targeted; null for every other action type |
action |
For a SERVICE_ACTION row, the verb: restart, stop or start; null otherwise. These two are the only parts of the action's config that are returned |
Get any action by id¶
Resolves any queue action by its globally-unique id — deploys, builds, restores, exec runs and everything else — so a caller holding an id needs no group or branch selector. Authorised against the action's owner, so it works regardless of which group you currently have selected.
Diagnose a stuck action¶
Composes a narrative for "why is this not moving": the action's own state, the admission-gate state, the action it depends on, the older action for the same app that is blocking it, and one actionable hint naming the next command to run. Read-only.
Prioritize¶
Scope: deploy or higher.
target is either an app slug — which bumps every non-terminal action for
that app — or an action-id prefix, which bumps that single action and cascades
to the deploy depending on it when the match is a build. The target must
resolve inside the caller's group.
Admission Override¶
Scope: admin — and ownership of the action (BUGS #1082).
Writes a single-use override so the agent admits this one action past the
database-connection budget gate on its next poll. reason is required and is
audited alongside the caller. ttl_seconds is clamped to 60–3600; the 300-second
default is long enough for the poll cycle to see it, short enough that a
forgotten override cannot wake up later and admit an unrelated retry.
⚠ admin scope alone is not the boundary, and never was. A group OWNER
resolves to admin, and every self-registered user owns a personal group — so
before BUGS #1082 this route was reachable by any signed-up tenant against
any action in the environment. Authorization is now ownership of the
action: the caller must be a platform operator, or scoped to / a member of the
group that owns the app the action belongs to. It is deliberately not
operator-only — it is a tenant's escape hatch for their own held-back deploy,
which is what mecca queue diagnose composes it for.
{action_id} must be a full UUID or a prefix of at least 8 characters drawn
from the id charset (hex and hyphens). Shorter or out-of-charset values are
refused before any lookup.
Errors:
400 Bad Request— the action is no longerqueued. Reaching this response proves you own the row, so the current status is echoed back to you.404 Not Found— returned identically for an unknown action, a malformed or too-short id, and an action you do not own. ⚠ The three are byte-identical by design: a response that distinguished "exists, not yours" from "nothing matches" would turn the route into a cross-tenant action-id oracle — which is precisely how it was used to enumerate live ids one character at a time before this was closed.
Deployment History¶
Returns immutable audit records of past deployments.
Query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
app_name |
string | required | App name |
limit |
integer | 50 |
Max results (1-200) |
offset |
integer | 0 |
Pagination offset |
Response: 200 OK
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"deployment_id": "abc12345",
"app_name": "myapp",
"branch": "main",
"image_tag": "main-abc12345",
"previous_tag": "main-def67890",
"status": "completed",
"duration_seconds": 45,
"env_vars_snapshot": {"LOG_LEVEL": "info"},
"created_by": "alice",
"started_at": "2026-03-15T09:14:15Z",
"completed_at": "2026-03-15T09:15:00Z"
}
],
"total": 1
}
Deployment Statuses¶
| Status | Description |
|---|---|
queued |
Waiting for the agent to pick it up |
setup_in_progress |
The elected leader is preparing routing |
ready_to_deploy |
Routing ready, containers deploying |
in_progress |
Containers being replaced |
running |
Deployment active and healthy |
completed |
Deployment finished successfully |
failed |
Deployment failed (see error_message) |
cancelled |
Cancelled by user |
undeploying |
Undeploy in progress |
Data Model¶
Deployments use two PostgreSQL tables:
deployments— Active deployment state (one row per app/env/branch, upserted on each deploy)deployment_history— Immutable audit records (one row per completed deployment, never updated)
See Schema Reference for full table definitions.