mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-23 03:27:27 +09:00
fix: run publish checks in background (#6476)
## Summary - Start dynamic and static publish diagnostics in the background, without waiting for them before sending the publish request. - Error findings, failed or hanging checks, and failures while reporting them cannot block or delay publishing. - Keep the standalone Validate action's reporting behavior unchanged. - If diagnostics fail, the dialog explains that the publish request was sent without waiting for them. - The publish operation remains authoritative for authorization and publish-job failures. ## Verification - `pnpm --filter @webstudio-is/builder test -- app/builder/features/publish/publish-preflight.test.ts app/builder/features/publish/publish-error.test.ts` (8 tests passed) - `pnpm --filter @webstudio-is/builder typecheck` - `pnpm exec oxfmt` on changed files - `git diff --check`
This commit is contained in:
1 parent
a6a9049399
commit
e700a24210
4 files changed
+147
-40
No files matched your search
@@ -8,7 +8,7 @@ import {
|
||||
import { formatAssetFolderPath } from "~/builder/shared/asset-manager/asset-folder-utils";
|
||||
|
||||
export const publishValidationTimeoutMessage =
|
||||
"Publish validation timed out. Publishing was not started. Please try again.";
|
||||
"Publish validation timed out. Try again to view the diagnostics.";
|
||||
|
||||
export const getPublishValidationErrorMessage = (
|
||||
error: unknown,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { runPublishAfterBestEffortChecks } from "./publish-preflight";
|
||||
|
||||
describe("runPublishAfterBestEffortChecks", () => {
|
||||
test.each([
|
||||
["error findings", async () => ({ passed: false, findings: ["error"] })],
|
||||
[
|
||||
"a failed check",
|
||||
async () => {
|
||||
throw new Error("check failed");
|
||||
},
|
||||
],
|
||||
])("publishes after %s", async (_label, checks) => {
|
||||
const publish = vi.fn().mockResolvedValue("publish started");
|
||||
|
||||
const result = await runPublishAfterBestEffortChecks({
|
||||
checks,
|
||||
onCheckFailure: vi.fn(),
|
||||
publish,
|
||||
});
|
||||
|
||||
expect(result).toBe("publish started");
|
||||
expect(publish).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
test("starts publishing without waiting for checks to finish", async () => {
|
||||
const order: string[] = [];
|
||||
const checks = vi.fn(
|
||||
() =>
|
||||
new Promise<void>(() => {
|
||||
order.push("checks started");
|
||||
})
|
||||
);
|
||||
const publish = vi.fn(async () => {
|
||||
order.push("publish started");
|
||||
return "publish started";
|
||||
});
|
||||
|
||||
await expect(
|
||||
runPublishAfterBestEffortChecks({
|
||||
checks,
|
||||
onCheckFailure: vi.fn(),
|
||||
publish,
|
||||
})
|
||||
).resolves.toBe("publish started");
|
||||
|
||||
expect(order).toEqual(["publish started", "checks started"]);
|
||||
});
|
||||
|
||||
test("publishes even if reporting a check failure throws", async () => {
|
||||
const publish = vi.fn().mockResolvedValue("publish started");
|
||||
const consoleError = vi.spyOn(console, "error").mockImplementation(() => {
|
||||
throw new Error("logging failed");
|
||||
});
|
||||
let finishReporting!: () => void;
|
||||
const reportFinished = new Promise<void>((resolve) => {
|
||||
finishReporting = resolve;
|
||||
});
|
||||
|
||||
const result = await runPublishAfterBestEffortChecks({
|
||||
checks: async () => {
|
||||
throw new Error("check failed");
|
||||
},
|
||||
onCheckFailure: () => {
|
||||
finishReporting();
|
||||
throw new Error("report failed");
|
||||
},
|
||||
publish,
|
||||
});
|
||||
await reportFinished;
|
||||
|
||||
expect(result).toBe("publish started");
|
||||
expect(publish).toHaveBeenCalledOnce();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
export const runPublishAfterBestEffortChecks = <T>({
|
||||
checks,
|
||||
onCheckFailure,
|
||||
publish,
|
||||
}: {
|
||||
checks: () => Promise<unknown>;
|
||||
onCheckFailure: (error: unknown) => void;
|
||||
publish: () => Promise<T>;
|
||||
}): Promise<T> => {
|
||||
const reportFailure = (error: unknown) => {
|
||||
try {
|
||||
onCheckFailure(error);
|
||||
} catch (reportError) {
|
||||
try {
|
||||
console.error("Could not report publish diagnostics", reportError);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
void Promise.resolve().then(checks).catch(reportFailure);
|
||||
return publish();
|
||||
};
|
||||
@@ -129,6 +129,7 @@ import {
|
||||
} from "./publish-actions";
|
||||
import { flushExternalContentProject } from "~/shared/external-content-roots";
|
||||
import { getPublishValidationErrorMessage } from "./publish-error";
|
||||
import { runPublishAfterBestEffortChecks } from "./publish-preflight";
|
||||
|
||||
const PrePublishAuditMessage = ({
|
||||
finding,
|
||||
@@ -238,14 +239,7 @@ const getPrePublishAuditFindings = (): PublishValidationFinding[] => {
|
||||
});
|
||||
};
|
||||
|
||||
const runPublishValidation = async (
|
||||
projectId: Project["id"],
|
||||
checkPermission = false
|
||||
) => {
|
||||
if (checkPermission) {
|
||||
await nativeClient.build.checkProjectBuildPermission.query({ projectId });
|
||||
}
|
||||
await flushExternalContentProject({ projectId });
|
||||
const runPublishDiagnostics = async (projectId: Project["id"]) => {
|
||||
const auditFindings = getPrePublishAuditFindings();
|
||||
const diagnostics =
|
||||
await nativeClient.build.contentDatabasePublishDiagnostics.query({
|
||||
@@ -269,14 +263,29 @@ const runPublishValidation = async (
|
||||
};
|
||||
};
|
||||
|
||||
const runPublishValidation = async (
|
||||
projectId: Project["id"],
|
||||
checkPermission = false
|
||||
) => {
|
||||
if (checkPermission) {
|
||||
await nativeClient.build.checkProjectBuildPermission.query({ projectId });
|
||||
}
|
||||
await flushExternalContentProject({ projectId });
|
||||
return runPublishDiagnostics(projectId);
|
||||
};
|
||||
|
||||
const reportPublishValidationFailure = (
|
||||
error: unknown,
|
||||
setFindings: (findings: PublishValidationFinding[]) => void
|
||||
setFindings: (findings: PublishValidationFinding[]) => void,
|
||||
publishingContinues = false
|
||||
) => {
|
||||
const message = getPublishValidationErrorMessage(error, {
|
||||
const diagnosticMessage = getPublishValidationErrorMessage(error, {
|
||||
assets: $assets.get(),
|
||||
assetFolders: $assetFolders.get(),
|
||||
});
|
||||
const message = publishingContinues
|
||||
? `${diagnosticMessage}\n\nThe publish request was sent without waiting for these diagnostics.`
|
||||
: diagnosticMessage;
|
||||
if ($publishDialog.get() === "none") {
|
||||
toast.error(message);
|
||||
return;
|
||||
@@ -298,7 +307,9 @@ const reportPublishValidationFailure = (
|
||||
setFindings([
|
||||
{
|
||||
severity: "error",
|
||||
title: "Unable to complete publish validation",
|
||||
title: publishingContinues
|
||||
? "Publish checks couldn’t complete"
|
||||
: "Unable to complete publish validation",
|
||||
details,
|
||||
reportText: `ERROR: Unable to complete publish validation\n${message}`,
|
||||
},
|
||||
@@ -810,18 +821,15 @@ const Publish = ({
|
||||
|
||||
startTransition(async () => {
|
||||
setIsPublishing(true);
|
||||
|
||||
try {
|
||||
const checks = await runPublishValidation(project.id, true);
|
||||
setPublishFindings(checks.findings);
|
||||
if (checks.passed === false) {
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
reportPublishValidationFailure(error, setPublishFindings);
|
||||
return;
|
||||
}
|
||||
await publish(domains);
|
||||
await runPublishAfterBestEffortChecks({
|
||||
checks: async () => {
|
||||
const checks = await runPublishDiagnostics(project.id);
|
||||
setPublishFindings(checks.findings);
|
||||
},
|
||||
onCheckFailure: (error) =>
|
||||
reportPublishValidationFailure(error, setPublishFindings, true),
|
||||
publish: () => publish(domains),
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -947,22 +955,23 @@ const PublishStatic = ({
|
||||
startTransition(async () => {
|
||||
try {
|
||||
setIsPendingOptimistic(true);
|
||||
let checks: Awaited<ReturnType<typeof runPublishValidation>>;
|
||||
try {
|
||||
checks = await runPublishValidation(projectId);
|
||||
} catch (error) {
|
||||
reportPublishValidationFailure(error, setPublishFindings);
|
||||
return;
|
||||
}
|
||||
setPublishFindings(checks.findings);
|
||||
if (checks.passed === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await nativeClient.domain.publish.mutate({
|
||||
projectId,
|
||||
destination: "static",
|
||||
templates: [...templates],
|
||||
const result = await runPublishAfterBestEffortChecks({
|
||||
checks: async () => {
|
||||
const checks = await runPublishDiagnostics(projectId);
|
||||
setPublishFindings(checks.findings);
|
||||
},
|
||||
onCheckFailure: (error) =>
|
||||
reportPublishValidationFailure(
|
||||
error,
|
||||
setPublishFindings,
|
||||
true
|
||||
),
|
||||
publish: () =>
|
||||
nativeClient.domain.publish.mutate({
|
||||
projectId,
|
||||
destination: "static",
|
||||
templates: [...templates],
|
||||
}),
|
||||
});
|
||||
|
||||
if (result.success === false) {
|
||||
|
||||
Reference in new issue
Block a user