Skip to content

Commit 28d90a2

Browse files
authored
Merge pull request #2520 from appwrite/fix-SER-457-resolve-forever-pending-deployments
feat: add build timeout handling to prevent stuck build UI
2 parents 1c09aff + add4296 commit 28d90a2

13 files changed

Lines changed: 163 additions & 66 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
},
2323
"dependencies": {
2424
"@ai-sdk/svelte": "^1.1.24",
25-
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@fe3277e",
25+
"@appwrite.io/console": "https://pkg.pr.new/appwrite-labs/cloud/@appwrite.io/console@2752",
2626
"@appwrite.io/pink-icons": "0.25.0",
2727
"@appwrite.io/pink-icons-svelte": "https://pkg.vc/-/@appwrite/@appwrite.io/pink-icons-svelte@46f65c7",
2828
"@appwrite.io/pink-legacy": "^1.0.3",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/helpers/buildTimeout.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import type { Models } from '@appwrite.io/console';
2+
3+
/**
4+
* Checks if a build has exceeded the maximum build timeout duration
5+
*/
6+
function isBuildTimedOut(createdAt: string, status: string, timeoutSeconds: number): boolean {
7+
if (!['waiting', 'processing', 'building'].includes(status)) {
8+
return false;
9+
}
10+
11+
if (!timeoutSeconds || timeoutSeconds <= 0) {
12+
return false;
13+
}
14+
15+
const created = new Date(createdAt);
16+
const elapsedSeconds = Math.floor((Date.now() - created.getTime()) / 1000);
17+
18+
return elapsedSeconds > timeoutSeconds;
19+
}
20+
21+
/**
22+
* Gets the effective status for a build, considering timeout
23+
*/
24+
export function getEffectiveBuildStatus(
25+
originalStatus: string,
26+
createdAt: string,
27+
consoleVariables: Models.ConsoleVariables | undefined
28+
): string {
29+
const timeoutSeconds = getBuildTimeoutSeconds(consoleVariables);
30+
if (isBuildTimedOut(createdAt, originalStatus, timeoutSeconds)) {
31+
return 'failed';
32+
}
33+
return originalStatus;
34+
}
35+
36+
/**
37+
* Helper to get timeout value from console variables
38+
*/
39+
function getBuildTimeoutSeconds(consoleVariables: Models.ConsoleVariables | undefined): number {
40+
if (!consoleVariables?._APP_COMPUTE_BUILD_TIMEOUT) {
41+
return 0;
42+
}
43+
const timeout = parseInt(String(consoleVariables._APP_COMPUTE_BUILD_TIMEOUT), 10);
44+
return isNaN(timeout) ? 0 : timeout;
45+
}

src/routes/(console)/project-[region]-[project]/functions/function-[function]/(components)/deploymentCard.svelte

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
import { DeploymentSource, DeploymentCreatedBy, DeploymentDomains } from '$lib/components/git';
1616
import { func } from '../store';
1717
import { capitalize } from '$lib/helpers/string';
18+
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
19+
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
1820
import { isCloud } from '$lib/system';
1921
import { IconInfo } from '@appwrite.io/pink-icons-svelte';
2022
import Link from '$lib/elements/link.svelte';
@@ -36,6 +38,9 @@
3638
footer?: Snippet;
3739
} = $props();
3840
41+
let effectiveStatus = $derived(
42+
getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables)
43+
);
3944
let totalSize = $derived(humanFileSize(deployment?.totalSize ?? 0));
4045
</script>
4146

@@ -122,11 +127,11 @@
122127
</Layout.Stack>
123128

124129
<Layout.Stack direction="row" gap="xl">
125-
{#if deployment.status === 'failed'}
130+
{#if effectiveStatus === 'failed'}
126131
<Layout.Stack gap="xxs" inline>
127132
{@render titleSnippet('Status')}
128133
<Typography.Text variant="m-400" color="--fgcolor-neutral-primary">
129-
<Status status={deployment.status} label={deployment.status} />
134+
<Status status={effectiveStatus} label={effectiveStatus} />
130135
</Typography.Text>
131136
</Layout.Stack>
132137
{:else}

src/routes/(console)/project-[region]-[project]/functions/function-[function]/deployment-[deployment]/+page.svelte

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
} from '@appwrite.io/pink-svelte';
2525
import { capitalize } from '$lib/helpers/string';
2626
import { formatTimeDetailed } from '$lib/helpers/timeConversion';
27+
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
28+
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
2729
import { timer } from '$lib/actions/timer';
2830
import { app } from '$lib/stores/app';
2931
import { IconDotsHorizontal, IconRefresh, IconTrash } from '@appwrite.io/pink-icons-svelte';
@@ -36,22 +38,29 @@
3638
import { readOnly } from '$lib/stores/billing';
3739
import RedeployModal from '../(modals)/redeployModal.svelte';
3840
39-
export let data;
41+
let { data } = $props();
4042
41-
let showDelete = false;
42-
let showCancel = false;
43-
let showActivate = false;
44-
let showRedeploy = false;
43+
let effectiveStatus = $derived(
44+
getEffectiveBuildStatus(
45+
data.deployment.status,
46+
data.deployment.$createdAt,
47+
$regionalConsoleVariables
48+
)
49+
);
50+
let showDelete = $state(false);
51+
let showCancel = $state(false);
52+
let showActivate = $state(false);
53+
let showRedeploy = $state(false);
4554
4655
onMount(() => {
47-
return realtime.forProject(page.params.region, 'console', (response) => {
56+
return realtime.forConsole(page.params.region, 'console', (message) => {
4857
if (
49-
response.events.includes(
58+
message.events.includes(
5059
`functions.${page.params.function}.deployments.${page.params.deployment}.update`
5160
)
5261
) {
53-
const payload = response.payload as Models.Deployment;
54-
if (payload.status === 'ready') {
62+
const payload = message.payload as Models.Deployment;
63+
if (['ready', 'failed'].includes(payload.status)) {
5564
invalidate(Dependencies.DEPLOYMENT);
5665
}
5766
}
@@ -78,7 +87,7 @@
7887
<DeploymentCard proxyRuleList={data.proxyRuleList} deployment={data.deployment}>
7988
{#snippet footer()}
8089
<Layout.Stack direction="row" alignItems="center" inline>
81-
{#if data.deployment.status === 'processing' || data.deployment.status === 'building' || data.deployment.status === 'waiting'}
90+
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
8291
<Button
8392
text
8493
on:click={() => {
@@ -152,9 +161,9 @@
152161
<Card.Base padding="s">
153162
<Accordion
154163
title="Deployment logs"
155-
badge={capitalize(data.deployment.status)}
164+
badge={capitalize(effectiveStatus)}
156165
open
157-
badgeType={badgeTypeDeployment(data.deployment.status)}
166+
badgeType={badgeTypeDeployment(effectiveStatus)}
158167
hideDivider>
159168
<Layout.Stack gap="xl">
160169
{#key data.deployment.buildLogs}
@@ -167,7 +176,7 @@
167176

168177
<svelte:fragment slot="end">
169178
<Layout.Stack direction="row" alignItems="center" inline>
170-
{#if ['processing', 'building'].includes(data.deployment.status)}
179+
{#if ['processing', 'building'].includes(effectiveStatus)}
171180
<Typography.Code color="--fgcolor-neutral-secondary">
172181
<Layout.Stack direction="row" alignItems="center" inline>
173182
<p use:timer={{ start: data.deployment.$createdAt }}></p>

src/routes/(console)/project-[region]-[project]/functions/function-[function]/table.svelte

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
import Delete from './(modals)/deleteModal.svelte';
2929
import { capitalize } from '$lib/helpers/string';
3030
import { deploymentStatusConverter } from '$lib/stores/git';
31+
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
32+
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
3133
import DownloadActionMenuItem from './(components)/downloadActionMenuItem.svelte';
3234
import { Menu } from '$lib/components/menu';
3335
import { sdk } from '$lib/stores/sdk';
@@ -82,9 +84,13 @@
8284
{/each}
8385
<Table.Header.Cell column="actions" {root} />
8486
{/snippet}
85-
8687
{#snippet children(root)}
8788
{#each data.deploymentList.deployments as deployment (deployment.$id)}
89+
{@const effectiveStatus = getEffectiveBuildStatus(
90+
deployment.status,
91+
deployment.$createdAt,
92+
$regionalConsoleVariables
93+
)}
8894
<Table.Row.Link
8995
{root}
9096
id={deployment.$id}
@@ -96,23 +102,21 @@
96102
<Id value={deployment.$id}>{deployment.$id}</Id>
97103
{/key}
98104
{:else if column.id === 'status'}
99-
{@const status = deployment.status}
100-
101105
{#if data?.activeDeployment?.$id === deployment?.$id}
102106
<Status status="complete" label="Active" />
103107
{:else}
104108
<Status
105-
status={deploymentStatusConverter(status)}
106-
label={capitalize(status)} />
109+
status={deploymentStatusConverter(effectiveStatus)}
110+
label={capitalize(effectiveStatus)} />
107111
{/if}
108112
{:else if column.id === 'type'}
109113
<DeploymentSource {deployment} />
110114
{:else if column.id === '$updatedAt'}
111115
<DeploymentCreatedBy {deployment} />
112116
{:else if column.id === 'buildDuration'}
113-
{#if ['waiting'].includes(deployment.status)}
117+
{#if ['waiting'].includes(effectiveStatus)}
114118
-
115-
{:else if ['processing', 'building'].includes(deployment.status)}
119+
{:else if ['processing', 'building'].includes(effectiveStatus)}
116120
<span use:timer={{ start: deployment.$createdAt }}></span>
117121
{:else}
118122
{formatTimeDetailed(deployment.buildDuration)}
@@ -167,7 +171,7 @@
167171

168172
<DownloadActionMenuItem {deployment} {toggle} />
169173

170-
{#if deployment.status === 'processing' || deployment.status === 'building' || deployment.status === 'waiting'}
174+
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
171175
<ActionMenu.Item.Button
172176
trailingIcon={IconXCircle}
173177
on:click={() => {
@@ -180,7 +184,7 @@
180184
Cancel
181185
</ActionMenu.Item.Button>
182186
{/if}
183-
{#if deployment.status !== 'building' && deployment.status !== 'processing' && deployment.status !== 'waiting'}
187+
{#if effectiveStatus !== 'building' && effectiveStatus !== 'processing' && effectiveStatus !== 'waiting'}
184188
<ActionMenu.Item.Button
185189
leadingIcon={IconTrash}
186190
status="danger"

src/routes/(console)/project-[region]-[project]/sites/(components)/deploymentActionMenu.svelte

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
IconXCircle
1616
} from '@appwrite.io/pink-icons-svelte';
1717
import { ActionMenu, Icon, Tooltip } from '@appwrite.io/pink-svelte';
18+
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
19+
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
1820
1921
export let selectedDeployment: Models.Deployment;
2022
export let deployment: Models.Deployment;
@@ -51,6 +53,11 @@
5153
</Button>
5254
<svelte:fragment slot="menu" let:toggle>
5355
<ActionMenu.Root>
56+
{@const effectiveStatus = getEffectiveBuildStatus(
57+
deployment.status,
58+
deployment.$createdAt,
59+
$regionalConsoleVariables
60+
)}
5461
{#if !inCard}
5562
<Tooltip disabled={selectedDeployment?.sourceSize !== 0} placement={'bottom'}>
5663
<div>
@@ -70,7 +77,7 @@
7077
<div slot="tooltip">Source is empty</div>
7178
</Tooltip>
7279
{/if}
73-
{#if deployment?.status === 'ready' && deployment?.$id !== activeDeployment}
80+
{#if effectiveStatus === 'ready' && deployment?.$id !== activeDeployment}
7481
<ActionMenu.Item.Button
7582
leadingIcon={IconLightningBolt}
7683
on:click={(e) => {
@@ -82,7 +89,7 @@
8289
Activate
8390
</ActionMenu.Item.Button>
8491
{/if}
85-
{#if deployment?.status === 'ready' || deployment?.status === 'failed' || deployment?.status === 'building'}
92+
{#if effectiveStatus === 'ready' || effectiveStatus === 'failed' || effectiveStatus === 'building'}
8693
<SubMenu>
8794
<ActionMenu.Root noPadding>
8895
<ActionMenu.Item.Button
@@ -101,7 +108,7 @@
101108
</ActionMenu.Item.Anchor>
102109

103110
<ActionMenu.Item.Anchor
104-
disabled={deployment?.status !== 'ready'}
111+
disabled={effectiveStatus !== 'ready'}
105112
on:click={toggle}
106113
href={getOutputDownload(deployment.$id)}
107114
external>
@@ -112,7 +119,7 @@
112119
</SubMenu>
113120
{/if}
114121

115-
{#if deployment?.status === 'processing' || deployment?.status === 'building' || deployment.status === 'waiting'}
122+
{#if effectiveStatus === 'processing' || effectiveStatus === 'building' || effectiveStatus === 'waiting'}
116123
<ActionMenu.Item.Button
117124
leadingIcon={IconXCircle}
118125
status="danger"

src/routes/(console)/project-[region]-[project]/sites/(components)/logs.svelte

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
<script lang="ts">
1919
import { capitalize } from '$lib/helpers/string';
2020
import { app } from '$lib/stores/app';
21+
import { getEffectiveBuildStatus } from '$lib/helpers/buildTimeout';
22+
import { regionalConsoleVariables } from '$routes/(console)/project-[region]-[project]/store';
2123
import type { Models } from '@appwrite.io/console';
2224
import { Badge, Card, Layout, Logs, Spinner, Typography } from '@appwrite.io/pink-svelte';
2325
import LogsTimer from './logsTimer.svelte';
@@ -38,15 +40,19 @@
3840
emptyCopy?: string;
3941
} = $props();
4042
43+
let effectiveStatus = $derived(
44+
getEffectiveBuildStatus(deployment.status, deployment.$createdAt, $regionalConsoleVariables)
45+
);
46+
4147
function setCopy() {
42-
if (deployment.status === 'failed') {
48+
if (effectiveStatus === 'failed') {
4349
return 'Your deployment has failed.';
44-
} else if (deployment.status === 'building') {
50+
} else if (effectiveStatus === 'building') {
4551
//Do not remove empty space before the string it's an invisible character
4652
return 'Preparing for build ... \n';
47-
} else if (deployment.status === 'waiting') {
53+
} else if (effectiveStatus === 'waiting') {
4854
return 'Preparing for build ... \n';
49-
} else if (deployment.status === 'processing') {
55+
} else if (effectiveStatus === 'processing') {
5056
return 'Preparing for build ... \n';
5157
} else {
5258
return emptyCopy;
@@ -62,16 +68,16 @@
6268
Deployment logs
6369
</Typography.Text>
6470
<Badge
65-
content={capitalize(deployment.status)}
71+
content={capitalize(effectiveStatus)}
6672
size="xs"
6773
variant="secondary"
68-
type={badgeTypeDeployment(deployment.status)} />
74+
type={badgeTypeDeployment(effectiveStatus)} />
6975
</Layout.Stack>
70-
<LogsTimer status={deployment.status} {deployment} />
76+
<LogsTimer status={effectiveStatus} {deployment} />
7177
</Layout.Stack>
7278
{/if}
7379

74-
{#if ['waiting', 'processing'].includes(deployment.status) || (deployment.status === 'building' && !deployment?.buildLogs?.length)}
80+
{#if ['waiting', 'processing'].includes(effectiveStatus) || (effectiveStatus === 'building' && !deployment?.buildLogs?.length)}
7581
<Card.Base variant="secondary">
7682
<Layout.Stack direction="row" justifyContent="center" gap="s">
7783
<Spinner /> Waiting for build to start...

0 commit comments

Comments
 (0)