Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/runware_serverless.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -25,7 +25,7 @@ Deploy, monitor, and manage Runware serverless applications on the platform

* [runware](runware.md) - CLI tool for the Runware API
* [runware serverless apps](runware_serverless_apps.md) - Manage deployed serverless applications
* [runware serverless deploy](runware_serverless_deploy.md) - Deploy a new serverless application
* [runware serverless deploy](runware_serverless_deploy.md) - Create or update a serverless application
* [runware serverless gpus](runware_serverless_gpus.md) - List available GPU types and pricing
* [runware serverless open](runware_serverless_open.md) - Open an application in the Runware dashboard
* [runware serverless secrets](runware_serverless_secrets.md) - Manage organisation secrets for serverless applications
Expand Down
31 changes: 21 additions & 10 deletions docs/runware_serverless_deploy.md
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
## runware serverless deploy

Deploy a new serverless application
Create or update a serverless application

### Synopsis

Create a new serverless application from Python code or a container source.
Create or update a serverless application from Python code or a container source.

A first deploy with a new --id creates the application. A later deploy with the
same --id uploads a new source, records version N+1, and rolls it when the
build is ready. Create-only flags (--gpu-type, worker settings, --volume,
--env, --env-file, --name) apply only to create; passing them when the
application already exists is an error. Change workers with 'apps scale' and
environment with 'apps env'. A source update on a stopped application is 409.

A code deploy takes a Python entry file. The whole source directory is zipped
and submitted as the application source, so the entry file can import its own
Expand DownExpand Up@@ -33,19 +40,20 @@ what a project keeps out of version control is a different question from what it
ships. Either way .env files are never uploaded, and neither are .git,
__pycache__, .venv, node_modules or the usual build and tool caches.

Environment variables must be supplied here with --env or --env-file. An app's
environment is frozen into the version this command creates, which is what the
worker is rendered from, so setting one afterwards with 'apps env set' stores it
without it ever reaching a pod. Prefer --env-file for anything secret: a value
passed as --env is visible in the process list and recorded in shell history.
Environment variables must be supplied at create with --env or --env-file. An
app's environment is frozen into the version this command creates, which is
what the worker is rendered from, so setting one afterwards with 'apps env set'
stores it without it ever reaching a pod. Prefer --env-file for anything secret:
a value passed as --env is visible in the process list and recorded in shell
history.

Anything the app downloads at runtime belongs on a --volume. The app runs in a
sandbox whose filesystem is part of the checkpointed state, so an unmounted
download is copied into every checkpoint and fetched again on every cold start.
A volume keeps it out of both.

Worker settings are supplied via flags. Endpoints are derived server-side from
the SDK (code) or from container.yaml (container).
Worker settings are supplied via flags on create. Endpoints are derived
server-side from the SDK (code) or from container.yaml (container).

```
runware serverless deploy [file] [flags]
Expand All@@ -57,6 +65,9 @@ runware serverless deploy [file] [flags]
# deploy the current directory, with app.py as the entry point
runware serverless deploy ./app.py --id my-app --gpu-type h100

# update source on an existing application
runware serverless deploy ./app.py --id my-app --wait

# deploy a project that lives elsewhere; app.py is resolved inside --src-dir
runware serverless deploy app.py --src-dir ~/projects/my-app --id my-app --gpu-type h100

Expand DownExpand Up@@ -90,7 +101,7 @@ runware serverless deploy [file] [flags]
--container string Directory whose root contains Dockerfile and container.yaml
--env stringArray Environment variable as KEY=VALUE (repeatable)
--env-file stringArray File of KEY=VALUE lines to read environment variables from (repeatable)
--gpu-type string GPU type ID (see 'serverless gpus')
--gpu-type string GPU type ID (see 'serverless gpus'; required when creating)
--gpus-per-worker int32 GPUs allocated per worker (default 1)
-h, --help help for deploy
--id string Application ID (immutable, lowercase slug)
Expand Down
7 changes: 4 additions & 3 deletions internal/api/serverless/client.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -419,7 +419,8 @@ func AppDeployTerminal(status AppStatus) bool {
}

// UpdateApp patches an app in place. Omitted fields are left unchanged.
// Currently persisted: appName and configuration.
// appSource starts a build and records version N+1; name and configuration
// do not replace source.
func (c *Client) UpdateApp(ctx context.Context, appID string, body AppUpdate) (*App, error) {
if c.apiKey == "" {
return nil, transport.ErrNoAPIKey
Expand DownExpand Up@@ -884,7 +885,7 @@ func (c *Client) GetWorker(ctx context.Context, appID string, workerID uuid.UUID
}
}

// NewCodeAppSource builds an appSource for a code-based create.
// NewCodeAppSource builds an appSource for a code-based create or update.
func NewCodeAppSource(src CodeSourceUpsert) (AppSourceUpsert, error) {
var source gen.AppSourceUpsert_Source
if err := source.FromCodeSourceUpsert(src); err != nil {
Expand All@@ -896,7 +897,7 @@ func NewCodeAppSource(src CodeSourceUpsert) (AppSourceUpsert, error) {
}, nil
}

// NewContainerAppSource builds an appSource for a container-based create.
// NewContainerAppSource builds an appSource for a container-based create or update.
func NewContainerAppSource(src ContainerSource) (AppSourceUpsert, error) {
var source gen.AppSourceUpsert_Source
if err := source.FromContainerSource(src); err != nil {
Expand Down
79 changes: 76 additions & 3 deletions internal/api/serverless/client_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,6 +29,7 @@ const (
testCursorPage2 = "page-2"
testCursorPage3 = "page-3"
testStatusReady = "ready"
testModelFile = "model.py"
)

func TestListGpuTypes(t *testing.T) {
Expand DownExpand Up@@ -171,7 +172,7 @@ func TestCreateApp(t *testing.T) {
BaseImage: "python:3.11-slim",
Codebase: CodebaseSource{
SourceId: uuid.MustParse("019c7654-8b21-7abc-9123-abcdef123456"),
ModelFile: "model.py",
ModelFile: testModelFile,
},
})
if err != nil {
Expand DownExpand Up@@ -274,7 +275,7 @@ func TestNewCodeAppSource(t *testing.T) {
BaseImage: "python:3.11-slim",
Codebase: CodebaseSource{
SourceId: id,
ModelFile: "model.py",
ModelFile: testModelFile,
},
})
if err != nil {
Expand All@@ -287,7 +288,7 @@ func TestNewCodeAppSource(t *testing.T) {
if err != nil {
t.Fatalf("AsCodeSourceUpsert: %v", err)
}
if inner.Codebase.SourceId != id || inner.Codebase.ModelFile != "model.py" {
if inner.Codebase.SourceId != id || inner.Codebase.ModelFile != testModelFile {
t.Errorf("codebase = %+v", inner.Codebase)
}
}
Expand DownExpand Up@@ -648,6 +649,78 @@ func TestUpdateApp(t *testing.T) {
}
}

func TestUpdateApp_AppSource(t *testing.T) {
sourceID := uuid.MustParse("019c7654-8b21-7abc-9123-abcdef123456")
appSource, err := NewCodeAppSource(CodeSourceUpsert{
BaseImage: "python:3.11-slim",
Codebase: CodebaseSource{
SourceId: sourceID,
ModelFile: testModelFile,
},
})
if err != nil {
t.Fatalf("NewCodeAppSource: %v", err)
}

srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPatch || r.URL.Path != "/v1/apps/"+testAppID {
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
}
raw, err := io.ReadAll(r.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
var body AppUpdate
if err := json.Unmarshal(raw, &body); err != nil {
t.Fatalf("decode body: %v", err)
}
if body.AppSource == nil {
t.Fatalf("missing appSource: %s", raw)
}
if body.AppName != nil || body.Configuration != nil || body.Secrets != nil || body.EnvironmentVariables != nil {
t.Errorf("patch included out-of-scope fields: %s", raw)
}
var rawMap map[string]json.RawMessage
if err := json.Unmarshal(raw, &rawMap); err != nil {
t.Fatalf("decode raw map: %v", err)
}
if len(rawMap) != 1 {
t.Errorf("expected only appSource in body, got %s", raw)
}
if body.AppSource.Type != AppSourceTypeCode {
t.Errorf("appSource.type = %q, want code", body.AppSource.Type)
}
inner, err := body.AppSource.Source.AsCodeSourceUpsert()
if err != nil {
t.Fatalf("AsCodeSourceUpsert: %v", err)
}
if inner.Codebase.SourceId != sourceID || inner.Codebase.ModelFile != testModelFile {
t.Errorf("unexpected codebase: %+v", inner.Codebase)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"appId":"my-app",
"appName":"My App",
"status":"initializing",
"configuration":{"maxWorkers":1,"idleTtlSecs":60,"scalingDelaySecs":10,"minWorkers":0,"gpusPerWorker":1,"concurrency":1,"computeType":"gpu"},
"environmentVariables":[],
"secrets":[],
"createdAt":"2026-07-30T12:00:00Z",
"updatedAt":"2026-07-30T12:00:00Z"
}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
app, err := c.UpdateApp(context.Background(), testAppID, AppUpdate{AppSource: &appSource})
if err != nil {
t.Fatalf("UpdateApp: %v", err)
}
if app.AppId != testAppID || app.Status != AppStatusInitializing {
t.Errorf("unexpected app: %+v", app)
}
}

func TestUpdateApp_Unprocessable(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
Expand Down
Loading