mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: run stop build before starting a workspace with a failed start (#22925)
This commit is contained in:
@@ -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,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user