From 127e2e7d1b960980f23b520f30f3c6d0499bfecd Mon Sep 17 00:00:00 2001 From: Luke Kim <80174+lukekim@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:08:03 -0700 Subject: [PATCH 1/3] spice-cloud-management: fork projects through the Management API Document POST /v1/projects/{projectId}/forks and GET /v1/projects/{projectId}/forks: fields, what a fork copies, the shared_state check before its first deployment, error codes, and forked_from on project responses. Add a workflow to copy or move a project to another region, fork-project and list-forks helper commands, and an eval. --- CHANGELOG.md | 1 + skills/spice-cloud-management/SKILL.md | 83 +++++++++++++++++-- .../spice-cloud-management/evals/evals.json | 32 +++++++ .../scripts/spice-cloud.sh | 16 ++++ 4 files changed, 123 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 975f23c..d98f6f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +- Add project forks to spice-cloud-management. `POST /v1/projects/{projectId}/forks` creates a project from another project's spicepod, connections, secrets, and runtime settings, in the same place or in another region or cluster, and `GET /v1/projects/{projectId}/forks` lists a project's forks. Project responses include `forked_from`. The skill documents the fields, the `shared_state` check before the first deployment of a fork, and the error codes. It adds a workflow to copy or move a project to another region, `fork-project` and `list-forks` commands in `scripts/spice-cloud.sh`, and an eval. - Add the `spice-cookbook` skill, which sets up and runs recipes from the [Spice.ai cookbook](https://github.com/spiceai/cookbook). It picks a recipe by name or goal, fetches the cookbook (or a pull request or branch), and checks the runtime version, Docker, secrets, tools, and ports with `scripts/cookbook.sh` before running the README steps. The script reports whether each secret is set without printing its value. - Fix spice-setup and spicepod-config, which set bind addresses with `spice run -- --http ...`. That fails on every v2 CLI (`argument '--http' cannot be used multiple times`) because `spice run` already passes `--http`. Use `spice run --http-endpoint`, `--flight-endpoint`, and `--metrics-endpoint` instead. - Fix spice-search and spice-text-to-sql `rrf()` examples: the score column has been `_fused_score` since v2.0.0, not `fused_score`. diff --git a/skills/spice-cloud-management/SKILL.md b/skills/spice-cloud-management/SKILL.md index e070ed6..b7ea207 100644 --- a/skills/spice-cloud-management/SKILL.md +++ b/skills/spice-cloud-management/SKILL.md @@ -1,11 +1,11 @@ --- name: spice-cloud-management -description: Manage Spice.ai Cloud resources via the Management API — projects (formerly apps), deployments, secrets, API keys, and org members. Use this skill whenever the user wants to create or manage a Spice.ai Cloud project or app, trigger a deployment, manage cloud secrets or API keys, list regions or runtime versions, add/remove org members, or automate any Spice.ai Cloud operation. Also use when the user mentions "spice.ai cloud", "deploy to spice", "cloud API", or wants to use the Spice.ai hosted platform. For infrastructure-as-code with Terraform, see spice-terraform. +description: Manage Spice.ai Cloud resources via the Management API — projects (formerly apps), project forks, deployments, secrets, API keys, and org members. Use this skill whenever the user wants to create, fork, or manage a Spice.ai Cloud project or app, copy or move a project to another region or cluster, trigger a deployment, manage cloud secrets or API keys, list regions or runtime versions, add/remove org members, or automate any Spice.ai Cloud operation. Also use when the user mentions "spice.ai cloud", "deploy to spice", "fork a project", "cloud API", or wants to use the Spice.ai hosted platform. For infrastructure-as-code with Terraform, see spice-terraform. --- # Spice.ai Cloud Management -Manage Spice.ai Cloud resources through the Management API (control plane) at `https://api.spice.ai`. Create projects, trigger deployments, manage secrets and API keys, and administer organization members. +Manage Spice.ai Cloud resources through the Management API (control plane) at `https://api.spice.ai`. Create and fork projects, trigger deployments, manage secrets and API keys, and administer organization members. **Renamed in Aug 2026:** apps are now **projects**. The `/v1/projects` routes are canonical; every `/v1/apps` route is still served as a legacy alias (marked deprecated in the OpenAPI spec). Only the list envelope differs — `GET /v1/projects` returns `{"projects": [...]}`, `GET /v1/apps` returns `{"apps": [...]}`. Scope names stay `apps:*`. @@ -70,13 +70,15 @@ curl -H "Authorization: Bearer $SPICE_API_TOKEN" \ Manage Spice.ai Cloud projects. -| Operation | Method | Path | Scope | -| -------------- | -------- | -------------------------- | ------------- | -| List projects | `GET` | `/v1/projects` | `apps:read` | -| Create project | `POST` | `/v1/projects` | `apps:write` | -| Get project | `GET` | `/v1/projects/{projectId}` | `apps:read` | -| Update project | `PUT` | `/v1/projects/{projectId}` | `apps:write` | -| Delete project | `DELETE` | `/v1/projects/{projectId}` | `apps:delete` | +| Operation | Method | Path | Scope | +| -------------- | -------- | -------------------------------- | ------------- | +| List projects | `GET` | `/v1/projects` | `apps:read` | +| Create project | `POST` | `/v1/projects` | `apps:write` | +| Get project | `GET` | `/v1/projects/{projectId}` | `apps:read` | +| Update project | `PUT` | `/v1/projects/{projectId}` | `apps:write` | +| Delete project | `DELETE` | `/v1/projects/{projectId}` | `apps:delete` | +| Fork project | `POST` | `/v1/projects/{projectId}/forks` | `apps:write` | +| List forks | `GET` | `/v1/projects/{projectId}/forks` | `apps:read` | ### Create Project @@ -134,6 +136,34 @@ curl -X DELETE https://api.spice.ai/v1/projects/{projectId} \ Deletes the project and tears down its runtime resources. Returns `204`. +### Fork Project (Sep 2026) + +Create a new project from an existing project's configuration, in the same place or in another region or dedicated cluster. The fork keeps a link to its source: `GET /v1/projects/{projectId}` returns it as `forked_from` (`{"id", "name"}`, or `null` for a project that is not a fork), and `GET /v1/projects/{projectId}/forks` lists a project's forks. In the portal, fork from **Create Project → Fork a project** or the project's **Settings → Forks**. + +```bash +curl -X POST https://api.spice.ai/v1/projects/123/forks \ + -H "Authorization: Bearer $SPICE_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "analytics-west", "region": "us-west-2"}' +``` + +| Field | Type | Required | Notes | +| -------------------------- | ------ | ---------------------- | ---------------------------------------------------------------------------- | +| `name` | string | No | Defaults to `-fork`, then `-fork-2`, and so on | +| `region` | string | No | `us-east-1` or `us-west-2`; omit it and `cluster_name` to fork in place | +| `cluster_name` | string | No | Dedicated cluster from `GET /v1/clusters` | +| `scheduler_state_location` | string | For a distributed source | `s3://` URI that must not overlap the source's `runtime.scheduler.state_location` | + +The body is optional, and any other field is rejected with `400`. + +- **Copied:** the spicepod the source deploys (from its GitHub repository, the internal registry, or the stored config), linked connections, project secrets, linked organization secrets, update channel, version range, replica and executor counts, storage size, description, and tags. The platform-managed Postgres CDC replication slot is rebound to the fork. +- **Not copied:** API keys (the fork gets its own), deployments, the GitHub repository link, the instance size (the fork starts on the default instance), and visibility (forks are private). +- **Not deployed:** the `201` response is the new project with its config. Deploy it with `POST /v1/projects/{forkId}/deployments`. +- **Check `shared_state` first.** The response lists settings copied as-is that name state the source also uses — a replication slot or Kafka consumer group the spicepod names, or a snapshot location. Two projects on one replication slot or consumer group split the changes between them, so change these in the fork's spicepod (`PUT /v1/projects/{forkId}`) before deploying unless the projects should share them. +- A source on a BYOC cluster or a self-hosted (Cloud Connect) runtime has no placement a fork can share, and neither does one whose region or cluster is no longer available: set `region` or `cluster_name`. + +**Status codes:** `201` forked (not deployed), `400` validation error (codes in Troubleshooting), `402 ai_credits_exhausted` (the source runs on hosted AI credits the organization has used up), `404` source not found, `409` name taken or `fork_name_unavailable`, `422 fork_source_has_no_spicepod` or `fork_source_spicepod_invalid`, `429` rate limited + ## Deployments Deploy the project's current spicepod. @@ -348,6 +378,30 @@ curl -H "Authorization: Bearer $SPICE_API_TOKEN" \ "https://api.spice.ai/v1/projects/123/deployments?limit=1" ``` +### Copy or Move a Project to Another Region + +```bash +# 1. Fork project 123 into us-west-2. Note the fork's "id" and read "shared_state". +curl -X POST https://api.spice.ai/v1/projects/123/forks \ + -H "Authorization: Bearer $SPICE_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "analytics-west", "region": "us-west-2"}' + +# 2. If shared_state lists anything, update the fork's spicepod first (PUT /v1/projects/456). + +# 3. Deploy the fork (id 456 here) +curl -X POST https://api.spice.ai/v1/projects/456/deployments \ + -H "Authorization: Bearer $SPICE_API_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"commit_message": "Initial deployment of the fork"}' + +# 4. Wait for the deployment to succeed +curl -H "Authorization: Bearer $SPICE_API_TOKEN" \ + "https://api.spice.ai/v1/projects/456/deployments?limit=1" +``` + +Clients then use the fork's `endpoint` and API keys (`GET /v1/projects/456/api-keys`); the source's keys do not work on the fork. To finish a move, delete the source with `DELETE /v1/projects/123` — only after the user confirms, because deleting tears down the source's runtime. + ### Rotate API Keys (Zero Downtime) ```bash @@ -383,6 +437,12 @@ bash scripts/spice-cloud.sh list-projects # Create project bash scripts/spice-cloud.sh create-project my-project us-west-2 +# Fork project 123 as analytics-west in us-west-2 (name and region are optional) +bash scripts/spice-cloud.sh fork-project 123 analytics-west us-west-2 + +# List the forks of project 123 +bash scripts/spice-cloud.sh list-forks 123 + # Deploy project bash scripts/spice-cloud.sh deploy 123 @@ -400,6 +460,7 @@ bash scripts/spice-cloud.sh get-api-keys 123 When presenting management API results: - Show project IDs and names in a table for list operations +- For a fork, name the project it was forked from, say it is not deployed yet, and list any `shared_state` settings - Show deployment status clearly (queued/in_progress/succeeded/failed), with `error_code` and `error_message` for failures - Never display secret values — confirm creation/update only - Show API keys with a warning about secure storage @@ -415,6 +476,10 @@ When presenting management API results: | `403 image_tag_requires_enterprise` | Omit `image_tag`; select the runtime with `update_channel` and `version` instead | | `404 Project not found` | Verify `projectId` with `GET /v1/projects` in the right organization | | `409 Conflict` on create project | Project name already exists (names compare case-insensitively) | +| `400 fork_placement_required` | The source runs on a BYOC cluster, a self-hosted runtime, or a region or cluster that is gone; set `region` or `cluster_name` for the fork | +| `400 scheduler_state_location_*` | The source is distributed; set `scheduler_state_location` to an `s3://` URI outside the source's (`_required`, `_unsupported`, `_overlaps_source`). `_not_applicable` means the source is not distributed, so omit the field | +| `409 fork_name_unavailable` | Every default `-fork-N` name is taken; set `name` | +| `422 fork_source_*` | The source has no spicepod or an invalid one; fix the source's spicepod, then fork again | | `409` on deployment | A deployment is already in progress; wait for it to complete | | `400` on deployment | Project has no spicepod, is paused (`POST .../resume`), or `image_tag` isn't a published version for the channel | | `400` on create secret | Secret name must start with letter/underscore; letters, numbers, underscores only | diff --git a/skills/spice-cloud-management/evals/evals.json b/skills/spice-cloud-management/evals/evals.json index 2ce7d94..1b4b30e 100644 --- a/skills/spice-cloud-management/evals/evals.json +++ b/skills/spice-cloud-management/evals/evals.json @@ -46,6 +46,38 @@ "description": "Should emphasize zero-downtime approach" } ] + }, + { + "id": 3, + "prompt": "My Spice.ai Cloud project 123 runs in us-east-1. Make a copy of it in us-west-2 called analytics-west, with the same datasets, connections, and secrets, and get it running.", + "expected_output": "Fork the project with POST /v1/projects/123/forks and {\"name\": \"analytics-west\", \"region\": \"us-west-2\"} instead of recreating it by hand, check shared_state in the response, then deploy the fork with POST /v1/projects/{forkId}/deployments because a fork is not deployed on creation", + "files": [], + "assertions": [ + { + "name": "forks_project", + "type": "regex", + "value": "(/v1/(projects|apps)/[^/\\s]+/forks|fork-project)", + "description": "Should fork the source project rather than recreate it" + }, + { + "name": "targets_region", + "type": "regex", + "value": "\"region\"\\s*:\\s*\"us-west-2\"|fork-project \\S+ \\S+ us-west-2", + "description": "Should place the fork in us-west-2" + }, + { + "name": "deploys_fork", + "type": "regex", + "value": "(?i)(/deployments|\\bdeploy\\b)", + "description": "Should deploy the fork, which forking does not do" + }, + { + "name": "checks_shared_state", + "type": "regex", + "value": "shared_state", + "description": "Should check the settings the fork shares with its source before deploying" + } + ] } ] } diff --git a/skills/spice-cloud-management/scripts/spice-cloud.sh b/skills/spice-cloud-management/scripts/spice-cloud.sh index b50e2eb..81555e4 100755 --- a/skills/spice-cloud-management/scripts/spice-cloud.sh +++ b/skills/spice-cloud-management/scripts/spice-cloud.sh @@ -43,6 +43,8 @@ usage() { echo " create-project [desc] Create a project, region us-east-1|us-west-2 (create-app)" >&2 echo " update-project Update project with JSON body (update-app)" >&2 echo " delete-project Delete a project (delete-app)" >&2 + echo " fork-project [name] [region] Fork a project; the fork is not deployed" >&2 + echo " list-forks List the forks of a project" >&2 echo " list-deployments [status] List deployments" >&2 echo " deploy [branch] [message] Create a deployment" >&2 echo " list-secrets List secrets (values masked)" >&2 @@ -90,6 +92,20 @@ case "${1:-}" in api_delete "/v1/projects/$2" echo "Project $2 deleted" >&2 ;; + fork-project) + [ -z "${2:-}" ] && { echo "Error: projectId required" >&2; exit 1; } + BODY="{" + SEP="" + if [ -n "${3:-}" ]; then BODY="$BODY${SEP}\"name\":\"$3\""; SEP=","; fi + if [ -n "${4:-}" ]; then BODY="$BODY${SEP}\"region\":\"$4\""; SEP=","; fi + BODY="$BODY}" + echo "Forking project $2..." >&2 + api_post "/v1/projects/$2/forks" "$BODY" + ;; + list-forks) + [ -z "${2:-}" ] && { echo "Error: projectId required" >&2; exit 1; } + api_get "/v1/projects/$2/forks" + ;; list-deployments) [ -z "${2:-}" ] && { echo "Error: projectId required" >&2; exit 1; } STATUS_FILTER="" From f8aa957f1ed800aec3b02c7edfebb1d85cb8cf59 Mon Sep 17 00:00:00 2001 From: Luke Kim <80174+lukekim@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:40:08 -0700 Subject: [PATCH 2/3] docs(spice-cloud-management): fork GitHub-connected projects --- skills/spice-cloud-management/SKILL.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/skills/spice-cloud-management/SKILL.md b/skills/spice-cloud-management/SKILL.md index b7ea207..7152a69 100644 --- a/skills/spice-cloud-management/SKILL.md +++ b/skills/spice-cloud-management/SKILL.md @@ -158,11 +158,12 @@ The body is optional, and any other field is rejected with `400`. - **Copied:** the spicepod the source deploys (from its GitHub repository, the internal registry, or the stored config), linked connections, project secrets, linked organization secrets, update channel, version range, replica and executor counts, storage size, description, and tags. The platform-managed Postgres CDC replication slot is rebound to the fork. - **Not copied:** API keys (the fork gets its own), deployments, the GitHub repository link, the instance size (the fork starts on the default instance), and visibility (forks are private). +- **GitHub-connected sources:** the spicepod is read from the source's repository, and a fork that cannot read it fails with `502 fork_source_repository_unreadable` instead of copying an older stored spicepod. The API copies only `spicepod.yaml`. It refuses a source that loads other files from the repository — component `ref`s, view `sql_ref`s, model or embedding `files`, or relative `file:` sources — with `422 fork_source_loads_repository_files`. To keep a fork in Git, fork the repository on GitHub, name the fork after the new project, then fork the project in the portal with **Deploy from a fork of the repository**. The portal connects the new project to that repository. - **Not deployed:** the `201` response is the new project with its config. Deploy it with `POST /v1/projects/{forkId}/deployments`. - **Check `shared_state` first.** The response lists settings copied as-is that name state the source also uses — a replication slot or Kafka consumer group the spicepod names, or a snapshot location. Two projects on one replication slot or consumer group split the changes between them, so change these in the fork's spicepod (`PUT /v1/projects/{forkId}`) before deploying unless the projects should share them. - A source on a BYOC cluster or a self-hosted (Cloud Connect) runtime has no placement a fork can share, and neither does one whose region or cluster is no longer available: set `region` or `cluster_name`. -**Status codes:** `201` forked (not deployed), `400` validation error (codes in Troubleshooting), `402 ai_credits_exhausted` (the source runs on hosted AI credits the organization has used up), `404` source not found, `409` name taken or `fork_name_unavailable`, `422 fork_source_has_no_spicepod` or `fork_source_spicepod_invalid`, `429` rate limited +**Status codes:** `201` forked (not deployed), `400` validation error (codes in Troubleshooting), `402 ai_credits_exhausted` (the source runs on hosted AI credits the organization has used up), `404` source not found, `409` name taken or `fork_name_unavailable`, `422 fork_source_has_no_spicepod`, `fork_source_spicepod_invalid`, or `fork_source_loads_repository_files`, `429` rate limited, `502 fork_source_repository_unreadable` ## Deployments @@ -479,7 +480,9 @@ When presenting management API results: | `400 fork_placement_required` | The source runs on a BYOC cluster, a self-hosted runtime, or a region or cluster that is gone; set `region` or `cluster_name` for the fork | | `400 scheduler_state_location_*` | The source is distributed; set `scheduler_state_location` to an `s3://` URI outside the source's (`_required`, `_unsupported`, `_overlaps_source`). `_not_applicable` means the source is not distributed, so omit the field | | `409 fork_name_unavailable` | Every default `-fork-N` name is taken; set `name` | -| `422 fork_source_*` | The source has no spicepod or an invalid one; fix the source's spicepod, then fork again | +| `422 fork_source_has_no_spicepod` / `fork_source_spicepod_invalid` | The source has no spicepod or an invalid one; fix the source's spicepod, then fork again | +| `422 fork_source_loads_repository_files` | The source's spicepod loads other files from its GitHub repository; fork the repository on GitHub, then fork the project in the portal, which deploys from the forked repository | +| `502 fork_source_repository_unreadable` | The source's spicepod could not be read from its GitHub repository; retry, or fix the repository or the GitHub App's access to it | | `409` on deployment | A deployment is already in progress; wait for it to complete | | `400` on deployment | Project has no spicepod, is paused (`POST .../resume`), or `image_tag` isn't a published version for the channel | | `400` on create secret | Secret name must start with letter/underscore; letters, numbers, underscores only | From 91c457814d51f1a983c98f1e43d679abc2918000 Mon Sep 17 00:00:00 2001 From: Luke Kim <80174+lukekim@users.noreply.github.com> Date: Wed, 23 Sep 2026 21:40:10 -0700 Subject: [PATCH 3/3] docs(changelog): note GitHub-connected project forks --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d98f6f0..28329cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## Unreleased -- Add project forks to spice-cloud-management. `POST /v1/projects/{projectId}/forks` creates a project from another project's spicepod, connections, secrets, and runtime settings, in the same place or in another region or cluster, and `GET /v1/projects/{projectId}/forks` lists a project's forks. Project responses include `forked_from`. The skill documents the fields, the `shared_state` check before the first deployment of a fork, and the error codes. It adds a workflow to copy or move a project to another region, `fork-project` and `list-forks` commands in `scripts/spice-cloud.sh`, and an eval. +- Add project forks to spice-cloud-management. `POST /v1/projects/{projectId}/forks` creates a project from another project's spicepod, connections, secrets, and runtime settings, in the same place or in another region or cluster, and `GET /v1/projects/{projectId}/forks` lists a project's forks. Project responses include `forked_from`. The skill documents the fields, the `shared_state` check before the first deployment of a fork, how a source connected to a GitHub repository is forked, and the error codes. It adds a workflow to copy or move a project to another region, `fork-project` and `list-forks` commands in `scripts/spice-cloud.sh`, and an eval. - Add the `spice-cookbook` skill, which sets up and runs recipes from the [Spice.ai cookbook](https://github.com/spiceai/cookbook). It picks a recipe by name or goal, fetches the cookbook (or a pull request or branch), and checks the runtime version, Docker, secrets, tools, and ports with `scripts/cookbook.sh` before running the README steps. The script reports whether each secret is set without printing its value. - Fix spice-setup and spicepod-config, which set bind addresses with `spice run -- --http ...`. That fails on every v2 CLI (`argument '--http' cannot be used multiple times`) because `spice run` already passes `--http`. Use `spice run --http-endpoint`, `--flight-endpoint`, and `--metrics-endpoint` instead. - Fix spice-search and spice-text-to-sql `rrf()` examples: the score column has been `_fused_score` since v2.0.0, not `fused_score`.