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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jobs:
with:
go-version-file: go.mod

- name: Check Go code is modernized
run: make check-go-fix

- name: Build
run: make build-all

Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ make build # Build binary for current platform -> ./bin/runware
make build-all # Build all platforms -> ./bin
make test # Run all tests (go test -race ./...)
make lint # golangci-lint
make fix # Modernize Go code with go fix
make check-go-fix # Fail when go fix would change Go source
make snapshot # GoReleaser snapshot build
make docs # Generate command reference docs
make install # go install for current platform
Expand Down Expand Up @@ -42,6 +44,7 @@ make run ARGS="..." # Run without building
- Commit messages follow conventional commits (`feat:`, `fix:`, `docs:`, `test:`)
- Tests live alongside source files (`*_test.go`)
- Config shorthand keys in CLI map to nested yaml paths (e.g. `model` → `defaults.model`)
- Run `make fix` after every change that touches Go code and retain its modernizations; `make check-go-fix` and CI reject code that `go fix` would change
- Run `go fmt` on any files you modify before considering work complete
- Run `make lint` before considering work complete; fix all lint errors
- Error naming follows https://go.dev/wiki/Errors: error types end in `"Error"` (e.g. `type NotTabularError struct`), error variables start with `"Err"` (e.g. `var ErrNotFound = errors.New(...)`)
Expand Down
10 changes: 8 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GOFLAGS = -trimpath

BINARY=runware

.PHONY: build build-all windows-amd64 windows-arm64 darwin darwin-arm64 darwin-amd64 linux-amd64 linux-arm64 run test lint clean install snapshot docs go-tidy
.PHONY: build build-all windows-amd64 windows-arm64 darwin darwin-arm64 darwin-amd64 linux-amd64 linux-arm64 run test lint fix check-go-fix clean install snapshot docs go-tidy

build:
go build $(GOFLAGS) -ldflags "$(LDFLAGS)" -o bin/${BINARY} ./cmd/runware
Expand Down Expand Up @@ -44,9 +44,15 @@ run:
test:
go test -race ./...

lint:
lint: check-go-fix
golangci-lint run

fix:
go fix ./...

check-go-fix:
go fix -diff ./...

clean:
rm -rf bin dist

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ make build-all # Build all platforms → ./bin/
make install # go install for current platform
make run ARGS="..." # Run without building (e.g. make run ARGS="ping")
make test # Run all tests
make lint # Run golangci-lint
make fix # Apply go fix modernizations
make check-go-fix # Fail when go fix would change Go source
make lint # Check go fix, then run golangci-lint
make docs # Regenerate ./docs/ command reference
make snapshot # GoReleaser snapshot build
make clean # Remove ./bin and ./dist
Expand Down
5 changes: 2 additions & 3 deletions internal/api/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"log/slog"
"maps"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -165,9 +166,7 @@ func TestSend_NoAPIKey(t *testing.T) {
func successItem(t *testing.T, extra map[string]any) json.RawMessage {
t.Helper()
m := map[string]any{fieldStatus: "success"}
for k, v := range extra {
m[k] = v
}
maps.Copy(m, extra)
return rawJSON(t, m)
}

Expand Down
3 changes: 1 addition & 2 deletions internal/api/transport/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -474,8 +474,7 @@ func IsAuthError(err error) bool {
if errors.Is(err, ErrNoAPIKey) {
return true
}
var re *RunwareError
if errors.As(err, &re) {
if re, ok := errors.AsType[*RunwareError](err); ok {
return re.Code == CodeAuth
}
return false
Expand Down
4 changes: 2 additions & 2 deletions internal/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ type TeamMember struct {
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
JoinedAt time.Time `json:"joinedAt,omitempty"`
JoinedAt time.Time `json:"joinedAt,omitzero" yaml:"joinedat,omitempty"`
}

// APIKeyInfo describes a single API key on the account.
Expand All @@ -147,7 +147,7 @@ type APIKeyInfo struct {
Description string `json:"description,omitempty"`
Enabled bool `json:"enabled"`
CreatedAt time.Time `json:"createdAt"`
LastUsedAt time.Time `json:"lastUsedAt,omitempty"`
LastUsedAt time.Time `json:"lastUsedAt,omitzero" yaml:"lastusedat,omitempty"`
Requests int `json:"requests,omitempty"`
}

Expand Down
3 changes: 1 addition & 2 deletions internal/api/upload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,8 +275,7 @@ func TestModelUpload_StreamErrorPropagated(t *testing.T) {
}

_, err := NewClient(mock, slog.Default()).ModelUpload(context.Background(), minimalUploadRequest(), ModelUploadOptions{})
var re *transport.RunwareError
if !errors.As(err, &re) {
if _, ok := errors.AsType[*transport.RunwareError](err); !ok {
t.Fatalf("expected *transport.RunwareError, got %T: %v", err, err)
}
}
Expand Down
4 changes: 1 addition & 3 deletions internal/cmd/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@ The model positional argument may be omitted when --preset supplies one.`,
// so that --preset runs fail consistently with non-preset runs.
func mergePresetParams(presetParams map[string]string, kvArgs []string) (map[string]string, error) {
merged := make(map[string]string, len(presetParams)+len(kvArgs))
for k, v := range presetParams {
merged[k] = v
}
maps.Copy(merged, presetParams)
for _, kv := range kvArgs {
k, v, ok := strings.Cut(kv, "=")
if !ok {
Expand Down
6 changes: 2 additions & 4 deletions internal/cmdutil/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,7 @@ func PrintErrorTo(logger *log.Logger, w io.Writer, format output.Format, err err
return
}

var re *transport.RunwareError
if errors.As(err, &re) {
if re, ok := errors.AsType[*transport.RunwareError](err); ok {
if isStructuredFormat(format) {
writeStructuredError(w, format, structuredErrors(re.APIFields()))
return
Expand Down Expand Up @@ -78,8 +77,7 @@ func PrintErrorMsg(logger *log.Logger, format output.Format, message string, err

// PrintErrorMsgTo logs a custom message and writes structured output to w.
func PrintErrorMsgTo(logger *log.Logger, w io.Writer, format output.Format, message string, err error) {
var re *transport.RunwareError
if errors.As(err, &re) {
if re, ok := errors.AsType[*transport.RunwareError](err); ok {
if isStructuredFormat(format) {
writeStructuredError(w, format, map[string]any{
fieldMessage: message,
Expand Down
30 changes: 13 additions & 17 deletions internal/schema/schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1257,13 +1257,11 @@ const (
testTypeNumber = "number"
)

func f64(v float64) *float64 { return &v }

// boundedWidthNode mirrors a real image model's width: [512, 2048], step 16.
func boundedWidthNode() schema.Node {
return schema.Node{
Properties: map[string]schema.Node{
testFieldWidth: {Type: schema.TypeInteger, Minimum: f64(512), Maximum: f64(2048), MultipleOf: f64(16)},
testFieldWidth: {Type: schema.TypeInteger, Minimum: new(float64(512)), Maximum: new(float64(2048)), MultipleOf: new(float64(16))},
},
}
}
Expand Down Expand Up @@ -1318,7 +1316,7 @@ func TestValidateNumericConstraints_NoConstraint_Passes(t *testing.T) {
func TestValidateNumericConstraints_NegativeMinimumAllowed(t *testing.T) {
node := schema.Node{
Properties: map[string]schema.Node{
testFieldWeight: {Type: testTypeNumber, Minimum: f64(-4), Maximum: f64(4), MultipleOf: f64(0.01)},
testFieldWeight: {Type: testTypeNumber, Minimum: new(float64(-4)), Maximum: new(float64(4)), MultipleOf: new(0.01)},
},
}
if err := schema.ValidateConstraints(node, map[string]any{testFieldWeight: -3.5}); err != nil {
Expand All @@ -1332,7 +1330,7 @@ func TestValidateNumericConstraints_NestedObject(t *testing.T) {
testFieldAccel: {
Type: schema.TypeObject,
Properties: map[string]schema.Node{
testFieldCachePct: {Type: schema.TypeInteger, Minimum: f64(1), Maximum: f64(100)},
testFieldCachePct: {Type: schema.TypeInteger, Minimum: new(float64(1)), Maximum: new(float64(100))},
},
},
},
Expand All @@ -1355,7 +1353,7 @@ func TestValidateNumericConstraints_ArrayItems(t *testing.T) {
Items: &schema.Node{
Type: schema.TypeObject,
Properties: map[string]schema.Node{
testFieldWeight: {Type: testTypeNumber, Minimum: f64(-4), Maximum: f64(4)},
testFieldWeight: {Type: testTypeNumber, Minimum: new(float64(-4)), Maximum: new(float64(4))},
},
},
},
Expand All @@ -1374,7 +1372,7 @@ func TestValidateNumericConstraints_ArrayItems(t *testing.T) {
func TestValidateNumericConstraints_FloatMultipleOf(t *testing.T) {
node := schema.Node{
Properties: map[string]schema.Node{
testFieldCFGScale: {Type: testTypeNumber, Minimum: f64(1), Maximum: f64(20), MultipleOf: f64(0.01)},
testFieldCFGScale: {Type: testTypeNumber, Minimum: new(float64(1)), Maximum: new(float64(20)), MultipleOf: new(0.01)},
},
}
if err := schema.ValidateConstraints(node, map[string]any{testFieldCFGScale: 4.5}); err != nil {
Expand All @@ -1391,8 +1389,6 @@ func TestValidateNumericConstraints_FloatMultipleOf(t *testing.T) {

// ---- ValidateConstraints: NaN / Inf / exclusive bounds / string / array ----

func iptr(v int) *int { return &v }

const (
testFieldImageURL = "imageURL"
testFieldTaskUUID = "taskUUID"
Expand All @@ -1403,7 +1399,7 @@ const (

func TestValidateConstraints_RejectsNaN(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldCFGScale: {Type: testTypeNumber, Minimum: f64(1), Maximum: f64(20)},
testFieldCFGScale: {Type: testTypeNumber, Minimum: new(float64(1)), Maximum: new(float64(20))},
}}
err := schema.ValidateConstraints(node, map[string]any{testFieldCFGScale: math.NaN()})
if err == nil {
Expand All @@ -1416,7 +1412,7 @@ func TestValidateConstraints_RejectsNaN(t *testing.T) {

func TestValidateConstraints_RejectsInf(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldCFGScale: {Type: testTypeNumber, Minimum: f64(1), Maximum: f64(20)},
testFieldCFGScale: {Type: testTypeNumber, Minimum: new(float64(1)), Maximum: new(float64(20))},
}}
err := schema.ValidateConstraints(node, map[string]any{testFieldCFGScale: math.Inf(1)})
if err == nil {
Expand All @@ -1429,7 +1425,7 @@ func TestValidateConstraints_RejectsInf(t *testing.T) {

func TestValidateConstraints_ExclusiveMinimum(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldCFGScale: {Type: testTypeNumber, ExclusiveMinimum: f64(0)},
testFieldCFGScale: {Type: testTypeNumber, ExclusiveMinimum: new(float64(0))},
}}
if err := schema.ValidateConstraints(node, map[string]any{testFieldCFGScale: 0.0}); err == nil {
t.Fatal("expected error for value equal to the exclusive minimum")
Expand All @@ -1441,7 +1437,7 @@ func TestValidateConstraints_ExclusiveMinimum(t *testing.T) {

func TestValidateConstraints_ExclusiveMaximum(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldCFGScale: {Type: testTypeNumber, ExclusiveMaximum: f64(1)},
testFieldCFGScale: {Type: testTypeNumber, ExclusiveMaximum: new(float64(1))},
}}
if err := schema.ValidateConstraints(node, map[string]any{testFieldCFGScale: 1.0}); err == nil {
t.Fatal("expected error for value equal to the exclusive maximum")
Expand All @@ -1450,7 +1446,7 @@ func TestValidateConstraints_ExclusiveMaximum(t *testing.T) {

func TestValidateConstraints_MinLength(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldPositivePrompt: {Type: schema.TypeString, MinLength: iptr(3)},
testFieldPositivePrompt: {Type: schema.TypeString, MinLength: new(3)},
}}
if err := schema.ValidateConstraints(node, map[string]any{testFieldPositivePrompt: "ab"}); err == nil {
t.Fatal("expected error for a string below minLength")
Expand All @@ -1462,7 +1458,7 @@ func TestValidateConstraints_MinLength(t *testing.T) {

func TestValidateConstraints_MaxLength(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldPositivePrompt: {Type: schema.TypeString, MaxLength: iptr(5)},
testFieldPositivePrompt: {Type: schema.TypeString, MaxLength: new(5)},
}}
err := schema.ValidateConstraints(node, map[string]any{testFieldPositivePrompt: "abcdef"})
if err == nil {
Expand Down Expand Up @@ -1536,7 +1532,7 @@ func TestValidateConstraints_UnknownFormatPasses(t *testing.T) {

func TestValidateConstraints_MinItems(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldTags: {Type: schema.TypeArray, MinItems: iptr(2)},
testFieldTags: {Type: schema.TypeArray, MinItems: new(2)},
}}
err := schema.ValidateConstraints(node, map[string]any{testFieldTags: []any{"a"}})
if err == nil {
Expand All @@ -1549,7 +1545,7 @@ func TestValidateConstraints_MinItems(t *testing.T) {

func TestValidateConstraints_MaxItems(t *testing.T) {
node := schema.Node{Properties: map[string]schema.Node{
testFieldTags: {Type: schema.TypeArray, MaxItems: iptr(2)},
testFieldTags: {Type: schema.TypeArray, MaxItems: new(2)},
}}
if err := schema.ValidateConstraints(node, map[string]any{testFieldTags: []any{"a", "b", "c"}}); err == nil {
t.Fatal("expected error for an array above maxItems")
Expand Down