Spice Cloud Deploy
ActionsAbout
Tags
(2)Deploy a Spice.ai Cloud app from any GitHub workflow using OAuth client credentials, then optionally smoke-test the deployed runtime with SQL, NSQL, search, chat, and MCP probes.
- OAuth client credentials — no long-lived personal tokens. Generate an OAuth client in the Spice Cloud Portal and store the secret in your repo's secrets.
- One step, full lifecycle — resolve or create the app, push your
spicepod.yaml, upsert app secrets, trigger the deployment, and (optionally) wait for it to finish. - Post-deploy smoke tests — verify the freshly-deployed runtime answers SQL, NSQL, search, chat-completion, and MCP requests before declaring the workflow green. SQL/NSQL/health probes use the official
@spiceai/spiceSDK. - Tags, region, replicas, channel — all the dials you'd expect, plus deployment metadata (
branch,commit_sha,commit_message) auto-populated from the GitHub event. - Cross-platform — Node 24 JavaScript action; runs on
ubuntu-latest,macos-latest, andwindows-latestrunners.
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: spicehq/spice-cloud-deploy-action@v1
with:
client-id: ${{ secrets.SPICE_CLIENT_ID }}
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
app-name: my-app
spicepod: spicepod.yaml
test-sql: SELECT 1The action returns once the deployment is succeeded (or fails the job if it's failed / timed-out).
- Sign in to the Spice.ai Portal.
- Open Profile → OAuth Clients and click Create.
- Grant the scopes you need (see the table below). The action will fail with
403 Forbiddenif a required scope is missing. - Copy the client ID and client secret — the secret is shown only once.
- In your GitHub repo (or org), add two secrets:
SPICE_CLIENT_IDandSPICE_CLIENT_SECRET.
The action exchanges the client credentials at https://spice.ai/api/oauth/token for a short-lived bearer token (cached for the run).
Grant exactly the scopes for the features you use. The "All-in" row at the bottom is what you'd typically pick for a CI client that does everything this action supports.
| Use this action to… | Required scopes |
|---|---|
| Resolve an existing app and trigger a deployment | apps:read, deployments:read, deployments:write |
Create the app on first run (create-app-if-missing: true) |
+ apps:write |
Push a spicepod.yaml manifest to the app before deploying |
+ apps:write |
Set or merge app tags |
+ apps:write |
Upsert app secrets before deploying |
+ secrets:write |
Run runtime smoke tests (test-sql, test-nsql, etc.) |
no extra scope — uses apps:read, already required |
| All-in (recommended for a single CI client) | apps:read apps:write deployments:read deployments:write secrets:write |
Avoid the
*wildcard scope in production — it grantsapps:delete,secrets:read(decrypted via the portal), andmembers:*, which this action never needs.
| Input | Required | Default | Description |
|---|---|---|---|
client-id |
yes | — | OAuth client ID. |
client-secret |
yes | — | OAuth client secret. Mark as a repo/env secret. |
app-id |
conditional | — | Numeric app ID. Either this or app-name is required. |
app-name |
conditional | — | App name. Required when create-app-if-missing is true. |
create-app-if-missing |
no | false |
Create the app if it doesn't exist (requires app-name and region). |
region |
conditional | — | Spice Cloud region (e.g. us-east-1, us-west-2). Required for new apps. |
visibility |
no | private |
public or private — only used on app creation. |
tags |
no | — | YAML or JSON map of tag key/value pairs. Merged into existing app tags. Tag values may contain only alphanumerics and _@-. The action auto-adds a repository tag from GITHUB_REPOSITORY (sanitizing / → _) unless you override it. |
spicepod |
no | spicepod.yaml |
Path to the Spicepod manifest. Pushed to the app before deploy when present. |
working-directory |
no | . |
Working directory used to resolve relative paths. |
image-tag |
no | — | Override the runtime image tag (e.g. 1.5.0-models). |
channel |
no | — | stable, preview, nightly, or internal. |
replicas |
no | — | Replica count for this deployment (1-10). |
branch |
no | ${{ github.ref_name }} |
Git branch attributed to the deployment. |
commit-sha |
no | ${{ github.sha }} |
Commit SHA attributed to the deployment. |
commit-message |
no | head-commit message | Commit message attributed to the deployment. |
debug |
no | false |
Enable runtime debug mode for this deployment. |
secrets |
no | — | YAML or JSON map of app secrets to upsert before deploy. Values are masked in logs. |
wait-for-completion |
no | true |
Poll the deployment until it finishes. |
timeout-seconds |
no | 600 |
Max wait when wait-for-completion is true. |
poll-interval-seconds |
no | 10 |
Seconds between status polls. |
test-sql |
no | — | SQL query smoke test (uses @spiceai/spice SDK). |
test-nsql |
no | — | Natural-language → SQL smoke test. |
test-chat |
no | — | Plain prompt or JSON body for /v1/chat/completions. |
test-chat-model |
no | — | Model used for test-chat and test-nsql. |
test-search |
no | — | JSON body for /v1/search. |
test-mcp-tool |
no | — | MCP tool name to invoke against /v1/mcp. |
test-mcp-arguments |
no | {} |
JSON-encoded arguments for the MCP tool call. |
test-warmup-seconds |
no | 60 |
Max wait for isSpiceReady() before running probes. |
test-timeout-seconds |
no | 30 |
Per-probe HTTP timeout. |
dataset-ready-timeout-seconds |
no | 300 |
Max wait for every dataset (via GET /v1/datasets?status=true) to reach a terminal-ok state (ready/disabled/refreshing). The job fails the moment any dataset enters error, or when the timeout elapses while any dataset is still pending — independent of fail-on-test-error. Set to 0 to skip the check. |
runtime-url |
no | derived | Override probe base URL. By default derived from the app's region as https://<region>-prod-aws-data.spiceai.io. |
fail-on-test-error |
no | true |
Fail the job when any probe fails. |
api-url |
no | https://api.spice.ai |
Management API base URL. |
oauth-token-url |
no | https://spice.ai/api/oauth/token |
OAuth token endpoint. |
scope |
no | — | Optional space-separated OAuth scopes to request. |
| Output | Description |
|---|---|
app-id |
Resolved numeric app ID. |
app-name |
Resolved app name. |
app-url |
Portal URL of the deployed app: https://spice.ai/<org>/<app-name>. The <org> slug comes from the org input when set, otherwise the owner part of GITHUB_REPOSITORY. |
deployment-id |
Created deployment ID. |
deployment-status |
Final status (queued, in_progress, succeeded, failed). |
deployment-created-at |
ISO 8601 timestamp the deployment was created. |
test-results |
JSON array of { name, ok, durationMs, detail?, error? }. |
datasets |
JSON array of { name, status, from?, error?, error_message? } from GET /v1/datasets?status=true after the deployment succeeds. |
- uses: spicehq/spice-cloud-deploy-action@v1
with:
client-id: ${{ secrets.SPICE_CLIENT_ID }}
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
app-name: analytics
region: us-east-1
create-app-if-missing: true
visibility: private
tags: |
environment: production
team: data-platform
commit: ${{ github.sha }}
tagsaccepts either a YAML block mapping (shown above) or a JSON object string (e.g.tags: '{"environment":"production","team":"data-platform"}'). Tags are merged into the app's existing tags on every run.Tag value rule: the Spice Cloud API allows only alphanumerics and
_@-. The action validates this locally so you fail fast with a clear error instead of a400 Bad Requeston the server.Auto-captured tags: when
GITHUB_REPOSITORYis set (always true on GitHub-hosted runners), the action adds arepositorytag derived from that env var, with/rewritten to_so the value passes API validation. Settingrepository:explicitly in yourtagsoverrides the auto-captured value.
- uses: spicehq/spice-cloud-deploy-action@v1
with:
client-id: ${{ secrets.SPICE_CLIENT_ID }}
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
app-name: analytics
secrets: |
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
PG_PASSWORD: ${{ secrets.PG_PASSWORD }}
test-sql: SELECT count(*) FROM taxi_trips- uses: spicehq/spice-cloud-deploy-action@v1
with:
client-id: ${{ secrets.SPICE_CLIENT_ID }}
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
app-name: analytics
test-chat: "Summarize today's top 3 trips"
test-chat-model: gpt-4o-mini
test-search: |
{"datasets":["docs"],"text":"refund policy","limit":3}
test-mcp-tool: list_resources
test-mcp-arguments: '{"limit":5}'on:
pull_request:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: spicehq/spice-cloud-deploy-action@v1
with:
client-id: ${{ secrets.SPICE_CLIENT_ID }}
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
app-name: ${{ github.event_name == 'pull_request' && format('analytics-pr-{0}', github.event.number) || 'analytics' }}
region: us-east-1
create-app-if-missing: true
channel: ${{ github.event_name == 'pull_request' && 'preview' || 'stable' }}- id: deploy
uses: spicehq/spice-cloud-deploy-action@v1
with:
client-id: ${{ secrets.SPICE_CLIENT_ID }}
client-secret: ${{ secrets.SPICE_CLIENT_SECRET }}
app-name: analytics
- name: Comment on the PR
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const url = '${{ steps.deploy.outputs.app-url }}';
const status = '${{ steps.deploy.outputs.deployment-status }}';
github.rest.issues.createComment({
...context.repo,
issue_number: context.issue.number,
body: `Spice Cloud: **${status}** → ${url}`
});- Authenticate. The action POSTs
client_id/client_secrettohttps://spice.ai/api/oauth/tokenwithgrant_type=client_credentialsand caches the bearer token for the run. - Resolve or create the app. If
app-idis given, it's fetched directly. Otherwise the action looks upapp-nameviaGET /v1/apps. Withcreate-app-if-missing: true, a missing app is created in the requestedregion. - Sync metadata. Tags from the
tagsinput are merged into the app's existing tags viaPUT /v1/apps/{id}. - Push the Spicepod. When
spicepod.yamlexists atworking-directory, its contents are pushed to the app viaPUT /v1/apps/{id}(spicepodfield). - Upsert secrets. Each
KEY=VALUEline insecretsis sent toPOST /v1/apps/{id}/secrets(upsert). - Trigger the deployment.
POST /v1/apps/{id}/deploymentswithbranch,commit_sha,commit_message, plus anyimage-tag/channel/replicasoverrides. - Poll until terminal.
GET /v1/apps/{id}/deploymentsis polled everypoll-interval-secondsup totimeout-seconds. - Smoke-test. When the deployment succeeds, the action fetches the app's primary API key, instantiates a
SpiceClientagainst the regional runtime URL (https://<region>-prod-aws-data.spiceai.io), waits forisSpiceReady(), and runs each configured probe.
The Action job step summary records the deployment metadata and a per-probe pass/fail table.
Wondering which scopes to grant the OAuth client? See the Scope cheat sheet above.
- Runs on
ubuntu-latest,macos-latest, andwindows-latest(Node 24 JavaScript action). - Bundles the
@spiceai/spiceSDK in HTTP mode — no Apache Arrow Flight gRPC dependencies are required at runtime. - Compatible with self-hosted runners that allow outbound HTTPS to
spice.ai,api.spice.ai, and the regional*-prod-aws-data.spiceai.iohost.
| Symptom | Likely cause | Fix |
|---|---|---|
Authentication failed (401) |
Bad client ID/secret or revoked client | Re-create the OAuth client in the Spice Portal and update the repo secrets. |
Authentication failed (403) |
Token lacks required scopes | Re-issue the OAuth client with the scopes listed above. |
App "X" not found |
App doesn't exist in the org | Set create-app-if-missing: true (and provide region), or pass an app-id. |
409 Conflict on deployment |
Another deployment is in progress | Wait for it to finish, or use the workflow concurrency key to serialize deploys. |
Cannot determine runtime URL |
Existing app has no region/cname and no runtime-url was given |
Pass region or runtime-url explicitly. |
Runtime not ready within Ns |
Deployment finished but the runtime hasn't come up | Increase test-warmup-seconds, or split deploy + tests into two steps. |
npm install
npm run lint
npm run typecheck
npm test
npm run build # produces dist/index.jsThe dist/ folder is committed — GitHub Actions runs the bundled JavaScript directly and does not install dependencies on the runner.
- Land changes on
trunk. CI must be green anddist/must be up to date. - Tag a SemVer release:
git tag v1.2.3 && git push origin v1.2.3. - The release workflow updates the floating
v1tag and publishes a GitHub release.
Consumers should pin to a major (@v1) for ongoing patches or to an exact tag (@v1.2.3) for reproducible builds.
Apache 2.0 — see LICENSE.
Spice Cloud Deploy is not certified by GitHub. It is provided by a third-party and is governed by separate terms of service, privacy policy, and support documentation.