Track real website deploy success/failure and support master branch.

Deploy agent waits for build completion, writes status, and checks out origin/HEAD (main or master) so empty-main repos like mashinify can deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-08-10 08:52:49 +03:30
co-authored by Cursor
parent acc8f35682
commit f7cee26975
5 changed files with 177 additions and 28 deletions
+23 -14
View File
@@ -417,7 +417,7 @@ export class DomainAdminService {
throw new BadRequestException('This domain has no storefront deploy configured');
}
const markDeploy = async (status: 'started' | 'failed') => {
const markDeploy = async (status: 'started' | 'success' | 'failed') => {
const updated = await this.prisma.domain.update({
where: { id: domainId },
data: {
@@ -428,28 +428,37 @@ export class DomainAdminService {
return updated;
};
await markDeploy('started');
try {
await this.websiteDeployAgent.deploy(slug);
const result = await this.websiteDeployAgent.deploy(slug, { wait: true });
const updated = await markDeploy(
result.status === 'success' || result.status === 'accepted'
? 'success'
: 'failed',
);
return {
status: 'ok' as const,
slug,
host: domain.host,
message:
result.detail?.trim() ||
(updated.lastDeployStatus === 'success'
? 'Deploy succeeded on websites server'
: 'Deploy finished with unknown status'),
lastDeployedAt: updated.lastDeployedAt?.toISOString() ?? null,
lastDeployStatus: updated.lastDeployStatus,
};
} catch (err) {
await markDeploy('failed');
if (err instanceof ServiceUnavailableException) {
throw err;
}
throw new ServiceUnavailableException(
err instanceof Error ? err.message : 'Deploy failed to start',
err instanceof Error ? err.message : 'Deploy failed',
);
}
const updated = await markDeploy('started');
return {
status: 'accepted' as const,
slug,
host: domain.host,
message: 'Deploy started on websites server',
lastDeployedAt: updated.lastDeployedAt?.toISOString() ?? null,
lastDeployStatus: updated.lastDeployStatus,
};
}
async update(domainIdRaw: string, dto: UpdateDomainAdminDto, actor: AuthUser) {
@@ -47,8 +47,9 @@ export class WebsiteDeployAgentService {
return response.json().catch(() => ({ status: 'accepted' }));
}
async deploy(slug: string) {
async deploy(slug: string, options?: { wait?: boolean }) {
const { deployUrl, token } = this.credentials();
const wait = options?.wait !== false;
let response: Response;
try {
@@ -58,20 +59,43 @@ export class WebsiteDeployAgentService {
'Content-Type': 'application/json',
'X-Deploy-Token': token,
},
body: JSON.stringify({ slug }),
body: JSON.stringify({ slug, wait }),
signal: AbortSignal.timeout(15 * 60 * 1000),
});
} catch {
} catch (err) {
if (err instanceof Error && err.name === 'TimeoutError') {
throw new ServiceUnavailableException(
'Deploy timed out waiting for websites server (15m)',
);
}
throw new ServiceUnavailableException('Could not reach website deploy agent');
}
const text = await response.text().catch(() => '');
let payload: {
status?: string;
slug?: string;
detail?: string;
error?: string;
} = {};
try {
payload = text ? (JSON.parse(text) as typeof payload) : {};
} catch {
/* keep raw */
}
if (!response.ok) {
const text = await response.text().catch(() => '');
const detail = payload.detail || payload.error || text;
throw new ServiceUnavailableException(
`Deploy agent rejected request (${response.status})${text ? `: ${text}` : ''}`,
`Deploy failed (${response.status})${detail ? `: ${detail}` : ''}`,
);
}
return response.json().catch(() => ({ status: 'accepted', slug }));
return {
status: (payload.status as string) || (wait ? 'success' : 'accepted'),
slug: payload.slug || slug,
detail: payload.detail ?? null,
};
}
/** Issue / renew Let's Encrypt cert for a storefront host on the websites VM. */