detached bffa8f6

refactor(preview): replace status magic strings with PreviewStatus typed enum

commit
bffa8f635b36de29d17dec8d63eb0f4dad91462c
author
brice · · 2026-03-18 21:25 UTC
committer
brice · · 2026-03-18 19:52 UTC
parent
1aaeb90
signature
unsigned
5 files · +30 -21
M internal/adapters/in/cli/preview.go +3 -3
@@ -319,7 +319,7 @@ func waitForPreview(ctx context.Context, out io.Writer, name, ttl string, noData
319319 continue // not found yet, keep polling
320320 }
321321 switch p.Status {
322- case "running":
322+ case domain.PreviewStatusRunning:
323323 scheme := "http"
324324 if p.HTTPS {
325325 scheme = "https"
@@ -329,10 +329,10 @@ func waitForPreview(ctx context.Context, out io.Writer, name, ttl string, noData
329329 return err
330330 }
331331 return cliWriteLine(out, cliRenderMeta("URL:", previewURL))
332- case "failed":
332+ case domain.PreviewStatusFailed:
333333 return fmt.Errorf("preview deployment failed")
334334 }
335- // status is "deploying", keep polling
335+ // status is PreviewStatusDeploying, keep polling
336336 }
337337 }
338338 }
@@ -319,7 +319,7 @@ func waitForPreview(ctx context.Context, out io.Writer, name, ttl string, noData
319 continue // not found yet, keep polling319 continue // not found yet, keep polling
320 }320 }
321 switch p.Status {321 switch p.Status {
322- case "running":322+ case domain.PreviewStatusRunning:
323 scheme := "http"323 scheme := "http"
324 if p.HTTPS {324 if p.HTTPS {
325 scheme = "https"325 scheme = "https"
@@ -329,10 +329,10 @@ func waitForPreview(ctx context.Context, out io.Writer, name, ttl string, noData
329 return err329 return err
330 }330 }
331 return cliWriteLine(out, cliRenderMeta("URL:", previewURL))331 return cliWriteLine(out, cliRenderMeta("URL:", previewURL))
332- case "failed":332+ case domain.PreviewStatusFailed:
333 return fmt.Errorf("preview deployment failed")333 return fmt.Errorf("preview deployment failed")
334 }334 }
335- // status is "deploying", keep polling335+ // status is PreviewStatusDeploying, keep polling
336 }336 }
337 }337 }
338 }338 }
M internal/domain/preview.go +19 -10
@@ -2,18 +2,27 @@ package domain
22
33 import "time"
44
5+// PreviewStatus represents the lifecycle state of a preview environment.
6+type PreviewStatus string
7+
8+const (
9+ PreviewStatusDeploying PreviewStatus = "deploying"
10+ PreviewStatusRunning PreviewStatus = "running"
11+ PreviewStatusFailed PreviewStatus = "failed"
12+)
13+
514 // PreviewRoute represents an ephemeral preview environment.
615 type PreviewRoute struct {
7- Domain string `json:"domain"`
8- Image string `json:"image"`
9- BaseRoute string `json:"base_route"`
10- Name string `json:"name"`
11- CreatedAt time.Time `json:"created_at"`
12- ExpiresAt time.Time `json:"expires_at"`
13- HTTPS bool `json:"https"`
14- Status string `json:"status"` // "deploying", "running", "failed"
15- Volumes []string `json:"volumes"`
16- Containers []string `json:"containers"`
16+ Domain string `json:"domain"`
17+ Image string `json:"image"`
18+ BaseRoute string `json:"base_route"`
19+ Name string `json:"name"`
20+ CreatedAt time.Time `json:"created_at"`
21+ ExpiresAt time.Time `json:"expires_at"`
22+ HTTPS bool `json:"https"`
23+ Status PreviewStatus `json:"status"`
24+ Volumes []string `json:"volumes"`
25+ Containers []string `json:"containers"`
1726 }
1827
1928 // IsExpired returns true if the preview has exceeded its TTL.
@@ -2,18 +2,27 @@ package domain
2 2
3 import "time"3 import "time"
4 4
5+// PreviewStatus represents the lifecycle state of a preview environment.
6+type PreviewStatus string
7+
8+const (
9+ PreviewStatusDeploying PreviewStatus = "deploying"
10+ PreviewStatusRunning PreviewStatus = "running"
11+ PreviewStatusFailed PreviewStatus = "failed"
12+)
13+
5 // PreviewRoute represents an ephemeral preview environment.14 // PreviewRoute represents an ephemeral preview environment.
6 type PreviewRoute struct {15 type PreviewRoute struct {
7- Domain string `json:"domain"`16+ Domain string `json:"domain"`
8- Image string `json:"image"`17+ Image string `json:"image"`
9- BaseRoute string `json:"base_route"`18+ BaseRoute string `json:"base_route"`
10- Name string `json:"name"`19+ Name string `json:"name"`
11- CreatedAt time.Time `json:"created_at"`20+ CreatedAt time.Time `json:"created_at"`
12- ExpiresAt time.Time `json:"expires_at"`21+ ExpiresAt time.Time `json:"expires_at"`
13- HTTPS bool `json:"https"`22+ HTTPS bool `json:"https"`
14- Status string `json:"status"` // "deploying", "running", "failed"23+ Status PreviewStatus `json:"status"`
15- Volumes []string `json:"volumes"`24+ Volumes []string `json:"volumes"`
16- Containers []string `json:"containers"`25+ Containers []string `json:"containers"`
17 }26 }
18 27
19 // IsExpired returns true if the preview has exceeded its TTL.28 // IsExpired returns true if the preview has exceeded its TTL.
M internal/usecase/auto/preview/integration_test.go +1 -1
@@ -176,6 +176,6 @@ func TestPreviewLifecycle_WithDeploy(t *testing.T) {
176176 all, err := svc.List(ctx)
177177 require.NoError(t, err)
178178 require.Len(t, all, 1)
179- assert.Equal(t, "running", all[0].Status)
179+ assert.Equal(t, domain.PreviewStatusRunning, all[0].Status)
180180 assert.Equal(t, []string{"gordon-myapp--test-feat.example.com"}, all[0].Containers)
181181 }
@@ -176,6 +176,6 @@ func TestPreviewLifecycle_WithDeploy(t *testing.T) {
176 all, err := svc.List(ctx)176 all, err := svc.List(ctx)
177 require.NoError(t, err)177 require.NoError(t, err)
178 require.Len(t, all, 1)178 require.Len(t, all, 1)
179- assert.Equal(t, "running", all[0].Status)179+ assert.Equal(t, domain.PreviewStatusRunning, all[0].Status)
180 assert.Equal(t, []string{"gordon-myapp--test-feat.example.com"}, all[0].Containers)180 assert.Equal(t, []string{"gordon-myapp--test-feat.example.com"}, all[0].Containers)
181 }181 }
M internal/usecase/auto/preview/service.go +4 -4
@@ -338,7 +338,7 @@ func (s *Service) CreatePreview(ctx context.Context, req CreatePreviewRequest) e
338338 CreatedAt: now,
339339 ExpiresAt: now.Add(req.PreviewConfig.TTL),
340340 HTTPS: req.HTTPS,
341- Status: "deploying",
341+ Status: domain.PreviewStatusDeploying,
342342 }
343343
344344 if err := s.Add(ctx, preview); err != nil {
@@ -361,7 +361,7 @@ func (s *Service) CreatePreview(ctx context.Context, req CreatePreviewRequest) e
361361 Image: imageRef,
362362 HTTPS: req.HTTPS,
363363 }); err != nil {
364- preview.Status = "failed"
364+ preview.Status = domain.PreviewStatusFailed
365365 _ = s.Update(ctx, preview)
366366 return fmt.Errorf("register preview route %q: %w", req.Domain, err)
367367 }
@@ -376,14 +376,14 @@ func (s *Service) CreatePreview(ctx context.Context, req CreatePreviewRequest) e
376376 HTTPS: req.HTTPS,
377377 })
378378 if err != nil {
379- preview.Status = "failed"
379+ preview.Status = domain.PreviewStatusFailed
380380 _ = s.Update(ctx, preview)
381381 return fmt.Errorf("deploy preview %q: %w", req.Name, err)
382382 }
383383 preview.Containers = []string{container.Name}
384384 }
385385
386- preview.Status = "running"
386+ preview.Status = domain.PreviewStatusRunning
387387 if err := s.Update(ctx, preview); err != nil {
388388 return fmt.Errorf("finalize preview %q: %w", req.Name, err)
389389 }
@@ -338,7 +338,7 @@ func (s *Service) CreatePreview(ctx context.Context, req CreatePreviewRequest) e
338 CreatedAt: now,338 CreatedAt: now,
339 ExpiresAt: now.Add(req.PreviewConfig.TTL),339 ExpiresAt: now.Add(req.PreviewConfig.TTL),
340 HTTPS: req.HTTPS,340 HTTPS: req.HTTPS,
341- Status: "deploying",341+ Status: domain.PreviewStatusDeploying,
342 }342 }
343 343
344 if err := s.Add(ctx, preview); err != nil {344 if err := s.Add(ctx, preview); err != nil {
@@ -361,7 +361,7 @@ func (s *Service) CreatePreview(ctx context.Context, req CreatePreviewRequest) e
361 Image: imageRef,361 Image: imageRef,
362 HTTPS: req.HTTPS,362 HTTPS: req.HTTPS,
363 }); err != nil {363 }); err != nil {
364- preview.Status = "failed"364+ preview.Status = domain.PreviewStatusFailed
365 _ = s.Update(ctx, preview)365 _ = s.Update(ctx, preview)
366 return fmt.Errorf("register preview route %q: %w", req.Domain, err)366 return fmt.Errorf("register preview route %q: %w", req.Domain, err)
367 }367 }
@@ -376,14 +376,14 @@ func (s *Service) CreatePreview(ctx context.Context, req CreatePreviewRequest) e
376 HTTPS: req.HTTPS,376 HTTPS: req.HTTPS,
377 })377 })
378 if err != nil {378 if err != nil {
379- preview.Status = "failed"379+ preview.Status = domain.PreviewStatusFailed
380 _ = s.Update(ctx, preview)380 _ = s.Update(ctx, preview)
381 return fmt.Errorf("deploy preview %q: %w", req.Name, err)381 return fmt.Errorf("deploy preview %q: %w", req.Name, err)
382 }382 }
383 preview.Containers = []string{container.Name}383 preview.Containers = []string{container.Name}
384 }384 }
385 385
386- preview.Status = "running"386+ preview.Status = domain.PreviewStatusRunning
387 if err := s.Update(ctx, preview); err != nil {387 if err := s.Update(ctx, preview); err != nil {
388 return fmt.Errorf("finalize preview %q: %w", req.Name, err)388 return fmt.Errorf("finalize preview %q: %w", req.Name, err)
389 }389 }
M internal/usecase/auto/preview/service_test.go +3 -3
@@ -111,7 +111,7 @@ func TestPreviewService_GetExpired(t *testing.T) {
111111 func TestPreviewService_Update(t *testing.T) {
112112 store := &fakeStore{
113113 previews: []domain.PreviewRoute{
114- {Name: "feat", Domain: "myapp--feat.example.com", Status: "deploying"},
114+ {Name: "feat", Domain: "myapp--feat.example.com", Status: domain.PreviewStatusDeploying},
115115 },
116116 }
117117 svc := NewService(store, 48*time.Hour)
@@ -120,7 +120,7 @@ func TestPreviewService_Update(t *testing.T) {
120120 updated := domain.PreviewRoute{
121121 Name: "feat",
122122 Domain: "myapp--feat.example.com",
123- Status: "running",
123+ Status: domain.PreviewStatusRunning,
124124 Containers: []string{"gordon-myapp--feat.example.com"},
125125 }
126126 err := svc.Update(t.Context(), updated)
@@ -128,7 +128,7 @@ func TestPreviewService_Update(t *testing.T) {
128128
129129 p, err := svc.Get(t.Context(), "feat")
130130 require.NoError(t, err)
131- assert.Equal(t, "running", p.Status)
131+ assert.Equal(t, domain.PreviewStatusRunning, p.Status)
132132 assert.Equal(t, []string{"gordon-myapp--feat.example.com"}, p.Containers)
133133 }
134134
@@ -111,7 +111,7 @@ func TestPreviewService_GetExpired(t *testing.T) {
111 func TestPreviewService_Update(t *testing.T) {111 func TestPreviewService_Update(t *testing.T) {
112 store := &fakeStore{112 store := &fakeStore{
113 previews: []domain.PreviewRoute{113 previews: []domain.PreviewRoute{
114- {Name: "feat", Domain: "myapp--feat.example.com", Status: "deploying"},114+ {Name: "feat", Domain: "myapp--feat.example.com", Status: domain.PreviewStatusDeploying},
115 },115 },
116 }116 }
117 svc := NewService(store, 48*time.Hour)117 svc := NewService(store, 48*time.Hour)
@@ -120,7 +120,7 @@ func TestPreviewService_Update(t *testing.T) {
120 updated := domain.PreviewRoute{120 updated := domain.PreviewRoute{
121 Name: "feat",121 Name: "feat",
122 Domain: "myapp--feat.example.com",122 Domain: "myapp--feat.example.com",
123- Status: "running",123+ Status: domain.PreviewStatusRunning,
124 Containers: []string{"gordon-myapp--feat.example.com"},124 Containers: []string{"gordon-myapp--feat.example.com"},
125 }125 }
126 err := svc.Update(t.Context(), updated)126 err := svc.Update(t.Context(), updated)
@@ -128,7 +128,7 @@ func TestPreviewService_Update(t *testing.T) {
128 128
129 p, err := svc.Get(t.Context(), "feat")129 p, err := svc.Get(t.Context(), "feat")
130 require.NoError(t, err)130 require.NoError(t, err)
131- assert.Equal(t, "running", p.Status)131+ assert.Equal(t, domain.PreviewStatusRunning, p.Status)
132 assert.Equal(t, []string{"gordon-myapp--feat.example.com"}, p.Containers)132 assert.Equal(t, []string{"gordon-myapp--feat.example.com"}, p.Containers)
133 }133 }
134 134

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close