From 752f0c0c1418608db1461c018960bceda2751971 Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Wed, 28 Jan 2026 14:32:00 -0800 Subject: [PATCH 1/3] handle cost field --- cmd/environment.go | 8 ++++- cmd/project.go | 8 ++++- cmd/templates/environment.get.md.tmpl | 10 ++++-- cmd/templates/project.get.md.tmpl | 10 ++++-- pkg/api/cost.go | 34 +++++++++++++++---- pkg/api/environment_test.go | 38 +++++++++++---------- pkg/api/genqlient.graphql | 6 ++++ pkg/api/schema.graphql | 16 ++++----- pkg/api/zz_generated.go | 48 +++++++++++++-------------- 9 files changed, 116 insertions(+), 62 deletions(-) diff --git a/cmd/environment.go b/cmd/environment.go index efc08ffe..514c8fac 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -156,7 +156,13 @@ func runEnvironmentList(cmd *cobra.Command, args []string) error { tbl := cli.NewTable("ID/Slug", "Name", "Description", "Monthly $", "Daily $") for _, env := range environments { - tbl.AddRow(env.Slug, env.Name, env.Description, env.Cost.Monthly.Average.Amount, env.Cost.Daily.Average.Amount) + monthly := "-" + daily := "-" + if env.Cost != nil { + monthly = env.Cost.Monthly.Average.DisplayAmount() + daily = env.Cost.Daily.Average.DisplayAmount() + } + tbl.AddRow(env.Slug, env.Name, env.Description, monthly, daily) } tbl.Print() diff --git a/cmd/project.go b/cmd/project.go index c2164c66..2001a5f4 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -163,7 +163,13 @@ func runProjectList(cmd *cobra.Command, args []string) error { tbl := cli.NewTable("ID/Slug", "Name", "Description", "Monthly $", "Daily $") for _, project := range projects { - tbl.AddRow(project.Slug, project.Name, project.Description, project.Cost.Monthly.Average.Amount, project.Cost.Daily.Average.Amount) + monthly := "-" + daily := "-" + if project.Cost != nil { + monthly = project.Cost.Monthly.Average.DisplayAmount() + daily = project.Cost.Daily.Average.DisplayAmount() + } + tbl.AddRow(project.Slug, project.Name, project.Description, monthly, daily) } tbl.Print() diff --git a/cmd/templates/environment.get.md.tmpl b/cmd/templates/environment.get.md.tmpl index 03eec12c..b2608b67 100644 --- a/cmd/templates/environment.get.md.tmpl +++ b/cmd/templates/environment.get.md.tmpl @@ -12,6 +12,12 @@ {{- end}} ## Cost -**Monthly Average:** ${{.Cost.Monthly.Average.Amount}} +{{- if .Cost}} +**Monthly Average:** {{.Cost.Monthly.Average.DisplayAmountUSD}} -**Daily Average:** ${{.Cost.Daily.Average.Amount}} +**Daily Average:** {{.Cost.Daily.Average.DisplayAmountUSD}} +{{- else}} +**Monthly Average:** - + +**Daily Average:** - +{{- end}} diff --git a/cmd/templates/project.get.md.tmpl b/cmd/templates/project.get.md.tmpl index 8a5d316a..5dccf63a 100644 --- a/cmd/templates/project.get.md.tmpl +++ b/cmd/templates/project.get.md.tmpl @@ -10,6 +10,12 @@ {{end}} ## Cost -**Monthly Average:** ${{.Cost.Monthly.Average.Amount}} +{{- if .Cost}} +**Monthly Average:** {{.Cost.Monthly.Average.DisplayAmountUSD}} -**Daily Average:** ${{.Cost.Daily.Average.Amount}} +**Daily Average:** {{.Cost.Daily.Average.DisplayAmountUSD}} +{{- else}} +**Monthly Average:** - + +**Daily Average:** - +{{- end}} diff --git a/pkg/api/cost.go b/pkg/api/cost.go index 7f43e74f..80fe4876 100644 --- a/pkg/api/cost.go +++ b/pkg/api/cost.go @@ -1,14 +1,36 @@ package api +import "fmt" + type Cost struct { - Monthly *CostType `json:"monthly"` - Daily *CostType `json:"daily"` + Monthly Summary `json:"monthly"` + Daily Summary `json:"daily"` +} + +// Summary of costs over a time period. +type Summary struct { + Previous CostSample `json:"previous"` + Average CostSample `json:"average"` +} + +// A single cost measurement. Fields may be null when no cost data exists. +type CostSample struct { + Amount *float64 `json:"amount"` + Currency *string `json:"currency"` } -type CostType struct { - Average *CostSummary `json:"average"` +// DisplayAmount returns a human-friendly value for rendering in tables. +func (cs CostSample) DisplayAmount() string { + if cs.Amount == nil { + return "-" + } + return fmt.Sprintf("%v", *cs.Amount) } -type CostSummary struct { - Amount float64 `json:"amount"` +// DisplayAmountUSD returns a value suitable for markdown templates where we want a "$" prefix. +func (cs CostSample) DisplayAmountUSD() string { + if cs.Amount == nil { + return "-" + } + return fmt.Sprintf("$%v", *cs.Amount) } diff --git a/pkg/api/environment_test.go b/pkg/api/environment_test.go index bc9074cd..f0f4d96c 100644 --- a/pkg/api/environment_test.go +++ b/pkg/api/environment_test.go @@ -9,6 +9,8 @@ import ( "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" ) +func floatPtr(v float64) *float64 { return &v } + func TestGetEnvironment(t *testing.T) { want := api.Environment{ ID: "env-uuid1", @@ -16,14 +18,14 @@ func TestGetEnvironment(t *testing.T) { Slug: "env", Description: "This is a test environment", Cost: &api.Cost{ - Daily: &api.CostType{ - Average: &api.CostSummary{ - Amount: 10.0, + Daily: api.Summary{ + Average: api.CostSample{ + Amount: floatPtr(10.0), }, }, - Monthly: &api.CostType{ - Average: &api.CostSummary{ - Amount: 300.0, + Monthly: api.Summary{ + Average: api.CostSample{ + Amount: floatPtr(300.0), }, }, }, @@ -151,14 +153,14 @@ func TestGetEnvironmentsByPackage(t *testing.T) { Slug: "env1", Description: "First test environment", Cost: &api.Cost{ - Daily: &api.CostType{ - Average: &api.CostSummary{ - Amount: 5.0, + Daily: api.Summary{ + Average: api.CostSample{ + Amount: floatPtr(5.0), }, }, - Monthly: &api.CostType{ - Average: &api.CostSummary{ - Amount: 150.0, + Monthly: api.Summary{ + Average: api.CostSample{ + Amount: floatPtr(150.0), }, }, }, @@ -173,14 +175,14 @@ func TestGetEnvironmentsByPackage(t *testing.T) { Slug: "env2", Description: "Second test environment", Cost: &api.Cost{ - Daily: &api.CostType{ - Average: &api.CostSummary{ - Amount: 8.0, + Daily: api.Summary{ + Average: api.CostSample{ + Amount: floatPtr(8.0), }, }, - Monthly: &api.CostType{ - Average: &api.CostSummary{ - Amount: 240.0, + Monthly: api.Summary{ + Average: api.CostSample{ + Amount: floatPtr(240.0), }, }, }, diff --git a/pkg/api/genqlient.graphql b/pkg/api/genqlient.graphql index 2384db4f..b4446eca 100644 --- a/pkg/api/genqlient.graphql +++ b/pkg/api/genqlient.graphql @@ -222,11 +222,13 @@ query getEnvironmentById($organizationId: ID!, $id: ID!) { cost { monthly { average { + # @genqlient(pointer: true) amount } } daily { average { + # @genqlient(pointer: true) amount } } @@ -278,11 +280,13 @@ query getEnvironmentsByProject($organizationId: ID!, $projectId: ID!) { cost { monthly { average { + # @genqlient(pointer: true) amount } } daily { average { + # @genqlient(pointer: true) amount } } @@ -529,11 +533,13 @@ query getProjects($organizationId: ID!){ cost{ monthly{ average{ + # @genqlient(pointer: true) amount } } daily{ average{ + # @genqlient(pointer: true) amount } } diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index aec9e414..3cb652bc 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -2028,28 +2028,28 @@ type RecentDeployment { "Cost information for a resource" type Cost { "Monthly cost summary" - monthly: Summary + monthly: Summary! "Daily cost summary" - daily: Summary + daily: Summary! } "Summary of costs over a time period" type Summary { - "Previous period's cost sample" + "Previous period's cost sample (amount/currency may be null)" previous: CostSample! - "Average cost sample for the period" + "Average cost sample for the period (amount/currency may be null)" average: CostSample! } "A single cost measurement" type CostSample { - "The cost amount" - amount: Float! + "The cost amount (null if no data available)" + amount: Float - "The currency code (e.g. USD)" - currency: String! + "The currency code, e.g. USD (null if no data available)" + currency: String } type ContainerRepositoryAuth { diff --git a/pkg/api/zz_generated.go b/pkg/api/zz_generated.go index 0f0615c1..f1005e0c 100644 --- a/pkg/api/zz_generated.go +++ b/pkg/api/zz_generated.go @@ -2732,7 +2732,7 @@ func (v *getEnvironmentByIdEnvironmentCost) GetDaily() getEnvironmentByIdEnviron // // Summary of costs over a time period type getEnvironmentByIdEnvironmentCostDailySummary struct { - // Average cost sample for the period + // Average cost sample for the period (amount/currency may be null) Average getEnvironmentByIdEnvironmentCostDailySummaryAverageCostSample `json:"average"` } @@ -2746,12 +2746,12 @@ func (v *getEnvironmentByIdEnvironmentCostDailySummary) GetAverage() getEnvironm // // A single cost measurement type getEnvironmentByIdEnvironmentCostDailySummaryAverageCostSample struct { - // The cost amount - Amount float64 `json:"amount"` + // The cost amount (null if no data available) + Amount *float64 `json:"amount"` } // GetAmount returns getEnvironmentByIdEnvironmentCostDailySummaryAverageCostSample.Amount, and is useful for accessing the field via an interface. -func (v *getEnvironmentByIdEnvironmentCostDailySummaryAverageCostSample) GetAmount() float64 { +func (v *getEnvironmentByIdEnvironmentCostDailySummaryAverageCostSample) GetAmount() *float64 { return v.Amount } @@ -2760,7 +2760,7 @@ func (v *getEnvironmentByIdEnvironmentCostDailySummaryAverageCostSample) GetAmou // // Summary of costs over a time period type getEnvironmentByIdEnvironmentCostMonthlySummary struct { - // Average cost sample for the period + // Average cost sample for the period (amount/currency may be null) Average getEnvironmentByIdEnvironmentCostMonthlySummaryAverageCostSample `json:"average"` } @@ -2774,12 +2774,12 @@ func (v *getEnvironmentByIdEnvironmentCostMonthlySummary) GetAverage() getEnviro // // A single cost measurement type getEnvironmentByIdEnvironmentCostMonthlySummaryAverageCostSample struct { - // The cost amount - Amount float64 `json:"amount"` + // The cost amount (null if no data available) + Amount *float64 `json:"amount"` } // GetAmount returns getEnvironmentByIdEnvironmentCostMonthlySummaryAverageCostSample.Amount, and is useful for accessing the field via an interface. -func (v *getEnvironmentByIdEnvironmentCostMonthlySummaryAverageCostSample) GetAmount() float64 { +func (v *getEnvironmentByIdEnvironmentCostMonthlySummaryAverageCostSample) GetAmount() *float64 { return v.Amount } @@ -3195,7 +3195,7 @@ func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCost) GetDaily() // // Summary of costs over a time period type getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummary struct { - // Average cost sample for the period + // Average cost sample for the period (amount/currency may be null) Average getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummaryAverageCostSample `json:"average"` } @@ -3209,12 +3209,12 @@ func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummary) // // A single cost measurement type getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummaryAverageCostSample struct { - // The cost amount - Amount float64 `json:"amount"` + // The cost amount (null if no data available) + Amount *float64 `json:"amount"` } // GetAmount returns getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummaryAverageCostSample.Amount, and is useful for accessing the field via an interface. -func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummaryAverageCostSample) GetAmount() float64 { +func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummaryAverageCostSample) GetAmount() *float64 { return v.Amount } @@ -3223,7 +3223,7 @@ func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostDailySummaryA // // Summary of costs over a time period type getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummary struct { - // Average cost sample for the period + // Average cost sample for the period (amount/currency may be null) Average getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummaryAverageCostSample `json:"average"` } @@ -3237,12 +3237,12 @@ func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummar // // A single cost measurement type getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummaryAverageCostSample struct { - // The cost amount - Amount float64 `json:"amount"` + // The cost amount (null if no data available) + Amount *float64 `json:"amount"` } // GetAmount returns getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummaryAverageCostSample.Amount, and is useful for accessing the field via an interface. -func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummaryAverageCostSample) GetAmount() float64 { +func (v *getEnvironmentsByProjectProjectEnvironmentsEnvironmentCostMonthlySummaryAverageCostSample) GetAmount() *float64 { return v.Amount } @@ -4273,7 +4273,7 @@ func (v *getProjectsProjectsProjectCost) GetDaily() getProjectsProjectsProjectCo // // Summary of costs over a time period type getProjectsProjectsProjectCostDailySummary struct { - // Average cost sample for the period + // Average cost sample for the period (amount/currency may be null) Average getProjectsProjectsProjectCostDailySummaryAverageCostSample `json:"average"` } @@ -4287,12 +4287,12 @@ func (v *getProjectsProjectsProjectCostDailySummary) GetAverage() getProjectsPro // // A single cost measurement type getProjectsProjectsProjectCostDailySummaryAverageCostSample struct { - // The cost amount - Amount float64 `json:"amount"` + // The cost amount (null if no data available) + Amount *float64 `json:"amount"` } // GetAmount returns getProjectsProjectsProjectCostDailySummaryAverageCostSample.Amount, and is useful for accessing the field via an interface. -func (v *getProjectsProjectsProjectCostDailySummaryAverageCostSample) GetAmount() float64 { +func (v *getProjectsProjectsProjectCostDailySummaryAverageCostSample) GetAmount() *float64 { return v.Amount } @@ -4301,7 +4301,7 @@ func (v *getProjectsProjectsProjectCostDailySummaryAverageCostSample) GetAmount( // // Summary of costs over a time period type getProjectsProjectsProjectCostMonthlySummary struct { - // Average cost sample for the period + // Average cost sample for the period (amount/currency may be null) Average getProjectsProjectsProjectCostMonthlySummaryAverageCostSample `json:"average"` } @@ -4315,12 +4315,12 @@ func (v *getProjectsProjectsProjectCostMonthlySummary) GetAverage() getProjectsP // // A single cost measurement type getProjectsProjectsProjectCostMonthlySummaryAverageCostSample struct { - // The cost amount - Amount float64 `json:"amount"` + // The cost amount (null if no data available) + Amount *float64 `json:"amount"` } // GetAmount returns getProjectsProjectsProjectCostMonthlySummaryAverageCostSample.Amount, and is useful for accessing the field via an interface. -func (v *getProjectsProjectsProjectCostMonthlySummaryAverageCostSample) GetAmount() float64 { +func (v *getProjectsProjectsProjectCostMonthlySummaryAverageCostSample) GetAmount() *float64 { return v.Amount } From f8ba06b1e838a4c387d73d8cd81f99d25f1a74de Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Wed, 28 Jan 2026 15:16:00 -0800 Subject: [PATCH 2/3] fixing cost nil pointer --- cmd/environment.go | 12 +- cmd/project.go | 12 +- cmd/templates/environment.get.md.tmpl | 10 +- cmd/templates/project.get.md.tmpl | 10 +- cmd/version.go | 26 +++- pkg/api/cost.go | 18 --- pkg/api/environment.go | 2 +- pkg/api/environment_test.go | 6 +- pkg/api/project.go | 2 +- pkg/api/schema.graphql | 199 ++++++++++++++++++-------- pkg/api/zz_generated.go | 7 +- 11 files changed, 186 insertions(+), 118 deletions(-) diff --git a/cmd/environment.go b/cmd/environment.go index 514c8fac..f42b7f10 100644 --- a/cmd/environment.go +++ b/cmd/environment.go @@ -156,11 +156,13 @@ func runEnvironmentList(cmd *cobra.Command, args []string) error { tbl := cli.NewTable("ID/Slug", "Name", "Description", "Monthly $", "Daily $") for _, env := range environments { - monthly := "-" - daily := "-" - if env.Cost != nil { - monthly = env.Cost.Monthly.Average.DisplayAmount() - daily = env.Cost.Daily.Average.DisplayAmount() + monthly := "" + daily := "" + if env.Cost.Monthly.Average.Amount != nil { + monthly = fmt.Sprintf("%v", *env.Cost.Monthly.Average.Amount) + } + if env.Cost.Daily.Average.Amount != nil { + daily = fmt.Sprintf("%v", *env.Cost.Daily.Average.Amount) } tbl.AddRow(env.Slug, env.Name, env.Description, monthly, daily) } diff --git a/cmd/project.go b/cmd/project.go index 2001a5f4..ea22c689 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -163,11 +163,13 @@ func runProjectList(cmd *cobra.Command, args []string) error { tbl := cli.NewTable("ID/Slug", "Name", "Description", "Monthly $", "Daily $") for _, project := range projects { - monthly := "-" - daily := "-" - if project.Cost != nil { - monthly = project.Cost.Monthly.Average.DisplayAmount() - daily = project.Cost.Daily.Average.DisplayAmount() + monthly := "" + daily := "" + if project.Cost.Monthly.Average.Amount != nil { + monthly = fmt.Sprintf("%v", *project.Cost.Monthly.Average.Amount) + } + if project.Cost.Daily.Average.Amount != nil { + daily = fmt.Sprintf("%v", *project.Cost.Daily.Average.Amount) } tbl.AddRow(project.Slug, project.Name, project.Description, monthly, daily) } diff --git a/cmd/templates/environment.get.md.tmpl b/cmd/templates/environment.get.md.tmpl index b2608b67..47094d8e 100644 --- a/cmd/templates/environment.get.md.tmpl +++ b/cmd/templates/environment.get.md.tmpl @@ -12,12 +12,6 @@ {{- end}} ## Cost -{{- if .Cost}} -**Monthly Average:** {{.Cost.Monthly.Average.DisplayAmountUSD}} +**Monthly Average:** {{with .Monthly.Average.Amount}}${{.}}{{end}} -**Daily Average:** {{.Cost.Daily.Average.DisplayAmountUSD}} -{{- else}} -**Monthly Average:** - - -**Daily Average:** - -{{- end}} +**Daily Average:** {{with .Daily.Average.Amount}}${{.}}{{end}} diff --git a/cmd/templates/project.get.md.tmpl b/cmd/templates/project.get.md.tmpl index 5dccf63a..fbf0b846 100644 --- a/cmd/templates/project.get.md.tmpl +++ b/cmd/templates/project.get.md.tmpl @@ -10,12 +10,6 @@ {{end}} ## Cost -{{- if .Cost}} -**Monthly Average:** {{.Cost.Monthly.Average.DisplayAmountUSD}} +**Monthly Average:** {{with .Monthly.Average.Amount}}${{.}}{{end}} -**Daily Average:** {{.Cost.Daily.Average.DisplayAmountUSD}} -{{- else}} -**Monthly Average:** - - -**Daily Average:** - -{{- end}} +**Daily Average:** {{with .Daily.Average.Amount}}${{.}}{{end}} diff --git a/cmd/version.go b/cmd/version.go index b2a743b5..7727721a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,10 +1,13 @@ package cmd import ( + "context" "fmt" + "github.com/massdriver-cloud/mass/pkg/api" "github.com/massdriver-cloud/mass/pkg/prettylogs" "github.com/massdriver-cloud/mass/pkg/version" + "github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client" "github.com/spf13/cobra" ) @@ -21,15 +24,24 @@ func NewCmdVersion() *cobra.Command { } func runVersion(cmd *cobra.Command, args []string) { - latestVersion, err := version.GetLatestVersion() + massVersionColor := prettylogs.Green(version.MassVersion()) + fmt.Printf("🧰 CLI: %v (git SHA: %v)\n", massVersionColor, version.MassGitSHA()) + + // Best-effort: check whether a newer CLI is available (does not affect exit code). + if latestVersion, err := version.GetLatestVersion(); err == nil { + if isOld, _ := version.CheckForNewerVersionAvailable(latestVersion); isOld { + fmt.Printf("⬆️ Update: %v\n", version.LatestReleaseURL) + } + } + + // Best-effort: if we can authenticate, show the Massdriver server version too. + ctx := context.Background() + mdClient, err := client.New() if err != nil { - fmt.Printf("Could not check for newer version, skipping. url:%s error:%s", version.LatestReleaseURL, err.Error()) return } - isOld, _ := version.CheckForNewerVersionAvailable(latestVersion) - if isOld { - fmt.Printf("A newer version of the CLI is available, you can download it here: %v\n", version.LatestReleaseURL) + + if server, err := api.GetServer(ctx, mdClient); err == nil && server != nil && server.Version != "" { + fmt.Printf("🌐 Server: %v\n", prettylogs.Green(server.Version)) } - massVersionColor := prettylogs.Green(version.MassVersion()) - fmt.Printf("Mass CLI version: %v (git SHA: %v) \n", massVersionColor, version.MassGitSHA()) } diff --git a/pkg/api/cost.go b/pkg/api/cost.go index 80fe4876..e1c87cce 100644 --- a/pkg/api/cost.go +++ b/pkg/api/cost.go @@ -1,7 +1,5 @@ package api -import "fmt" - type Cost struct { Monthly Summary `json:"monthly"` Daily Summary `json:"daily"` @@ -18,19 +16,3 @@ type CostSample struct { Amount *float64 `json:"amount"` Currency *string `json:"currency"` } - -// DisplayAmount returns a human-friendly value for rendering in tables. -func (cs CostSample) DisplayAmount() string { - if cs.Amount == nil { - return "-" - } - return fmt.Sprintf("%v", *cs.Amount) -} - -// DisplayAmountUSD returns a value suitable for markdown templates where we want a "$" prefix. -func (cs CostSample) DisplayAmountUSD() string { - if cs.Amount == nil { - return "-" - } - return fmt.Sprintf("$%v", *cs.Amount) -} diff --git a/pkg/api/environment.go b/pkg/api/environment.go index 1984dd0f..b32b9411 100644 --- a/pkg/api/environment.go +++ b/pkg/api/environment.go @@ -16,7 +16,7 @@ type Environment struct { Name string `json:"name"` Slug string `json:"slug"` Description string `json:"description,omitempty"` - Cost *Cost `json:"cost,omitempty" mapstructure:"cost,omitempty"` + Cost Cost `json:"cost" mapstructure:"cost"` Packages []Package `json:"packages,omitempty" mapstructure:"packages,omitempty"` Project *Project `json:"project,omitempty" mapstructure:"project,omitempty"` } diff --git a/pkg/api/environment_test.go b/pkg/api/environment_test.go index f0f4d96c..6ad6a146 100644 --- a/pkg/api/environment_test.go +++ b/pkg/api/environment_test.go @@ -17,7 +17,7 @@ func TestGetEnvironment(t *testing.T) { Name: "Test Environment", Slug: "env", Description: "This is a test environment", - Cost: &api.Cost{ + Cost: api.Cost{ Daily: api.Summary{ Average: api.CostSample{ Amount: floatPtr(10.0), @@ -152,7 +152,7 @@ func TestGetEnvironmentsByPackage(t *testing.T) { Name: "Test Environment 1", Slug: "env1", Description: "First test environment", - Cost: &api.Cost{ + Cost: api.Cost{ Daily: api.Summary{ Average: api.CostSample{ Amount: floatPtr(5.0), @@ -174,7 +174,7 @@ func TestGetEnvironmentsByPackage(t *testing.T) { Name: "Test Environment 2", Slug: "env2", Description: "Second test environment", - Cost: &api.Cost{ + Cost: api.Cost{ Daily: api.Summary{ Average: api.CostSample{ Amount: floatPtr(8.0), diff --git a/pkg/api/project.go b/pkg/api/project.go index 859dd8bc..f6ef7219 100644 --- a/pkg/api/project.go +++ b/pkg/api/project.go @@ -16,7 +16,7 @@ type Project struct { Slug string `json:"slug"` Description string `json:"description"` DefaultParams map[string]any `json:"defaultParams"` - Cost *Cost `json:"cost,omitempty"` + Cost Cost `json:"cost"` Environments []Environment `json:"environments"` } diff --git a/pkg/api/schema.graphql b/pkg/api/schema.graphql index 3cb652bc..876ac285 100644 --- a/pkg/api/schema.graphql +++ b/pkg/api/schema.graphql @@ -217,7 +217,7 @@ type RootQueryType { integrationTypes: [IntegrationType] "List all integrations enabled for an organization" - integrations(organizationId: ID!): [Integration] + integrations(organizationId: ID!): [IntegrationConfig] environment( organizationId: ID! @@ -375,7 +375,22 @@ type RootMutationType { revokeGroupAccess(organizationId: ID!, groupId: ID!, permission: PermissionRevokeInput!): ProjectPayload "Create an artifact" - createArtifact(organizationId: ID!, name: String!, type: String!, specs: JSON!, data: JSON!): ArtifactPayload + createArtifact( + organizationId: ID! + + name: String! + + type: String! + + "[Deprecated] Use payload instead. Required when payload is not provided." + specs: JSON + + "[Deprecated] Use payload instead. Required when payload is not provided." + data: JSON + + "Artifact payload containing all artifact data. Provide this OR both data and specs." + payload: JSON + ): ArtifactPayload "Update an artifact" updateArtifact(organizationId: ID!, id: ID!, params: ArtifactUpdateParams!): ArtifactPayload @@ -423,12 +438,6 @@ type RootMutationType { id: ID! ): BundlePayload - "Assign a credential to be used to retrieve cloud costs" - assignCloudCostCredential(organizationId: ID!, artifactId: ID!): CloudCostCredentialPayload - - "Dismisses a credential to be used to retrieve cloud costs" - dismissCloudCostCredential(organizationId: ID!, artifactId: ID!): CloudCostCredentialPayload - "Enqueues a package for deployment" deployPackage( organizationId: ID! @@ -474,11 +483,17 @@ type RootMutationType { unlinkManifests(organizationId: ID!, linkId: ID!): LinkPayload - "Enable an integration for an organization" - enableIntegration(organizationId: ID!, integrationTypeId: String!, config: JSON!, auth: JSON!): IntegrationPayload + "Create an integration for an organization" + createIntegration(organizationId: ID!, input: CreateIntegrationInput!): IntegrationPayload + + "Enable an integration" + enableIntegration(organizationId: ID!, id: ID!): IntegrationPayload - "Disable an integration for an organization" - disableIntegration(organizationId: ID!, integrationTypeId: String!): Boolean + "Disable an integration" + disableIntegration(organizationId: ID!, id: ID!): IntegrationPayload + + "Delete an integration" + deleteIntegration(organizationId: ID!, id: ID!): IntegrationPayload "Create an environment" createEnvironment(organizationId: ID!, projectId: ID!, name: String!, slug: String!, description: String): EnvironmentPayload @@ -497,7 +512,29 @@ type RootMutationType { updateEnvironment(organizationId: ID!, id: ID!, name: String!, description: String): EnvironmentPayload "Removes an environment from a project. This will fail if infrastructure is still provisioned in the environment." - deleteEnvironment(organizationId: ID!, id: ID!): EnvironmentPayload + deleteEnvironment(organizationId: ID!, id: ID!, orphanForks: Boolean): EnvironmentPayload + + "Forks an environment, creating a new environment with optional copying of secrets, env defaults, and remote references." + forkEnvironment( + organizationId: ID! + + "The ID of the environment to fork from" + parentId: ID! + + "Input containing the new environment details and options for what to copy from the parent" + input: ForkEnvironmentInput! + ): EnvironmentPayload + + "Merges a forked environment back into its parent. By default copies params, with options for secrets, remote references, and env defaults." + mergeEnvironment( + organizationId: ID! + + "The ID of the forked environment to merge" + id: ID! + + "Input containing options for what to copy from the fork back to the parent environment" + input: MergeEnvironmentInput + ): EnvironmentPayload "Connect an environment as the default environment type for a given environment" createEnvironmentConnection(organizationId: ID!, artifactId: ID!, environmentId: ID!): EnvironmentConnectionPayload @@ -987,7 +1024,7 @@ type Project { defaultParams: JSON @deprecated(reason: "Default params are being deprecated for preview environments in favor of inherit\/override PEs from specific environments. This field will be removed in a future release.") "Cloud provider costs for this project" - cost: Cost + cost: Cost! } type ProjectPayload { @@ -1066,6 +1103,18 @@ type Dimension { value: String! } +"A flattened artifact payload property summary with display name, path, and value. Sensitive fields are masked as [SENSITIVE]." +type ArtifactPropertySummary { + "Display name for the property summary (e.g., 'Database: Hostname', 'Database: Port')" + name: String! + + "Full path to the value in the artifact payload (e.g., '.database.hostname', '.database.port')" + path: String! + + "The scalar value. Sensitive fields (marked with $md.sensitive: true) are masked as '[SENSITIVE]'" + value: JSON +} + "An alarm is a condition that triggers a notification. It is defined by a metric, a comparison operator, a threshold, and a period." type Alarm { "Unique identifier for the alarm." @@ -1211,11 +1260,14 @@ type Package { "Artifacts provisioned by this package" artifacts: [Artifact] + "Flattened list of artifact payload property summaries suitable for display in a table. Sensitive fields are automatically masked." + propertySummaries: [ArtifactPropertySummary] + "Artifacts from a remote source like another project or a resource not managed by massdriver" remoteReferences: [RemoteReference] "Cloud provider costs for this package" - cost: Cost + cost: Cost! } type PackagePayload { @@ -1649,6 +1701,37 @@ input PreviewEnvironmentInput { ciContext: JSON! } +input ForkEnvironmentInput { + "Name for the new forked environment" + name: String! + + "Slug for the new forked environment" + slug: String! + + "Optional description for the new forked environment" + description: String + + "Whether to copy secrets from the parent environment to the fork" + copySecrets: Boolean + + "Whether to copy environment defaults (target connections) from the parent environment to the fork" + copyEnvDefaults: Boolean + + "Whether to copy remote references from the parent environment to the fork" + copyRemoteReferences: Boolean +} + +input MergeEnvironmentInput { + "Whether to copy secrets from the fork back to the parent environment" + copySecrets: Boolean + + "Whether to copy environment defaults (target connections) from the fork back to the parent environment" + copyEnvDefaults: Boolean + + "Whether to copy remote references from the fork back to the parent environment" + copyRemoteReferences: Boolean +} + type Environment { id: ID @@ -1658,6 +1741,8 @@ type Environment { description: String + parent: Environment + deletable: EnvironmentDeletionLifecycle! createdAt: DateTime @@ -1675,8 +1760,8 @@ type Environment { defaultConnections: [DefaultEnvironmentConnection] - "Cloud provider costs for this target" - cost: Cost + "Cloud provider costs for this environment" + cost: Cost! } type EnvironmentPayload { @@ -1690,21 +1775,31 @@ type EnvironmentPayload { result: Environment } +enum IntegrationStatus { + DISABLED + ENABLED + ENABLING + DISABLING +} + type IntegrationType { name: String! id: String! + description: String configSchema: JSON! authSchema: JSON! + docs: String! } -type Integration { +type IntegrationConfig { id: ID! organizationId: ID! integrationType: String! config: JSON! - auth: JSON! + status: IntegrationStatus! createdAt: DateTime! updatedAt: DateTime! + nextRunAt: DateTime } type IntegrationPayload { @@ -1715,7 +1810,19 @@ type IntegrationPayload { messages: [ValidationMessage] "The object created\/updated\/deleted by the mutation. May be null if mutation failed." - result: Integration + result: IntegrationConfig +} + +"Input for creating an integration configuration" +input CreateIntegrationInput { + "The integration type identifier (e.g., 'aws-cost-and-usage-reports')" + integrationTypeId: String! + + "Integration-specific configuration. Must conform to the integration type's config_schema." + config: JSON! + + "Authentication credentials. Must conform to the integration type's auth_schema." + auth: JSON! } "A connection between two nodes in the diagram" @@ -2036,10 +2143,10 @@ type Cost { "Summary of costs over a time period" type Summary { - "Previous period's cost sample (amount/currency may be null)" + "Previous period's cost sample (amount\/currency may be null)" previous: CostSample! - "Average cost sample for the period (amount/currency may be null)" + "Average cost sample for the period (amount\/currency may be null)" average: CostSample! } @@ -2084,39 +2191,6 @@ type Connection { updatedAt: DateTime } -"Indicates whether the artifact is used to fetch cloud cost data" -enum CloudCostStatus { - "Currently used to fetch cloud cost data" - ACTIVE - - "Currently using the credential to build dependencies for fetching cloud costs" - PENDING - - "Attempted to create cloud cost dependencies but failed to do so" - FAILED - - "Artifact does not support fetching cloud cost data" - UNSUPPORTED - - "Eligable to be used for fetching cloud costs but is not in use" - INACTIVE -} - -type CloudCostCredential { - status: CloudCostStatus! -} - -type CloudCostCredentialPayload { - "Indicates if the mutation completed successfully or not." - successful: Boolean! - - "A list of failed validations. May be blank or null if mutation succeeded." - messages: [ValidationMessage] - - "The object created\/updated\/deleted by the mutation. May be null if mutation failed." - result: CloudCostCredential -} - input Cursor { "Maximum number of items to return" limit: Int @@ -2795,14 +2869,19 @@ input ArtifactsInput { filter: ArtifactsFilters } -"Allowed params used in updated artifacts. Provisioned artifacts can only have their name updated. Imported artifacts can update specs, data, or name" +"Allowed params used in updated artifacts. Provisioned artifacts can only have their name updated. Imported artifacts can update specs, data, payload, or name. Use payload for new integrations; data\/specs are deprecated." input ArtifactUpdateParams { "The new name of the artifact" name: String + "[Deprecated] Use payload instead. Artifact specs for backward compatibility." specs: JSON + "[Deprecated] Use payload instead. Artifact data for backward compatibility." data: JSON + + "Artifact payload containing all artifact data." + payload: JSON } type Artifact { @@ -2818,7 +2897,11 @@ type Artifact { "The bundle's artifact field (output field) that produced this artifact." field: String - specs: JSON + "Artifact specs for backward compatibility." + specs: JSON @deprecated(reason: "Use payload['specs'] instead if applicable") + + "Complete artifact payload containing all artifact data. Fields marked with $md.sensitive in the artifact definition will be masked as [SENSITIVE]. Use downloadArtifact to retrieve unmasked values. See https:\/\/docs.massdriver.cloud\/json-schema-cheat-sheet\/massdriver-annotations for more details." + payload: JSON packageId: ID @deprecated(reason: "Use package{id} instead") @@ -2846,8 +2929,6 @@ type Artifact { "Packages that remotely reference this artifact" referencedBy: [Package]! - - cloudCostStatus: CloudCostStatus! } type ArtifactPayload { diff --git a/pkg/api/zz_generated.go b/pkg/api/zz_generated.go index f1005e0c..0f20e5c4 100644 --- a/pkg/api/zz_generated.go +++ b/pkg/api/zz_generated.go @@ -1895,7 +1895,8 @@ type getArtifactArtifact struct { Name string `json:"name"` Type string `json:"type"` // The bundle's artifact field (output field) that produced this artifact. - Field string `json:"field"` + Field string `json:"field"` + // Artifact specs for backward compatibility. Specs map[string]any `json:"-"` // Download formats supported for this artifact Formats []string `json:"formats"` @@ -2675,7 +2676,7 @@ type getEnvironmentByIdEnvironment struct { Name string `json:"name"` Slug string `json:"slug"` Description string `json:"description"` - // Cloud provider costs for this target + // Cloud provider costs for this environment Cost getEnvironmentByIdEnvironmentCost `json:"cost"` Packages []getEnvironmentByIdEnvironmentPackagesPackage `json:"packages"` Project getEnvironmentByIdEnvironmentProject `json:"project"` @@ -3134,7 +3135,7 @@ type getEnvironmentsByProjectProjectEnvironmentsEnvironment struct { Name string `json:"name"` Slug string `json:"slug"` Description string `json:"description"` - // Cloud provider costs for this target + // Cloud provider costs for this environment Cost getEnvironmentsByProjectProjectEnvironmentsEnvironmentCost `json:"cost"` Packages []getEnvironmentsByProjectProjectEnvironmentsEnvironmentPackagesPackage `json:"packages"` Project getEnvironmentsByProjectProjectEnvironmentsEnvironmentProject `json:"project"` From ee36affb941753799728a059aa4e3cd9b78a34e5 Mon Sep 17 00:00:00 2001 From: Cory O'Daniel Date: Wed, 28 Jan 2026 15:17:45 -0800 Subject: [PATCH 3/3] language --- cmd/version.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/version.go b/cmd/version.go index 7727721a..f8c82d6d 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -25,12 +25,12 @@ func NewCmdVersion() *cobra.Command { func runVersion(cmd *cobra.Command, args []string) { massVersionColor := prettylogs.Green(version.MassVersion()) - fmt.Printf("🧰 CLI: %v (git SHA: %v)\n", massVersionColor, version.MassGitSHA()) + fmt.Printf("🧰 CLI version: %v (git SHA: %v)\n", massVersionColor, version.MassGitSHA()) // Best-effort: check whether a newer CLI is available (does not affect exit code). if latestVersion, err := version.GetLatestVersion(); err == nil { if isOld, _ := version.CheckForNewerVersionAvailable(latestVersion); isOld { - fmt.Printf("⬆️ Update: %v\n", version.LatestReleaseURL) + fmt.Printf("⬆️ A newer version of the CLI is available, you can download it here: %v\n", version.LatestReleaseURL) } } @@ -42,6 +42,6 @@ func runVersion(cmd *cobra.Command, args []string) { } if server, err := api.GetServer(ctx, mdClient); err == nil && server != nil && server.Version != "" { - fmt.Printf("🌐 Server: %v\n", prettylogs.Green(server.Version)) + fmt.Printf("🌐 Server version: %v\n", prettylogs.Green(server.Version)) } }