Skip to content

Commit dcb27dd

Browse files
authored
Merge pull request #1077 from planetscale/fix-deploy-request-timestamp-bug
Fix deploy-request timestamp display bug caused by tableprinter library
2 parents 2901c27 + 12f54d3 commit dcb27dd

File tree

2 files changed

+188
-19
lines changed

2 files changed

+188
-19
lines changed

internal/cmd/deployrequest/dr.go

Lines changed: 58 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@ package deployrequest
22

33
import (
44
"encoding/json"
5+
"fmt"
6+
"time"
57

68
"github.com/planetscale/cli/internal/cmdutil"
7-
"github.com/planetscale/cli/internal/printer"
89
"github.com/planetscale/planetscale-go/planetscale"
910
"github.com/spf13/cobra"
1011
)
@@ -42,17 +43,17 @@ func DeployRequestCmd(ch *cmdutil.Helper) *cobra.Command {
4243
type DeployRequest struct {
4344
ID string `header:"id" json:"id"`
4445
Number uint64 `header:"number" json:"number"`
45-
Branch string `header:"branch,timestamp(ms|utc|human)" json:"branch"`
46-
IntoBranch string `header:"into_branch,timestamp(ms|utc|human)" json:"into_branch"`
46+
Branch string `header:"branch" json:"branch"`
47+
IntoBranch string `header:"into_branch" json:"into_branch"`
4748

4849
Approved bool `header:"approved" json:"approved"`
4950

5051
State string `header:"state" json:"state"`
5152

5253
Deployment inlineDeployment `header:"inline" json:"deployment"`
53-
CreatedAt int64 `header:"created_at,timestamp(ms|utc|human)" json:"created_at"`
54-
UpdatedAt int64 `header:"updated_at,timestamp(ms|utc|human)" json:"updated_at"`
55-
ClosedAt *int64 `header:"closed_at,timestamp(ms|utc|human),-" json:"closed_at"`
54+
CreatedAt string `header:"created_at" json:"created_at"`
55+
UpdatedAt string `header:"updated_at" json:"updated_at"`
56+
ClosedAt string `header:"closed_at" json:"closed_at"`
5657

5758
orig *planetscale.DeployRequest
5859
}
@@ -62,9 +63,9 @@ type inlineDeployment struct {
6263
Deployable bool `header:"deployable" json:"deployable"`
6364
InstantDDLEligible bool `header:"instant ddl eligible" json:"instant_ddl_eligible"`
6465

65-
QueuedAt *int64 `header:"queued_at,timestamp(ms|utc|human),-" json:"queued_at"`
66-
StartedAt *int64 `header:"started_at,timestamp(ms|utc|human),-" json:"started_at"`
67-
FinishedAt *int64 `header:"finished_at,timestamp(ms|utc|human),-" json:"finished_at"`
66+
QueuedAt string `header:"queued_at" json:"queued_at"`
67+
StartedAt string `header:"started_at" json:"started_at"`
68+
FinishedAt string `header:"finished_at" json:"finished_at"`
6869

6970
orig *planetscale.Deployment
7071
}
@@ -73,21 +74,59 @@ func (d *DeployRequest) MarshalCSVValue() interface{} {
7374
return []*DeployRequest{d}
7475
}
7576

77+
// formatTimestamp formats a timestamp to human readable "X ago" format
78+
func formatTimestamp(t *time.Time) string {
79+
if t == nil || t.IsZero() {
80+
return ""
81+
}
82+
83+
duration := time.Since(*t)
84+
85+
switch {
86+
case duration < time.Minute:
87+
return "less than a minute ago"
88+
case duration < time.Hour:
89+
minutes := int(duration.Minutes())
90+
if minutes == 1 {
91+
return "1 minute ago"
92+
}
93+
return fmt.Sprintf("%d minutes ago", minutes)
94+
case duration < 24*time.Hour:
95+
hours := int(duration.Hours())
96+
if hours == 1 {
97+
return "1 hour ago"
98+
}
99+
return fmt.Sprintf("%d hours ago", hours)
100+
default:
101+
days := int(duration.Hours() / 24)
102+
if days == 1 {
103+
return "1 day ago"
104+
}
105+
return fmt.Sprintf("%d days ago", days)
106+
}
107+
}
108+
109+
// formatTimestampRequired formats a required timestamp (non-pointer)
110+
func formatTimestampRequired(t time.Time) string {
111+
if t.IsZero() {
112+
return ""
113+
}
114+
return formatTimestamp(&t)
115+
}
116+
76117
func toInlineDeployment(d *planetscale.Deployment) inlineDeployment {
77118
if d == nil {
78119
return inlineDeployment{}
79120
}
80121

81122
return inlineDeployment{
82-
State: d.State,
83-
123+
State: d.State,
84124
Deployable: d.Deployable,
85125
InstantDDLEligible: d.InstantDDLEligible,
86-
FinishedAt: printer.GetMillisecondsIfExists(d.FinishedAt),
87-
StartedAt: printer.GetMillisecondsIfExists(d.StartedAt),
88-
QueuedAt: printer.GetMillisecondsIfExists(d.QueuedAt),
89-
90-
orig: d,
126+
QueuedAt: formatTimestamp(d.QueuedAt),
127+
StartedAt: formatTimestamp(d.StartedAt),
128+
FinishedAt: formatTimestamp(d.FinishedAt),
129+
orig: d,
91130
}
92131
}
93132

@@ -100,9 +139,9 @@ func toDeployRequest(dr *planetscale.DeployRequest) *DeployRequest {
100139
Approved: dr.Approved,
101140
State: dr.State,
102141
Deployment: toInlineDeployment(dr.Deployment),
103-
CreatedAt: printer.GetMilliseconds(dr.CreatedAt),
104-
UpdatedAt: printer.GetMilliseconds(dr.UpdatedAt),
105-
ClosedAt: printer.GetMillisecondsIfExists(dr.ClosedAt),
142+
CreatedAt: formatTimestampRequired(dr.CreatedAt),
143+
UpdatedAt: formatTimestampRequired(dr.UpdatedAt),
144+
ClosedAt: formatTimestamp(dr.ClosedAt),
106145
orig: dr,
107146
}
108147
}

internal/cmd/deployrequest/show_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import (
44
"bytes"
55
"context"
66
"strconv"
7+
"strings"
78
"testing"
9+
"time"
810

911
"github.com/planetscale/cli/internal/cmdutil"
1012
"github.com/planetscale/cli/internal/config"
@@ -116,3 +118,131 @@ func TestDeployRequest_ShowBranchName(t *testing.T) {
116118
res := &ps.DeployRequest{Number: number}
117119
c.Assert(buf.String(), qt.JSONEquals, res)
118120
}
121+
122+
func TestDeployRequest_ShowTimestampBug(t *testing.T) {
123+
c := qt.New(t)
124+
125+
var buf bytes.Buffer
126+
format := printer.Human // Use human format to test table output
127+
p := printer.NewPrinter(&format)
128+
p.SetResourceOutput(&buf)
129+
130+
org := "planetscale"
131+
db := "testdb"
132+
var number uint64 = 47
133+
134+
// Create timestamps for testing - make them different to verify correct field assignment
135+
createdAt := time.Date(2025, 6, 23, 22, 46, 42, 348000000, time.UTC) // Base timestamp
136+
updatedAt := time.Date(2025, 6, 23, 22, 52, 34, 76000000, time.UTC) // 6 minutes later
137+
startedAt := time.Date(2025, 6, 23, 22, 48, 52, 553000000, time.UTC) // 2 minutes after created
138+
queuedAt := time.Date(2025, 6, 23, 22, 48, 38, 809000000, time.UTC) // Just before started
139+
140+
svc := &mock.DeployRequestsService{
141+
GetFn: func(ctx context.Context, req *ps.GetDeployRequestRequest) (*ps.DeployRequest, error) {
142+
c.Assert(req.Organization, qt.Equals, org)
143+
c.Assert(req.Database, qt.Equals, db)
144+
c.Assert(req.Number, qt.Equals, number)
145+
146+
return &ps.DeployRequest{
147+
ID: "abcd1234efgh",
148+
Number: number,
149+
Branch: "feature-branch-2025", // String, not timestamp
150+
IntoBranch: "main", // String, not timestamp
151+
Approved: true,
152+
State: "open",
153+
CreatedAt: createdAt,
154+
UpdatedAt: updatedAt,
155+
Deployment: &ps.Deployment{
156+
ID: "deploy5678wxyz",
157+
State: "in_progress",
158+
Deployable: true,
159+
InstantDDLEligible: false,
160+
StartedAt: &startedAt,
161+
QueuedAt: &queuedAt,
162+
FinishedAt: nil, // This should show as empty, not showing incorrect timestamp
163+
},
164+
}, nil
165+
},
166+
}
167+
168+
ch := &cmdutil.Helper{
169+
Printer: p,
170+
Config: &config.Config{
171+
Organization: org,
172+
},
173+
Client: func() (*ps.Client, error) {
174+
return &ps.Client{
175+
DeployRequests: svc,
176+
}, nil
177+
},
178+
}
179+
180+
cmd := ShowCmd(ch)
181+
cmd.SetArgs([]string{db, strconv.FormatUint(number, 10)})
182+
err := cmd.Execute()
183+
184+
c.Assert(err, qt.IsNil)
185+
c.Assert(svc.GetFnInvoked, qt.IsTrue)
186+
187+
output := buf.String()
188+
189+
// Debug: Print the actual output to see what we get
190+
t.Logf("Table output:\n%s", output)
191+
192+
// Debug: Print the actual struct values being passed to tableprinter
193+
dr, _ := svc.GetFn(context.Background(), &ps.GetDeployRequestRequest{
194+
Organization: org,
195+
Database: db,
196+
Number: number,
197+
})
198+
converted := toDeployRequest(dr)
199+
t.Logf("Struct values - CreatedAt: %v, UpdatedAt: %v, FinishedAt: %v, StartedAt: %v, QueuedAt: %v",
200+
converted.CreatedAt, converted.UpdatedAt, converted.Deployment.FinishedAt, converted.Deployment.StartedAt, converted.Deployment.QueuedAt)
201+
202+
// Test the specific bug: FINISHED AT should be empty when deployment.finished_at is nil
203+
// Look for the FINISHED AT column in the table output
204+
lines := strings.Split(output, "\n")
205+
headerLine := ""
206+
dataLine := ""
207+
208+
for _, line := range lines {
209+
trimmed := strings.TrimSpace(line)
210+
if trimmed != "" && !strings.Contains(line, "---") {
211+
if headerLine == "" {
212+
headerLine = line
213+
} else if dataLine == "" {
214+
dataLine = line
215+
break
216+
}
217+
}
218+
}
219+
220+
c.Assert(headerLine, qt.Not(qt.Equals), "", qt.Commentf("Could not find header line"))
221+
c.Assert(dataLine, qt.Not(qt.Equals), "", qt.Commentf("Could not find data line"))
222+
223+
// Find the FINISHED AT column position
224+
finishedAtPos := strings.Index(headerLine, "FINISHED AT")
225+
c.Assert(finishedAtPos, qt.Not(qt.Equals), -1, qt.Commentf("Could not find FINISHED AT column in header"))
226+
227+
// Find the next column after FINISHED AT to know where this column ends
228+
remainingHeader := headerLine[finishedAtPos+len("FINISHED AT"):]
229+
nextColumnMatch := strings.Fields(remainingHeader)
230+
var finishedAtEndPos int
231+
if len(nextColumnMatch) > 0 {
232+
nextColumnPos := strings.Index(remainingHeader, nextColumnMatch[0])
233+
finishedAtEndPos = finishedAtPos + len("FINISHED AT") + nextColumnPos
234+
} else {
235+
finishedAtEndPos = len(headerLine)
236+
}
237+
238+
// Extract the FINISHED AT column value from the data line
239+
if finishedAtEndPos <= len(dataLine) {
240+
finishedAtValue := strings.TrimSpace(dataLine[finishedAtPos:finishedAtEndPos])
241+
242+
// The bug: FINISHED AT should be empty since deployment.finished_at is nil
243+
// If it contains "ago" or any timestamp value, that's the bug
244+
if finishedAtValue != "" && strings.Contains(finishedAtValue, "ago") {
245+
c.Errorf("FINISHED AT column shows '%s' but deployment.finished_at is nil - should be empty", finishedAtValue)
246+
}
247+
}
248+
}

0 commit comments

Comments
 (0)