fix: run stop build before starting a workspace with a failed start (#22925)

This commit is contained in:
Jon Ayers
2026-03-18 14:58:20 -05:00
committed by GitHub
parent 147d627505
commit eba7d943a0
5 changed files with 146 additions and 14 deletions
+29
View File
@@ -1483,6 +1483,35 @@ class ApiMethods {
await this.waitForBuild(startBuild);
};
/**
* Starts a workspace, but if the last build was a failed start,
* stops it first to give it a clean slate and the best chance
* of success.
*/
retryWorkspace = async (
workspace: TypesGen.Workspace,
templateVersionId: string,
logLevel?: TypesGen.ProvisionerLogLevel,
buildParameters?: TypesGen.WorkspaceBuildParameter[],
): Promise<TypesGen.WorkspaceBuild> => {
if (
workspace.latest_build.status === "failed" &&
workspace.latest_build.transition === "start"
) {
const stopBuild = await this.stopWorkspace(workspace.id, logLevel);
const awaitedStop = await this.waitForBuild(stopBuild);
if (awaitedStop?.status === "canceled") {
throw new Error("Cleanup stop was canceled");
}
}
return this.startWorkspace(
workspace.id,
templateVersionId,
logLevel,
buildParameters,
);
};
cancelTemplateVersionBuild = async (
templateVersionId: string,
): Promise<TypesGen.Response> => {
@@ -421,7 +421,9 @@ describe("WorkspacePage", () => {
const retryDebugButtonRe = /^Debug$/i;
describe("Retries a failed 'Start' transition", () => {
const mockStart = jest.spyOn(API, "startWorkspace");
const mockRetry = jest
.spyOn(API, "retryWorkspace")
.mockResolvedValue(MockWorkspaceBuild);
const failedStart: Workspace = {
...MockFailedWorkspace,
latest_build: {
@@ -431,10 +433,10 @@ describe("WorkspacePage", () => {
};
test("Retry with no debug", async () => {
await testButton(failedStart, retryButtonRe, mockStart);
await testButton(failedStart, retryButtonRe, mockRetry);
expect(mockStart).toBeCalledWith(
failedStart.id,
expect(mockRetry).toBeCalledWith(
failedStart,
failedStart.latest_build.template_version_id,
undefined,
undefined,
@@ -442,10 +444,10 @@ describe("WorkspacePage", () => {
});
test("Retry with debug logs", async () => {
await testButton(failedStart, retryDebugButtonRe, mockStart);
await testButton(failedStart, retryDebugButtonRe, mockRetry);
expect(mockStart).toBeCalledWith(
failedStart.id,
expect(mockRetry).toBeCalledWith(
failedStart,
failedStart.latest_build.template_version_id,
"debug",
undefined,
@@ -522,7 +524,9 @@ describe("WorkspacePage", () => {
return HttpResponse.json([parameter]);
}),
);
const startWorkspaceSpy = jest.spyOn(API, "startWorkspace");
const retryWorkspaceSpy = jest
.spyOn(API, "retryWorkspace")
.mockResolvedValue(MockWorkspaceBuild);
await renderWorkspacePage(workspace);
const retryWithBuildParametersButton = await screen.findByRole("button", {
@@ -539,8 +543,8 @@ describe("WorkspacePage", () => {
await user.click(submitButton);
await waitFor(() => {
expect(startWorkspaceSpy).toBeCalledWith(
workspace.id,
expect(retryWorkspaceSpy).toBeCalledWith(
workspace,
workspace.latest_build.template_version_id,
undefined,
[{ name: parameter.name, value: "some-value" }],
@@ -568,7 +572,9 @@ describe("WorkspacePage", () => {
return HttpResponse.json([parameter]);
}),
);
const startWorkspaceSpy = jest.spyOn(API, "startWorkspace");
const retryWorkspaceSpy = jest
.spyOn(API, "retryWorkspace")
.mockResolvedValue(MockWorkspaceBuild);
await renderWorkspacePage(workspace);
const retryWithBuildParametersButton = await screen.findByRole("button", {
@@ -585,8 +591,8 @@ describe("WorkspacePage", () => {
await user.click(submitButton);
await waitFor(() => {
expect(startWorkspaceSpy).toBeCalledWith(
workspace.id,
expect(retryWorkspaceSpy).toBeCalledWith(
workspace,
workspace.latest_build.template_version_id,
"debug",
[{ name: parameter.name, value: "some-value" }],
@@ -159,6 +159,28 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
},
});
// Retry workspace (stop-before-start for failed workspaces)
const retryWorkspaceMutation = useMutation({
...startWorkspace(workspace, queryClient),
mutationFn: ({
buildParameters,
logLevel,
}: {
buildParameters?: TypesGen.WorkspaceBuildParameter[];
logLevel?: TypesGen.ProvisionerLogLevel;
}) => {
return API.retryWorkspace(
workspace,
workspace.latest_build.template_version_id,
logLevel,
buildParameters,
);
},
onError: (error: unknown) => {
handleError(error);
},
});
// Toggle workspace favorite
const toggleFavoriteMutation = useMutation({
...toggleFavorite(workspace, queryClient),
@@ -246,7 +268,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
} else {
switch (workspace.latest_build.transition) {
case "start":
startWorkspaceMutation.mutate({
retryWorkspaceMutation.mutate({
logLevel,
buildParameters,
});