mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix(site): show startup script failure message without restart suggestion (#24449)
When a workspace agent's startup script fails, restarting the workspace will not resolve the issue since the script will keep failing. Previously all unhealthy workspaces showed the same generic notification with a Restart button regardless of cause. Now, when every failing agent has `lifecycle_state=start_error`, the workspace-level notification shows "A startup script has failed" and guides the user to contact their template admin instead of offering a Restart action. > Code written by Claude 🤖 reviewed by yours truly --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
ef2b3a7263
commit
b4eb0e20e5
@@ -98,6 +98,12 @@ const installScriptLogSource: WorkspaceAgentLogSource = {
|
||||
display_name: "Install Script",
|
||||
};
|
||||
|
||||
const startupScriptLogSource: WorkspaceAgentLogSource = {
|
||||
...M.MockWorkspaceAgentLogSource,
|
||||
id: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
display_name: "Startup Script",
|
||||
};
|
||||
|
||||
const tabbedLogs = [
|
||||
{
|
||||
id: 100,
|
||||
@@ -229,6 +235,61 @@ export const StartError: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const StartErrorWithTimings: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const scriptTab = await canvas.findByRole("tab", {
|
||||
name: "Startup Script",
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(scriptTab).toHaveAttribute("data-state", "active"),
|
||||
);
|
||||
},
|
||||
args: {
|
||||
agent: {
|
||||
...M.MockWorkspaceAgentStartError,
|
||||
logs_length: 2,
|
||||
log_sources: [startupScriptLogSource],
|
||||
},
|
||||
agentScriptTimings: [
|
||||
{
|
||||
display_name: "Startup Script",
|
||||
exit_code: 1,
|
||||
stage: "start",
|
||||
status: "exit_failure",
|
||||
started_at: "2021-05-05T00:00:00.000Z",
|
||||
ended_at: "2021-05-05T00:00:01.000Z",
|
||||
workspace_agent_id: M.MockWorkspaceAgentStartError.id,
|
||||
workspace_agent_name: M.MockWorkspaceAgentStartError.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
parameters: {
|
||||
webSocket: [
|
||||
{
|
||||
event: "message",
|
||||
data: JSON.stringify([
|
||||
{
|
||||
id: 200,
|
||||
level: "info",
|
||||
output: "startup: preparing workspace",
|
||||
source_id: M.MockWorkspaceAgentLogSource.id,
|
||||
created_at: fixedLogTimestamp,
|
||||
},
|
||||
{
|
||||
id: 201,
|
||||
level: "error",
|
||||
output: "startup script: command not found",
|
||||
source_id: startupScriptLogSource.id,
|
||||
created_at: fixedLogTimestamp,
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const ShuttingDown: Story = {
|
||||
args: {
|
||||
agent: M.MockWorkspaceAgentShuttingDown,
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Link as RouterLink } from "react-router";
|
||||
import AutoSizer from "react-virtualized-auto-sizer";
|
||||
import type { FixedSizeList as List, ListOnScrollProps } from "react-window";
|
||||
import type {
|
||||
AgentScriptTiming,
|
||||
Template,
|
||||
Workspace,
|
||||
WorkspaceAgent,
|
||||
@@ -47,8 +48,14 @@ import { useKebabMenu } from "#/components/Tabs/utils/useKebabMenu";
|
||||
import { useProxy } from "#/contexts/ProxyContext";
|
||||
import { useClipboard } from "#/hooks/useClipboard";
|
||||
import { useFeatureVisibility } from "#/modules/dashboard/useFeatureVisibility";
|
||||
import { getAgentHealthIssues } from "#/modules/workspaces/health";
|
||||
import { AgentAlert } from "#/pages/WorkspacePage/AgentAlert";
|
||||
import {
|
||||
agentScriptMessages,
|
||||
getAgentHealthIssues,
|
||||
} from "#/modules/workspaces/health";
|
||||
import {
|
||||
AgentAlert,
|
||||
StartScriptFailureDetail,
|
||||
} from "#/pages/WorkspacePage/AgentAlert";
|
||||
import { AppStatuses } from "#/pages/WorkspacePage/AppStatuses";
|
||||
import { cn } from "#/utils/cn";
|
||||
import { AgentApps, organizeAgentApps } from "./AgentApps/AgentApps";
|
||||
@@ -75,6 +82,7 @@ interface AgentRowProps {
|
||||
workspace: Workspace;
|
||||
template: Template;
|
||||
initialMetadata?: WorkspaceAgentMetadata[];
|
||||
agentScriptTimings?: readonly AgentScriptTiming[];
|
||||
onUpdateAgent: () => void;
|
||||
}
|
||||
|
||||
@@ -124,6 +132,7 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
template,
|
||||
onUpdateAgent,
|
||||
initialMetadata,
|
||||
agentScriptTimings,
|
||||
}) => {
|
||||
const { browser_only, workspace_external_agent } = useFeatureVisibility();
|
||||
const appSections = organizeAgentApps(agent.apps);
|
||||
@@ -141,6 +150,12 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
const hasStartupFeatures = Boolean(agent.logs_length);
|
||||
const healthIssues = getAgentHealthIssues(agent);
|
||||
const hasAgentIssues = healthIssues.length > 0;
|
||||
const failedStartTimings = agentScriptTimings?.filter(
|
||||
(t) =>
|
||||
t.workspace_agent_id === agent.id &&
|
||||
t.stage === "start" &&
|
||||
t.exit_code !== 0,
|
||||
);
|
||||
const { proxy } = useProxy();
|
||||
const [showLogs, setShowLogs] = useState(
|
||||
(["starting", "start_timeout"].includes(agent.lifecycle_state) ||
|
||||
@@ -220,7 +235,14 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
agent,
|
||||
Boolean(hasDevcontainerErrors || shouldShowWildcardWarning),
|
||||
);
|
||||
const [selectedLogTab, setSelectedLogTab] = useState("all");
|
||||
const failedStartupScriptSource = hasAgentIssues
|
||||
? agent.log_sources.find(
|
||||
(s) => s.display_name === STARTUP_SCRIPT_DISPLAY_NAME,
|
||||
)
|
||||
: undefined;
|
||||
const [selectedLogTab, setSelectedLogTab] = useState(
|
||||
failedStartupScriptSource?.id ?? "all",
|
||||
);
|
||||
const sourceLogTabs = agent.log_sources
|
||||
.filter((logSource) => {
|
||||
// Remove the logSources that have no entries.
|
||||
@@ -482,13 +504,27 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
<div className={cn("px-4", hasStartupFeatures ? "pb-4" : "py-4")}>
|
||||
{healthIssues.length > 0 && (
|
||||
<div className="mb-4 flex flex-col gap-3">
|
||||
{healthIssues.map((issue) => (
|
||||
<AgentAlert
|
||||
key={`${issue.title}-${issue.detail}`}
|
||||
{...issue}
|
||||
troubleshootingURL={agent.troubleshooting_url}
|
||||
/>
|
||||
))}
|
||||
{healthIssues.map((issue) => {
|
||||
const isStartError =
|
||||
issue.title === agentScriptMessages.start_error.title;
|
||||
const detail =
|
||||
isStartError && failedStartTimings?.length ? (
|
||||
<StartScriptFailureDetail
|
||||
baseDetail={issue.detail}
|
||||
timings={failedStartTimings}
|
||||
/>
|
||||
) : (
|
||||
issue.detail
|
||||
);
|
||||
return (
|
||||
<AgentAlert
|
||||
key={`${issue.title}-${issue.detail}`}
|
||||
{...issue}
|
||||
detail={detail}
|
||||
troubleshootingURL={agent.troubleshooting_url}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{hasStartupFeatures && hasAnyLogs && (
|
||||
|
||||
Reference in New Issue
Block a user