feat: production-ready deployment with multi-language UI
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m3s
Some checks failed
Build and Push Docker Image / build-and-push (push) Failing after 1m3s
- Add multi-language support (NL, AR, EN) with RTL
- Improve health checks (SSL-tolerant, multi-endpoint)
- Add DELETE /api/stack/:name for cleanup
- Add persistent storage (portal-ai-workspace-{name})
- Improve rollback (delete domain, app, project)
- Increase SSE timeout to 255s
- Add deployment strategy documentation
This commit is contained in:
@@ -266,7 +266,7 @@ export class ProductionDeployer {
|
||||
config: DeploymentConfig
|
||||
): Promise<void> {
|
||||
state.phase = 'configuring_application';
|
||||
state.progress = 55;
|
||||
state.progress = 50;
|
||||
state.message = 'Configuring application with Docker image';
|
||||
|
||||
if (!state.resources.applicationId) {
|
||||
@@ -278,7 +278,22 @@ export class ProductionDeployer {
|
||||
sourceType: 'docker',
|
||||
});
|
||||
|
||||
state.message = 'Application configured';
|
||||
state.progress = 55;
|
||||
state.message = 'Creating persistent storage';
|
||||
|
||||
const volumeName = `portal-ai-workspace-${config.stackName}`;
|
||||
try {
|
||||
await this.client.createMount(
|
||||
state.resources.applicationId,
|
||||
volumeName,
|
||||
'/workspace'
|
||||
);
|
||||
console.log(`Created persistent volume: ${volumeName}`);
|
||||
} catch (error) {
|
||||
console.warn(`Volume creation failed (may already exist): ${error}`);
|
||||
}
|
||||
|
||||
state.message = 'Application configured with storage';
|
||||
}
|
||||
|
||||
private async createOrFindDomain(
|
||||
@@ -340,24 +355,42 @@ export class ProductionDeployer {
|
||||
const interval = config.healthCheckInterval || 5000; // 5 seconds
|
||||
const startTime = Date.now();
|
||||
|
||||
// Try multiple endpoints - the container may not have /health
|
||||
const endpoints = ['/', '/health', '/api'];
|
||||
|
||||
while (Date.now() - startTime < timeout) {
|
||||
try {
|
||||
const healthUrl = `${state.url}/health`;
|
||||
const response = await fetch(healthUrl, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000),
|
||||
});
|
||||
for (const endpoint of endpoints) {
|
||||
try {
|
||||
const checkUrl = `${state.url}${endpoint}`;
|
||||
const response = await fetch(checkUrl, {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(5000),
|
||||
tls: { rejectUnauthorized: false },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
state.message = 'Application is healthy';
|
||||
// Accept ANY HTTP response (even 404) as "server is alive"
|
||||
// Only connection errors mean the container isn't ready
|
||||
console.log(`Health check ${checkUrl} returned ${response.status}`);
|
||||
state.message = 'Application is responding';
|
||||
return;
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
|
||||
// SSL cert errors mean server IS responding, just cert issue during provisioning
|
||||
if (errorMsg.includes('certificate') || errorMsg.includes('SSL') || errorMsg.includes('TLS')) {
|
||||
console.log(`Health check SSL error (treating as alive): ${errorMsg}`);
|
||||
state.message = 'Application is responding (SSL provisioning)';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Health check failed: ${errorMsg}`);
|
||||
}
|
||||
|
||||
console.log(`Health check returned ${response.status}, retrying...`);
|
||||
} catch (error) {
|
||||
console.log(`Health check failed: ${error instanceof Error ? error.message : String(error)}, retrying...`);
|
||||
}
|
||||
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||
state.message = `Waiting for application to start (${elapsed}s)...`;
|
||||
this.notifyProgress(state);
|
||||
|
||||
await this.sleep(interval);
|
||||
}
|
||||
|
||||
@@ -370,10 +403,15 @@ export class ProductionDeployer {
|
||||
state.message = 'Rolling back deployment';
|
||||
|
||||
try {
|
||||
// Rollback in reverse order
|
||||
if (state.resources.domainId) {
|
||||
console.log(`Rolling back: deleting domain ${state.resources.domainId}`);
|
||||
try {
|
||||
await this.client.deleteDomain(state.resources.domainId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete domain during rollback:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We don't delete domain as it might be reused
|
||||
// Delete application if created
|
||||
if (state.resources.applicationId) {
|
||||
console.log(`Rolling back: deleting application ${state.resources.applicationId}`);
|
||||
try {
|
||||
@@ -383,8 +421,14 @@ export class ProductionDeployer {
|
||||
}
|
||||
}
|
||||
|
||||
// Note: We don't delete the project as it might have other resources
|
||||
// or be reused in future deployments
|
||||
if (state.resources.projectId) {
|
||||
console.log(`Rolling back: deleting project ${state.resources.projectId}`);
|
||||
try {
|
||||
await this.client.deleteProject(state.resources.projectId);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete project during rollback:', error);
|
||||
}
|
||||
}
|
||||
|
||||
state.message = 'Rollback completed';
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user