Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go 1.26.3
- name: Setup Go 1.26.4
uses: actions/setup-go@v5
with:
go-version: 1.26.3
go-version: 1.26.4
# You can test your matrix by printing the current Go version
- name: Display Go version
run: go version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: "1.26.3"
go-version: "1.26.4"

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
Expand Down
14 changes: 7 additions & 7 deletions api/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ func AddModelToRepo(input *AddModelToRepoInput) (*Model, error) {
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("statuscode %d: %s", res.StatusCode, string(rawData))
}

Expand Down Expand Up @@ -374,7 +374,7 @@ query %s {
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("statuscode %d: %s", res.StatusCode, string(rawData))
}

Expand Down Expand Up @@ -404,7 +404,7 @@ query %s {
}

if models == nil {
return nil, fmt.Errorf("data is nil: %s", string(rawData))
models = []*Model{}
}
if input != nil {
if input.Provider != "" {
Expand Down Expand Up @@ -492,7 +492,7 @@ func GetModel(input *GetModelInput) (*Model, error) {
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("statuscode %d: %s", res.StatusCode, string(rawData))
}

Expand Down Expand Up @@ -580,7 +580,7 @@ func RemoveModel(input *RemoveModelInput) (*ModelRepoMutationResult, error) {
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("statuscode %d: %s", res.StatusCode, string(rawData))
}

Expand Down Expand Up @@ -714,7 +714,7 @@ func CreateModelRepoUpload(input *CreateModelRepoUploadInput) (*ModelRepoMutatio
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("statuscode %d: %s", res.StatusCode, string(rawData))
}

Expand Down Expand Up @@ -863,7 +863,7 @@ func UpdateModelVersionStatus(hash, status string) (*ModelVersion, error) {
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("statuscode %d: %s", res.StatusCode, string(rawData))
}

Expand Down
8 changes: 4 additions & 4 deletions cmd/model/addModelToRepo.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func setModelGraphQLTimeout(cmd *cobra.Command) {
}

viper.Set(api.GraphQLTimeoutKey, modelGraphQLTimeoutValue)
fmt.Printf("--graphql-timeout not set; defaulting to %s for model creation operations\n", modelGraphQLTimeoutValue)
fmt.Fprintf(os.Stderr, "--graphql-timeout not set; defaulting to %s for model creation operations\n", modelGraphQLTimeoutValue)
return
}

Expand All @@ -68,7 +68,7 @@ func setModelGraphQLTimeout(cmd *cobra.Command) {
}

viper.Set(api.GraphQLTimeoutKey, modelGraphQLTimeoutValue)
fmt.Printf("defaulting graphql timeout to %s for model creation operations\n", modelGraphQLTimeoutValue)
fmt.Fprintf(os.Stderr, "defaulting graphql timeout to %s for model creation operations\n", modelGraphQLTimeoutValue)
}

type completedPart struct {
Expand Down Expand Up @@ -207,7 +207,7 @@ func runAddModel(cmd *cobra.Command, args []string) {
uploadInput.Name = addModelName

if len(modelFiles) > 0 {
err := uploadModelFiles(modelFiles, uploadInput)
err = uploadModelFiles(modelFiles, uploadInput)
cobra.CheckErr(err)
return
}
Expand Down Expand Up @@ -400,7 +400,7 @@ func completeModelUpload(upload *api.ModelRepoUpload, artifactPath string) error
err = fmt.Errorf("upload part %d missing ETag", part.PartNumber)
return
}
completed = append(completed, completedPart{PartNumber: part.PartNumber, ETag: fmt.Sprintf("\"%s\"", etag)})
completed = append(completed, completedPart{PartNumber: part.PartNumber, ETag: fmt.Sprintf("%q", etag)})
}()
if err != nil {
return err
Expand Down
12 changes: 11 additions & 1 deletion cmd/model/addModelToRepo_timeout_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,21 @@ func TestSetModelGraphQLTimeoutWithoutInheritedFlag(t *testing.T) {
viper.Reset()

cmd := &cobra.Command{Use: "add"}
setModelGraphQLTimeout(cmd)
// CLAUDE.md: informational output must not corrupt stdout for JSON
// consumers — the "defaulting graphql timeout" notice must land on stderr.
stdout, stderr := captureStdStreams(t, func() {
setModelGraphQLTimeout(cmd)
})

if got := viper.GetDuration(api.GraphQLTimeoutKey); got != modelGraphQLTimeoutValue {
t.Fatalf("expected graphql timeout %s, got %s", modelGraphQLTimeoutValue, got)
}
if stdout != "" {
t.Fatalf("stdout must remain empty, got %q", stdout)
}
if stderr == "" {
t.Fatal("expected timeout-default notice on stderr, got empty")
}
}

func TestSetModelGraphQLTimeoutRespectsExistingConfiguredValue(t *testing.T) {
Expand Down
6 changes: 3 additions & 3 deletions cmd/model/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ package model

import (
"errors"
"fmt"
"strings"

"github.com/runpod/runpodctl/api"
"github.com/runpod/runpodctl/internal/output"
)

func handleModelRepoError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, api.ErrModelRepoNotImplemented) {
fmt.Println(api.ErrModelRepoNotImplemented.Error())
output.Error(api.ErrModelRepoNotImplemented)
return true
}
if strings.Contains(err.Error(), "Model Repo feature is not enabled for this user") {
fmt.Println(err.Error())
output.Error(err)
return true
}
return false
Expand Down
122 changes: 122 additions & 0 deletions cmd/model/errors_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package model

import (
"errors"
"io"
"os"
"strings"
"sync"
"testing"

"github.com/runpod/runpodctl/api"
)

// captureStdStreams runs fn with os.Stdout and os.Stderr replaced by pipes and
// returns whatever each stream received. It exists to assert that informational
// and error output goes to the correct stream — a regression class CLAUDE.md
// explicitly calls out (legacy commands losing stderr ⇒ corrupts stdout for
// JSON-consuming agents).
func captureStdStreams(t *testing.T, fn func()) (stdout, stderr string) {
t.Helper()

origStdout, origStderr := os.Stdout, os.Stderr
stdoutR, stdoutW, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe stdout: %v", err)
}
stderrR, stderrW, err := os.Pipe()
if err != nil {
t.Fatalf("os.Pipe stderr: %v", err)
}
os.Stdout, os.Stderr = stdoutW, stderrW

var (
wg sync.WaitGroup
stdoutBuf, stderrBuf strings.Builder
)
wg.Add(2)
go func() {
defer wg.Done()
_, _ = io.Copy(&stdoutBuf, stdoutR)
}()
go func() {
defer wg.Done()
_, _ = io.Copy(&stderrBuf, stderrR)
}()

defer func() {
os.Stdout, os.Stderr = origStdout, origStderr
}()

fn()

_ = stdoutW.Close()
_ = stderrW.Close()
wg.Wait()
_ = stdoutR.Close()
_ = stderrR.Close()

return stdoutBuf.String(), stderrBuf.String()
}

func TestHandleModelRepoError(t *testing.T) {
tests := []struct {
name string
err error
wantHandled bool
wantStderrSub string // empty = stderr must be empty
wantStdoutSub string // empty = stdout must be empty
}{
{
name: "nil error is a no-op",
err: nil,
wantHandled: false,
},
{
name: "ErrModelRepoNotImplemented routes to stderr",
err: api.ErrModelRepoNotImplemented,
wantHandled: true,
wantStderrSub: api.ErrModelRepoNotImplemented.Error(),
},
{
name: "feature-not-enabled message routes to stderr",
err: errors.New("Model Repo feature is not enabled for this user"),
wantHandled: true,
wantStderrSub: "Model Repo feature is not enabled for this user",
},
{
name: "unrelated error is not handled",
err: errors.New("some other failure"),
wantHandled: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var handled bool
stdout, stderr := captureStdStreams(t, func() {
handled = handleModelRepoError(tt.err)
})

if handled != tt.wantHandled {
t.Fatalf("handled = %v, want %v", handled, tt.wantHandled)
}

// CLAUDE.md: deprecation warnings / handled errors must go to stderr
// only; stdout must stay clean for JSON-consuming agents.
if stdout != "" {
t.Fatalf("stdout must remain empty, got %q", stdout)
}

if tt.wantStderrSub == "" {
if stderr != "" {
t.Fatalf("expected empty stderr, got %q", stderr)
}
return
}
if !strings.Contains(stderr, tt.wantStderrSub) {
t.Fatalf("stderr = %q, want substring %q", stderr, tt.wantStderrSub)
}
})
}
}
Loading