detached 90c5cb7

refactor(cli): route pin command output through cliWriteLine/cliWritef, wrap errors

commit
90c5cb76110de9d81e9305f42f157e96a2143045
author
brice · · 2026-04-28 03:25 UTC
parent
9dd1391
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +22 -20
M internal/adapters/in/cli/pin.go +22 -20
@@ -4,7 +4,6 @@ import (
44 "context"
55 "fmt"
66 "io"
7- "os"
87 "slices"
98 "sort"
109 "strings"
@@ -32,7 +31,7 @@ Examples:
3231 gordon pin list myapp.example.com --remote ...`,
3332 Args: cobra.ExactArgs(1),
3433 RunE: func(cmd *cobra.Command, args []string) error {
35- return runPin(cmd.Context(), args[0], targetTag)
34+ return runPin(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0], targetTag)
3635 },
3736 }
3837
@@ -59,10 +58,10 @@ func newPinListCmd() *cobra.Command {
5958 return cmd
6059 }
6160
62-func runPin(ctx context.Context, pinDomain, targetTag string) error {
61+func runPin(ctx context.Context, out, ew io.Writer, pinDomain, targetTag string) error {
6362 handle, err := resolveControlPlane(configPath)
6463 if err != nil {
65- return err
64+ return fmt.Errorf("failed to resolve control plane: %w", err)
6665 }
6766 defer handle.close()
6867
@@ -78,26 +77,28 @@ func runPin(ctx context.Context, pinDomain, targetTag string) error {
7877
7978 tags, err := fetchAndSortTags(ctx, handle.plane, imageName)
8079 if err != nil {
81- return err
80+ return fmt.Errorf("failed to fetch tags: %w", err)
8281 }
8382
84- selectedTag, err := selectTag(targetTag, tags, currentTag, pinDomain)
85- if err != nil || selectedTag == "" {
86- return err
83+ selectedTag, err := selectTag(targetTag, tags, currentTag, pinDomain, out)
84+ if err != nil {
85+ return fmt.Errorf("failed to select tag: %w", err)
86+ }
87+ if selectedTag == "" {
88+ return nil
8789 }
8890
8991 if selectedTag == currentTag {
90- fmt.Println(styles.RenderWarning(fmt.Sprintf("Already running %s", selectedTag)))
91- return nil
92+ return cliWriteLine(out, styles.RenderWarning(fmt.Sprintf("Already running %s", selectedTag)))
9293 }
9394
94- return deploySelectedTag(ctx, handle.plane, route, pinDomain, imageName, selectedTag)
95+ return deploySelectedTag(ctx, handle.plane, route, out, ew, pinDomain, imageName, selectedTag)
9596 }
9697
9798 func runPinList(ctx context.Context, w io.Writer, pinDomain string, jsonOut bool) error {
9899 handle, err := resolveControlPlane(configPath)
99100 if err != nil {
100- return err
101+ return fmt.Errorf("failed to resolve control plane: %w", err)
101102 }
102103 defer handle.close()
103104
@@ -113,7 +114,7 @@ func runPinList(ctx context.Context, w io.Writer, pinDomain string, jsonOut bool
113114
114115 tags, err := fetchAndSortTags(ctx, handle.plane, imageName)
115116 if err != nil {
116- return err
117+ return fmt.Errorf("failed to fetch tags: %w", err)
117118 }
118119
119120 return printPinTags(w, pinDomain, currentTag, tags, jsonOut)
@@ -193,7 +194,7 @@ func sortSemverTags(tags []string) []string {
193194 return append(semverTags, otherTags...)
194195 }
195196
196-func selectTag(targetTag string, tags []string, currentTag, pinDomain string) (string, error) {
197+func selectTag(targetTag string, tags []string, currentTag, pinDomain string, out io.Writer) (string, error) {
197198 if targetTag != "" {
198199 if !validateTagExists(targetTag, tags) {
199200 return "", fmt.Errorf("tag %s not found. Available: %s", targetTag, strings.Join(tags, ", "))
@@ -210,7 +211,7 @@ func selectTag(targetTag string, tags []string, currentTag, pinDomain string) (s
210211 return "", err
211212 }
212213 if selectedTag == "" {
213- fmt.Println("Cancelled.")
214+ _ = cliWriteLine(out, "Cancelled.")
214215 }
215216 return selectedTag, nil
216217 }
@@ -219,12 +220,14 @@ func validateTagExists(targetTag string, tags []string) bool {
219220 return slices.Contains(tags, targetTag)
220221 }
221222
222-func deploySelectedTag(ctx context.Context, cp ControlPlane, route *domain.Route, pinDomain, imageName, selectedTag string) error {
223+func deploySelectedTag(ctx context.Context, cp ControlPlane, route *domain.Route, out, ew io.Writer, pinDomain, imageName, selectedTag string) error {
223224 registry, _, _ := parseImageRef(route.Image)
224225 oldImage := route.Image
225226 route.Image = fmt.Sprintf("%s/%s:%s", registry, imageName, selectedTag)
226227
227- fmt.Printf("Pinning to %s...\n", styles.Theme.Bold.Render(selectedTag))
228+ if err := cliWritef(out, "Pinning to %s...\n", styles.Theme.Bold.Render(selectedTag)); err != nil {
229+ return err
230+ }
228231
229232 if err := cp.UpdateRoute(ctx, *route); err != nil {
230233 return fmt.Errorf("failed to update route: %w", err)
@@ -235,12 +238,11 @@ func deploySelectedTag(ctx context.Context, cp ControlPlane, route *domain.Route
235238 // Attempt to revert route to previous image
236239 route.Image = oldImage
237240 if revertErr := cp.UpdateRoute(ctx, *route); revertErr != nil {
238- fmt.Fprintf(os.Stderr, "WARNING: deploy failed and could not revert route: %v\n", revertErr)
241+ _ = cliWritef(ew, "WARNING: deploy failed and could not revert route: %v\n", revertErr)
239242 return fmt.Errorf("failed to deploy; revert failed: %v; deploy error: %w", revertErr, err)
240243 }
241244 return fmt.Errorf("failed to deploy (route reverted): %w", err)
242245 }
243246 containerID := shortContainerID(result.ContainerID)
244- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Pinned %s to %s (container: %s)", pinDomain, selectedTag, containerID)))
245- return nil
247+ return cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Pinned %s to %s (container: %s)", pinDomain, selectedTag, containerID)))
246248 }
@@ -4,7 +4,6 @@ import (
4 "context"4 "context"
5 "fmt"5 "fmt"
6 "io"6 "io"
7- "os"
8 "slices"7 "slices"
9 "sort"8 "sort"
10 "strings"9 "strings"
@@ -32,7 +31,7 @@ Examples:
32 gordon pin list myapp.example.com --remote ...`,31 gordon pin list myapp.example.com --remote ...`,
33 Args: cobra.ExactArgs(1),32 Args: cobra.ExactArgs(1),
34 RunE: func(cmd *cobra.Command, args []string) error {33 RunE: func(cmd *cobra.Command, args []string) error {
35- return runPin(cmd.Context(), args[0], targetTag)34+ return runPin(cmd.Context(), cmd.OutOrStdout(), cmd.ErrOrStderr(), args[0], targetTag)
36 },35 },
37 }36 }
38 37
@@ -59,10 +58,10 @@ func newPinListCmd() *cobra.Command {
59 return cmd58 return cmd
60 }59 }
61 60
62-func runPin(ctx context.Context, pinDomain, targetTag string) error {61+func runPin(ctx context.Context, out, ew io.Writer, pinDomain, targetTag string) error {
63 handle, err := resolveControlPlane(configPath)62 handle, err := resolveControlPlane(configPath)
64 if err != nil {63 if err != nil {
65- return err64+ return fmt.Errorf("failed to resolve control plane: %w", err)
66 }65 }
67 defer handle.close()66 defer handle.close()
68 67
@@ -78,26 +77,28 @@ func runPin(ctx context.Context, pinDomain, targetTag string) error {
78 77
79 tags, err := fetchAndSortTags(ctx, handle.plane, imageName)78 tags, err := fetchAndSortTags(ctx, handle.plane, imageName)
80 if err != nil {79 if err != nil {
81- return err80+ return fmt.Errorf("failed to fetch tags: %w", err)
82 }81 }
83 82
84- selectedTag, err := selectTag(targetTag, tags, currentTag, pinDomain)83+ selectedTag, err := selectTag(targetTag, tags, currentTag, pinDomain, out)
85- if err != nil || selectedTag == "" {84+ if err != nil {
86- return err85+ return fmt.Errorf("failed to select tag: %w", err)
86+ }
87+ if selectedTag == "" {
88+ return nil
87 }89 }
88 90
89 if selectedTag == currentTag {91 if selectedTag == currentTag {
90- fmt.Println(styles.RenderWarning(fmt.Sprintf("Already running %s", selectedTag)))92+ return cliWriteLine(out, styles.RenderWarning(fmt.Sprintf("Already running %s", selectedTag)))
91- return nil
92 }93 }
93 94
94- return deploySelectedTag(ctx, handle.plane, route, pinDomain, imageName, selectedTag)95+ return deploySelectedTag(ctx, handle.plane, route, out, ew, pinDomain, imageName, selectedTag)
95 }96 }
96 97
97 func runPinList(ctx context.Context, w io.Writer, pinDomain string, jsonOut bool) error {98 func runPinList(ctx context.Context, w io.Writer, pinDomain string, jsonOut bool) error {
98 handle, err := resolveControlPlane(configPath)99 handle, err := resolveControlPlane(configPath)
99 if err != nil {100 if err != nil {
100- return err101+ return fmt.Errorf("failed to resolve control plane: %w", err)
101 }102 }
102 defer handle.close()103 defer handle.close()
103 104
@@ -113,7 +114,7 @@ func runPinList(ctx context.Context, w io.Writer, pinDomain string, jsonOut bool
113 114
114 tags, err := fetchAndSortTags(ctx, handle.plane, imageName)115 tags, err := fetchAndSortTags(ctx, handle.plane, imageName)
115 if err != nil {116 if err != nil {
116- return err117+ return fmt.Errorf("failed to fetch tags: %w", err)
117 }118 }
118 119
119 return printPinTags(w, pinDomain, currentTag, tags, jsonOut)120 return printPinTags(w, pinDomain, currentTag, tags, jsonOut)
@@ -193,7 +194,7 @@ func sortSemverTags(tags []string) []string {
193 return append(semverTags, otherTags...)194 return append(semverTags, otherTags...)
194 }195 }
195 196
196-func selectTag(targetTag string, tags []string, currentTag, pinDomain string) (string, error) {197+func selectTag(targetTag string, tags []string, currentTag, pinDomain string, out io.Writer) (string, error) {
197 if targetTag != "" {198 if targetTag != "" {
198 if !validateTagExists(targetTag, tags) {199 if !validateTagExists(targetTag, tags) {
199 return "", fmt.Errorf("tag %s not found. Available: %s", targetTag, strings.Join(tags, ", "))200 return "", fmt.Errorf("tag %s not found. Available: %s", targetTag, strings.Join(tags, ", "))
@@ -210,7 +211,7 @@ func selectTag(targetTag string, tags []string, currentTag, pinDomain string) (s
210 return "", err211 return "", err
211 }212 }
212 if selectedTag == "" {213 if selectedTag == "" {
213- fmt.Println("Cancelled.")214+ _ = cliWriteLine(out, "Cancelled.")
214 }215 }
215 return selectedTag, nil216 return selectedTag, nil
216 }217 }
@@ -219,12 +220,14 @@ func validateTagExists(targetTag string, tags []string) bool {
219 return slices.Contains(tags, targetTag)220 return slices.Contains(tags, targetTag)
220 }221 }
221 222
222-func deploySelectedTag(ctx context.Context, cp ControlPlane, route *domain.Route, pinDomain, imageName, selectedTag string) error {223+func deploySelectedTag(ctx context.Context, cp ControlPlane, route *domain.Route, out, ew io.Writer, pinDomain, imageName, selectedTag string) error {
223 registry, _, _ := parseImageRef(route.Image)224 registry, _, _ := parseImageRef(route.Image)
224 oldImage := route.Image225 oldImage := route.Image
225 route.Image = fmt.Sprintf("%s/%s:%s", registry, imageName, selectedTag)226 route.Image = fmt.Sprintf("%s/%s:%s", registry, imageName, selectedTag)
226 227
227- fmt.Printf("Pinning to %s...\n", styles.Theme.Bold.Render(selectedTag))228+ if err := cliWritef(out, "Pinning to %s...\n", styles.Theme.Bold.Render(selectedTag)); err != nil {
229+ return err
230+ }
228 231
229 if err := cp.UpdateRoute(ctx, *route); err != nil {232 if err := cp.UpdateRoute(ctx, *route); err != nil {
230 return fmt.Errorf("failed to update route: %w", err)233 return fmt.Errorf("failed to update route: %w", err)
@@ -235,12 +238,11 @@ func deploySelectedTag(ctx context.Context, cp ControlPlane, route *domain.Route
235 // Attempt to revert route to previous image238 // Attempt to revert route to previous image
236 route.Image = oldImage239 route.Image = oldImage
237 if revertErr := cp.UpdateRoute(ctx, *route); revertErr != nil {240 if revertErr := cp.UpdateRoute(ctx, *route); revertErr != nil {
238- fmt.Fprintf(os.Stderr, "WARNING: deploy failed and could not revert route: %v\n", revertErr)241+ _ = cliWritef(ew, "WARNING: deploy failed and could not revert route: %v\n", revertErr)
239 return fmt.Errorf("failed to deploy; revert failed: %v; deploy error: %w", revertErr, err)242 return fmt.Errorf("failed to deploy; revert failed: %v; deploy error: %w", revertErr, err)
240 }243 }
241 return fmt.Errorf("failed to deploy (route reverted): %w", err)244 return fmt.Errorf("failed to deploy (route reverted): %w", err)
242 }245 }
243 containerID := shortContainerID(result.ContainerID)246 containerID := shortContainerID(result.ContainerID)
244- fmt.Println(styles.RenderSuccess(fmt.Sprintf("Pinned %s to %s (container: %s)", pinDomain, selectedTag, containerID)))247+ return cliWriteLine(out, styles.RenderSuccess(fmt.Sprintf("Pinned %s to %s (container: %s)", pinDomain, selectedTag, containerID)))
245- return nil
246 }248 }

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