detached 7944c3c

fix: final cleanup from coderabbit review

Remove 'It' sentence starts in bootstrap docs. Add insecure_tls to
remotes JSON output. Normalize nil attachments in secrets JSON.
Remove misplaced cliRenderTitle in backup. Use cmd.Context() in
bootstrap command.

Part of #98.
commit
7944c3c8902e10f475a2db31e702b600323be1a3
author
brice · · 2026-03-13 21:04 UTC
parent
952bc8b
signature
unsigned
6 files · +48 -18
M docs/cli/bootstrap.md +4 -4
@@ -30,10 +30,10 @@ gordon bootstrap <domain> <image> [options]
3030
3131 `gordon bootstrap` is the recommended first-step setup workflow.
3232
33-- It creates the route when it does not exist.
34-- It updates the route when it already exists.
35-- It can attach services and set environment variables as part of the same command.
36-- It does not push or deploy the image.
33+- Creates the route when it does not exist.
34+- Updates the route when it already exists.
35+- Can attach services and set environment variables as part of the same command.
36+- Does not push or deploy the image.
3737
3838 Unlike `gordon push`, `gordon bootstrap` does not require the route to exist first. Run `gordon push` separately after bootstrap to upload and deploy an image.
3939
@@ -30,10 +30,10 @@ gordon bootstrap <domain> <image> [options]
30 30
31 `gordon bootstrap` is the recommended first-step setup workflow.31 `gordon bootstrap` is the recommended first-step setup workflow.
32 32
33-- It creates the route when it does not exist.33+- Creates the route when it does not exist.
34-- It updates the route when it already exists.34+- Updates the route when it already exists.
35-- It can attach services and set environment variables as part of the same command.35+- Can attach services and set environment variables as part of the same command.
36-- It does not push or deploy the image.36+- Does not push or deploy the image.
37 37
38 Unlike `gordon push`, `gordon bootstrap` does not require the route to exist first. Run `gordon push` separately after bootstrap to upload and deploy an image.38 Unlike `gordon push`, `gordon bootstrap` does not require the route to exist first. Run `gordon push` separately after bootstrap to upload and deploy an image.
39 39
M internal/adapters/in/cli/backup.go +1 -2
@@ -34,11 +34,10 @@ instance when --remote targeting is configured.`,
3434 func newBackupListCmd() *cobra.Command {
3535 var jsonOut bool
3636
37- cliRenderTitle("Backups")
38-
3937 cmd := &cobra.Command{
4038 Use: "list [domain]",
4139 Short: "List backups",
40+ Long: cliRenderMuted("List backups for all domains or a specific domain."),
4241 Args: cobra.MaximumNArgs(1),
4342 RunE: func(cmd *cobra.Command, args []string) error {
4443 handle, err := resolveControlPlane(configPath)
@@ -34,11 +34,10 @@ instance when --remote targeting is configured.`,
34 func newBackupListCmd() *cobra.Command {34 func newBackupListCmd() *cobra.Command {
35 var jsonOut bool35 var jsonOut bool
36 36
37- cliRenderTitle("Backups")
38-
39 cmd := &cobra.Command{37 cmd := &cobra.Command{
40 Use: "list [domain]",38 Use: "list [domain]",
41 Short: "List backups",39 Short: "List backups",
40+ Long: cliRenderMuted("List backups for all domains or a specific domain."),
42 Args: cobra.MaximumNArgs(1),41 Args: cobra.MaximumNArgs(1),
43 RunE: func(cmd *cobra.Command, args []string) error {42 RunE: func(cmd *cobra.Command, args []string) error {
44 handle, err := resolveControlPlane(configPath)43 handle, err := resolveControlPlane(configPath)
M internal/adapters/in/cli/bootstrap.go +1 -2
@@ -1,7 +1,6 @@
11 package cli
22
33 import (
4- "context"
54 "fmt"
65 "os"
76 "sort"
@@ -23,7 +22,7 @@ func newBootstrapCmd() *cobra.Command {
2322 Short: "Create a route, attachments, and secrets together",
2423 Args: cobra.ExactArgs(2),
2524 RunE: func(cmd *cobra.Command, args []string) error {
26- ctx := context.Background()
25+ ctx := cmd.Context()
2726 req := dto.BootstrapRequest{
2827 Domain: args[0],
2928 Image: args[1],
@@ -1,7 +1,6 @@
1 package cli1 package cli
2 2
3 import (3 import (
4- "context"
5 "fmt"4 "fmt"
6 "os"5 "os"
7 "sort"6 "sort"
@@ -23,7 +22,7 @@ func newBootstrapCmd() *cobra.Command {
23 Short: "Create a route, attachments, and secrets together",22 Short: "Create a route, attachments, and secrets together",
24 Args: cobra.ExactArgs(2),23 Args: cobra.ExactArgs(2),
25 RunE: func(cmd *cobra.Command, args []string) error {24 RunE: func(cmd *cobra.Command, args []string) error {
26- ctx := context.Background()25+ ctx := cmd.Context()
27 req := dto.BootstrapRequest{26 req := dto.BootstrapRequest{
28 Domain: args[0],27 Domain: args[0],
29 Image: args[1],28 Image: args[1],
M internal/adapters/in/cli/json_test.go +35 -7
@@ -147,26 +147,29 @@ func TestTokenList_JSONShape_RoundTripsTokens(t *testing.T) {
147147
148148 func TestRemotesList_JSONShape_RoundTripsRemoteObjects(t *testing.T) {
149149 items := []struct {
150- Name string `json:"name"`
151- URL string `json:"url"`
152- Active bool `json:"active"`
150+ Name string `json:"name"`
151+ URL string `json:"url"`
152+ Active bool `json:"active"`
153+ InsecureTLS bool `json:"insecure_tls"`
153154 }{
154- {Name: "prod", URL: "https://prod.example.com", Active: true},
155+ {Name: "prod", URL: "https://prod.example.com", Active: true, InsecureTLS: true},
155156 }
156157
157158 payload, err := json.Marshal(items)
158159 require.NoError(t, err)
159160
160161 var got []struct {
161- Name string `json:"name"`
162- URL string `json:"url"`
163- Active bool `json:"active"`
162+ Name string `json:"name"`
163+ URL string `json:"url"`
164+ Active bool `json:"active"`
165+ InsecureTLS bool `json:"insecure_tls"`
164166 }
165167 require.NoError(t, json.Unmarshal(payload, &got))
166168 require.Len(t, got, 1)
167169 assert.Equal(t, "prod", got[0].Name)
168170 assert.Equal(t, "https://prod.example.com", got[0].URL)
169171 assert.True(t, got[0].Active)
172+ assert.True(t, got[0].InsecureTLS)
170173 }
171174
172175 func TestAttachmentsList_JSONShape_RoundTripsTargetPayload(t *testing.T) {
@@ -218,6 +221,31 @@ func TestSecretsList_JSONShape_RoundTripsPayload(t *testing.T) {
218221 assert.Equal(t, payload.Attachments, got.Attachments)
219222 }
220223
224+func TestSecretsList_JSONShape_NormalizesNilAttachmentsToEmptySlice(t *testing.T) {
225+ payload := struct {
226+ Domain string `json:"domain"`
227+ Keys []string `json:"keys"`
228+ Attachments []remote.AttachmentSecrets `json:"attachments"`
229+ }{
230+ Domain: "app.example.com",
231+ Keys: []string{"API_KEY"},
232+ Attachments: []remote.AttachmentSecrets{},
233+ }
234+
235+ encoded, err := json.Marshal(payload)
236+ require.NoError(t, err)
237+ assert.Contains(t, string(encoded), `"attachments":[]`)
238+
239+ var got struct {
240+ Domain string `json:"domain"`
241+ Keys []string `json:"keys"`
242+ Attachments []remote.AttachmentSecrets `json:"attachments"`
243+ }
244+ require.NoError(t, json.Unmarshal(encoded, &got))
245+ assert.NotNil(t, got.Attachments)
246+ assert.Empty(t, got.Attachments)
247+}
248+
221249 func TestRoutesList_JSONShape_RoundTripsLocalPayload(t *testing.T) {
222250 payload := []struct {
223251 Domain string `json:"domain"`
@@ -147,26 +147,29 @@ func TestTokenList_JSONShape_RoundTripsTokens(t *testing.T) {
147 147
148 func TestRemotesList_JSONShape_RoundTripsRemoteObjects(t *testing.T) {148 func TestRemotesList_JSONShape_RoundTripsRemoteObjects(t *testing.T) {
149 items := []struct {149 items := []struct {
150- Name string `json:"name"`150+ Name string `json:"name"`
151- URL string `json:"url"`151+ URL string `json:"url"`
152- Active bool `json:"active"`152+ Active bool `json:"active"`
153+ InsecureTLS bool `json:"insecure_tls"`
153 }{154 }{
154- {Name: "prod", URL: "https://prod.example.com", Active: true},155+ {Name: "prod", URL: "https://prod.example.com", Active: true, InsecureTLS: true},
155 }156 }
156 157
157 payload, err := json.Marshal(items)158 payload, err := json.Marshal(items)
158 require.NoError(t, err)159 require.NoError(t, err)
159 160
160 var got []struct {161 var got []struct {
161- Name string `json:"name"`162+ Name string `json:"name"`
162- URL string `json:"url"`163+ URL string `json:"url"`
163- Active bool `json:"active"`164+ Active bool `json:"active"`
165+ InsecureTLS bool `json:"insecure_tls"`
164 }166 }
165 require.NoError(t, json.Unmarshal(payload, &got))167 require.NoError(t, json.Unmarshal(payload, &got))
166 require.Len(t, got, 1)168 require.Len(t, got, 1)
167 assert.Equal(t, "prod", got[0].Name)169 assert.Equal(t, "prod", got[0].Name)
168 assert.Equal(t, "https://prod.example.com", got[0].URL)170 assert.Equal(t, "https://prod.example.com", got[0].URL)
169 assert.True(t, got[0].Active)171 assert.True(t, got[0].Active)
172+ assert.True(t, got[0].InsecureTLS)
170 }173 }
171 174
172 func TestAttachmentsList_JSONShape_RoundTripsTargetPayload(t *testing.T) {175 func TestAttachmentsList_JSONShape_RoundTripsTargetPayload(t *testing.T) {
@@ -218,6 +221,31 @@ func TestSecretsList_JSONShape_RoundTripsPayload(t *testing.T) {
218 assert.Equal(t, payload.Attachments, got.Attachments)221 assert.Equal(t, payload.Attachments, got.Attachments)
219 }222 }
220 223
224+func TestSecretsList_JSONShape_NormalizesNilAttachmentsToEmptySlice(t *testing.T) {
225+ payload := struct {
226+ Domain string `json:"domain"`
227+ Keys []string `json:"keys"`
228+ Attachments []remote.AttachmentSecrets `json:"attachments"`
229+ }{
230+ Domain: "app.example.com",
231+ Keys: []string{"API_KEY"},
232+ Attachments: []remote.AttachmentSecrets{},
233+ }
234+
235+ encoded, err := json.Marshal(payload)
236+ require.NoError(t, err)
237+ assert.Contains(t, string(encoded), `"attachments":[]`)
238+
239+ var got struct {
240+ Domain string `json:"domain"`
241+ Keys []string `json:"keys"`
242+ Attachments []remote.AttachmentSecrets `json:"attachments"`
243+ }
244+ require.NoError(t, json.Unmarshal(encoded, &got))
245+ assert.NotNil(t, got.Attachments)
246+ assert.Empty(t, got.Attachments)
247+}
248+
221 func TestRoutesList_JSONShape_RoundTripsLocalPayload(t *testing.T) {249 func TestRoutesList_JSONShape_RoundTripsLocalPayload(t *testing.T) {
222 payload := []struct {250 payload := []struct {
223 Domain string `json:"domain"`251 Domain string `json:"domain"`
M internal/adapters/in/cli/remotes.go +4 -3
@@ -80,9 +80,10 @@ func runRemotesList(out io.Writer, jsonOut bool) error {
8080 for _, name := range names {
8181 r := remotes[name]
8282 payload = append(payload, map[string]any{
83- "name": name,
84- "url": r.URL,
85- "active": name == active,
83+ "name": name,
84+ "url": r.URL,
85+ "active": name == active,
86+ "insecure_tls": r.InsecureTLS,
8687 })
8788 }
8889 return writeJSON(out, payload)
@@ -80,9 +80,10 @@ func runRemotesList(out io.Writer, jsonOut bool) error {
80 for _, name := range names {80 for _, name := range names {
81 r := remotes[name]81 r := remotes[name]
82 payload = append(payload, map[string]any{82 payload = append(payload, map[string]any{
83- "name": name,83+ "name": name,
84- "url": r.URL,84+ "url": r.URL,
85- "active": name == active,85+ "active": name == active,
86+ "insecure_tls": r.InsecureTLS,
86 })87 })
87 }88 }
88 return writeJSON(out, payload)89 return writeJSON(out, payload)
M internal/adapters/in/cli/secrets.go +3 -0
@@ -87,6 +87,9 @@ func runSecretsListCmd(cmd *cobra.Command, args []string, jsonOut bool) error {
8787 }
8888
8989 if jsonOut {
90+ if attachments == nil {
91+ attachments = []remote.AttachmentSecrets{}
92+ }
9093 return writeJSON(cmd.OutOrStdout(), map[string]any{
9194 "domain": secretDomain,
9295 "keys": keys,
@@ -87,6 +87,9 @@ func runSecretsListCmd(cmd *cobra.Command, args []string, jsonOut bool) error {
87 }87 }
88 88
89 if jsonOut {89 if jsonOut {
90+ if attachments == nil {
91+ attachments = []remote.AttachmentSecrets{}
92+ }
90 return writeJSON(cmd.OutOrStdout(), map[string]any{93 return writeJSON(cmd.OutOrStdout(), map[string]any{
91 "domain": secretDomain,94 "domain": secretDomain,
92 "keys": keys,95 "keys": keys,

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 8

Tags 106

↑↓ moveenter openesc close