detached 292f2ec

fix(cli): sanitize deploy fallback warnings

commit
292f2ecab8557053cff7246393008b9d525cd712
author
brice · · 2026-04-06 17:11 UTC
parent
2f5cb8f
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +34 -4
M internal/adapters/in/cli/deploy.go +2 -2
@@ -97,10 +97,10 @@ func runDeploy(ctx context.Context, deployer deployer, isRemote bool, deployDoma
9797 func handleLocalDeployFallback(err error, deployDomain string, out io.Writer, jsonOut bool) error {
9898 domain, localErr := sendDeploySignal(deployDomain)
9999 if localErr != nil {
100- return fmt.Errorf("failed to deploy: remote error: %w; local fallback also failed: %v", err, localErr)
100+ return fmt.Errorf("failed to deploy: remote error: %w; local fallback also failed: %w", err, localErr)
101101 }
102102
103- warning := fmt.Sprintf("Remote deploy failed (%v), used local signal fallback", err)
103+ warning := sanitizeDeployLogLine(fmt.Sprintf("Remote deploy failed (%v), used local signal fallback", err))
104104 if jsonOut {
105105 return writeJSON(out, map[string]string{
106106 "warning": warning,
@@ -97,10 +97,10 @@ func runDeploy(ctx context.Context, deployer deployer, isRemote bool, deployDoma
97 func handleLocalDeployFallback(err error, deployDomain string, out io.Writer, jsonOut bool) error {97 func handleLocalDeployFallback(err error, deployDomain string, out io.Writer, jsonOut bool) error {
98 domain, localErr := sendDeploySignal(deployDomain)98 domain, localErr := sendDeploySignal(deployDomain)
99 if localErr != nil {99 if localErr != nil {
100- return fmt.Errorf("failed to deploy: remote error: %w; local fallback also failed: %v", err, localErr)100+ return fmt.Errorf("failed to deploy: remote error: %w; local fallback also failed: %w", err, localErr)
101 }101 }
102 102
103- warning := fmt.Sprintf("Remote deploy failed (%v), used local signal fallback", err)103+ warning := sanitizeDeployLogLine(fmt.Sprintf("Remote deploy failed (%v), used local signal fallback", err))
104 if jsonOut {104 if jsonOut {
105 return writeJSON(out, map[string]string{105 return writeJSON(out, map[string]string{
106 "warning": warning,106 "warning": warning,
M internal/adapters/in/cli/deploy_error_test.go +1 -2
@@ -65,7 +65,6 @@ func TestFormatDeployFailure_SkipsEmptySanitizedLogsSection(t *testing.T) {
6565 err := formatDeployFailure(root)
6666
6767 require.Error(t, err)
68- assert.Contains(t, err.Error(), "failed to deploy\nCause: health check failed")
69- assert.NotContains(t, err.Error(), "Recent container logs:")
68+ assert.Equal(t, "failed to deploy\nCause: health check failed", err.Error())
7069 assert.ErrorIs(t, err, root)
7170 }
@@ -65,7 +65,6 @@ func TestFormatDeployFailure_SkipsEmptySanitizedLogsSection(t *testing.T) {
65 err := formatDeployFailure(root)65 err := formatDeployFailure(root)
66 66
67 require.Error(t, err)67 require.Error(t, err)
68- assert.Contains(t, err.Error(), "failed to deploy\nCause: health check failed")68+ assert.Equal(t, "failed to deploy\nCause: health check failed", err.Error())
69- assert.NotContains(t, err.Error(), "Recent container logs:")
70 assert.ErrorIs(t, err, root)69 assert.ErrorIs(t, err, root)
71 }70 }
M internal/adapters/in/cli/deploy_test.go +31 -0
@@ -86,3 +86,34 @@ func TestRunDeploy_RemoteFallbackUsesJSONWhenRequested(t *testing.T) {
8686 require.NoError(t, err)
8787 assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable: Registry Unavailable), used local signal fallback"}`, out.String())
8888 }
89+
90+func TestHandleLocalDeployFallback_WrapsRemoteAndLocalErrors(t *testing.T) {
91+ originalSendDeploySignal := sendDeploySignal
92+ localErr := errors.New("local signal failed")
93+ sendDeploySignal = func(string) (string, error) {
94+ return "", localErr
95+ }
96+ defer func() { sendDeploySignal = originalSendDeploySignal }()
97+
98+ remoteErr := errors.New("remote deploy failed")
99+ err := handleLocalDeployFallback(remoteErr, "app.example.com", &bytes.Buffer{}, false)
100+
101+ require.Error(t, err)
102+ assert.Equal(t, "failed to deploy: remote error: remote deploy failed; local fallback also failed: local signal failed", err.Error())
103+ assert.ErrorIs(t, err, remoteErr)
104+ assert.ErrorIs(t, err, localErr)
105+}
106+
107+func TestRunDeploy_RemoteFallbackSanitizesWarning(t *testing.T) {
108+ originalSendDeploySignal := sendDeploySignal
109+ sendDeploySignal = func(string) (string, error) {
110+ return "app.example.com", nil
111+ }
112+ defer func() { sendDeploySignal = originalSendDeploySignal }()
113+
114+ var out bytes.Buffer
115+ err := runDeploy(context.Background(), &deployTestDeployer{err: errors.New("503 Service Unavailable:\n\x1b[31mRegistry Unavailable\x1b[0m")}, true, "app.example.com", &out, true)
116+
117+ require.NoError(t, err)
118+ assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable:Registry Unavailable), used local signal fallback"}`, out.String())
119+}
@@ -86,3 +86,34 @@ func TestRunDeploy_RemoteFallbackUsesJSONWhenRequested(t *testing.T) {
86 require.NoError(t, err)86 require.NoError(t, err)
87 assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable: Registry Unavailable), used local signal fallback"}`, out.String())87 assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable: Registry Unavailable), used local signal fallback"}`, out.String())
88 }88 }
89+
90+func TestHandleLocalDeployFallback_WrapsRemoteAndLocalErrors(t *testing.T) {
91+ originalSendDeploySignal := sendDeploySignal
92+ localErr := errors.New("local signal failed")
93+ sendDeploySignal = func(string) (string, error) {
94+ return "", localErr
95+ }
96+ defer func() { sendDeploySignal = originalSendDeploySignal }()
97+
98+ remoteErr := errors.New("remote deploy failed")
99+ err := handleLocalDeployFallback(remoteErr, "app.example.com", &bytes.Buffer{}, false)
100+
101+ require.Error(t, err)
102+ assert.Equal(t, "failed to deploy: remote error: remote deploy failed; local fallback also failed: local signal failed", err.Error())
103+ assert.ErrorIs(t, err, remoteErr)
104+ assert.ErrorIs(t, err, localErr)
105+}
106+
107+func TestRunDeploy_RemoteFallbackSanitizesWarning(t *testing.T) {
108+ originalSendDeploySignal := sendDeploySignal
109+ sendDeploySignal = func(string) (string, error) {
110+ return "app.example.com", nil
111+ }
112+ defer func() { sendDeploySignal = originalSendDeploySignal }()
113+
114+ var out bytes.Buffer
115+ err := runDeploy(context.Background(), &deployTestDeployer{err: errors.New("503 Service Unavailable:\n\x1b[31mRegistry Unavailable\x1b[0m")}, true, "app.example.com", &out, true)
116+
117+ require.NoError(t, err)
118+ assert.JSONEq(t, `{"domain":"app.example.com","status":"success","warning":"Remote deploy failed (503 Service Unavailable:Registry Unavailable), used local signal fallback"}`, out.String())
119+}

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