Skip to content

Commit c2b7a72

Browse files
d-csTrigger.dev RepoOps
authored andcommitted
fix(webapp): smooth out the first GitHub deployment in onboarding
Improve the first GitHub deployment experience: Deploy now explains when a branch doesn't exist on GitHub, a harmless first-build cache message no longer shows as an error, the deployment panel stays on screen after the first deploy finishes, the empty development Tasks page uses the new setup layout, and the deployment setup screen is vertically centered. Mono-RevId: 07d4623e6fbe912906e1976f513f962c5ec42aa6
1 parent b576a9a commit c2b7a72

23 files changed

Lines changed: 759 additions & 80 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
area: webapp
3+
type: fix
4+
---
5+
6+
"Deploy now" now tells you when the branch doesn't exist on GitHub instead of showing a generic error, and a first deployment no longer flags a harmless build-cache message as an error.

apps/webapp/app/components/BlankStatePanels.tsx

Lines changed: 91 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
BellAlertIcon,
55
BookOpenIcon,
66
ChatBubbleLeftRightIcon,
7+
CheckIcon,
78
PlusIcon,
89
QuestionMarkCircleIcon,
910
Squares2X2Icon,
@@ -116,10 +117,17 @@ function DeployDocsLinks() {
116117
);
117118
}
118119

119-
export function HasNoTasksDev({ initializedAt }: { initializedAt: Date | string | null }) {
120+
export function HasNoTasksDev({
121+
initializedAt,
122+
enhanced = false,
123+
}: {
124+
initializedAt: Date | string | null;
125+
enhanced?: boolean;
126+
}) {
120127
const { isConnected } = useDevPresence();
121128
const initialized = !!initializedAt;
122129
const devConnected = isConnected === true;
130+
const complete = <CheckIcon className="size-5 text-success" aria-label="Complete" />;
123131

124132
return (
125133
<PackageManagerProvider>
@@ -163,49 +171,88 @@ export function HasNoTasksDev({ initializedAt }: { initializedAt: Date | string
163171
</div>
164172
</>
165173
)}
166-
<StepNumber
167-
stepNumber="1"
168-
title={initialized ? "Project initialized" : "Initialize your project"}
169-
complete={initialized}
170-
/>
171-
<StepContentContainer>
172-
{initialized ? (
173-
<Paragraph>
174-
Your project is initialized. Your tasks live in the{" "}
175-
<InlineCode variant="small">trigger</InlineCode> directory.
176-
</Paragraph>
177-
) : (
178-
<>
179-
<InitCommandV3 />
180-
<Paragraph spacing>
181-
Run this in an existing project. You'll notice a new folder called{" "}
182-
<InlineCode variant="small">trigger</InlineCode> with a few example tasks to help
183-
you get started.
184-
</Paragraph>
185-
</>
186-
)}
187-
</StepContentContainer>
188-
<StepNumber
189-
stepNumber="2"
190-
title={devConnected ? "Dev server connected" : "Start the dev server"}
191-
complete={devConnected}
192-
/>
193-
<StepContentContainer>
194-
{devConnected ? (
195-
<Paragraph>
196-
Your dev server is connected. Your tasks will appear here automatically as soon as
197-
they register.
198-
</Paragraph>
199-
) : (
200-
<>
201-
<TriggerDevStepV3 />
202-
<Paragraph spacing>
203-
Keep this running while you develop. Once your tasks register, this page updates
204-
automatically.
205-
</Paragraph>
206-
</>
207-
)}
208-
</StepContentContainer>
174+
{enhanced ? (
175+
<div>
176+
<SettingsRow
177+
bordered={false}
178+
title={initialized ? "Project initialized" : "Initialize your project"}
179+
description={
180+
initialized ? (
181+
<>
182+
Your project is initialized. Your tasks live in the{" "}
183+
<InlineCode variant="extra-small">trigger</InlineCode> directory.
184+
</>
185+
) : (
186+
<>
187+
Run this in an existing project. You'll notice a new folder called{" "}
188+
<InlineCode variant="extra-small">trigger</InlineCode> with a few example tasks
189+
to help you get started.
190+
</>
191+
)
192+
}
193+
action={initialized ? complete : undefined}
194+
/>
195+
{!initialized && <InitCommandV3 />}
196+
<SettingsRow
197+
className="border-t border-grid-dimmed"
198+
bordered={false}
199+
title={devConnected ? "Dev server connected" : "Start the dev server"}
200+
description={
201+
devConnected
202+
? "Your dev server is connected. Your tasks will appear here automatically as soon as they register."
203+
: "Keep this running while you develop. Once your tasks register, this page updates automatically."
204+
}
205+
action={devConnected ? complete : undefined}
206+
/>
207+
{!devConnected && <TriggerDevStepV3 />}
208+
</div>
209+
) : (
210+
<>
211+
<StepNumber
212+
stepNumber="1"
213+
title={initialized ? "Project initialized" : "Initialize your project"}
214+
complete={initialized}
215+
/>
216+
<StepContentContainer>
217+
{initialized ? (
218+
<Paragraph>
219+
Your project is initialized. Your tasks live in the{" "}
220+
<InlineCode variant="small">trigger</InlineCode> directory.
221+
</Paragraph>
222+
) : (
223+
<>
224+
<InitCommandV3 />
225+
<Paragraph spacing>
226+
Run this in an existing project. You'll notice a new folder called{" "}
227+
<InlineCode variant="small">trigger</InlineCode> with a few example tasks to
228+
help you get started.
229+
</Paragraph>
230+
</>
231+
)}
232+
</StepContentContainer>
233+
<StepNumber
234+
stepNumber="2"
235+
title={devConnected ? "Dev server connected" : "Start the dev server"}
236+
complete={devConnected}
237+
/>
238+
<StepContentContainer>
239+
{devConnected ? (
240+
<Paragraph>
241+
Your dev server is connected. Your tasks will appear here automatically as soon as
242+
they register.
243+
</Paragraph>
244+
) : (
245+
<>
246+
<TriggerDevStepV3 />
247+
<Paragraph spacing>
248+
Keep this running while you develop. Once your tasks register, this page updates
249+
automatically.
250+
</Paragraph>
251+
</>
252+
)}
253+
</StepContentContainer>
254+
</>
255+
)}
209256
</div>
210257
</PackageManagerProvider>
211258
);

apps/webapp/app/components/deployments/GitHubDeploymentOnboarding.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { LocaleContextProvider } from "~/components/primitives/LocaleProvider";
1010
import { expect, it } from "vitest";
1111
import { ShortcutsProvider } from "~/components/primitives/ShortcutsProvider";
1212
import { OperatingSystemContextProvider } from "~/components/primitives/OperatingSystemProvider";
13-
import { GitHubDeploymentOnboarding } from "./GitHubDeploymentOnboarding";
13+
import { deployNowRequestError, GitHubDeploymentOnboarding } from "./GitHubDeploymentOnboarding";
1414
import { OnboardingDeploymentLogs } from "./OnboardingDeploymentLogs";
1515

1616
function render(node: React.ReactNode) {
@@ -276,3 +276,11 @@ it("keeps the legacy connected-repository form on its original layout and explic
276276
expect(html.includes(">Save<")).toBe(true);
277277
}
278278
});
279+
280+
it("tells the user a missing branch has to be pushed instead of asking them to retry", () => {
281+
const missing = 'The branch "test" doesn\'t exist in acme/app. Push it to GitHub, then deploy.';
282+
expect(deployNowRequestError({ code: "BRANCH_NOT_FOUND", error: missing })).toBe(missing);
283+
expect(deployNowRequestError({ error: "Couldn't start the deploy" })).toBe(
284+
"Couldn't start the deployment. Try again."
285+
);
286+
});

apps/webapp/app/components/deployments/GitHubDeploymentOnboarding.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import { LinkButton } from "~/components/primitives/Buttons";
88
import { Spinner } from "~/components/primitives/Spinner";
99
import { VercelAtomicDeploymentNotice } from "./VercelAtomicDeploymentNotice";
1010

11+
export function deployNowRequestError(data: { code?: string; error?: string }): string {
12+
// Retrying can't fix a missing branch, so say what the user has to do instead.
13+
if (data.code === "BRANCH_NOT_FOUND" && data.error) return data.error;
14+
return "Couldn't start the deployment. Try again.";
15+
}
16+
1117
const statusTitles: Record<WorkerDeploymentStatus, string> = {
1218
PENDING: "Your deployment is queued",
1319
INSTALLING: "Installing dependencies",

apps/webapp/app/components/deployments/GitHubDeploymentOnboardingPanel.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import {
99
} from "~/hooks/useOnboardingDeploymentLogs";
1010
import { deployNowPath } from "~/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deploy-now";
1111
import { OnboardingDeploymentLogs } from "./OnboardingDeploymentLogs";
12-
import { GitHubDeploymentOnboarding, type OnboardingBuild } from "./GitHubDeploymentOnboarding";
12+
import {
13+
deployNowRequestError,
14+
GitHubDeploymentOnboarding,
15+
type OnboardingBuild,
16+
} from "./GitHubDeploymentOnboarding";
1317

1418
export function GitHubDeploymentOnboardingPanel({
1519
renderConnection,
@@ -49,7 +53,7 @@ export function GitHubDeploymentOnboardingPanel({
4953
});
5054
const [settingsDirty, setSettingsDirty] = useState(false);
5155
const [settingsSaving, setSettingsSaving] = useState(false);
52-
const fetcher = useFetcher<{ ok: boolean; code?: string; vercelUrl?: string }>();
56+
const fetcher = useFetcher<{ ok: boolean; code?: string; error?: string; vercelUrl?: string }>();
5357
const revalidator = useRevalidator();
5458
const [delayed, setDelayed] = useState(false);
5559
const submitting = fetcher.state !== "idle";
@@ -80,7 +84,7 @@ export function GitHubDeploymentOnboardingPanel({
8084
historyHref={build ? historyHref : undefined}
8185
requestError={
8286
fetcher.data?.ok === false && !submitting && !build
83-
? "Couldn't start the deployment. Try again."
87+
? deployNowRequestError(fetcher.data)
8488
: undefined
8589
}
8690
deployAction={

apps/webapp/app/components/layout/AppLayout.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,19 +65,26 @@ export function MainCenteredContainer({
6565
}: {
6666
children: React.ReactNode;
6767
className?: string;
68-
variant?: "default" | "onboarding";
68+
variant?: "default" | "onboarding" | "centered";
6969
}) {
7070
return (
7171
<div
7272
className={cn(
7373
"h-full w-full overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control",
74-
variant === "onboarding" && "flex flex-col p-4 lg:p-0"
74+
variant === "onboarding" && "flex flex-col p-4 lg:p-0",
75+
variant === "centered" && "flex flex-col"
7576
)}
7677
>
7778
<div
7879
className={cn(
7980
"mx-auto max-w-xs p-1",
80-
variant === "onboarding" ? "m-auto lg:mx-auto lg:mb-0 lg:mt-[22vh]" : "mt-6 md:mt-[22vh]",
81+
variant === "onboarding"
82+
? "m-auto lg:mx-auto lg:mb-0 lg:mt-[22vh]"
83+
: variant === "centered"
84+
? // Auto margins center the block when it fits and collapse to 0 when it
85+
// overflows, so tall content scrolls from the top instead of clipping.
86+
"my-auto py-6"
87+
: "mt-6 md:mt-[22vh]",
8188
className
8289
)}
8390
>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { DeploymentLogEntry } from "~/components/runs/v3/deploymentLogsCache";
2+
3+
// BuildKit reports a missing registry cache as a step ERROR, then builds without it.
4+
// That always happens on a project's first build, so keep the line for history but
5+
// don't let it read (or count) as a failure.
6+
const REGISTRY_CACHE_MISS =
7+
/^#\d+ ERROR: failed to configure registry cache importer: \S+: not found$/;
8+
9+
export function classifyDeploymentLog(entry: DeploymentLogEntry): DeploymentLogEntry {
10+
if (entry.level === "error" && REGISTRY_CACHE_MISS.test(entry.message.trim())) {
11+
return { ...entry, level: "info" };
12+
}
13+
return entry;
14+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { expect, it } from "vitest";
2+
import { resolveOnboardingLatch } from "./deploymentOnboardingLatch";
3+
4+
const building = {
5+
showGitHubOnboarding: true,
6+
onboardingDetails: { deployment: { shortCode: "abc", status: "BUILDING" as const } },
7+
};
8+
const idle = { showGitHubOnboarding: true, onboardingDetails: undefined };
9+
const done = { showGitHubOnboarding: false, onboardingDetails: undefined };
10+
const here = "/orgs/o/projects/p/env/prod/deployments";
11+
12+
it("keeps showing the watched build after the loader stops selecting onboarding", () => {
13+
const watching = resolveOnboardingLatch(undefined, building, here);
14+
expect(watching).toMatchObject({ shown: building, latched: false });
15+
16+
const completed = resolveOnboardingLatch(watching.latch, done, here);
17+
expect(completed).toMatchObject({ shown: building, latched: true });
18+
19+
// Later polls keep it on screen too.
20+
expect(resolveOnboardingLatch(completed.latch, done, here).shown).toBe(building);
21+
});
22+
23+
it("keeps showing it when the loader drops onboarding entirely (tasks appeared)", () => {
24+
const { latch } = resolveOnboardingLatch(undefined, building, here);
25+
expect(resolveOnboardingLatch(latch, undefined, here)).toMatchObject({
26+
shown: building,
27+
latched: true,
28+
});
29+
});
30+
31+
it("lets the normal page through after navigating", () => {
32+
const { latch } = resolveOnboardingLatch(undefined, building, here);
33+
expect(resolveOnboardingLatch(latch, done, `${here}/abc`)).toEqual({
34+
latch: undefined,
35+
shown: done,
36+
latched: false,
37+
});
38+
});
39+
40+
it("doesn't latch before a build has started", () => {
41+
const { latch } = resolveOnboardingLatch(undefined, idle, here);
42+
expect(latch).toBeUndefined();
43+
expect(resolveOnboardingLatch(latch, done, here).shown).toBe(done);
44+
});
45+
46+
it("keeps the same latch while the loader data is unchanged", () => {
47+
const first = resolveOnboardingLatch(undefined, building, here);
48+
expect(resolveOnboardingLatch(first.latch, building, here).latch).toBe(first.latch);
49+
});
50+
51+
it("keeps the same latch when a caller rebuilds the wrapper object each render", () => {
52+
const first = resolveOnboardingLatch(undefined, { ...building }, here);
53+
expect(resolveOnboardingLatch(first.latch, { ...building }, here).latch).toBe(first.latch);
54+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import type { WorkerDeploymentStatus } from "@trigger.dev/database";
2+
3+
type OnboardingState = {
4+
showGitHubOnboarding: boolean;
5+
onboardingDetails?: { deployment: { shortCode: string; status: WorkerDeploymentStatus } };
6+
};
7+
8+
export type OnboardingLatch<T> = { value: T; location: string } | undefined;
9+
10+
/**
11+
* Keeps a watched first build on screen after the loader stops selecting onboarding, until the
12+
* user navigates away (or reloads, which starts without a latch).
13+
*/
14+
export function resolveOnboardingLatch<T extends OnboardingState>(
15+
latch: OnboardingLatch<T>,
16+
current: T | undefined,
17+
location: string
18+
): { latch: OnboardingLatch<T>; shown: T | undefined; latched: boolean } {
19+
if (current?.showGitHubOnboarding) {
20+
if (!current.onboardingDetails) return { latch: undefined, shown: current, latched: false };
21+
const next =
22+
latch?.value.onboardingDetails === current.onboardingDetails && latch.location === location
23+
? latch
24+
: { value: current, location };
25+
return { latch: next, shown: current, latched: false };
26+
}
27+
28+
if (latch && latch.location === location) {
29+
return { latch, shown: latch.value, latched: true };
30+
}
31+
32+
return { latch: undefined, shown: current, latched: false };
33+
}

apps/webapp/app/hooks/useDeploymentLogs.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { S2, S2Error } from "@s2-dev/streamstore";
22
import { DeploymentEventFromString } from "@trigger.dev/core/v3/schemas";
33
import type { WorkerDeploymentStatus } from "@trigger.dev/database";
44
import { useEffect, useState } from "react";
5+
import { classifyDeploymentLog } from "./deploymentLogFilter";
56
import {
67
deploymentLogsCache,
78
type DeploymentLogEntry,
@@ -72,7 +73,7 @@ export function useDeploymentLogs({ eventStream, status }: UseDeploymentLogsOpti
7273
};
7374

7475
const push = (entry: DeploymentLogEntry) => {
75-
pending.push(entry);
76+
pending.push(classifyDeploymentLog(entry));
7677
flushTimer ??= setTimeout(flush, 0);
7778
};
7879

0 commit comments

Comments
 (0)