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_apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,6 @@ runware serverless apps [flags]
* [runware serverless apps show](runware_serverless_apps_show.md) - Show details for a serverless application
* [runware serverless apps stop](runware_serverless_apps_stop.md) - Stop a serverless application
* [runware serverless apps usage](runware_serverless_apps_usage.md) - Show usage for a serverless application
* [runware serverless apps versions](runware_serverless_apps_versions.md) - List versions of a serverless application
* [runware serverless apps versions](runware_serverless_apps_versions.md) - Inspect application versions
* [runware serverless apps workers](runware_serverless_apps_workers.md) - List workers for a serverless application

20 changes: 7 additions & 13 deletions docs/runware_serverless_apps_versions.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,19 @@
## runware serverless apps versions

List versions of a serverless application
Inspect application versions

```
runware serverless apps versions <appId> [flags]
```
### Synopsis

### Examples
List and inspect immutable versions of a serverless application.

```
# list deployed versions
runware serverless apps versions my-app

# page through results
runware serverless apps versions my-app --limit 20 --cursor <nextCursor>
runware serverless apps versions [flags]
```

### Options

```
--cursor string Pagination cursor from a previous nextCursor
-h, --help help for versions
--limit int Maximum number of versions to return (1-100)
-h, --help help for versions
```

### Options inherited from parent commands
Expand All @@ -36,4 +28,6 @@ runware serverless apps versions <appId> [flags]
### SEE ALSO

* [runware serverless apps](runware_serverless_apps.md) - Manage deployed serverless applications
* [runware serverless apps versions list](runware_serverless_apps_versions_list.md) - List versions of a serverless application
* [runware serverless apps versions show](runware_serverless_apps_versions_show.md) - Show a version of a serverless application

45 changes: 45 additions & 0 deletions docs/runware_serverless_apps_versions_list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## runware serverless apps versions list

List versions of a serverless application

### Synopsis

List immutable versions of an application.

The Build column is empty for container-sourced versions.

```
runware serverless apps versions list <appId> [flags]
```

### Examples

```
# list deployed versions
runware serverless apps versions list my-app

# page through results
runware serverless apps versions list my-app --limit 20 --cursor <nextCursor>
```

### Options

```
--cursor string Pagination cursor from a previous nextCursor
-h, --help help for list
--limit int Maximum number of versions to return (1-100)
```

### Options inherited from parent commands

```
--debug Show full debug output
-F, --format string CLI output format: table, json, yaml (default "table")
--transport string Transport protocol: ws (WebSocket) or http (REST) (default "ws")
-v, --verbose Show request/response details
```

### SEE ALSO

* [runware serverless apps versions](runware_serverless_apps_versions.md) - Inspect application versions

38 changes: 38 additions & 0 deletions docs/runware_serverless_apps_versions_show.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
## runware serverless apps versions show

Show a version of a serverless application

### Synopsis

Show a single immutable version by number.

```
runware serverless apps versions show <appId> <versionNumber> [flags]
```

### Examples

```
# show a version
runware serverless apps versions show my-app 1
```

### Options

```
-h, --help help for show
```

### Options inherited from parent commands

```
--debug Show full debug output
-F, --format string CLI output format: table, json, yaml (default "table")
--transport string Transport protocol: ws (WebSocket) or http (REST) (default "ws")
-v, --verbose Show request/response details
```

### SEE ALSO

* [runware serverless apps versions](runware_serverless_apps_versions.md) - Inspect application versions

36 changes: 36 additions & 0 deletions internal/api/serverless/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -402,6 +402,42 @@ func (c *Client) ListVersions(ctx context.Context, deploymentID string, params *
}
}

// GetVersion returns a single version by number.
func (c *Client) GetVersion(ctx context.Context, deploymentID string, versionNumber int32) (*Version, error) {
if c.apiKey == "" {
return nil, transport.ErrNoAPIKey
}

resp, err := c.inner.GetVersionWithResponse(ctx, deploymentID, versionNumber)
if err != nil {
return nil, fmt.Errorf("get version: %w", err)
}

if c.logger != nil && c.logger.Enabled(ctx, slog.LevelDebug) {
c.logger.Debug("serverless response", //nolint:errcheck,gosec
"path", fmt.Sprintf("/v1/deployments/%s/versions/%d", deploymentID, versionNumber),
"status", resp.StatusCode(),
"body", string(resp.Body),
)
}

switch resp.StatusCode() {
case http.StatusOK:
if resp.JSON200 == nil {
return nil, fmt.Errorf("get version: empty 200 response")
}
return resp.JSON200, nil
case http.StatusUnauthorized:
return nil, problemToError(resp.ApplicationproblemJSON401, http.StatusUnauthorized)
case http.StatusForbidden:
return nil, problemToError(resp.ApplicationproblemJSON403, http.StatusForbidden)
case http.StatusNotFound:
return nil, problemToError(resp.ApplicationproblemJSON404, http.StatusNotFound)
default:
return nil, problemFromBody(resp.Body, resp.StatusCode())
}
}

// ListBuilds returns a page of builds for a deployment.
func (c *Client) ListBuilds(ctx context.Context, deploymentID string, params *ListBuildsParams) (Page[Build], error) {
if c.apiKey == "" {
Expand Down
71 changes: 65 additions & 6 deletions internal/api/serverless/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package serverless
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
Expand All @@ -14,10 +15,12 @@ import (
)

const (
testDeploymentID = "my-app"
testBuildID = "33333333-3333-3333-3333-333333333333"
testCursorPage2 = "page-2"
testCursorPage3 = "page-3"
testDeploymentID = "my-app"
testBuildID = "33333333-3333-3333-3333-333333333333"
testVersionID = "22222222-2222-2222-2222-222222222222"
testVersionNumber = int32(1)
testCursorPage2 = "page-2"
testCursorPage3 = "page-3"
)

func TestListGpuTypes(t *testing.T) {
Expand Down Expand Up @@ -556,9 +559,9 @@ func TestListVersions(t *testing.T) {
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"data":[{
"id":"22222222-2222-2222-2222-222222222222",
"id":"` + testVersionID + `",
"deploymentId":"my-app",
"buildId":"33333333-3333-3333-3333-333333333333",
"buildId":"` + testBuildID + `",
"versionNumber":1,
"createdAt":"2026-07-30T12:00:00Z"
}]}`))
Expand All @@ -575,6 +578,62 @@ func TestListVersions(t *testing.T) {
}
}

func TestGetVersion(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
want := fmt.Sprintf("/v1/deployments/%s/versions/%d", testDeploymentID, testVersionNumber)
if r.Method != http.MethodGet || r.URL.Path != want {
t.Errorf("unexpected %s %s", r.Method, r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{
"id":"` + testVersionID + `",
"deploymentId":"` + testDeploymentID + `",
"buildId":"` + testBuildID + `",
"versionNumber":1,
"createdAt":"2026-07-30T12:00:00Z"
}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
v, err := c.GetVersion(context.Background(), testDeploymentID, testVersionNumber)
if err != nil {
t.Fatalf("GetVersion: %v", err)
}
if v.Id.String() != testVersionID || v.VersionNumber != testVersionNumber {
t.Errorf("unexpected version: %+v", v)
}
if v.BuildId == nil || v.BuildId.String() != testBuildID {
t.Errorf("unexpected buildId: %+v", v.BuildId)
}
}

func TestGetVersion_NotFound(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/problem+json")
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte(`{"type":"about:blank","title":"Not Found","status":404,"detail":"No version exists"}`))
}))
defer srv.Close()

c := newClient("test-key", srv.URL, slog.Default(), srv.Client())
_, err := c.GetVersion(context.Background(), testDeploymentID, 99999)
var re *transport.RunwareError
if !errors.As(err, &re) {
t.Fatalf("expected *transport.RunwareError, got %T: %v", err, err)
}
if re.StatusCode != http.StatusNotFound {
t.Errorf("expected status 404, got %d", re.StatusCode)
}
}

func TestGetVersion_NoAPIKey(t *testing.T) {
c := NewClient("", "https://example.invalid", slog.Default())
if _, err := c.GetVersion(context.Background(), testDeploymentID, testVersionNumber); !errors.Is(err, transport.ErrNoAPIKey) {
t.Fatalf("expected ErrNoAPIKey, got %v", err)
}
}

func TestListWorkers(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
want := "/v1/deployments/" + testDeploymentID + "/workers"
Expand Down
46 changes: 0 additions & 46 deletions internal/cmd/serverless/apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,52 +184,6 @@ func newAppsEndpointsCmd(logger *log.Logger) *cobra.Command {
return cmd
}

func newAppsVersionsCmd(logger *log.Logger) *cobra.Command {
var (
limit int
cursor string
)

cmd := &cobra.Command{
Use: "versions <appId>",
Short: "List versions of a serverless application",
Example: ` # list deployed versions
runware serverless apps versions my-app
# page through results
runware serverless apps versions my-app --limit 20 --cursor <nextCursor>`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if err := validateListLimit(limit); err != nil {
return err
}
id := args[0]
var params *serverlessapi.ListVersionsParams
if limit > 0 || cursor != "" {
params = &serverlessapi.ListVersionsParams{}
params.Limit, params.Cursor = listPageParams(limit, cursor)
}

spin := cmdutil.NewSpinner(fmt.Sprintf("Fetching versions for %s...", id))
spin.Start()

client := serverlessapi.NewClient(config.GetAPIKey(), config.GetServerlessBaseURL(), slog.New(logger))
page, err := client.ListVersions(cmd.Context(), id, params)
if err != nil {
spin.Stop()
return err
}
spin.Stop()

return printPage(cmdutil.FormatFor(cmd), page, versionsResult(page.Data), cmd.ErrOrStderr(), "")
},
}

cmd.Flags().IntVar(&limit, "limit", 0, "Maximum number of versions to return (1-100)")
cmd.Flags().StringVar(&cursor, "cursor", "", "Pagination cursor from a previous nextCursor")
return cmd
}

func newAppsLogsCmd() *cobra.Command {
return stubLeaf(
"logs <appId>",
Expand Down
Loading