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

This PR introduces new build reason values to identify what type of
connection triggered a workspace build, helping to troubleshoot
workspace-related issues.

## Database Migration
Added migration 000349_extend_workspace_build_reason.up.sql that extends
the build_reason enum with new values:
```
dashboard, cli, ssh_connection, vscode_connection, jetbrains_connection
```

## Implementation
The build reason is specified through the API when creating new
workspace builds:

- Dashboard: Automatically sets reason to `dashboard` when users start
workspaces via the web interface
- CLI `start` command: Sets reason to `cli` when workspaces are started
via the command line
- CLI `ssh` command: Sets reason to ssh_connection when workspaces are
started due to SSH connections
- VS Code connections: Will be set to `vscode_connection` by the VS Code
extension through CLI hidden flag
(https://github.com/coder/vscode-coder/pull/550)
- JetBrains connections: Will be set to `jetbrains_connection` by the
Jetbrains Toolbox
(https://github.com/coder/coder-jetbrains-toolbox/pull/150) and
Jetbrains Gateway extension
(https://github.com/coder/jetbrains-coder/pull/561)

## UI Changes:
* Tooltip with reason in Build history
<img width="309" height="457" alt="image"
src="https://github.com/user-attachments/assets/bde8440b-bf3b-49a1-a244-ed7e8eb9763c"
/>

* Reason in Audit Logs Row tooltip
<img width="906" height="237" alt="image"
src="https://github.com/user-attachments/assets/ebbb62c7-cf07-4398-afbf-323c83fb6426"
/>

<img width="909" height="188" alt="image"
src="https://github.com/user-attachments/assets/1ddbab07-44bf-4dee-8867-b4e2cd56ae96"
/>
This commit is contained in:
Kacper Sawicki
2025-07-22 13:11:27 +02:00
committed by GitHub
parent 0ebd4356a0
commit 482463c51a
25 changed files with 388 additions and 36 deletions
+44 -2
View File
@@ -11448,13 +11448,23 @@ const docTemplate = `{
"initiator",
"autostart",
"autostop",
"dormancy"
"dormancy",
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"BuildReasonInitiator",
"BuildReasonAutostart",
"BuildReasonAutostop",
"BuildReasonDormancy"
"BuildReasonDormancy",
"BuildReasonDashboard",
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
]
},
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
@@ -12070,6 +12080,23 @@ const docTemplate = `{
}
}
},
"codersdk.CreateWorkspaceBuildReason": {
"type": "string",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"CreateWorkspaceBuildReasonDashboard",
"CreateWorkspaceBuildReasonCLI",
"CreateWorkspaceBuildReasonSSHConnection",
"CreateWorkspaceBuildReasonVSCodeConnection",
"CreateWorkspaceBuildReasonJetbrainsConnection"
]
},
"codersdk.CreateWorkspaceBuildRequest": {
"type": "object",
"required": [
@@ -12094,6 +12121,21 @@ const docTemplate = `{
"description": "Orphan may be set for the Destroy transition.",
"type": "boolean"
},
"reason": {
"description": "Reason sets the reason for the workspace build.",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.CreateWorkspaceBuildReason"
}
]
},
"rich_parameter_values": {
"description": "ParameterValues are optional. It will write params to the 'workspace' scope.\nThis will overwrite any existing parameters with the same name.\nThis will not delete old params not included in this list.",
"type": "array",
+49 -2
View File
@@ -10179,12 +10179,27 @@
},
"codersdk.BuildReason": {
"type": "string",
"enum": ["initiator", "autostart", "autostop", "dormancy"],
"enum": [
"initiator",
"autostart",
"autostop",
"dormancy",
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"BuildReasonInitiator",
"BuildReasonAutostart",
"BuildReasonAutostop",
"BuildReasonDormancy"
"BuildReasonDormancy",
"BuildReasonDashboard",
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
]
},
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
@@ -10758,6 +10773,23 @@
}
}
},
"codersdk.CreateWorkspaceBuildReason": {
"type": "string",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"CreateWorkspaceBuildReasonDashboard",
"CreateWorkspaceBuildReasonCLI",
"CreateWorkspaceBuildReasonSSHConnection",
"CreateWorkspaceBuildReasonVSCodeConnection",
"CreateWorkspaceBuildReasonJetbrainsConnection"
]
},
"codersdk.CreateWorkspaceBuildRequest": {
"type": "object",
"required": ["transition"],
@@ -10778,6 +10810,21 @@
"description": "Orphan may be set for the Destroy transition.",
"type": "boolean"
},
"reason": {
"description": "Reason sets the reason for the workspace build.",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.CreateWorkspaceBuildReason"
}
]
},
"rich_parameter_values": {
"description": "ParameterValues are optional. It will write params to the 'workspace' scope.\nThis will overwrite any existing parameters with the same name.\nThis will not delete old params not included in this list.",
"type": "array",
+6 -1
View File
@@ -51,7 +51,12 @@ CREATE TYPE build_reason AS ENUM (
'autostop',
'dormancy',
'failedstop',
'autodelete'
'autodelete',
'dashboard',
'cli',
'ssh_connection',
'vscode_connection',
'jetbrains_connection'
);
CREATE TYPE connection_status AS ENUM (
@@ -0,0 +1 @@
-- It's not possible to delete enum values.
@@ -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';
+22 -7
View File
@@ -349,12 +349,17 @@ func AllAutomaticUpdatesValues() []AutomaticUpdates {
type BuildReason string
const (
BuildReasonInitiator BuildReason = "initiator"
BuildReasonAutostart BuildReason = "autostart"
BuildReasonAutostop BuildReason = "autostop"
BuildReasonDormancy BuildReason = "dormancy"
BuildReasonFailedstop BuildReason = "failedstop"
BuildReasonAutodelete BuildReason = "autodelete"
BuildReasonInitiator BuildReason = "initiator"
BuildReasonAutostart BuildReason = "autostart"
BuildReasonAutostop BuildReason = "autostop"
BuildReasonDormancy BuildReason = "dormancy"
BuildReasonFailedstop BuildReason = "failedstop"
BuildReasonAutodelete BuildReason = "autodelete"
BuildReasonDashboard BuildReason = "dashboard"
BuildReasonCli BuildReason = "cli"
BuildReasonSshConnection BuildReason = "ssh_connection"
BuildReasonVscodeConnection BuildReason = "vscode_connection"
BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection"
)
func (e *BuildReason) Scan(src interface{}) error {
@@ -399,7 +404,12 @@ func (e BuildReason) Valid() bool {
BuildReasonAutostop,
BuildReasonDormancy,
BuildReasonFailedstop,
BuildReasonAutodelete:
BuildReasonAutodelete,
BuildReasonDashboard,
BuildReasonCli,
BuildReasonSshConnection,
BuildReasonVscodeConnection,
BuildReasonJetbrainsConnection:
return true
}
return false
@@ -413,6 +423,11 @@ func AllBuildReasonValues() []BuildReason {
BuildReasonDormancy,
BuildReasonFailedstop,
BuildReasonAutodelete,
BuildReasonDashboard,
BuildReasonCli,
BuildReasonSshConnection,
BuildReasonVscodeConnection,
BuildReasonJetbrainsConnection,
}
}
+7 -1
View File
@@ -329,13 +329,15 @@ 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)).
@@ -343,6 +345,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
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
+24
View File
@@ -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) {