Skip to content

Deployments Endpoints

Manage deployments, view the deploy queue, and access deployment history.

How a deploy travels

A deploy request is a single PostgreSQL write:

  1. A deployments row is created with status queued.
  2. 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

GET /api/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

GET /api/deployments/{deployment_id}

Response: 200 OK — single DeploymentResponse.

Errors: 404 Not Found — unknown deployment, or it belongs to another group.


Deployment Logs

GET /api/deployments/{deployment_id}/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

POST /api/deployments

Scope: deploy or higher.

Request body:

{
    "app_name": "myapp",
    "branch": "main",
    "image_tag": "main-abc12345"
}
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:

  1. Creates a deployment record with status queued.
  2. Queues the action for the agent.
  3. 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

POST /api/deployments/{deployment_id}/cancel

Scope: deploy or higher.

Request body (optional):

{
    "reason": "Rolling back to previous version"
}

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

POST /api/deployments/{app_name}/undeploy

Scope: deploy or higher.

Request body:

{
    "branch": "main",
    "purge": false
}
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

GET /api/deployments/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 list must 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:

limit = page_size + 1
truncated = len(rows) > page_size
rows = rows[:page_size]

⚠ 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

GET /api/deployments/queue/{action_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

GET /api/deployments/queue/{action_id}/diagnose

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

POST /api/deployments/queue/prioritize

Scope: deploy or higher.

{"target": "myapp"}

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

POST /api/deployments/queue/{action_id}/admit-force

Scope: admin — and ownership of the action (BUGS #1082).

{"reason": "urgent hotfix, accepting the tighter budget", "ttl_seconds": 300}

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 longer queued. 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

GET /api/deployments/history?app_name={app}

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.