Skip to content

Commit c61727f

Browse files
coryodanielclaude
andauthored
Add deployment info to mass pkg get (#223)
* Add deployment info to mass pkg get User prompts: - "update mass pkg get's query to fetch this additional detail. in the mass pkg get standard output just show the latest/active deployment id / status / version / created at." Changes: - Add latestDeployment, activeDeployment, deployedVersion to getPackage GraphQL query - Add PackageDeployment struct and new fields to Package model - Update package.get.md.tmpl to display deployment info in text output - Nil-out empty deployments from mapstructure decoding Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Address PR review comments for deployment info User prompts: - "This pr needs to have main pulled and then merged in, it refactored everything into an internal dir ... also, address the PR comments." - "push the pr and give me the link again" Changes: - Make DeployedVersion a *string to handle nullable GraphQL field - Add nil-out logic in toPackage for empty DeployedVersion pointer - Fix UTC timestamp formatting in template (call .UTC before .Format) - Add deref template function for *string rendering - Add genqlient @pointer directive for deployedVersion field - Add tests for nil and present deployment decode paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 8c21bc4 commit c61727f

7 files changed

Lines changed: 297 additions & 60 deletions

File tree

cmd/package.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,15 @@ func renderPackage(pkg *api.Package) error {
204204
return fmt.Errorf("failed to read template: %w", err)
205205
}
206206

207-
tmpl, err := template.New("package").Parse(string(tmplBytes))
207+
funcMap := template.FuncMap{
208+
"deref": func(s *string) string {
209+
if s == nil {
210+
return ""
211+
}
212+
return *s
213+
},
214+
}
215+
tmpl, err := template.New("package").Funcs(funcMap).Parse(string(tmplBytes))
208216
if err != nil {
209217
return fmt.Errorf("failed to parse template: %w", err)
210218
}

cmd/templates/package.get.md.tmpl

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,30 @@
44

55
**Environment:** {{.Environment.Slug}}
66

7+
**Deployed Version:** {{if .DeployedVersion}}{{deref .DeployedVersion}}{{else}}N/A{{end}}
8+
9+
{{if .LatestDeployment -}}
10+
## Latest Deployment
11+
12+
| Field | Value |
13+
|-------|-------|
14+
| ID | {{.LatestDeployment.ID}} |
15+
| Status | {{.LatestDeployment.Status}} |
16+
| Action | {{.LatestDeployment.Action}} |
17+
| Version | {{.LatestDeployment.Version}} |
18+
| Created At | {{.LatestDeployment.CreatedAt.UTC.Format "2006-01-02 15:04:05 UTC"}} |
19+
{{end}}
20+
{{- if .ActiveDeployment -}}
21+
## Active Deployment
22+
23+
| Field | Value |
24+
|-------|-------|
25+
| ID | {{.ActiveDeployment.ID}} |
26+
| Status | {{.ActiveDeployment.Status}} |
27+
| Action | {{.ActiveDeployment.Action}} |
28+
| Version | {{.ActiveDeployment.Version}} |
29+
| Created At | {{.ActiveDeployment.CreatedAt.UTC.Format "2006-01-02 15:04:05 UTC"}} |
30+
{{end}}
731
## Parameters
832
```json
933
{{.ParamsJSON}}

internal/api/genqlient.graphql

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,23 @@ query getPackage($organizationId: ID!, $id: ID!) {
440440
id
441441
slug
442442
status
443+
# @genqlient(pointer: true)
444+
deployedVersion
443445
params
446+
latestDeployment {
447+
id
448+
status
449+
action
450+
version
451+
createdAt
452+
}
453+
activeDeployment {
454+
id
455+
status
456+
action
457+
version
458+
createdAt
459+
}
444460
artifacts {
445461
id
446462
name

internal/api/package.go

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,35 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"time"
78

89
"github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client"
910
"github.com/mitchellh/mapstructure"
1011
)
1112

13+
// PackageDeployment represents a deployment summary embedded in a package response.
14+
type PackageDeployment struct {
15+
ID string `json:"id" mapstructure:"id"`
16+
Status string `json:"status" mapstructure:"status"`
17+
Action string `json:"action" mapstructure:"action"`
18+
Version string `json:"version" mapstructure:"version"`
19+
CreatedAt time.Time `json:"createdAt" mapstructure:"createdAt"`
20+
}
21+
1222
// Package represents a deployed bundle instance within a Massdriver environment.
1323
type Package struct {
14-
ID string `json:"id" mapstructure:"id"`
15-
Slug string `json:"slug" mapstructure:"slug"`
16-
Status string `json:"status" mapstructure:"status"`
17-
Artifacts []Artifact `json:"artifacts,omitempty" mapstructure:"artifacts"`
18-
RemoteReferences []RemoteReference `json:"remoteReferences,omitempty" mapstructure:"remoteReferences"`
19-
Bundle *Bundle `json:"bundle,omitempty" mapstructure:"bundle,omitempty"`
20-
Params map[string]any `json:"params" mapstructure:"params"`
21-
Manifest *Manifest `json:"manifest" mapstructure:"manifest,omitempty"`
22-
Environment *Environment `json:"environment,omitempty" mapstructure:"environment,omitempty"`
24+
ID string `json:"id" mapstructure:"id"`
25+
Slug string `json:"slug" mapstructure:"slug"`
26+
Status string `json:"status" mapstructure:"status"`
27+
DeployedVersion *string `json:"deployedVersion,omitempty" mapstructure:"deployedVersion"`
28+
LatestDeployment *PackageDeployment `json:"latestDeployment,omitempty" mapstructure:"latestDeployment"`
29+
ActiveDeployment *PackageDeployment `json:"activeDeployment,omitempty" mapstructure:"activeDeployment"`
30+
Artifacts []Artifact `json:"artifacts,omitempty" mapstructure:"artifacts"`
31+
RemoteReferences []RemoteReference `json:"remoteReferences,omitempty" mapstructure:"remoteReferences"`
32+
Bundle *Bundle `json:"bundle,omitempty" mapstructure:"bundle,omitempty"`
33+
Params map[string]any `json:"params" mapstructure:"params"`
34+
Manifest *Manifest `json:"manifest" mapstructure:"manifest,omitempty"`
35+
Environment *Environment `json:"environment,omitempty" mapstructure:"environment,omitempty"`
2336
}
2437

2538
// ParamsJSON returns the package parameters serialized as a pretty-printed JSON string.
@@ -46,6 +59,18 @@ func toPackage(p any) (*Package, error) {
4659
if err := mapstructure.Decode(p, &pkg); err != nil {
4760
return nil, fmt.Errorf("failed to decode package: %w", err)
4861
}
62+
63+
// mapstructure creates empty structs/pointers for nil values; nil them out
64+
if pkg.DeployedVersion != nil && *pkg.DeployedVersion == "" {
65+
pkg.DeployedVersion = nil
66+
}
67+
if pkg.LatestDeployment != nil && pkg.LatestDeployment.ID == "" {
68+
pkg.LatestDeployment = nil
69+
}
70+
if pkg.ActiveDeployment != nil && pkg.ActiveDeployment.ID == "" {
71+
pkg.ActiveDeployment = nil
72+
}
73+
4974
return &pkg, nil
5075
}
5176

internal/api/package_test.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"github.com/massdriver-cloud/mass/internal/api"
88
"github.com/massdriver-cloud/mass/internal/gqlmock"
99
"github.com/massdriver-cloud/massdriver-sdk-go/massdriver/client"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
1012
)
1113

1214
func TestGetPackage(t *testing.T) {
@@ -122,3 +124,90 @@ func TestResetPackage(t *testing.T) {
122124
t.Errorf("got %v, wanted %v", pkg.Status, "ready")
123125
}
124126
}
127+
128+
func TestGetPackage_NilDeployments(t *testing.T) {
129+
pkgName := "ecomm-prod-cache"
130+
131+
gqlClient := gqlmock.NewClientWithSingleJSONResponse(map[string]any{
132+
"data": map[string]any{
133+
"package": map[string]any{
134+
"slug": pkgName,
135+
"status": "provisioned",
136+
"deployedVersion": nil,
137+
"latestDeployment": nil,
138+
"activeDeployment": nil,
139+
"bundle": map[string]any{
140+
"id": "bundle-id",
141+
},
142+
"manifest": map[string]any{
143+
"id": "manifest-id",
144+
},
145+
"environment": map[string]any{
146+
"id": "target-id",
147+
},
148+
},
149+
},
150+
})
151+
mdClient := client.Client{
152+
GQL: gqlClient,
153+
}
154+
155+
got, err := api.GetPackage(t.Context(), &mdClient, pkgName)
156+
require.NoError(t, err)
157+
158+
assert.Nil(t, got.DeployedVersion, "DeployedVersion should be nil for never-deployed packages")
159+
assert.Nil(t, got.LatestDeployment, "LatestDeployment should be nil when not present")
160+
assert.Nil(t, got.ActiveDeployment, "ActiveDeployment should be nil when not present")
161+
}
162+
163+
func TestGetPackage_WithDeployments(t *testing.T) {
164+
pkgName := "ecomm-prod-cache"
165+
version := "0.1.0"
166+
167+
gqlClient := gqlmock.NewClientWithSingleJSONResponse(map[string]any{
168+
"data": map[string]any{
169+
"package": map[string]any{
170+
"slug": pkgName,
171+
"status": "provisioned",
172+
"deployedVersion": version,
173+
"latestDeployment": map[string]any{
174+
"id": "deploy-1",
175+
"status": "COMPLETED",
176+
"action": "PROVISION",
177+
"version": version,
178+
"createdAt": "2026-01-15T10:30:00Z",
179+
},
180+
"activeDeployment": map[string]any{
181+
"id": "deploy-1",
182+
"status": "COMPLETED",
183+
"action": "PROVISION",
184+
"version": version,
185+
"createdAt": "2026-01-15T10:30:00Z",
186+
},
187+
"bundle": map[string]any{
188+
"id": "bundle-id",
189+
},
190+
"manifest": map[string]any{
191+
"id": "manifest-id",
192+
},
193+
"environment": map[string]any{
194+
"id": "target-id",
195+
},
196+
},
197+
},
198+
})
199+
mdClient := client.Client{
200+
GQL: gqlClient,
201+
}
202+
203+
got, err := api.GetPackage(t.Context(), &mdClient, pkgName)
204+
require.NoError(t, err)
205+
206+
require.NotNil(t, got.DeployedVersion)
207+
assert.Equal(t, version, *got.DeployedVersion)
208+
require.NotNil(t, got.LatestDeployment)
209+
assert.Equal(t, "deploy-1", got.LatestDeployment.ID)
210+
assert.Equal(t, "COMPLETED", got.LatestDeployment.Status)
211+
require.NotNil(t, got.ActiveDeployment)
212+
assert.Equal(t, "deploy-1", got.ActiveDeployment.ID)
213+
}

internal/api/schema.graphql

Lines changed: 8 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -281,12 +281,7 @@ type RootQueryType {
281281
): Package
282282

283283
"Finds a package by its naming convention, the name prefix (project-target-manifest) without the random unique suffix."
284-
getPackageByNamingConvention(
285-
organizationId: ID!
286-
287-
"Package ID or {project.slug}-{environment.slug}-{manifest.slug} i.e.: ecomm-staging-database"
288-
name: String!
289-
): Package @deprecated(reason: "Use package(id: $id){}")
284+
getPackageByNamingConvention(organizationId: ID!, name: String!): Package @deprecated(reason: "Use package(id: $id){}")
290285

291286
"""
292287
List all bundle repositories with pagination, sorting, and search capabilities.
@@ -541,34 +536,13 @@ type RootMutationType {
541536
createManifest(organizationId: ID!, bundleId: ID!, projectId: ID!, name: String!, slug: String!, description: String): ManifestPayload
542537

543538
"Update a manifest"
544-
updateManifest(
545-
organizationId: ID!
546-
547-
"Manifest ID or {project.slug}-{manifest.slug} i.e.: ecomm-database"
548-
id: ID!
549-
550-
name: String!
551-
552-
description: String
553-
): ManifestPayload
539+
updateManifest(organizationId: ID!, id: ID!, name: String!, description: String): ManifestPayload
554540

555541
"Removes a manifest from a project. This will fail if infrastructure is still provisioned in a target."
556-
deleteManifest(
557-
organizationId: ID!
558-
559-
"Manifest ID or {project.slug}-{manifest.slug} i.e.: ecomm-database"
560-
id: ID!
561-
): ManifestPayload
542+
deleteManifest(organizationId: ID!, id: ID!): ManifestPayload
562543

563544
"Set the manifest position in the graph page"
564-
setManifestPosition(
565-
organizationId: ID!
566-
567-
"Manifest ID or {project.slug}-{manifest.slug} i.e.: ecomm-database"
568-
id: ID!
569-
570-
params: GraphPositionParams!
571-
): ManifestPayload
545+
setManifestPosition(organizationId: ID!, id: ID!, params: GraphPositionParams!): ManifestPayload
572546

573547
"Adds a Service Account to a group"
574548
addServiceAccountToGroup(groupId: ID!, organizationId: ID!, serviceAccountId: ID!): ServiceAccountMemberPayload
@@ -623,7 +597,6 @@ type RootMutationType {
623597
resetPackage(
624598
organizationId: ID!
625599

626-
"Package ID or {project.slug}-{environment.slug}-{manifest.slug} i.e.: ecomm-staging-database"
627600
id: ID!
628601

629602
"Destroy the state of the package"
@@ -689,24 +662,10 @@ type RootMutationType {
689662
): PackagePayload
690663

691664
"Set a secret value for the package."
692-
setPackageSecret(
693-
organizationId: ID!
694-
695-
"Package ID or {project.slug}-{environment.slug}-{manifest.slug} i.e.: ecomm-staging-database"
696-
id: ID!
697-
698-
input: SetSecretValueInput!
699-
): SecretMetadataPayload
665+
setPackageSecret(organizationId: ID!, id: ID!, input: SetSecretValueInput!): SecretMetadataPayload
700666

701667
"Remove a secret value from the package."
702-
unsetPackageSecret(
703-
organizationId: ID!
704-
705-
"Package ID or {project.slug}-{environment.slug}-{manifest.slug} i.e.: ecomm-staging-database"
706-
id: ID!
707-
708-
input: UnsetSecretValueInput!
709-
): SecretMetadataPayload
668+
unsetPackageSecret(organizationId: ID!, id: ID!, input: UnsetSecretValueInput!): SecretMetadataPayload
710669

711670
"Create a project"
712671
createProject(organizationId: ID!, name: String!, description: String, slug: String!): ProjectPayload
@@ -883,7 +842,6 @@ enum ServerMode {
883842
}
884843

885844
enum EmailAuthMethodType {
886-
PASSWORDLESS
887845
PASSKEY
888846
}
889847

@@ -1399,14 +1357,14 @@ type Organization {
13991357

14001358
name: String!
14011359

1402-
slug: String!
1403-
14041360
createdAt: DateTime
14051361

14061362
updatedAt: DateTime
14071363

14081364
attribution: String
14091365

1366+
slug: String!
1367+
14101368
"Statistics for projects you have access to within this organization."
14111369
dashboard: DashboardStatistics
14121370

0 commit comments

Comments
 (0)