From 15f24aca68a5900d5893c3ff3f6b28ab0315dffb Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Wed, 9 Jul 2025 14:05:42 +0000 Subject: [PATCH 01/12] add new build reasons to track what triggers workspace builds --- cli/parameter.go | 16 +++++- cli/ssh.go | 4 +- cli/start.go | 3 ++ coderd/apidoc/docs.go | 46 +++++++++++++++- coderd/apidoc/swagger.json | 51 +++++++++++++++++- ...349_extend_workspace_build_reason.down.sql | 1 + ...00349_extend_workspace_build_reason.up.sql | 5 ++ coderd/workspacebuilds.go | 4 ++ codersdk/workspacebuilds.go | 10 ++++ codersdk/workspaces.go | 12 +++++ docs/reference/api/builds.md | 1 + docs/reference/api/schemas.md | 54 ++++++++++++++----- site/src/api/typesGenerated.ts | 33 +++++++++++- .../WorkspaceParametersPage.tsx | 1 + .../WorkspaceParametersPageExperimental.tsx | 1 + site/src/utils/workspace.tsx | 12 +++++ 16 files changed, 235 insertions(+), 19 deletions(-) create mode 100644 coderd/database/migrations/000349_extend_workspace_build_reason.down.sql create mode 100644 coderd/database/migrations/000349_extend_workspace_build_reason.up.sql diff --git a/cli/parameter.go b/cli/parameter.go index 02ff4e11f63e4..97c551ffa5a7f 100644 --- a/cli/parameter.go +++ b/cli/parameter.go @@ -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 { @@ -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, + }, } } diff --git a/cli/ssh.go b/cli/ssh.go index 9327a0101c0cf..a2bca46c72f32 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -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: diff --git a/cli/start.go b/cli/start.go index 94f1a42ef7ac4..66c96cc9c4d75 100644 --- a/cli/start.go +++ b/cli/start.go @@ -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 } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 79cff80b1fbc5..18d2563c6c604 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -11367,13 +11367,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": { @@ -11856,6 +11866,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": [ @@ -11880,6 +11907,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", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5fa1d98030cb5..6c92424ed28a8 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -10106,12 +10106,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": { @@ -10552,6 +10567,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"], @@ -10572,6 +10604,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", diff --git a/coderd/database/migrations/000349_extend_workspace_build_reason.down.sql b/coderd/database/migrations/000349_extend_workspace_build_reason.down.sql new file mode 100644 index 0000000000000..383c118f65bef --- /dev/null +++ b/coderd/database/migrations/000349_extend_workspace_build_reason.down.sql @@ -0,0 +1 @@ +-- It's not possible to delete enum values. diff --git a/coderd/database/migrations/000349_extend_workspace_build_reason.up.sql b/coderd/database/migrations/000349_extend_workspace_build_reason.up.sql new file mode 100644 index 0000000000000..0cdd527c020c8 --- /dev/null +++ b/coderd/database/migrations/000349_extend_workspace_build_reason.up.sql @@ -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'; diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index c8b1008280b09..92f5f4ae2dc4d 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -343,6 +343,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Experiments(api.Experiments). TemplateVersionPresetID(createBuild.TemplateVersionPresetID) + if createBuild.Reason != "" { + builder = builder.Reason(database.BuildReason(createBuild.Reason)) + } + var ( previousWorkspaceBuild database.WorkspaceBuild workspaceBuild *database.WorkspaceBuild diff --git a/codersdk/workspacebuilds.go b/codersdk/workspacebuilds.go index 0960c6789dea4..53d2a89290bca 100644 --- a/codersdk/workspacebuilds.go +++ b/codersdk/workspacebuilds.go @@ -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. diff --git a/codersdk/workspaces.go b/codersdk/workspaces.go index c776f2cf5a473..934d07fe7dc56 100644 --- a/codersdk/workspaces.go +++ b/codersdk/workspaces.go @@ -93,6 +93,16 @@ const ( ProvisionerLogLevelDebug ProvisionerLogLevel = "debug" ) +type CreateWorkspaceBuildReason string + +const ( + CreateWorkspaceBuildReasonDashboard CreateWorkspaceBuildReason = "dashboard" + CreateWorkspaceBuildReasonCLI CreateWorkspaceBuildReason = "cli" + CreateWorkspaceBuildReasonSSHConnection CreateWorkspaceBuildReason = "ssh_connection" + CreateWorkspaceBuildReasonVSCodeConnection CreateWorkspaceBuildReason = "vscode_connection" + CreateWorkspaceBuildReasonJetbrainsConnection CreateWorkspaceBuildReason = "jetbrains_connection" +) + // CreateWorkspaceBuildRequest provides options to update the latest workspace build. type CreateWorkspaceBuildRequest struct { TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"` @@ -110,6 +120,8 @@ type CreateWorkspaceBuildRequest struct { LogLevel ProvisionerLogLevel `json:"log_level,omitempty" validate:"omitempty,oneof=debug"` // TemplateVersionPresetID is the ID of the template version preset to use for the build. TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"` + // Reason sets the reason for the workspace build. + Reason CreateWorkspaceBuildReason `json:"reason,omitempty" validate:"omitempty,oneof=dashboard cli ssh_connection vscode_connection jetbrains_connection"` } type WorkspaceOptions struct { diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md index 686f19316a8c0..fb491405df362 100644 --- a/docs/reference/api/builds.md +++ b/docs/reference/api/builds.md @@ -1762,6 +1762,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \ "dry_run": true, "log_level": "debug", "orphan": true, + "reason": "dashboard", "rich_parameter_values": [ { "name": "string", diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index 6ca1cfb9dfe51..fc86035fba523 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1044,12 +1044,17 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in #### Enumerated Values -| Value | -|-------------| -| `initiator` | -| `autostart` | -| `autostop` | -| `dormancy` | +| Value | +|------------------------| +| `initiator` | +| `autostart` | +| `autostop` | +| `dormancy` | +| `dashboard` | +| `cli` | +| `ssh_connection` | +| `vscode_connection` | +| `jetbrains_connection` | ## codersdk.ChangePasswordWithOneTimePasscodeRequest @@ -1467,6 +1472,24 @@ This is required on creation to enable a user-flow of validating a template work | `user_status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | User status defaults to UserStatusDormant. | | `username` | string | true | | | +## codersdk.CreateWorkspaceBuildReason + +```json +"dashboard" +``` + +### Properties + +#### Enumerated Values + +| Value | +|------------------------| +| `dashboard` | +| `cli` | +| `ssh_connection` | +| `vscode_connection` | +| `jetbrains_connection` | + ## codersdk.CreateWorkspaceBuildRequest ```json @@ -1474,6 +1497,7 @@ This is required on creation to enable a user-flow of validating a template work "dry_run": true, "log_level": "debug", "orphan": true, + "reason": "dashboard", "rich_parameter_values": [ { "name": "string", @@ -1496,6 +1520,7 @@ This is required on creation to enable a user-flow of validating a template work | `dry_run` | boolean | false | | | | `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). | | `orphan` | boolean | false | | Orphan may be set for the Destroy transition. | +| `reason` | [codersdk.CreateWorkspaceBuildReason](#codersdkcreateworkspacebuildreason) | false | | Reason sets the reason for the workspace build. | | `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. | | `state` | array of integer | false | | | | `template_version_id` | string | false | | | @@ -1504,12 +1529,17 @@ This is required on creation to enable a user-flow of validating a template work #### Enumerated Values -| Property | Value | -|--------------|----------| -| `log_level` | `debug` | -| `transition` | `start` | -| `transition` | `stop` | -| `transition` | `delete` | +| Property | Value | +|--------------|------------------------| +| `log_level` | `debug` | +| `reason` | `dashboard` | +| `reason` | `cli` | +| `reason` | `ssh_connection` | +| `reason` | `vscode_connection` | +| `reason` | `jetbrains_connection` | +| `transition` | `start` | +| `transition` | `stop` | +| `transition` | `delete` | ## codersdk.CreateWorkspaceProxyRequest diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 53dc919df2df3..755c8d78d1191 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -275,13 +275,27 @@ export interface BuildInfoResponse { } // From codersdk/workspacebuilds.go -export type BuildReason = "autostart" | "autostop" | "dormancy" | "initiator"; +export type BuildReason = + | "autostart" + | "autostop" + | "cli" + | "dashboard" + | "dormancy" + | "initiator" + | "jetbrains_connection" + | "ssh_connection" + | "vscode_connection"; export const BuildReasons: BuildReason[] = [ "autostart", "autostop", + "cli", + "dashboard", "dormancy", "initiator", + "jetbrains_connection", + "ssh_connection", + "vscode_connection", ]; // From codersdk/client.go @@ -461,6 +475,22 @@ export interface CreateUserRequestWithOrgs { readonly organization_ids: readonly string[]; } +// From codersdk/workspaces.go +export type CreateWorkspaceBuildReason = + | "cli" + | "dashboard" + | "jetbrains_connection" + | "ssh_connection" + | "vscode_connection"; + +export const CreateWorkspaceBuildReasons: CreateWorkspaceBuildReason[] = [ + "cli", + "dashboard", + "jetbrains_connection", + "ssh_connection", + "vscode_connection", +]; + // From codersdk/workspaces.go export interface CreateWorkspaceBuildRequest { readonly template_version_id?: string; @@ -471,6 +501,7 @@ export interface CreateWorkspaceBuildRequest { readonly rich_parameter_values?: readonly WorkspaceBuildParameter[]; readonly log_level?: ProvisionerLogLevel; readonly template_version_preset_id?: string; + readonly reason?: CreateWorkspaceBuildReason; } // From codersdk/workspaceproxy.go diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx index 50f2eedaeec26..30b8ca943795f 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.tsx @@ -35,6 +35,7 @@ const WorkspaceParametersPage: FC = () => { API.postWorkspaceBuild(workspace.id, { transition: "start", rich_parameter_values: buildParameters, + reason: "dashboard", }), onSuccess: () => { navigate(`/${workspace.owner_name}/${workspace.name}`); diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx index 14cffafa064c1..c6c0391b1fd29 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPageExperimental.tsx @@ -150,6 +150,7 @@ const WorkspaceParametersPageExperimental: FC = () => { transition: "start", template_version_id: templateVersionId, rich_parameter_values: buildParameters, + reason: "dashboard", }), onSuccess: () => { navigate(`/@${workspace.owner_name}/${workspace.name}`); diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index c88ffc9d8edaa..db0361c268d26 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -83,6 +83,18 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( case "autostop": case "dormancy": return "Coder"; + case "dashboard": + return "Dashboard"; + case "cli": + return "CLI"; + case "ssh_connection": + return "User SSH Connection"; + case "vscode_connection": + return "VSCode Connection"; + case "jetbrains_connection": + return "JetBrains Connection"; + default: + return "Unknown"; } return undefined; }; From 905f7551a9d3446b9268c1af6d7a4a99447a04d6 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Thu, 10 Jul 2025 08:51:04 +0000 Subject: [PATCH 02/12] change ssh_connection UI label --- site/src/utils/workspace.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index db0361c268d26..f19fbb6c459d2 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -88,7 +88,7 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( case "cli": return "CLI"; case "ssh_connection": - return "User SSH Connection"; + return "SSH Connection"; case "vscode_connection": return "VSCode Connection"; case "jetbrains_connection": From 5dd67fea3750ba440d4842c98266911d8aed5d82 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Thu, 10 Jul 2025 13:44:32 +0000 Subject: [PATCH 03/12] Use UserAgent header to detect jetbrains connection, regenerate models --- cli/parameter.go | 3 +-- coderd/database/dump.sql | 7 ++++++- coderd/database/models.go | 29 ++++++++++++++++++++++------- coderd/workspacebuilds.go | 16 +++++++++++++--- site/src/utils/workspace.tsx | 2 -- 5 files changed, 42 insertions(+), 15 deletions(-) diff --git a/cli/parameter.go b/cli/parameter.go index 97c551ffa5a7f..29753ea613247 100644 --- a/cli/parameter.go +++ b/cli/parameter.go @@ -164,12 +164,11 @@ This is useful for troubleshooting build issues.`, }, { Flag: "reason", - Description: `Sets the reason for the workspace build (cli, vscode_connection, jetbrains_connection).`, + Description: `Sets the reason for the workspace build (cli, vscode_connection).`, Value: serpent.EnumOf( &bf.reason, string(codersdk.BuildReasonCLI), string(codersdk.BuildReasonVSCodeConnection), - string(codersdk.BuildReasonJetbrainsConnection), ), Default: string(codersdk.BuildReasonCLI), Hidden: true, diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 54f984294fa4e..af19aacf4fcd8 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -49,7 +49,12 @@ CREATE TYPE build_reason AS ENUM ( 'autostop', 'dormancy', 'failedstop', - 'autodelete' + 'autodelete', + 'dashboard', + 'cli', + 'ssh_connection', + 'vscode_connection', + 'jetbrains_connection' ); CREATE TYPE crypto_key_feature AS ENUM ( diff --git a/coderd/database/models.go b/coderd/database/models.go index 749de51118152..b3d3d425420ec 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -348,12 +348,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 { @@ -398,7 +403,12 @@ func (e BuildReason) Valid() bool { BuildReasonAutostop, BuildReasonDormancy, BuildReasonFailedstop, - BuildReasonAutodelete: + BuildReasonAutodelete, + BuildReasonDashboard, + BuildReasonCli, + BuildReasonSshConnection, + BuildReasonVscodeConnection, + BuildReasonJetbrainsConnection: return true } return false @@ -412,6 +422,11 @@ func AllBuildReasonValues() []BuildReason { BuildReasonDormancy, BuildReasonFailedstop, BuildReasonAutodelete, + BuildReasonDashboard, + BuildReasonCli, + BuildReasonSshConnection, + BuildReasonVscodeConnection, + BuildReasonJetbrainsConnection, } } diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 92f5f4ae2dc4d..9d9b8423f83f9 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -11,6 +11,7 @@ import ( "slices" "sort" "strconv" + "strings" "time" "github.com/go-chi/chi/v5" @@ -329,13 +330,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)). + transition := database.WorkspaceTransition(createBuild.Transition) + builder := wsbuilder.New(workspace, transition). Initiator(apiKey.UserID). RichParameterValues(createBuild.RichParameterValues). LogLevel(string(createBuild.LogLevel)). @@ -343,8 +346,15 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Experiments(api.Experiments). TemplateVersionPresetID(createBuild.TemplateVersionPresetID) - if createBuild.Reason != "" { - builder = builder.Reason(database.BuildReason(createBuild.Reason)) + if transition == database.WorkspaceTransitionStart { + if createBuild.Reason == "" { + userAgent := r.Header.Get("User-Agent") + if strings.HasPrefix(userAgent, "Coder Toolbox") || strings.HasPrefix(userAgent, "Coder Gateway") { + builder = builder.Reason(database.BuildReasonJetbrainsConnection) + } + } else { + builder = builder.Reason(database.BuildReason(createBuild.Reason)) + } } var ( diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index f19fbb6c459d2..77dfa62acea05 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -93,8 +93,6 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( return "VSCode Connection"; case "jetbrains_connection": return "JetBrains Connection"; - default: - return "Unknown"; } return undefined; }; From 0c4280c11698275d4d1fd4167fa894b703634216 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Thu, 10 Jul 2025 14:18:54 +0000 Subject: [PATCH 04/12] Add tests --- cli/start_test.go | 36 +++++++++++++++++++++++ coderd/workspacebuilds_test.go | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/cli/start_test.go b/cli/start_test.go index ec5f0b4735b39..85b7b88374f72 100644 --- a/cli/start_test.go +++ b/cli/start_test.go @@ -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) +} diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index ebab0770b71b4..7b5d81d652426 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1802,6 +1802,59 @@ 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) + }) + t.Run("WithCoderToolboxUserAgent", func(t *testing.T) { + t.Parallel() + client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{ + IncludeProvisionerDaemon: true, + }) + client.HTTPClient.Transport = &codersdk.HeaderTransport{ + Transport: http.DefaultTransport, + Header: http.Header{ + "User-Agent": {"Coder Toolbox/1.0"}, + }, + } + 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, + }) + require.NoError(t, err) + require.Equal(t, codersdk.BuildReasonJetbrainsConnection, build.Reason) + }) } func TestWorkspaceBuildTimings(t *testing.T) { From ed3cb9af0ef7eff455d002f45a9101df372ab9d5 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Thu, 10 Jul 2025 14:45:30 +0000 Subject: [PATCH 05/12] Fix UI test --- .../WorkspaceParametersPage/WorkspaceParametersPage.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx index 667f529d9e96a..dc4c127b9506e 100644 --- a/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx +++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceParametersPage/WorkspaceParametersPage.test.tsx @@ -67,6 +67,7 @@ test("Submit the workspace settings page successfully", async () => { // Assert that the API calls were made with the correct data await waitFor(() => { expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(MockWorkspace.id, { + reason: "dashboard", transition: "start", rich_parameter_values: [ { name: MockTemplateVersionParameter1.name, value: "new-value" }, From 99e548f67c3c2bffbdfbe3e619c8ad1b5b9c2e93 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Mon, 14 Jul 2025 12:57:23 +0000 Subject: [PATCH 06/12] Remove User-Agent check --- cli/parameter.go | 1 + coderd/workspacebuilds.go | 12 ++---------- coderd/workspacebuilds_test.go | 29 ----------------------------- 3 files changed, 3 insertions(+), 39 deletions(-) diff --git a/cli/parameter.go b/cli/parameter.go index 29753ea613247..0a0971635cba0 100644 --- a/cli/parameter.go +++ b/cli/parameter.go @@ -169,6 +169,7 @@ This is useful for troubleshooting build issues.`, &bf.reason, string(codersdk.BuildReasonCLI), string(codersdk.BuildReasonVSCodeConnection), + string(codersdk.BuildReasonJetbrainsConnection), ), Default: string(codersdk.BuildReasonCLI), Hidden: true, diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 9d9b8423f83f9..39f9dd1e946e6 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -11,7 +11,6 @@ import ( "slices" "sort" "strconv" - "strings" "time" "github.com/go-chi/chi/v5" @@ -346,15 +345,8 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) { Experiments(api.Experiments). TemplateVersionPresetID(createBuild.TemplateVersionPresetID) - if transition == database.WorkspaceTransitionStart { - if createBuild.Reason == "" { - userAgent := r.Header.Get("User-Agent") - if strings.HasPrefix(userAgent, "Coder Toolbox") || strings.HasPrefix(userAgent, "Coder Gateway") { - builder = builder.Reason(database.BuildReasonJetbrainsConnection) - } - } else { - builder = builder.Reason(database.BuildReason(createBuild.Reason)) - } + if transition == database.WorkspaceTransitionStart && createBuild.Reason != "" { + builder = builder.Reason(database.BuildReason(createBuild.Reason)) } var ( diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 7b5d81d652426..b1ec42dda31d3 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -1826,35 +1826,6 @@ func TestPostWorkspaceBuild(t *testing.T) { require.NoError(t, err) require.Equal(t, codersdk.BuildReasonDashboard, build.Reason) }) - t.Run("WithCoderToolboxUserAgent", func(t *testing.T) { - t.Parallel() - client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{ - IncludeProvisionerDaemon: true, - }) - client.HTTPClient.Transport = &codersdk.HeaderTransport{ - Transport: http.DefaultTransport, - Header: http.Header{ - "User-Agent": {"Coder Toolbox/1.0"}, - }, - } - 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, - }) - require.NoError(t, err) - require.Equal(t, codersdk.BuildReasonJetbrainsConnection, build.Reason) - }) } func TestWorkspaceBuildTimings(t *testing.T) { From c87cb537efce7ba1b5de9c0996f96ae47ac88236 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Mon, 14 Jul 2025 15:09:34 +0000 Subject: [PATCH 07/12] Update flag description --- cli/parameter.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cli/parameter.go b/cli/parameter.go index 0a0971635cba0..97c551ffa5a7f 100644 --- a/cli/parameter.go +++ b/cli/parameter.go @@ -164,7 +164,7 @@ This is useful for troubleshooting build issues.`, }, { Flag: "reason", - Description: `Sets the reason for the workspace build (cli, vscode_connection).`, + Description: `Sets the reason for the workspace build (cli, vscode_connection, jetbrains_connection).`, Value: serpent.EnumOf( &bf.reason, string(codersdk.BuildReasonCLI), From 6803acebadb3bf7d1b8f0a264e1b17200f8aa402 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Tue, 15 Jul 2025 12:51:03 +0000 Subject: [PATCH 08/12] enhance workspace build data and audit log with build reason display --- site/src/api/api.ts | 1 + .../WorkspaceBuildData/WorkspaceBuildData.tsx | 16 ++++++++++++++++ .../BuildAuditDescription.tsx | 3 ++- .../pages/AuditPage/AuditLogRow/AuditLogRow.tsx | 11 +++++++++-- site/src/utils/workspace.tsx | 13 +++++++++++++ 5 files changed, 41 insertions(+), 3 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index dd8d3d77998d2..31630ff109887 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1255,6 +1255,7 @@ class ApiMethods { template_version_id: templateVersionId, log_level: logLevel, rich_parameter_values: buildParameters, + reason: "dashboard", }); }; diff --git a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx index 57e1a35353f63..3b12e09d9e66e 100644 --- a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx @@ -1,12 +1,15 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; +import Tooltip from "@mui/material/Tooltip"; import Skeleton from "@mui/material/Skeleton"; import type { WorkspaceBuild } from "api/typesGenerated"; import { BuildIcon } from "components/BuildIcon/BuildIcon"; import { createDayString } from "utils/createDayString"; import { + getDisplayInitiatorBuildReason, getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceBuildStatus, } from "utils/workspace"; +import { InfoIcon } from "lucide-react"; export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { const theme = useTheme(); @@ -29,6 +32,9 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { textOverflow: "ellipsis", overflow: "hidden", whiteSpace: "nowrap", + display: "flex", + alignItems: "center", + gap: 4, }} > {build.transition}{" "} @@ -36,6 +42,16 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { {getDisplayWorkspaceBuildInitiatedBy(build)} + {getDisplayInitiatorBuildReason(build.reason) && build.transition === 'start' && ( + + ({ + color: theme.palette.info.light, + })} + className="size-icon-xs -mt-px" + /> + + )}
= ({ }) => { const workspaceName = auditLog.additional_fields?.workspace_name?.trim(); // workspaces can be started/stopped/deleted by a user, or kicked off automatically by Coder + const systemReasons = ["autostart", "autostop", "dormancy"]; const user = auditLog.additional_fields?.build_reason && - auditLog.additional_fields?.build_reason !== "initiator" + systemReasons.includes(auditLog.additional_fields?.build_reason) ? "Coder automatically" : auditLog.user ? auditLog.user.username.trim() diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index a123e83214775..c0ef1ec4c0c08 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -21,6 +21,7 @@ import { determineGroupDiff, determineIdPSyncMappingDiff, } from "./AuditLogDiff/auditUtils"; +import { getDisplayInitiatorBuildReason } from "utils/workspace"; const httpStatusColor = (httpStatus: number): ThemeRole => { // Treat server errors (500) as errors @@ -182,10 +183,10 @@ export const AuditLogRow: FC = ({
)} - {auditLog.additional_fields?.reason && ( + {getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason) && auditLog.action === 'start' && (

Reason:

-
{auditLog.additional_fields?.reason}
+
{getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason)}
)} @@ -219,6 +220,12 @@ export const AuditLogRow: FC = ({ )} + {getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason) && auditLog.action === 'start' && ( + + Reason: + {getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason)} + + )} )} diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index 77dfa62acea05..e6f58ff4caca1 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -78,11 +78,24 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( ): string | undefined => { switch (build.reason) { case "initiator": + case "dashboard": + case "cli": + case "ssh_connection": + case "vscode_connection": + case "jetbrains_connection": return build.initiator_name; case "autostart": case "autostop": case "dormancy": return "Coder"; + } + return undefined; +}; + +export const getDisplayInitiatorBuildReason = (buildReason: string): string | undefined => { + switch (buildReason) { + case "initiator": + return "API"; case "dashboard": return "Dashboard"; case "cli": From 2b5c4f25ee2d613d3a2d510d49584577341459fe Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Tue, 15 Jul 2025 13:02:02 +0000 Subject: [PATCH 09/12] Reformat code --- .../WorkspaceBuildData/WorkspaceBuildData.tsx | 25 ++++++------ .../AuditPage/AuditLogRow/AuditLogRow.tsx | 40 +++++++++++++------ site/src/utils/workspace.tsx | 4 +- 3 files changed, 43 insertions(+), 26 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx index 3b12e09d9e66e..77524eb157a3c 100644 --- a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx @@ -1,15 +1,15 @@ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import Tooltip from "@mui/material/Tooltip"; import Skeleton from "@mui/material/Skeleton"; +import Tooltip from "@mui/material/Tooltip"; import type { WorkspaceBuild } from "api/typesGenerated"; import { BuildIcon } from "components/BuildIcon/BuildIcon"; +import { InfoIcon } from "lucide-react"; import { createDayString } from "utils/createDayString"; import { getDisplayInitiatorBuildReason, getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceBuildStatus, } from "utils/workspace"; -import { InfoIcon } from "lucide-react"; export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { const theme = useTheme(); @@ -42,16 +42,17 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { {getDisplayWorkspaceBuildInitiatedBy(build)} - {getDisplayInitiatorBuildReason(build.reason) && build.transition === 'start' && ( - - ({ - color: theme.palette.info.light, - })} - className="size-icon-xs -mt-px" - /> - - )} + {getDisplayInitiatorBuildReason(build.reason) && + build.transition === "start" && ( + + ({ + color: theme.palette.info.light, + })} + className="size-icon-xs -mt-px" + /> + + )}
{ // Treat server errors (500) as errors @@ -183,12 +183,19 @@ export const AuditLogRow: FC = ({
)} - {getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason) && auditLog.action === 'start' && ( -
-

Reason:

-
{getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason)}
-
- )} + {getDisplayInitiatorBuildReason( + auditLog.additional_fields?.build_reason, + ) && + auditLog.action === "start" && ( +
+

Reason:

+
+ {getDisplayInitiatorBuildReason( + auditLog.additional_fields?.build_reason, + )} +
+
+ )} } > @@ -220,12 +227,19 @@ export const AuditLogRow: FC = ({ )} - {getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason) && auditLog.action === 'start' && ( - - Reason: - {getDisplayInitiatorBuildReason(auditLog.additional_fields?.build_reason)} - - )} + {getDisplayInitiatorBuildReason( + auditLog.additional_fields?.build_reason, + ) && + auditLog.action === "start" && ( + + Reason: + + {getDisplayInitiatorBuildReason( + auditLog.additional_fields?.build_reason, + )} + + + )} )} diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index e6f58ff4caca1..d2605922ba11c 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -92,7 +92,9 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( return undefined; }; -export const getDisplayInitiatorBuildReason = (buildReason: string): string | undefined => { +export const getDisplayInitiatorBuildReason = ( + buildReason: string, +): string | undefined => { switch (buildReason) { case "initiator": return "API"; From 3d33137a910ccd70c5bd8f35291a8a48ea221cbb Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Tue, 15 Jul 2025 13:16:25 +0000 Subject: [PATCH 10/12] Fix migration numbers --- ...son.down.sql => 000350_extend_workspace_build_reason.down.sql} | 0 ..._reason.up.sql => 000350_extend_workspace_build_reason.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000349_extend_workspace_build_reason.down.sql => 000350_extend_workspace_build_reason.down.sql} (100%) rename coderd/database/migrations/{000349_extend_workspace_build_reason.up.sql => 000350_extend_workspace_build_reason.up.sql} (100%) diff --git a/coderd/database/migrations/000349_extend_workspace_build_reason.down.sql b/coderd/database/migrations/000350_extend_workspace_build_reason.down.sql similarity index 100% rename from coderd/database/migrations/000349_extend_workspace_build_reason.down.sql rename to coderd/database/migrations/000350_extend_workspace_build_reason.down.sql diff --git a/coderd/database/migrations/000349_extend_workspace_build_reason.up.sql b/coderd/database/migrations/000350_extend_workspace_build_reason.up.sql similarity index 100% rename from coderd/database/migrations/000349_extend_workspace_build_reason.up.sql rename to coderd/database/migrations/000350_extend_workspace_build_reason.up.sql From da376fdbebcbdb11774ea4e19c41f3e1d55b1aa3 Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Wed, 16 Jul 2025 09:34:42 +0000 Subject: [PATCH 11/12] replace getDisplayInitiatorBuildReason with buildReasonLabels and systemBuildReasons --- .../WorkspaceBuildData/WorkspaceBuildData.tsx | 7 ++-- .../BuildAuditDescription.tsx | 4 +-- .../AuditPage/AuditLogRow/AuditLogRow.tsx | 24 ++++++-------- site/src/utils/workspace.tsx | 33 +++++++++---------- 4 files changed, 31 insertions(+), 37 deletions(-) diff --git a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx index 77524eb157a3c..b849b59caa8f3 100644 --- a/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx +++ b/site/src/modules/workspaces/WorkspaceBuildData/WorkspaceBuildData.tsx @@ -6,9 +6,10 @@ import { BuildIcon } from "components/BuildIcon/BuildIcon"; import { InfoIcon } from "lucide-react"; import { createDayString } from "utils/createDayString"; import { - getDisplayInitiatorBuildReason, + buildReasonLabels, getDisplayWorkspaceBuildInitiatedBy, getDisplayWorkspaceBuildStatus, + systemBuildReasons, } from "utils/workspace"; export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { @@ -42,9 +43,9 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => { {getDisplayWorkspaceBuildInitiatedBy(build)} - {getDisplayInitiatorBuildReason(build.reason) && + {!systemBuildReasons.includes(build.reason) && build.transition === "start" && ( - + ({ color: theme.palette.info.light, diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx index 6060aea38277a..354eb59713174 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogDescription/BuildAuditDescription.tsx @@ -2,6 +2,7 @@ import Link from "@mui/material/Link"; import type { AuditLog } from "api/typesGenerated"; import { type FC, useMemo } from "react"; import { Link as RouterLink } from "react-router-dom"; +import { systemBuildReasons } from "utils/workspace"; interface BuildAuditDescriptionProps { auditLog: AuditLog; @@ -12,10 +13,9 @@ export const BuildAuditDescription: FC = ({ }) => { const workspaceName = auditLog.additional_fields?.workspace_name?.trim(); // workspaces can be started/stopped/deleted by a user, or kicked off automatically by Coder - const systemReasons = ["autostart", "autostop", "dormancy"]; const user = auditLog.additional_fields?.build_reason && - systemReasons.includes(auditLog.additional_fields?.build_reason) + systemBuildReasons.includes(auditLog.additional_fields?.build_reason) ? "Coder automatically" : auditLog.user ? auditLog.user.username.trim() diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index 19dc7aa9ae61c..a4413cb4eb651 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -3,7 +3,7 @@ import Collapse from "@mui/material/Collapse"; import Link from "@mui/material/Link"; import TableCell from "@mui/material/TableCell"; import Tooltip from "@mui/material/Tooltip"; -import type { AuditLog } from "api/typesGenerated"; +import type { AuditLog, BuildReason } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { DropdownArrow } from "components/DropdownArrow/DropdownArrow"; import { Stack } from "components/Stack/Stack"; @@ -14,7 +14,7 @@ import { NetworkIcon } from "lucide-react"; import { type FC, useState } from "react"; import { Link as RouterLink } from "react-router-dom"; import userAgentParser from "ua-parser-js"; -import { getDisplayInitiatorBuildReason } from "utils/workspace"; +import { buildReasonLabels } from "utils/workspace"; import { AuditLogDescription } from "./AuditLogDescription/AuditLogDescription"; import { AuditLogDiff } from "./AuditLogDiff/AuditLogDiff"; import { @@ -167,16 +167,14 @@ export const AuditLogRow: FC = ({ )} - {getDisplayInitiatorBuildReason( - auditLog.additional_fields?.build_reason, - ) && + {auditLog.additional_fields?.build_reason && auditLog.action === "start" && (

Reason:

- {getDisplayInitiatorBuildReason( - auditLog.additional_fields?.build_reason, - )} + {buildReasonLabels[ + auditLog.additional_fields.build_reason as BuildReason + ]}
)} @@ -211,16 +209,14 @@ export const AuditLogRow: FC = ({ )} - {getDisplayInitiatorBuildReason( - auditLog.additional_fields?.build_reason, - ) && + {auditLog.additional_fields?.build_reason && auditLog.action === "start" && ( Reason: - {getDisplayInitiatorBuildReason( - auditLog.additional_fields?.build_reason, - )} + {buildReasonLabels[ + auditLog.additional_fields.build_reason as BuildReason + ]} )} diff --git a/site/src/utils/workspace.tsx b/site/src/utils/workspace.tsx index d2605922ba11c..49e885581497d 100644 --- a/site/src/utils/workspace.tsx +++ b/site/src/utils/workspace.tsx @@ -92,24 +92,21 @@ export const getDisplayWorkspaceBuildInitiatedBy = ( return undefined; }; -export const getDisplayInitiatorBuildReason = ( - buildReason: string, -): string | undefined => { - switch (buildReason) { - case "initiator": - return "API"; - case "dashboard": - return "Dashboard"; - case "cli": - return "CLI"; - case "ssh_connection": - return "SSH Connection"; - case "vscode_connection": - return "VSCode Connection"; - case "jetbrains_connection": - return "JetBrains Connection"; - } - return undefined; +export const systemBuildReasons = ["autostart", "autostop", "dormancy"]; + +export const buildReasonLabels: Record = { + // User build reasons + initiator: "API", + dashboard: "Dashboard", + cli: "CLI", + ssh_connection: "SSH Connection", + vscode_connection: "VSCode Connection", + jetbrains_connection: "JetBrains Connection", + + // System build reasons + autostart: "Autostart", + autostop: "Autostop", + dormancy: "Dormancy", }; const getWorkspaceBuildDurationInSeconds = ( From cc202936ab88fb9b678fcc737ceae80ddcabc6eb Mon Sep 17 00:00:00 2001 From: Kacper Sawicki Date: Wed, 16 Jul 2025 09:35:08 +0000 Subject: [PATCH 12/12] Reformat --- .../AuditPage/AuditLogRow/AuditLogRow.tsx | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx index a4413cb4eb651..cccdcdf5e6e49 100644 --- a/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx +++ b/site/src/pages/AuditPage/AuditLogRow/AuditLogRow.tsx @@ -172,9 +172,12 @@ export const AuditLogRow: FC = ({

Reason:

- {buildReasonLabels[ - auditLog.additional_fields.build_reason as BuildReason - ]} + { + buildReasonLabels[ + auditLog.additional_fields + .build_reason as BuildReason + ] + }
)} @@ -214,9 +217,12 @@ export const AuditLogRow: FC = ({ Reason: - {buildReasonLabels[ - auditLog.additional_fields.build_reason as BuildReason - ]} + { + buildReasonLabels[ + auditLog.additional_fields + .build_reason as BuildReason + ] + } )}