feat!: add ability to cancel pending workspace build (#18713)

Closes #17791 

This PR adds ability to cancel workspace builds that are in "pending"
status.

Breaking changes:
- CancelWorkspaceBuild method in codersdk now accepts an optional
request parameter

API:
- Added `expect_status` query parameter to the cancel workspace build
endpoint
- This parameter ensures the job hasn't changed state before canceling
- API returns `412 Precondition Failed` if the job is not in the
expected status
- Valid values: `running` or `pending`
- Wrapped the entire cancel method in a database transaction

UI:
- Added confirmation dialog to the `Cancel` button, since it's a
destructive operation

![image](https://github.com/user-attachments/assets/437aa5f4-5669-45b6-82a0-e46f277114bf)

![image](https://github.com/user-attachments/assets/423b5cb1-a4fb-4a10-933b-c1c73f4b838c)


- Enabled cancel action for pending workspaces (`expect_status=pending`
is sent if workspace is in pending status)

![image](https://github.com/user-attachments/assets/32d35ff1-12e6-4f7b-9f6c-fde9da9de6cf)

---------

Co-authored-by: Dean Sheather <dean@deansheather.com>
This commit is contained in:
Kacper Sawicki
2025-07-08 11:02:58 +02:00
committed by GitHub
co-authored by Dean Sheather
parent 2f42b64182
commit 8202514ce0
20 changed files with 556 additions and 93 deletions
+4 -4
View File
@@ -164,7 +164,7 @@ func TestTools(t *testing.T) {
// Important: cancel the build. We don't run any provisioners, so this
// will remain in the 'pending' state indefinitely.
require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID))
require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID, codersdk.CancelWorkspaceBuildParams{}))
})
t.Run("Start", func(t *testing.T) {
@@ -184,7 +184,7 @@ func TestTools(t *testing.T) {
// Important: cancel the build. We don't run any provisioners, so this
// will remain in the 'pending' state indefinitely.
require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID))
require.NoError(t, client.CancelWorkspaceBuild(ctx, result.ID, codersdk.CancelWorkspaceBuildParams{}))
})
t.Run("TemplateVersionChange", func(t *testing.T) {
@@ -216,7 +216,7 @@ func TestTools(t *testing.T) {
require.Equal(t, r.Workspace.ID.String(), updateBuild.WorkspaceID.String())
require.Equal(t, newVersion.TemplateVersion.ID.String(), updateBuild.TemplateVersionID.String())
// Cancel the build so it doesn't remain in the 'pending' state indefinitely.
require.NoError(t, client.CancelWorkspaceBuild(ctx, updateBuild.ID))
require.NoError(t, client.CancelWorkspaceBuild(ctx, updateBuild.ID, codersdk.CancelWorkspaceBuildParams{}))
// Roll back to the original version
rollbackBuild, err := testTool(t, toolsdk.CreateWorkspaceBuild, tb, toolsdk.CreateWorkspaceBuildArgs{
@@ -229,7 +229,7 @@ func TestTools(t *testing.T) {
require.Equal(t, r.Workspace.ID.String(), rollbackBuild.WorkspaceID.String())
require.Equal(t, originalVersionID.String(), rollbackBuild.TemplateVersionID.String())
// Cancel the build so it doesn't remain in the 'pending' state indefinitely.
require.NoError(t, client.CancelWorkspaceBuild(ctx, rollbackBuild.ID))
require.NoError(t, client.CancelWorkspaceBuild(ctx, rollbackBuild.ID, codersdk.CancelWorkspaceBuildParams{}))
})
})
+22 -2
View File
@@ -123,9 +123,29 @@ func (c *Client) WorkspaceBuild(ctx context.Context, id uuid.UUID) (WorkspaceBui
return workspaceBuild, json.NewDecoder(res.Body).Decode(&workspaceBuild)
}
type CancelWorkspaceBuildStatus string
const (
CancelWorkspaceBuildStatusRunning CancelWorkspaceBuildStatus = "running"
CancelWorkspaceBuildStatusPending CancelWorkspaceBuildStatus = "pending"
)
type CancelWorkspaceBuildParams struct {
// ExpectStatus ensures the build is in the expected status before canceling.
ExpectStatus CancelWorkspaceBuildStatus `json:"expect_status,omitempty"`
}
func (c *CancelWorkspaceBuildParams) asRequestOption() RequestOption {
return func(r *http.Request) {
q := r.URL.Query()
q.Set("expect_status", string(c.ExpectStatus))
r.URL.RawQuery = q.Encode()
}
}
// CancelWorkspaceBuild marks a workspace build job as canceled.
func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/workspacebuilds/%s/cancel", id), nil)
func (c *Client) CancelWorkspaceBuild(ctx context.Context, id uuid.UUID, req CancelWorkspaceBuildParams) error {
res, err := c.Request(ctx, http.MethodPatch, fmt.Sprintf("/api/v2/workspacebuilds/%s/cancel", id), nil, req.asRequestOption())
if err != nil {
return err
}