Automating releases with a service client

This guide shows how a pipeline or another machine caller drives Stella Ops releases without a human session: it registers an OAuth service client in Authority, obtains tokens with the client-credentials grant, and calls the Release Orchestrator API or the stella release commands. Policy gates, the scanner-pass floor and deployment preflight are enforced by the server for these callers exactly as for a person. The only override is a signed, audited gate exception that needs the release:gate-bypass scope and an operator-signed decision; a pipeline client should not hold that scope.

Implementation status. Everything below exists in the shipped source. Two limits matter for automation:

  • stella release covers approval decisions, promote, deploy, rollback and show. Creating a release, listing releases and marking a release ready are API calls only; the CLI has no command for them (src/Cli/StellaOps.Cli/Commands/ReleaseCommandGroup.cs).
  • There is no MCP server or other agent-specific interface. An automated agent uses the same OAuth client and HTTP API as any other caller.

1. What the caller needs

ItemValue
Authority token endpoint/connect/token on the installation’s gateway, which forwards /connect to Authority
Release API basehttps://<stella-host>/api/v1/release-orchestrator
Grant typeclient_credentials
Audienceapi://release-orchestrator (Release Orchestrator also accepts stellaops)
Tenantthe tenant the releases belong to; sent as tenant on the token request and as the X-StellaOps-TenantId header on API calls

Scopes, from StellaOpsScopes.cs, mapped to the Release Orchestrator policies that require them (ReleaseOrchestratorPolicies):

ScopeLets the caller
release:readlist and read releases and their components (every release route requires it)
release:writecreate a release and mark it ready
release:publishpromote, deploy and roll back a release, and approve or reject an approval

Promote, deploy and roll back also succeed without release:publish when the caller holds Deployer access to both the release and the target environment through resource grants, or an accepted Operate & Deploy share grant for that release and environment. A pipeline client normally uses the scope.

Grant only what the pipeline does. A pipeline that only creates releases and asks for promotion needs release:read, release:write and release:publish; a read-only reporter needs release:read. There is no separate deploy scope.

2. Register a dedicated client

Use one confidential client per pipeline, so its secret can be rotated and its grants audited on their own. An administrator registers it through the Authority console admin API (POST /console/admin/clients). The call needs a fresh administrator session carrying ui.admin and authority:clients.write, and the X-StellaOps-TenantId header. An administrator can only register a client for their own tenant, with scopes their own token carries.

POST /console/admin/clients HTTP/1.1
Host: <stella-host>
Authorization: Bearer <fresh administrator token>
X-StellaOps-TenantId: <tenant>
Content-Type: application/json

{
  "clientId": "ci-release-pipeline",
  "displayName": "CI release pipeline",
  "grantTypes": ["client_credentials"],
  "scopes": ["release:read", "release:write", "release:publish"],
  "tenant": "<tenant>",
  "requireClientSecret": true,
  "audiences": ["api://release-orchestrator"]
}

The answer is 201 Created with the client summary; 409 means the client id exists. Rotate the secret with POST /console/admin/clients/<clientId>/rotate (client secret rotation). Keep the secret in the pipeline’s secret store; never in a repository or a command line.

Installations seed a broad automation client, stellaops-cli-automation, for local and bootstrap use. Its scope list is far wider than a release pipeline needs and it serves two tenants, so production pipelines should use a dedicated client as above.

3. Obtain a token

Send a form-encoded (application/x-www-form-urlencoded) request to the token endpoint from section 1:

FieldValue
grant_typeclient_credentials
client_idthe client id, for example ci-release-pipeline
client_secretthe secret, read from the pipeline’s secret store
tenantthe tenant the client works in
scopethe scopes the run needs, space-separated, for example release:read release:write release:publish

The answer carries access_token, token_type (Bearer) and expires_in. The token’s stellaops:tenant claim names the selected tenant and its aud claim lists the client’s audiences.

Authority checks, in order (ClientCredentialsHandlers.cs): the client exists and is enabled, allows client_credentials, and presents its secret; the requested tenant is assigned to the client (otherwise invalid_request, “Requested tenant is not assigned to this client.”); and every requested scope is allowed (otherwise invalid_scope). A request without tenant uses the client’s default tenant; a client assigned to several tenants without a default must name one (“Tenant selection is required for this client.”). A request without scope receives every scope the client is allowed. Each refusal is 400 with an OAuth error body, for example:

{
  "error": "invalid_scope",
  "error_description": "Scope 'release:gate-bypass' is not allowed for this client."
}

4. Drive a release through the API

Every call carries Authorization: Bearer <token> and X-StellaOps-TenantId: <tenant>.

StepRequestScopeSuccessRefusals
CreatePOST /api/v1/release-orchestrator/releases with name, version, optional targetEnvironment, components[] (each with name, imageRef/digest, tag, version)release:write201, Location of the new release (Draft)409 same name and version exists
ReadGET /api/v1/release-orchestrator/releases/{id}; list with GET /api/v1/release-orchestrator/releasesrelease:read200404
ReadyPOST /api/v1/release-orchestrator/releases/{id}/readyrelease:write200409 release_not_in_draft, release_has_no_components
PromotePOST /api/v1/release-orchestrator/releases/{id}/promote with targetEnvironment and optional justificationrelease:publish200 with the approval summary; an auto-approved promotion triggers its deploymentsee below
DeployPOST /api/v1/release-orchestrator/releases/{id}/deployrelease:publish202, Location: /api/v1/release-orchestrator/deployments/{id}see below
Roll backPOST /api/v1/release-orchestrator/releases/{id}/rollbackrelease:publish200 with the release, status rolled_back404, 403

Pin components by digest. A release shows each component’s sha256 digest beside its version.

A rollback records the release as rolled_back and clears its current environment. It does not dispatch a deployment of an earlier release; promote and deploy that release to restore it.

5. How a gate refusal reaches the caller

Gates are evaluated by the server; the caller sees the outcome in the response body and decides whether to stop or wait. No field of these requests turns a gate off.

ResponseBodyMeaning
403{"error":"forbidden","reason":"deploy_access_denied","releaseRole":...,"environmentRole":...,"requiredRole":"deployer",...}promote or deploy without release:publish, where resource grants apply: the caller lacks Deployer on the release, the environment or both
403{"error":"forbidden","message":"release:publish or an Operate & Deploy share grant is required."}promote, deploy or rollback without release:publish or a share grant
403{"error":"forbidden","message":"Authorization failed: missing required claim","requiredClaimType":"scope","requiredClaimValue":"release:write",...}a route policy refused the token, for example create or ready without release:write
409{"blocked":true,"reason":"target_environment_no_targets",...}the target environment has no deployment targets
409{"blocked":true,"reason":<release-truth code>,"message":...}release truth (approval, gate decision or verified evidence) is missing; promote and deploy fail closed
409{"blocked":true,"reason":<policy id>,"message":...,"approvalId":...,"policyId":...}a policy gate denied the promotion
409{"blocked":true,"waiting":true,"reason":...,"gateResults":[...],"approval":{...}}a gate is waiting (for example on SBOM results); poll the release or the approval and retry
403{"error":"blocked","floor":"vulnerability-scan","digest":...,"scanAction":...,"retryable":...}the deploy-time scanner-pass floor refused a component
403{"error":"blocked","reason":"preflight_failed","gate":"deploy-preflight","preflight":{...}}deployment preflight failed

A promotion that needs human approval returns 200 with a pending approval; a person, or a caller holding release:publish, approves it (stella release approve <approval-id>).

6. The same flow with stella release

The CLI authenticates with client credentials when no username or API key is configured (AuthorityTokenUtilities.cs):

export STELLAOPS_AUTHORITY_URL=https://<stella-host>
export STELLAOPS_AUTHORITY_CLIENT_ID=ci-release-pipeline
export STELLAOPS_AUTHORITY_CLIENT_SECRET="$(cat "$CLIENT_SECRET_FILE")"
export STELLAOPS_AUTHORITY_TENANT=<tenant>
export STELLAOPS_AUTHORITY_SCOPE="release:read release:write release:publish"
export STELLAOPS_RELEASE_ORCHESTRATOR_URL=https://<stella-host>

stella release show <release-id> --json
stella release promote <release-id> --to <environment> --reason "pipeline 1234"
stella release approve <approval-id> --comment "approved by pipeline policy"
stella release deploy <release-id>
stella release rollback <release-id>

promote sends --to as the target environment and --reason as the justification. deploy and rollback accept --reason, but the server request carries no reason field. Each command prints the server’s refusal (section 5) and exits non-zero. Signed operator decisions (--sign and its key options) are required only where the installation enables them (operator decision signing).