fix(coderd): use BuildReasonTaskAutoPause for task workspaces (#22126)

Relates to https://github.com/coder/internal/issues/1252

When a workspace with a TaskID hits its deadline, use
BuildReasonTaskAutoPause instead of BuildReasonAutostop. This allows
downstream systems to distinguish between regular autostop and task
workspace pauses.

Created by Mux using Opus 4.5.
This commit is contained in:
Cian Johnston
2026-02-17 15:11:04 +00:00
committed by GitHub
parent 90c11f3386
commit f8eea54e97
11 changed files with 171 additions and 9 deletions
+8 -2
View File
@@ -13553,7 +13553,10 @@ const docTemplate = `{
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
"jetbrains_connection",
"task_auto_pause",
"task_manual_pause",
"task_resume"
],
"x-enum-varnames": [
"BuildReasonInitiator",
@@ -13564,7 +13567,10 @@ const docTemplate = `{
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
"BuildReasonJetbrainsConnection",
"BuildReasonTaskAutoPause",
"BuildReasonTaskManualPause",
"BuildReasonTaskResume"
]
},
"codersdk.CORSBehavior": {
+8 -2
View File
@@ -12148,7 +12148,10 @@
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
"jetbrains_connection",
"task_auto_pause",
"task_manual_pause",
"task_resume"
],
"x-enum-varnames": [
"BuildReasonInitiator",
@@ -12159,7 +12162,10 @@
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
"BuildReasonJetbrainsConnection",
"BuildReasonTaskAutoPause",
"BuildReasonTaskManualPause",
"BuildReasonTaskResume"
]
},
"codersdk.CORSBehavior": {
+8
View File
@@ -525,10 +525,18 @@ func getNextTransition(
) {
switch {
case isEligibleForAutostop(user, ws, latestBuild, latestJob, currentTick):
// Use task-specific reason for AI task workspaces.
if ws.TaskID.Valid {
return database.WorkspaceTransitionStop, database.BuildReasonTaskAutoPause, nil
}
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick):
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick):
// Use task-specific reason for AI task workspaces.
if ws.TaskID.Valid {
return database.WorkspaceTransitionStop, database.BuildReasonTaskAutoPause, nil
}
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
case isEligibleForDormantStop(ws, templateSchedule, currentTick):
// Only stop started workspaces.
@@ -5,12 +5,113 @@ import (
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/schedule"
)
func Test_getNextTransition_TaskAutoPause(t *testing.T) {
t.Parallel()
// Set up a workspace that is eligible for autostop (past deadline).
now := time.Now()
pastDeadline := now.Add(-time.Hour)
okUser := database.User{Status: database.UserStatusActive}
okBuild := database.WorkspaceBuild{
Transition: database.WorkspaceTransitionStart,
Deadline: pastDeadline,
}
okJob := database.ProvisionerJob{
JobStatus: database.ProvisionerJobStatusSucceeded,
}
okTemplateSchedule := schedule.TemplateScheduleOptions{}
// Failed build setup for failedstop tests.
failedBuild := database.WorkspaceBuild{
Transition: database.WorkspaceTransitionStart,
}
failedJob := database.ProvisionerJob{
JobStatus: database.ProvisionerJobStatusFailed,
CompletedAt: sql.NullTime{Time: now.Add(-time.Hour), Valid: true},
}
failedTemplateSchedule := schedule.TemplateScheduleOptions{
FailureTTL: time.Minute, // TTL already elapsed since job completed an hour ago.
}
testCases := []struct {
Name string
Workspace database.Workspace
Build database.WorkspaceBuild
Job database.ProvisionerJob
TemplateSchedule schedule.TemplateScheduleOptions
ExpectedReason database.BuildReason
}{
{
Name: "RegularWorkspace_Autostop",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
},
Build: okBuild,
Job: okJob,
TemplateSchedule: okTemplateSchedule,
ExpectedReason: database.BuildReasonAutostop,
},
{
Name: "TaskWorkspace_Autostop_UsesTaskAutoPause",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
TaskID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
},
Build: okBuild,
Job: okJob,
TemplateSchedule: okTemplateSchedule,
ExpectedReason: database.BuildReasonTaskAutoPause,
},
{
Name: "RegularWorkspace_FailedStop",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
},
Build: failedBuild,
Job: failedJob,
TemplateSchedule: failedTemplateSchedule,
ExpectedReason: database.BuildReasonAutostop,
},
{
Name: "TaskWorkspace_FailedStop_UsesTaskAutoPause",
Workspace: database.Workspace{
DormantAt: sql.NullTime{Valid: false},
TaskID: uuid.NullUUID{UUID: uuid.New(), Valid: true},
},
Build: failedBuild,
Job: failedJob,
TemplateSchedule: failedTemplateSchedule,
ExpectedReason: database.BuildReasonTaskAutoPause,
},
}
for _, tc := range testCases {
t.Run(tc.Name, func(t *testing.T) {
t.Parallel()
transition, reason, err := getNextTransition(
okUser,
tc.Workspace,
tc.Build,
tc.Job,
tc.TemplateSchedule,
now,
)
require.NoError(t, err)
require.Equal(t, database.WorkspaceTransitionStop, transition)
require.Equal(t, tc.ExpectedReason, reason)
})
}
}
func Test_isEligibleForAutostart(t *testing.T) {
t.Parallel()
@@ -2019,5 +2019,11 @@ func TestExecutorTaskWorkspace(t *testing.T) {
assert.Contains(t, stats.Transitions, workspace.ID, "task workspace should be in transitions")
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID], "should autostop the workspace")
require.Empty(t, stats.Errors, "should have no errors when managing task workspaces")
// Then: The build reason should be TaskAutoPause (not regular Autostop)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
assert.Equal(t, codersdk.BuildReasonTaskAutoPause, workspace.LatestBuild.Reason, "task workspace should use TaskAutoPause build reason")
})
}
+9
View File
@@ -59,6 +59,15 @@ const (
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"
// BuildReasonTaskAutoPause "task_auto_pause" is used when a build to stop
// a task workspace is triggered by the lifecycle executor.
BuildReasonTaskAutoPause BuildReason = "task_auto_pause"
// BuildReasonTaskManualPause "task_manual_pause" is used when a build to
// stop a task workspace is triggered by a user.
BuildReasonTaskManualPause BuildReason = "task_manual_pause"
// BuildReasonTaskResume "task_resume" is used when a build to
// start a task workspace is triggered by a user.
BuildReasonTaskResume BuildReason = "task_resume"
)
// WorkspaceBuild is an at-point representation of a workspace state.
+3 -3
View File
@@ -1511,9 +1511,9 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
| Value(s) |
|-------------------------------------------------------------------------------------------------------------------------------------|
| `autostart`, `autostop`, `cli`, `dashboard`, `dormancy`, `initiator`, `jetbrains_connection`, `ssh_connection`, `vscode_connection` |
| Value(s) |
|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `autostart`, `autostop`, `cli`, `dashboard`, `dormancy`, `initiator`, `jetbrains_connection`, `ssh_connection`, `task_auto_pause`, `task_manual_pause`, `task_resume`, `vscode_connection` |
## codersdk.CORSBehavior
+6
View File
@@ -963,6 +963,9 @@ export type BuildReason =
| "initiator"
| "jetbrains_connection"
| "ssh_connection"
| "task_auto_pause"
| "task_manual_pause"
| "task_resume"
| "vscode_connection";
export const BuildReasons: BuildReason[] = [
@@ -974,6 +977,9 @@ export const BuildReasons: BuildReason[] = [
"initiator",
"jetbrains_connection",
"ssh_connection",
"task_auto_pause",
"task_manual_pause",
"task_resume",
"vscode_connection",
];
+1 -1
View File
@@ -220,7 +220,7 @@ export const TaskPausedTimeout: Story = {
latest_build: {
...MockWorkspaceBuildStop,
status: "stopped",
reason: "autostop",
reason: "task_auto_pause",
},
});
},
+7
View File
@@ -93,6 +93,13 @@ describe("util > workspace", () => {
},
"Coder",
],
[
{
...Mocks.MockWorkspaceBuild,
reason: "task_auto_pause",
},
"Coder",
],
])(
"getDisplayWorkspaceBuildInitiatedBy(%p) returns %p",
(build, initiatedBy) => {
+14 -1
View File
@@ -87,16 +87,26 @@ export const getDisplayWorkspaceBuildInitiatedBy = (
case "ssh_connection":
case "vscode_connection":
case "jetbrains_connection":
case "task_manual_pause":
case "task_resume":
return build.initiator_name;
case "autostart":
case "autostop":
case "dormancy":
case "task_auto_pause":
return "Coder";
}
return undefined;
};
export const systemBuildReasons = ["autostart", "autostop", "dormancy"];
export const systemBuildReasons = [
"autostart",
"autostop",
"dormancy",
"task_auto_pause",
"task_manual_pause",
"task_resume",
];
export const buildReasonLabels: Record<TypesGen.BuildReason, string> = {
// User build reasons
@@ -111,6 +121,9 @@ export const buildReasonLabels: Record<TypesGen.BuildReason, string> = {
autostart: "Autostart",
autostop: "Autostop",
dormancy: "Dormancy",
task_auto_pause: "Task Auto-Pause",
task_manual_pause: "Task Manual Pause",
task_resume: "Task Resume",
};
const getWorkspaceBuildDurationInSeconds = (