Skip to content

feat: extend workspace build reasons to track connection types #18827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Jul 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion cli/parameter.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,11 @@ func parseParameterMapFile(parameterFile string) (map[string]string, error) {
return parameterMap, nil
}

// buildFlags contains options relating to troubleshooting provisioner jobs.
// buildFlags contains options relating to troubleshooting provisioner jobs
// and setting the reason for the workspace build.
type buildFlags struct {
provisionerLogDebug bool
reason string
}

func (bf *buildFlags) cliOptions() []serpent.Option {
Expand All @@ -160,5 +162,17 @@ This is useful for troubleshooting build issues.`,
Value: serpent.BoolOf(&bf.provisionerLogDebug),
Hidden: true,
},
{
Flag: "reason",
Description: `Sets the reason for the workspace build (cli, vscode_connection, jetbrains_connection).`,
Value: serpent.EnumOf(
&bf.reason,
string(codersdk.BuildReasonCLI),
string(codersdk.BuildReasonVSCodeConnection),
string(codersdk.BuildReasonJetbrainsConnection),
),
Default: string(codersdk.BuildReasonCLI),
Hidden: true,
},
}
}
4 changes: 3 additions & 1 deletion cli/ssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,9 @@ func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *
// It's possible for a workspace build to fail due to the template requiring starting
// workspaces with the active version.
_, _ = fmt.Fprintf(inv.Stderr, "Workspace was stopped, starting workspace to allow connecting to %q...\n", workspace.Name)
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceStart)
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{
reason: string(codersdk.BuildReasonSSHConnection),
}, WorkspaceStart)
if cerr, ok := codersdk.AsError(err); ok {
switch cerr.StatusCode() {
case http.StatusConflict:
Expand Down
3 changes: 3 additions & 0 deletions cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
if buildFlags.provisionerLogDebug {
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
}
if buildFlags.reason != "" {
wbr.Reason = codersdk.CreateWorkspaceBuildReason(buildFlags.reason)
}

return wbr, nil
}
Expand Down
36 changes: 36 additions & 0 deletions cli/start_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,39 @@ func TestStart_NoWait(t *testing.T) {
pty.ExpectMatch("workspace has been started in no-wait mode")
_ = testutil.TryReceive(ctx, t, doneChan)
}

func TestStart_WithReason(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)

// Prepare user, template, workspace
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)

// Stop the workspace
build := coderdtest.CreateWorkspaceBuild(t, member, workspace, database.WorkspaceTransitionStop)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)

// Start the workspace with reason
inv, root := clitest.New(t, "start", workspace.Name, "--reason", "cli")
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()

pty.ExpectMatch("workspace has been started")
_ = testutil.TryReceive(ctx, t, doneChan)

workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
require.Equal(t, codersdk.BuildReasonCLI, workspace.LatestBuild.Reason)
}
46 changes: 44 additions & 2 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

51 changes: 49 additions & 2 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion coderd/database/dump.sql

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- It's not possible to delete enum values.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'dashboard';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'cli';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'ssh_connection';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'vscode_connection';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'jetbrains_connection';
29 changes: 22 additions & 7 deletions coderd/database/models.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion coderd/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,20 +329,26 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)

workspace := httpmw.WorkspaceParam(r)
var createBuild codersdk.CreateWorkspaceBuildRequest
if !httpapi.Read(ctx, rw, r, &createBuild) {
return
}

builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition), *api.BuildUsageChecker.Load()).
transition := database.WorkspaceTransition(createBuild.Transition)
builder := wsbuilder.New(workspace, transition, *api.BuildUsageChecker.Load()).
Initiator(apiKey.UserID).
RichParameterValues(createBuild.RichParameterValues).
LogLevel(string(createBuild.LogLevel)).
DeploymentValues(api.Options.DeploymentValues).
Experiments(api.Experiments).
TemplateVersionPresetID(createBuild.TemplateVersionPresetID)

if transition == database.WorkspaceTransitionStart && createBuild.Reason != "" {
builder = builder.Reason(database.BuildReason(createBuild.Reason))
}

var (
previousWorkspaceBuild database.WorkspaceBuild
workspaceBuild *database.WorkspaceBuild
Expand Down
24 changes: 24 additions & 0 deletions coderd/workspacebuilds_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,30 @@ func TestPostWorkspaceBuild(t *testing.T) {
assert.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
t.Run("WithReason", func(t *testing.T) {
t.Parallel()
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
_ = closeDaemon.Close()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()

build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
Reason: codersdk.CreateWorkspaceBuildReasonDashboard,
})
require.NoError(t, err)
require.Equal(t, codersdk.BuildReasonDashboard, build.Reason)
})
}

func TestWorkspaceBuildTimings(t *testing.T) {
Expand Down
10 changes: 10 additions & 0 deletions codersdk/workspacebuilds.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ const (
// BuildReasonDormancy "dormancy" is used when a build to stop a workspace is triggered due to inactivity (dormancy).
// The initiator id/username in this case is the workspace owner and can be ignored.
BuildReasonDormancy BuildReason = "dormancy"
// BuildReasonDashboard "dashboard" is used when a build to start a workspace is triggered by the dashboard.
BuildReasonDashboard BuildReason = "dashboard"
// BuildReasonCLI "cli" is used when a build to start a workspace is triggered by the CLI.
BuildReasonCLI BuildReason = "cli"
// BuildReasonSSHConnection "ssh_connection" is used when a build to start a workspace is triggered by an SSH connection.
BuildReasonSSHConnection BuildReason = "ssh_connection"
// BuildReasonVSCodeConnection "vscode_connection" is used when a build to start a workspace is triggered by a VS Code connection.
BuildReasonVSCodeConnection BuildReason = "vscode_connection"
// BuildReasonJetbrainsConnection "jetbrains_connection" is used when a build to start a workspace is triggered by a JetBrains connection.
BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection"
)

// WorkspaceBuild is an at-point representation of a workspace state.
Expand Down
Loading
Loading