Skip to content

Commit 641aacf

Browse files
authored
feat: show banner when workspace is outdated (#4926)
* feat: show banner when workspace is outdated * Address PR comments * Fix: writer
1 parent f15854c commit 641aacf

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

cli/root.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,14 +417,25 @@ func isTTY(cmd *cobra.Command) bool {
417417
// This accepts a reader to work with Cobra's "OutOrStdout"
418418
// function for simple testing.
419419
func isTTYOut(cmd *cobra.Command) bool {
420+
return isTTYWriter(cmd, cmd.OutOrStdout)
421+
}
422+
423+
// isTTYErr returns whether the passed reader is a TTY or not.
424+
// This accepts a reader to work with Cobra's "ErrOrStderr"
425+
// function for simple testing.
426+
func isTTYErr(cmd *cobra.Command) bool {
427+
return isTTYWriter(cmd, cmd.ErrOrStderr)
428+
}
429+
430+
func isTTYWriter(cmd *cobra.Command, writer func() io.Writer) bool {
420431
// If the `--force-tty` command is available, and set,
421432
// assume we're in a tty. This is primarily for cases on Windows
422433
// where we may not be able to reliably detect this automatically (ie, tests)
423434
forceTty, err := cmd.Flags().GetBool(varForceTty)
424435
if forceTty && err == nil {
425436
return true
426437
}
427-
file, ok := cmd.OutOrStdout().(*os.File)
438+
file, ok := writer().(*os.File)
428439
if !ok {
429440
return false
430441
}

cli/ssh.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"fmt"
77
"io"
8+
"net/url"
89
"os"
910
"path/filepath"
1011
"strings"
@@ -72,6 +73,11 @@ func ssh() *cobra.Command {
7273
return err
7374
}
7475

76+
updateWorkspaceBanner, outdated := verifyWorkspaceOutdated(client, workspace)
77+
if outdated && isTTYErr(cmd) {
78+
_, _ = fmt.Fprintln(cmd.ErrOrStderr(), updateWorkspaceBanner)
79+
}
80+
7581
// OpenSSH passes stderr directly to the calling TTY.
7682
// This is required in "stdio" mode so a connecting indicator can be displayed.
7783
err = cliui.Agent(ctx, cmd.ErrOrStderr(), cliui.AgentOptions{
@@ -343,3 +349,18 @@ func notifyCondition(ctx context.Context, client *codersdk.Client, workspaceID u
343349
return deadline.Truncate(time.Minute), callback
344350
}
345351
}
352+
353+
// Verify if the user workspace is outdated and prepare an actionable message for user.
354+
func verifyWorkspaceOutdated(client *codersdk.Client, workspace codersdk.Workspace) (string, bool) {
355+
if !workspace.Outdated {
356+
return "", false // workspace is up-to-date
357+
}
358+
359+
workspaceLink := buildWorkspaceLink(client.URL, workspace)
360+
return fmt.Sprintf("👋 Your workspace is outdated! Update it here: %s\n", workspaceLink), true
361+
}
362+
363+
// Build the user workspace link which navigates to the Coder web UI.
364+
func buildWorkspaceLink(serverURL *url.URL, workspace codersdk.Workspace) *url.URL {
365+
return serverURL.ResolveReference(&url.URL{Path: fmt.Sprintf("@%s/%s", workspace.OwnerName, workspace.Name)})
366+
}

cli/ssh_internal_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package cli
2+
3+
import (
4+
"net/url"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
10+
"github.com/coder/coder/codersdk"
11+
)
12+
13+
const (
14+
fakeOwnerName = "fake-owner-name"
15+
fakeServerURL = "https://fake-foo-url"
16+
fakeWorkspaceName = "fake-workspace-name"
17+
)
18+
19+
func TestVerifyWorkspaceOutdated(t *testing.T) {
20+
t.Parallel()
21+
22+
serverURL, err := url.Parse(fakeServerURL)
23+
require.NoError(t, err)
24+
25+
client := codersdk.Client{URL: serverURL}
26+
27+
t.Run("Up-to-date", func(t *testing.T) {
28+
t.Parallel()
29+
30+
workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName}
31+
32+
_, outdated := verifyWorkspaceOutdated(&client, workspace)
33+
34+
assert.False(t, outdated, "workspace should be up-to-date")
35+
})
36+
t.Run("Outdated", func(t *testing.T) {
37+
t.Parallel()
38+
39+
workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName, Outdated: true}
40+
41+
updateWorkspaceBanner, outdated := verifyWorkspaceOutdated(&client, workspace)
42+
43+
assert.True(t, outdated, "workspace should be outdated")
44+
assert.NotEmpty(t, updateWorkspaceBanner, "workspace banner should be present")
45+
})
46+
}
47+
48+
func TestBuildWorkspaceLink(t *testing.T) {
49+
t.Parallel()
50+
51+
serverURL, err := url.Parse(fakeServerURL)
52+
require.NoError(t, err)
53+
54+
workspace := codersdk.Workspace{Name: fakeWorkspaceName, OwnerName: fakeOwnerName}
55+
workspaceLink := buildWorkspaceLink(serverURL, workspace)
56+
57+
assert.Equal(t, workspaceLink.String(), fakeServerURL+"/@"+fakeOwnerName+"/"+fakeWorkspaceName)
58+
}

0 commit comments

Comments
 (0)