|
|
|
|
|
"use server"; |
|
|
|
|
|
import type { DeploymentFormInput } from "@/lib/schemas"; |
|
|
import type { Deployment, DeploymentStatus } from "@/lib/types"; |
|
|
import { analyzeDeploymentLogs as analyzeLogsFlow, type AnalyzeDeploymentLogsInput } from "@/ai/flows/analyze-deployment-logs"; |
|
|
import { getDb } from "@/lib/mongodb"; |
|
|
import { ObjectId } from 'mongodb'; |
|
|
import { getLoggedInUser } from "./auth"; |
|
|
import { getPlatformApiKey } from "./admin"; |
|
|
import { revalidatePath } from "next/cache"; |
|
|
|
|
|
const defaultGithubRepoUrl = "https://github.com/DavidCyrilTech/Anita-V4"; |
|
|
const DEPLOYMENT_COST = 10; |
|
|
|
|
|
const HEROKU_API_BASE_URL = "https://api.heroku.com"; |
|
|
const HEROKU_STACK = "heroku-22"; |
|
|
|
|
|
async function getHerokuApiKey(): Promise<string | null> { |
|
|
const result = await getPlatformApiKey(); |
|
|
if (result.success && result.apiKey && result.apiKey.trim() !== "") { |
|
|
return result.apiKey; |
|
|
} |
|
|
console.warn("Heroku API Key not found or is empty in database settings via Admin Panel. Deployments will fail."); |
|
|
return null; |
|
|
} |
|
|
|
|
|
async function herokuApiCall( |
|
|
endpoint: string, |
|
|
method: 'GET' | 'POST' | 'PATCH' | 'DELETE', |
|
|
apiKey: string, |
|
|
body?: any |
|
|
): Promise<any> { |
|
|
const headers = { |
|
|
'Accept': 'application/vnd.heroku+json; version=3', |
|
|
'Authorization': `Bearer ${apiKey}`, |
|
|
'Content-Type': 'application/json', |
|
|
}; |
|
|
|
|
|
const options: RequestInit = { |
|
|
method, |
|
|
headers, |
|
|
}; |
|
|
|
|
|
if (body) { |
|
|
options.body = JSON.stringify(body); |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${HEROKU_API_BASE_URL}${endpoint}`, options); |
|
|
if (!response.ok) { |
|
|
if (method === 'DELETE' && response.status === 404) { |
|
|
return { status: 404, message: "Resource not found on Heroku (already deleted or never existed)." }; |
|
|
} |
|
|
const errorBody = await response.json().catch(() => ({ message: response.statusText })); |
|
|
console.error(`Heroku API Error (${method} ${endpoint}): ${response.status}`, errorBody); |
|
|
throw new Error(`Heroku API Error: ${errorBody.message || response.statusText} (Status: ${response.status})`); |
|
|
} |
|
|
if (response.status === 204 || response.headers.get("content-length") === "0") { |
|
|
return null; |
|
|
} |
|
|
return response.json(); |
|
|
} catch (error) { |
|
|
console.error(`Network or parsing error during Heroku API call (${method} ${endpoint}):`, error); |
|
|
throw error; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export async function createNewDeployment(data: DeploymentFormInput): Promise<{ success: boolean; message: string; deployment?: Deployment }> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) { |
|
|
return { success: false, message: "You must be logged in to create a deployment." }; |
|
|
} |
|
|
|
|
|
const herokuApiKey = await getHerokuApiKey(); |
|
|
if (!herokuApiKey) { |
|
|
return { success: false, message: "Platform API Key is not configured. Deployment cannot proceed." }; |
|
|
} |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const deploymentsCollection = db.collection<Deployment>("deployments"); |
|
|
const usersCollection = db.collection("users"); |
|
|
|
|
|
|
|
|
|
|
|
if (user.role !== 'admin') { |
|
|
const freshUser = await usersCollection.findOne({ _id: user._id }); |
|
|
if (!freshUser || freshUser.coins < DEPLOYMENT_COST) { |
|
|
return { success: false, message: `Insufficient coins. You need ${DEPLOYMENT_COST} coins. You have ${freshUser?.coins || 0}.` }; |
|
|
} |
|
|
} |
|
|
|
|
|
const appNameInput = data.PLATFORM_APP_NAME || `anita-bot-${user.name.toLowerCase().replace(/[^a-z0-9]/g, '').slice(0,8)}-${new ObjectId().toString().slice(-4)}`; |
|
|
const herokuAppName = appNameInput.toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 30); |
|
|
const appNameProvidedByUser = !!data.PLATFORM_APP_NAME; |
|
|
|
|
|
const existingDeployment = await deploymentsCollection.findOne({ appName: herokuAppName }); |
|
|
if (existingDeployment) { |
|
|
return { success: false, message: `A deployment with app name '${herokuAppName}' already exists in our records. Please choose a different name or let one be auto-generated.` }; |
|
|
} |
|
|
|
|
|
const now = new Date(); |
|
|
const initialLogs: string[] = [ |
|
|
`[SYSTEM] Info: ${now.toISOString()} - Deployment creation initiated by user ${user.email} for Heroku app: ${herokuAppName}.`, |
|
|
`[SYSTEM] Info: ${now.toISOString()} - Target GitHub Repo: ${defaultGithubRepoUrl}` |
|
|
]; |
|
|
|
|
|
let herokuApp; |
|
|
try { |
|
|
initialLogs.push(`[SYSTEM] Info: ${now.toISOString()} - Attempting to create Heroku app '${herokuAppName}'...`); |
|
|
herokuApp = await herokuApiCall('/apps', 'POST', herokuApiKey, { name: herokuAppName, region: 'us', stack: HEROKU_STACK }); |
|
|
initialLogs.push(`[SYSTEM] Success: ${new Date().toISOString()} - Heroku app '${herokuAppName}' (ID: ${herokuApp.id}) created successfully. Web URL: ${herokuApp.web_url}`); |
|
|
} catch (error: any) { |
|
|
if (error.message && error.message.includes("Name") && error.message.includes("is already taken") && error.message.includes("(Status: 422)")) { |
|
|
initialLogs.push(`[SYSTEM] Error: ${new Date().toISOString()} - Heroku app name '${herokuAppName}' is already taken.`); |
|
|
let userMessage = `The app name '${herokuAppName}' is already taken on Heroku. `; |
|
|
if (appNameProvidedByUser) { |
|
|
userMessage += "Please choose a different name for your app and try again."; |
|
|
} else { |
|
|
userMessage += "This name was auto-generated. Please try deploying again, as a new name will be generated."; |
|
|
} |
|
|
return { success: false, message: userMessage }; |
|
|
} |
|
|
|
|
|
initialLogs.push(`[SYSTEM] Error: ${new Date().toISOString()} - Failed to create Heroku app: ${error.message}`); |
|
|
const tempFailDeployment: Omit<Deployment, '_id'> = { |
|
|
id: herokuAppName, |
|
|
userId: user._id, |
|
|
appName: herokuAppName, |
|
|
status: 'failed', |
|
|
createdAt: now.toISOString(), |
|
|
region: 'us', |
|
|
logs: initialLogs, |
|
|
envVariables: { ...data }, |
|
|
githubRepoUrl: defaultGithubRepoUrl, |
|
|
}; |
|
|
await deploymentsCollection.insertOne(tempFailDeployment); |
|
|
return { success: false, message: `Failed to create Heroku app: ${error.message}` }; |
|
|
} |
|
|
|
|
|
initialLogs.push(`[SYSTEM] Info: ${new Date().toISOString()} - Setting environment variables for '${herokuAppName}'...`); |
|
|
const envVarsToSet = { ...data }; |
|
|
delete envVarsToSet.PLATFORM_APP_NAME; |
|
|
const herokuConfigVars = { ...envVarsToSet, GITHUB_REPO_URL: defaultGithubRepoUrl }; |
|
|
|
|
|
try { |
|
|
await herokuApiCall(`/apps/${herokuApp.id}/config-vars`, 'PATCH', herokuApiKey, herokuConfigVars); |
|
|
initialLogs.push(`[SYSTEM] Success: ${new Date().toISOString()} - Environment variables set successfully.`); |
|
|
} catch (error: any) { |
|
|
initialLogs.push(`[SYSTEM] Error: ${new Date().toISOString()} - Failed to set environment variables: ${error.message}. Cleaning up Heroku app.`); |
|
|
await herokuApiCall(`/apps/${herokuApp.id}`, 'DELETE', herokuApiKey).catch(delErr => console.error("Cleanup error after failing to set config vars:", delErr)); |
|
|
return { success: false, message: `Failed to set Heroku environment variables: ${error.message}` }; |
|
|
} |
|
|
|
|
|
initialLogs.push(`[SYSTEM] Info: ${new Date().toISOString()} - Triggering build from GitHub repository '${defaultGithubRepoUrl}'...`); |
|
|
let build; |
|
|
try { |
|
|
build = await herokuApiCall(`/apps/${herokuApp.id}/builds`, 'POST', herokuApiKey, { |
|
|
source_blob: { |
|
|
url: `${defaultGithubRepoUrl}/tarball/main/`, |
|
|
version: 'main' |
|
|
}, |
|
|
}); |
|
|
initialLogs.push(`[SYSTEM] Success: ${new Date().toISOString()} - Build initiated successfully. Build ID: ${build.id}, Status: ${build.status}.`); |
|
|
} catch (error: any) { |
|
|
initialLogs.push(`[SYSTEM] Error: ${new Date().toISOString()} - Failed to trigger build: ${error.message}. Cleaning up Heroku app.`); |
|
|
await herokuApiCall(`/apps/${herokuApp.id}`, 'DELETE', herokuApiKey).catch(delErr => console.error("Cleanup error after failing to trigger build:", delErr)); |
|
|
return { success: false, message: `Failed to trigger Heroku build: ${error.message}` }; |
|
|
} |
|
|
|
|
|
const newDeploymentData: Omit<Deployment, '_id'> = { |
|
|
id: herokuApp.id, |
|
|
userId: user._id, |
|
|
appName: herokuApp.name, |
|
|
status: build.status === 'succeeded' ? 'succeeded' : 'deploying', |
|
|
createdAt: now.toISOString(), |
|
|
lastDeployedAt: build.status === 'succeeded' ? new Date(build.updated_at).toISOString() : undefined, |
|
|
region: herokuApp.region.name, |
|
|
url: herokuApp.web_url, |
|
|
logs: initialLogs, |
|
|
envVariables: { ...data }, |
|
|
githubRepoUrl: defaultGithubRepoUrl, |
|
|
}; |
|
|
|
|
|
const result = await deploymentsCollection.insertOne(newDeploymentData); |
|
|
if (!result.insertedId) { |
|
|
initialLogs.push(`[SYSTEM] Error: ${new Date().toISOString()} - Failed to save deployment to local database. Critical error. Heroku app '${herokuApp.name}' might be orphaned.`); |
|
|
await herokuApiCall(`/apps/${herokuApp.id}`, 'DELETE', herokuApiKey).catch(delErr => console.error("Critical: Cleanup error for orphaned Heroku app:", delErr)); |
|
|
return { success: false, message: "Critical: Failed to save deployment to local database after Heroku setup." }; |
|
|
} |
|
|
|
|
|
if (user.role !== 'admin') { |
|
|
await usersCollection.updateOne( |
|
|
{ _id: user._id }, |
|
|
{ $inc: { coins: -DEPLOYMENT_COST } } |
|
|
); |
|
|
} |
|
|
|
|
|
revalidatePath("/dashboard"); |
|
|
revalidatePath(`/dashboard/deployments/${newDeploymentData.id}`); |
|
|
|
|
|
return { |
|
|
success: true, |
|
|
message: `Heroku deployment for '${newDeploymentData.appName}' initiated! ${user.role !== 'admin' ? `${DEPLOYMENT_COST} coins deducted.` : ''} Build status: ${build.status}. Check deployment details for updates.`, |
|
|
deployment: { ...newDeploymentData, _id: result.insertedId.toString() } |
|
|
}; |
|
|
|
|
|
} catch (error: any) { |
|
|
console.error("Error creating new Heroku deployment:", error); |
|
|
const message = error.message || "An unexpected error occurred during Heroku deployment."; |
|
|
return { success: false, message }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function getDeployments(): Promise<Deployment[]> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) return []; |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const query = user.role === 'admin' ? {} : { userId: user._id }; |
|
|
const deployments = await db.collection<Deployment>("deployments") |
|
|
.find(query) |
|
|
.sort({ createdAt: -1 }) |
|
|
.toArray(); |
|
|
|
|
|
return deployments.map(d => ({ ...d, _id: d._id?.toString() })); |
|
|
} catch (error) { |
|
|
console.error("Error fetching deployments:", error); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function getDeploymentById(id: string): Promise<Deployment | null> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) return null; |
|
|
|
|
|
try { |
|
|
const db = await getDb(); |
|
|
const deployment = await db.collection<Deployment>("deployments").findOne({ id: id }); |
|
|
|
|
|
if (!deployment) return null; |
|
|
if (deployment.userId !== user._id && user.role !== 'admin') return null; |
|
|
|
|
|
if (deployment.status === 'deploying' || deployment.status === 'pending') { |
|
|
const herokuApiKey = await getHerokuApiKey(); |
|
|
if (herokuApiKey) { |
|
|
try { |
|
|
const formation = await herokuApiCall(`/apps/${deployment.id}/formation/web`, 'GET', herokuApiKey); |
|
|
let appStatusBasedOnDynos: DeploymentStatus = deployment.status; |
|
|
if (formation && formation.quantity > 0) { |
|
|
if(deployment.status === 'deploying') appStatusBasedOnDynos = 'succeeded'; |
|
|
} else if (formation && formation.quantity === 0 && deployment.status !== 'stopped') { |
|
|
appStatusBasedOnDynos = 'stopped'; |
|
|
} |
|
|
|
|
|
const builds = await herokuApiCall(`/apps/${deployment.id}/builds`, 'GET', herokuApiKey); |
|
|
let newStatusFromBuild = deployment.status; |
|
|
let lastDeployedAtFromBuild = deployment.lastDeployedAt; |
|
|
|
|
|
if (builds && builds.length > 0) { |
|
|
const latestBuild = builds.sort((a:any, b:any) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]; |
|
|
newStatusFromBuild = latestBuild.status === 'succeeded' ? 'succeeded' : latestBuild.status === 'failed' ? 'failed' : 'deploying'; |
|
|
lastDeployedAtFromBuild = new Date(latestBuild.updated_at).toISOString(); |
|
|
} |
|
|
|
|
|
let finalNewStatus = deployment.status; |
|
|
if (newStatusFromBuild === 'succeeded' || newStatusFromBuild === 'failed') { |
|
|
finalNewStatus = newStatusFromBuild; |
|
|
} else if (appStatusBasedOnDynos !== deployment.status) { |
|
|
finalNewStatus = appStatusBasedOnDynos; |
|
|
} |
|
|
|
|
|
if (finalNewStatus !== deployment.status || (finalNewStatus === 'succeeded' && lastDeployedAtFromBuild !== deployment.lastDeployedAt)) { |
|
|
deployment.status = finalNewStatus; |
|
|
deployment.lastDeployedAt = finalNewStatus === 'succeeded' ? lastDeployedAtFromBuild : deployment.lastDeployedAt; |
|
|
|
|
|
const statusUpdateLog = `[SYSTEM] Info: ${new Date().toISOString()} - Heroku status sync. App status: ${finalNewStatus}. Latest build status: ${newStatusFromBuild}.`; |
|
|
const updatedLogs = [...(deployment.logs || []), statusUpdateLog].slice(-500); |
|
|
|
|
|
await db.collection<Deployment>("deployments").updateOne( |
|
|
{ id: deployment.id }, |
|
|
{ $set: { status: finalNewStatus, lastDeployedAt: deployment.lastDeployedAt, logs: updatedLogs } } |
|
|
); |
|
|
deployment.logs = updatedLogs; |
|
|
revalidatePath(`/dashboard/deployments/${deployment.id}`); |
|
|
} |
|
|
} catch (herokuError: any) { |
|
|
const errorLog = `[SYSTEM] Warning: ${new Date().toISOString()} - Could not fetch live status from Heroku: ${herokuError.message}`; |
|
|
const updatedLogs = [...(deployment.logs || []), errorLog].slice(-500); |
|
|
await db.collection<Deployment>("deployments").updateOne( |
|
|
{ id: deployment.id }, |
|
|
{ $set: { logs: updatedLogs } } |
|
|
); |
|
|
deployment.logs = updatedLogs; |
|
|
} |
|
|
} |
|
|
} |
|
|
return { ...deployment, _id: deployment._id?.toString() }; |
|
|
} catch (error) { |
|
|
console.error(`Error fetching deployment by ID '${id}':`, error); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
export async function getDeploymentLogs(deploymentId: string): Promise<string[]> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) return [`[SYSTEM] Error: Unauthorized to fetch logs.`]; |
|
|
|
|
|
const herokuApiKey = await getHerokuApiKey(); |
|
|
if (!herokuApiKey) return [`[SYSTEM] Error: Heroku API Key not configured. Cannot fetch live logs.`]; |
|
|
|
|
|
const db = await getDb(); |
|
|
const deploymentsCollection = db.collection<Deployment>("deployments"); |
|
|
const deployment = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
|
|
|
if (!deployment) return [`[SYSTEM] Info: No deployment found with ID ${deploymentId}.`]; |
|
|
if (deployment.userId !== user._id && user.role !== 'admin') { |
|
|
return [`[SYSTEM] Error: Unauthorized to fetch logs for this deployment.`]; |
|
|
} |
|
|
|
|
|
const timestamp = new Date().toISOString(); |
|
|
let fetchedHerokuLogs: string[] = []; |
|
|
|
|
|
try { |
|
|
const logSession = await herokuApiCall( |
|
|
`/apps/${deployment.id}/log-sessions`, |
|
|
'POST', |
|
|
herokuApiKey, |
|
|
{ lines: 200, source: 'app', tail: false } |
|
|
); |
|
|
|
|
|
if (logSession && logSession.logplex_url) { |
|
|
const logStreamResponse = await fetch(logSession.logplex_url); |
|
|
if (logStreamResponse.ok) { |
|
|
const rawLogs = await logStreamResponse.text(); |
|
|
fetchedHerokuLogs = rawLogs.split('\\n').filter(line => line.trim() !== ''); |
|
|
if (fetchedHerokuLogs.length > 0) { |
|
|
fetchedHerokuLogs.unshift(`[SYSTEM] Info: ${timestamp} - Successfully fetched ${fetchedHerokuLogs.length} recent log lines from Heroku.`); |
|
|
} else { |
|
|
fetchedHerokuLogs.push(`[SYSTEM] Info: ${timestamp} - No recent logs returned from Heroku for this app.`); |
|
|
} |
|
|
|
|
|
const currentDbLogs = deployment.logs || []; |
|
|
const combinedLogs = [...currentDbLogs, ...fetchedHerokuLogs].slice(-500); |
|
|
|
|
|
await deploymentsCollection.updateOne( |
|
|
{ id: deployment.id }, |
|
|
{ $set: { logs: combinedLogs } } |
|
|
); |
|
|
revalidatePath(`/dashboard/deployments/${deployment.id}`); |
|
|
return combinedLogs; |
|
|
} else { |
|
|
const errorMsg = `[SYSTEM] Error: ${timestamp} - Failed to fetch logs from Heroku logplex_url. Status: ${logStreamResponse.status}`; |
|
|
fetchedHerokuLogs = [errorMsg]; |
|
|
} |
|
|
} else { |
|
|
const errorMsg = `[SYSTEM] Error: ${timestamp} - Failed to create Heroku log session. Response: ${JSON.stringify(logSession)}`; |
|
|
fetchedHerokuLogs = [errorMsg]; |
|
|
} |
|
|
} catch (error: any) { |
|
|
console.error(`Error fetching live logs from Heroku for '${deployment.appName}':`, error); |
|
|
const errorMsg = `[SYSTEM] Error: ${timestamp} - Exception during Heroku log fetch: ${error.message}`; |
|
|
fetchedHerokuLogs = [errorMsg]; |
|
|
} |
|
|
|
|
|
const currentDbLogs = deployment.logs || []; |
|
|
const logsToStore = [...currentDbLogs, ...fetchedHerokuLogs].slice(-500); |
|
|
|
|
|
await deploymentsCollection.updateOne( |
|
|
{ id: deployment.id }, |
|
|
{ $set: { logs: logsToStore } } |
|
|
); |
|
|
revalidatePath(`/dashboard/deployments/${deployment.id}`); |
|
|
return logsToStore; |
|
|
} |
|
|
|
|
|
|
|
|
export async function controlDeployment(deploymentId: string, action: "start" | "stop" | "restart"): Promise<{ success: boolean; message: string; newStatus?: DeploymentStatus }> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) return { success: false, message: "Unauthorized." }; |
|
|
|
|
|
const herokuApiKey = await getHerokuApiKey(); |
|
|
if (!herokuApiKey) return { success: false, message: "Heroku API Key is not configured." }; |
|
|
|
|
|
const db = await getDb(); |
|
|
const deploymentsCollection = db.collection<Deployment>("deployments"); |
|
|
const deployment = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
|
|
|
if (!deployment) return { success: false, message: "Deployment not found." }; |
|
|
if (deployment.userId !== user._id && user.role !== 'admin') { |
|
|
return { success: false, message: "Unauthorized to control this deployment." }; |
|
|
} |
|
|
|
|
|
const herokuAppIdOrName = deployment.id; |
|
|
let newDbStatus: DeploymentStatus = deployment.status; |
|
|
const timestamp = new Date().toISOString(); |
|
|
let logMessage = `[SYSTEM] Info: ${timestamp} - User ${user.email} requested Heroku action '${action}' on app '${deployment.appName}'.`; |
|
|
let herokuSuccess = false; |
|
|
|
|
|
try { |
|
|
switch (action) { |
|
|
case "start": |
|
|
logMessage += ` Attempting to scale web dynos to 1.`; |
|
|
await herokuApiCall(`/apps/${herokuAppIdOrName}/formation/web`, 'PATCH', herokuApiKey, { quantity: 1 }); |
|
|
newDbStatus = 'deploying'; |
|
|
logMessage += ` Heroku accepted scale command. App is starting. Status set to 'deploying'.`; |
|
|
herokuSuccess = true; |
|
|
break; |
|
|
case "stop": |
|
|
logMessage += ` Attempting to scale web dynos to 0.`; |
|
|
await herokuApiCall(`/apps/${herokuAppIdOrName}/formation/web`, 'PATCH', herokuApiKey, { quantity: 0 }); |
|
|
newDbStatus = 'stopped'; |
|
|
logMessage += ` Heroku accepted scale command. App should be stopping. Status set to 'stopped'.`; |
|
|
herokuSuccess = true; |
|
|
break; |
|
|
case "restart": |
|
|
logMessage += ` Attempting to restart all dynos.`; |
|
|
await herokuApiCall(`/apps/${herokuAppIdOrName}/dynos`, 'DELETE', herokuApiKey); |
|
|
newDbStatus = 'deploying'; |
|
|
logMessage += ` Heroku accepted restart command. App is restarting. Status set to 'deploying'.`; |
|
|
herokuSuccess = true; |
|
|
break; |
|
|
} |
|
|
|
|
|
if (herokuSuccess) { |
|
|
const updatedLogs = [...(deployment.logs || []), logMessage].slice(-500); |
|
|
await deploymentsCollection.updateOne( |
|
|
{ id: deploymentId }, |
|
|
{ $set: { status: newDbStatus, logs: updatedLogs, lastDeployedAt: new Date().toISOString() } } |
|
|
); |
|
|
revalidatePath(`/dashboard/deployments/${deploymentId}`); |
|
|
return { success: true, message: `Heroku action '${action}' on '${deployment.appName}' initiated. New status: ${newDbStatus}.`, newStatus: newDbStatus }; |
|
|
} else { |
|
|
logMessage += ` Failed to execute Heroku command for '${action}'.`; |
|
|
const updatedLogs = [...(deployment.logs || []), logMessage].slice(-500); |
|
|
await deploymentsCollection.updateOne({ id: deploymentId }, { $set: {logs: updatedLogs }}); |
|
|
return { success: false, message: `Failed to execute Heroku command for '${action}'.` }; |
|
|
} |
|
|
|
|
|
} catch (error: any) { |
|
|
logMessage += ` Error during Heroku action '${action}': ${error.message}.`; |
|
|
const updatedLogs = [...(deployment.logs || []), logMessage].slice(-500); |
|
|
const currentDeploymentRecord = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
if (currentDeploymentRecord) { |
|
|
await deploymentsCollection.updateOne({ id: deploymentId }, { $set: { logs: updatedLogs } }); |
|
|
} |
|
|
console.error(`Error controlling Heroku deployment '${deployment.appName}':`, error); |
|
|
return { success: false, message: `Heroku API operation failed: ${error.message}` }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function deleteDeployment(deploymentId: string): Promise<{ success: boolean; message: string }> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) return { success: false, message: "Unauthorized. Please log in." }; |
|
|
|
|
|
const herokuApiKey = await getHerokuApiKey(); |
|
|
if (!herokuApiKey) return { success: false, message: "Heroku API Key is not configured. Cannot delete deployment." }; |
|
|
|
|
|
const db = await getDb(); |
|
|
const deploymentsCollection = db.collection<Deployment>("deployments"); |
|
|
const deployment = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
|
|
|
if (!deployment) return { success: false, message: "Deployment not found in our records." }; |
|
|
|
|
|
if (deployment.userId !== user._id && user.role !== 'admin') { |
|
|
return { success: false, message: "Unauthorized. You can only delete your own deployments." }; |
|
|
} |
|
|
|
|
|
const timestamp = new Date().toISOString(); |
|
|
const initialDbLogs = deployment.logs || []; |
|
|
let logMessage = `[SYSTEM] Info: ${timestamp} - User ${user.email} initiated deletion for deployment '${deployment.appName}' (ID: ${deployment.id}).`; |
|
|
let herokuAppDeleted = false; |
|
|
|
|
|
|
|
|
try { |
|
|
logMessage += ` Attempting to delete Heroku app '${deployment.appName}'.`; |
|
|
await herokuApiCall(`/apps/${deployment.id}`, 'DELETE', herokuApiKey); |
|
|
logMessage += ` Heroku app '${deployment.appName}' delete command accepted by Heroku.`; |
|
|
herokuAppDeleted = true; |
|
|
} catch (error: any) { |
|
|
if (error.message && error.message.includes("(Status: 404)") || (typeof error === 'object' && error !== null && 'status' in error && error.status === 404)) { |
|
|
logMessage += ` Heroku app '${deployment.appName}' not found on Heroku (Status 404). Assuming already deleted or never fully created. Proceeding with local cleanup.`; |
|
|
herokuAppDeleted = true; |
|
|
} else { |
|
|
logMessage += ` Error deleting Heroku app '${deployment.appName}': ${error.message}. Local record will NOT be deleted.`; |
|
|
const updatedLogs = [...initialDbLogs, logMessage].slice(-500); |
|
|
const currentDeploymentRecord = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
if (currentDeploymentRecord) { |
|
|
await deploymentsCollection.updateOne({ id: deploymentId }, { $set: { logs: updatedLogs } }); |
|
|
revalidatePath(`/dashboard/deployments/${deploymentId}`); |
|
|
} |
|
|
return { success: false, message: `Failed to delete Heroku app: ${error.message}.` }; |
|
|
} |
|
|
} |
|
|
|
|
|
if (herokuAppDeleted) { |
|
|
try { |
|
|
const deleteResult = await deploymentsCollection.deleteOne({ id: deployment.id }); |
|
|
if (deleteResult.deletedCount === 0) { |
|
|
logMessage += ` Warning: Heroku app was targeted for deletion, but local record for '${deployment.appName}' not found or already deleted.`; |
|
|
console.warn(logMessage); |
|
|
} else { |
|
|
logMessage += ` Local database record for deployment '${deployment.appName}' deleted successfully.`; |
|
|
} |
|
|
revalidatePath("/dashboard"); |
|
|
revalidatePath(`/dashboard/deployments`); |
|
|
return { success: true, message: `Deployment '${deployment.appName}' and associated Heroku app have been successfully deleted (or confirmed deleted).` }; |
|
|
} catch (dbError: any) { |
|
|
logMessage += ` Critical Error: Heroku app '${deployment.appName}' was deleted (or confirmed deleted), but failed to delete local database record: ${dbError.message}. Manual cleanup may be required.`; |
|
|
console.error(logMessage); |
|
|
return { success: false, message: `Heroku app deleted, but failed to clean up local record: ${dbError.message}` }; |
|
|
} |
|
|
} |
|
|
return { success: false, message: "An unknown error occurred during the deletion process." }; |
|
|
} |
|
|
|
|
|
|
|
|
export async function analyzeDeploymentLogs(logs: string): Promise<{ success: boolean; analysis?: string; error?: string }> { |
|
|
if (!logs.trim()) { |
|
|
return { success: false, error: "Log content cannot be empty." }; |
|
|
} |
|
|
try { |
|
|
const input: AnalyzeDeploymentLogsInput = { deploymentLogs: logs }; |
|
|
const result = await analyzeLogsFlow(input); |
|
|
return { success: true, analysis: result.analysisResult }; |
|
|
} catch (error) { |
|
|
console.error("AI Log Analysis Error:", error); |
|
|
return { success: false, error: "Failed to analyze logs. Please try again." }; |
|
|
} |
|
|
} |
|
|
|
|
|
export async function updateDeploymentEnvVariables( |
|
|
deploymentId: string, |
|
|
newEnvVars: DeploymentFormInput |
|
|
): Promise<{ success: boolean; message: string }> { |
|
|
const user = await getLoggedInUser(); |
|
|
if (!user) { |
|
|
return { success: false, message: "Unauthorized. Please log in." }; |
|
|
} |
|
|
|
|
|
const herokuApiKey = await getHerokuApiKey(); |
|
|
if (!herokuApiKey) { |
|
|
return { success: false, message: "Heroku API Key is not configured." }; |
|
|
} |
|
|
|
|
|
const db = await getDb(); |
|
|
const deploymentsCollection = db.collection<Deployment>("deployments"); |
|
|
const deployment = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
|
|
|
if (!deployment) { |
|
|
return { success: false, message: "Deployment not found." }; |
|
|
} |
|
|
if (deployment.userId !== user._id && user.role !== 'admin') { |
|
|
return { success: false, message: "Unauthorized to modify this deployment." }; |
|
|
} |
|
|
|
|
|
const timestamp = new Date().toISOString(); |
|
|
let logMessage = `[SYSTEM] Info: ${timestamp} - User ${user.email} initiated environment variable update for '${deployment.appName}'.`; |
|
|
|
|
|
try { |
|
|
const herokuConfigVars = { ...newEnvVars }; |
|
|
if ('PLATFORM_APP_NAME' in herokuConfigVars) { |
|
|
delete (herokuConfigVars as any).PLATFORM_APP_NAME; |
|
|
} |
|
|
if (!(herokuConfigVars as any).GITHUB_REPO_URL) { |
|
|
(herokuConfigVars as any).GITHUB_REPO_URL = deployment.githubRepoUrl || defaultGithubRepoUrl; |
|
|
} |
|
|
|
|
|
|
|
|
logMessage += ` Attempting to update config vars on Heroku.`; |
|
|
await herokuApiCall(`/apps/${deployment.id}/config-vars`, 'PATCH', herokuApiKey, herokuConfigVars); |
|
|
logMessage += ` Heroku config vars updated successfully.`; |
|
|
|
|
|
logMessage += ` Attempting to restart Heroku app '${deployment.appName}' for changes to take effect.`; |
|
|
await herokuApiCall(`/apps/${deployment.id}/dynos`, 'DELETE', herokuApiKey); |
|
|
logMessage += ` Heroku app restart command accepted. Status set to 'deploying'.`; |
|
|
|
|
|
const updatedLogs = [...(deployment.logs || []), logMessage].slice(-500); |
|
|
await deploymentsCollection.updateOne( |
|
|
{ id: deployment.id }, |
|
|
{ $set: { envVariables: newEnvVars, logs: updatedLogs, status: 'deploying', lastDeployedAt: new Date().toISOString() } } |
|
|
); |
|
|
|
|
|
revalidatePath(`/dashboard/deployments/${deployment.id}`); |
|
|
return { success: true, message: "Environment variables updated and app restart initiated. Status is 'deploying'." }; |
|
|
|
|
|
} catch (error: any) { |
|
|
logMessage += ` Error during environment variable update or restart: ${error.message}.`; |
|
|
const updatedLogs = [...(deployment.logs || []), logMessage].slice(-500); |
|
|
const currentDeploymentRecord = await deploymentsCollection.findOne({ id: deploymentId }); |
|
|
if (currentDeploymentRecord) { |
|
|
await deploymentsCollection.updateOne({ id: deployment.id }, { $set: { logs: updatedLogs } }); |
|
|
} |
|
|
console.error(`Error updating env variables for '${deployment.appName}':`, error); |
|
|
return { success: false, message: `Failed to update environment variables: ${error.message}` }; |
|
|
} |
|
|
} |
|
|
|