Use per-deploy preview hosts under the site root domain.
Build hosts as <userPrefix>-<deploymentNumber>-preview.<rootDomain> from FRONTEND_URL, wire them through ingress/TLS, and open them from the preview API. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -6,6 +6,7 @@ import { Deployment } from './entities/deployment.entity';
|
||||
import { ApplicationsService } from '../applications/applications.service';
|
||||
import { KubernetesService } from '../kubernetes/kubernetes.service';
|
||||
import { BuildService, BuildProgress, BuildCancelledError } from '../build/build.service';
|
||||
import * as crypto from 'crypto';
|
||||
import {
|
||||
AppLifecycleStatus,
|
||||
DeploymentStatus,
|
||||
@@ -28,6 +29,16 @@ export class DeploymentsService {
|
||||
private clustersService: ClustersService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Stable "number" part for preview host, derived from deployment id.
|
||||
* We keep it numeric to match the "<number>" requirement.
|
||||
*/
|
||||
private computePreviewNumberFromDeploymentId(deploymentId: string): string {
|
||||
const hashHex = crypto.createHash('sha256').update(deploymentId).digest('hex');
|
||||
const num = parseInt(hashHex.slice(0, 8), 16) % 1_000_000; // 0..999999
|
||||
return String(num).padStart(6, '0');
|
||||
}
|
||||
|
||||
async triggerDeployment(applicationId: string, userId: string): Promise<Deployment> {
|
||||
const app = await this.applicationsService.findOne(applicationId, userId);
|
||||
|
||||
@@ -38,13 +49,22 @@ export class DeploymentsService {
|
||||
imageTag: `${app.name}:${Date.now()}`,
|
||||
status: DeploymentStatus.PENDING,
|
||||
version: `v${Date.now()}`,
|
||||
previewSubdomain: null,
|
||||
});
|
||||
const saved = await this.deploymentsRepository.save(deployment);
|
||||
|
||||
// Fill deterministic preview number after we have the deployment id.
|
||||
let previewSubdomain: string | null = null;
|
||||
if (!app.customDomain) {
|
||||
previewSubdomain = this.computePreviewNumberFromDeploymentId(saved.id);
|
||||
await this.deploymentsRepository.update(saved.id, { previewSubdomain });
|
||||
saved.previewSubdomain = previewSubdomain;
|
||||
}
|
||||
|
||||
// Trigger async pipeline (Helm-only for managed services, build+deploy for apps)
|
||||
const run = isManagedProductType(app.productType)
|
||||
? this.executeManagedPipeline(saved.id, app)
|
||||
: this.executePipeline(saved.id, app);
|
||||
: this.executePipeline(saved.id, app, previewSubdomain);
|
||||
run.catch((error) => {
|
||||
this.logger.error(`Pipeline failed for deployment ${saved.id}:`, error);
|
||||
});
|
||||
@@ -137,7 +157,11 @@ export class DeploymentsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async executePipeline(deploymentId: string, app: any): Promise<void> {
|
||||
private async executePipeline(
|
||||
deploymentId: string,
|
||||
app: any,
|
||||
previewSubdomain: string | null,
|
||||
): Promise<void> {
|
||||
try {
|
||||
// Step 1: Build image
|
||||
await this.updateStatus(deploymentId, DeploymentStatus.BUILDING);
|
||||
@@ -159,7 +183,13 @@ export class DeploymentsService {
|
||||
});
|
||||
|
||||
const hasDbDump = app.dbDumpPath && fs.existsSync(app.dbDumpPath);
|
||||
const { app: deployedApp, k8sResources } = await this.deployWithClusterFallback(deploymentId, app, imageUri, hasDbDump);
|
||||
const { app: deployedApp, k8sResources } = await this.deployWithClusterFallback(
|
||||
deploymentId,
|
||||
app,
|
||||
imageUri,
|
||||
hasDbDump,
|
||||
previewSubdomain,
|
||||
);
|
||||
app = deployedApp;
|
||||
|
||||
// Step 3.5: Restore DB dump if one was uploaded (must happen after deploy creates the namespace + DB)
|
||||
@@ -316,6 +346,7 @@ export class DeploymentsService {
|
||||
app: any,
|
||||
imageUri: string,
|
||||
hasDbDump: boolean,
|
||||
previewSubdomain: string | null,
|
||||
): Promise<{ app: any; k8sResources: Record<string, any> }> {
|
||||
const failedClusterIds: string[] = [];
|
||||
let currentApp = app;
|
||||
@@ -340,7 +371,9 @@ export class DeploymentsService {
|
||||
hasDbDump && !isManagedProductType(currentApp.productType)
|
||||
? { ...currentApp, replicas: 0 }
|
||||
: currentApp;
|
||||
const k8sResources = await this.kubernetesService.deployApplication(deployApp, imageUri);
|
||||
const k8sResources = await this.kubernetesService.deployApplication(deployApp, imageUri, {
|
||||
previewNumber: previewSubdomain,
|
||||
});
|
||||
return { app: currentApp, k8sResources };
|
||||
} catch (error: any) {
|
||||
lastError = error;
|
||||
@@ -618,11 +651,19 @@ export class DeploymentsService {
|
||||
imageTag: `${app.name}:${Date.now()}`,
|
||||
status: DeploymentStatus.PENDING,
|
||||
version: `v${Date.now()}`,
|
||||
previewSubdomain: null,
|
||||
});
|
||||
const saved = await this.deploymentsRepository.save(deployment);
|
||||
|
||||
let previewSubdomain: string | null = null;
|
||||
if (!app.customDomain) {
|
||||
previewSubdomain = this.computePreviewNumberFromDeploymentId(saved.id);
|
||||
await this.deploymentsRepository.update(saved.id, { previewSubdomain });
|
||||
saved.previewSubdomain = previewSubdomain;
|
||||
}
|
||||
|
||||
// Trigger async build & deploy pipeline (same as initial deploy)
|
||||
this.executePipeline(saved.id, app).catch((error) => {
|
||||
this.executePipeline(saved.id, app, previewSubdomain).catch((error) => {
|
||||
this.logger.error(`Redeploy pipeline failed for deployment ${saved.id}:`, error);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user