feat: improvements to update command

This commit is contained in:
syntaxbullet
2026-01-08 14:13:24 +01:00
parent 9e7f18787b
commit b2c7fa6e83
3 changed files with 547 additions and 136 deletions

View File

@@ -9,91 +9,168 @@ import {
getUpdatingEmbed,
getCancelledEmbed,
getTimeoutEmbed,
getErrorEmbed
getErrorEmbed,
getRollbackSuccessEmbed,
getRollbackFailedEmbed
} from "@/modules/admin/update.view";
export const update = createCommand({
data: new SlashCommandBuilder()
.setName("update")
.setDescription("Check for updates and restart the bot")
.addBooleanOption(option =>
option.setName("force")
.setDescription("Force update even if checks fail (not recommended)")
.setRequired(false)
.addSubcommand(sub =>
sub.setName("check")
.setDescription("Check for and apply available updates")
.addBooleanOption(option =>
option.setName("force")
.setDescription("Force update even if no changes detected")
.setRequired(false)
)
)
.addSubcommand(sub =>
sub.setName("rollback")
.setDescription("Rollback to the previous version")
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const force = interaction.options.getBoolean("force") || false;
const subcommand = interaction.options.getSubcommand();
try {
await interaction.editReply({ embeds: [getCheckingEmbed()] });
const { hasUpdates, log, branch } = await UpdateService.checkForUpdates();
if (!hasUpdates && !force) {
await interaction.editReply({ embeds: [getNoUpdatesEmbed()] });
return;
}
const { embeds, components } = getUpdatesAvailableMessage(branch, log, force);
const response = await interaction.editReply({ embeds, components });
try {
const confirmation = await response.awaitMessageComponent({
filter: (i) => i.user.id === interaction.user.id,
componentType: ComponentType.Button,
time: 30000
});
if (confirmation.customId === "confirm_update") {
await confirmation.update({
embeds: [getPreparingEmbed()],
components: []
});
// 1. Check what the update requires
const { needsInstall, needsMigrations } = await UpdateService.checkUpdateRequirements(branch);
// 2. Prepare context BEFORE update
await UpdateService.prepareRestartContext({
channelId: interaction.channelId,
userId: interaction.user.id,
timestamp: Date.now(),
runMigrations: needsMigrations,
installDependencies: needsInstall
});
// 3. Update UI to "Restarting" state
await interaction.editReply({ embeds: [getUpdatingEmbed(needsInstall)] });
// 4. Perform Update (Danger Zone)
await UpdateService.performUpdate(branch);
// 5. Trigger Restart (if we are still alive)
await UpdateService.triggerRestart();
} else {
await confirmation.update({
embeds: [getCancelledEmbed()],
components: []
});
}
} catch (e) {
if (e instanceof Error && e.message.includes("time")) {
await interaction.editReply({
embeds: [getTimeoutEmbed()],
components: []
});
} else {
throw e;
}
}
} catch (error) {
console.error("Update failed:", error);
await interaction.editReply({ embeds: [getErrorEmbed(error)] });
if (subcommand === "rollback") {
await handleRollback(interaction);
} else {
await handleUpdate(interaction);
}
}
});
async function handleUpdate(interaction: any) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
const force = interaction.options.getBoolean("force") || false;
try {
// 1. Check for updates
await interaction.editReply({ embeds: [getCheckingEmbed()] });
const updateInfo = await UpdateService.checkForUpdates();
if (!updateInfo.hasUpdates && !force) {
await interaction.editReply({
embeds: [getNoUpdatesEmbed(updateInfo.currentCommit)]
});
return;
}
// 2. Analyze requirements
const requirements = await UpdateService.checkUpdateRequirements(updateInfo.branch);
const categories = UpdateService.categorizeChanges(requirements.changedFiles);
// 3. Show confirmation with details
const { embeds, components } = getUpdatesAvailableMessage(
updateInfo,
requirements,
categories,
force
);
const response = await interaction.editReply({ embeds, components });
// 4. Wait for confirmation
try {
const confirmation = await response.awaitMessageComponent({
filter: (i: any) => i.user.id === interaction.user.id,
componentType: ComponentType.Button,
time: 30000
});
if (confirmation.customId === "confirm_update") {
await confirmation.update({
embeds: [getPreparingEmbed()],
components: []
});
// 5. Save rollback point
const previousCommit = await UpdateService.saveRollbackPoint();
// 6. Prepare restart context
await UpdateService.prepareRestartContext({
channelId: interaction.channelId,
userId: interaction.user.id,
timestamp: Date.now(),
runMigrations: requirements.needsMigrations,
installDependencies: requirements.needsRootInstall || requirements.needsWebInstall,
previousCommit: previousCommit.substring(0, 7),
newCommit: updateInfo.latestCommit
});
// 7. Show updating status
await interaction.editReply({
embeds: [getUpdatingEmbed(requirements)]
});
// 8. Perform update
await UpdateService.performUpdate(updateInfo.branch);
// 9. Trigger restart
await UpdateService.triggerRestart();
} else {
await confirmation.update({
embeds: [getCancelledEmbed()],
components: []
});
}
} catch (e) {
if (e instanceof Error && e.message.includes("time")) {
await interaction.editReply({
embeds: [getTimeoutEmbed()],
components: []
});
} else {
throw e;
}
}
} catch (error) {
console.error("Update failed:", error);
await interaction.editReply({
embeds: [getErrorEmbed(error)],
components: []
});
}
}
async function handleRollback(interaction: any) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
const hasRollback = await UpdateService.hasRollbackPoint();
if (!hasRollback) {
await interaction.editReply({
embeds: [getRollbackFailedEmbed("No rollback point available. Rollback is only possible after a recent update.")]
});
return;
}
const result = await UpdateService.rollback();
if (result.success) {
await interaction.editReply({
embeds: [getRollbackSuccessEmbed(result.message.split(" ").pop() || "unknown")]
});
// Restart after rollback
setTimeout(() => UpdateService.triggerRestart(), 1000);
} else {
await interaction.editReply({
embeds: [getRollbackFailedEmbed(result.message)]
});
}
} catch (error) {
console.error("Rollback failed:", error);
await interaction.editReply({
embeds: [getErrorEmbed(error)]
});
}
}

View File

@@ -2,7 +2,7 @@ import { exec } from "child_process";
import { promisify } from "util";
import { writeFile, readFile, unlink } from "fs/promises";
import { Client, TextChannel } from "discord.js";
import { getPostRestartEmbed, getInstallingDependenciesEmbed } from "./update.view";
import { getPostRestartEmbed, getInstallingDependenciesEmbed, getRunningMigrationsEmbed } from "./update.view";
import type { PostRestartResult } from "./update.view";
const execAsync = promisify(exec);
@@ -16,72 +16,221 @@ export interface RestartContext {
timestamp: number;
runMigrations: boolean;
installDependencies: boolean;
previousCommit: string;
newCommit: string;
}
export interface UpdateCheckResult {
needsInstall: boolean;
needsRootInstall: boolean;
needsWebInstall: boolean;
needsMigrations: boolean;
changedFiles: string[];
error?: Error;
}
export interface UpdateInfo {
hasUpdates: boolean;
branch: string;
currentCommit: string;
latestCommit: string;
commitCount: number;
commits: CommitInfo[];
}
export interface CommitInfo {
hash: string;
message: string;
author: string;
}
export class UpdateService {
private static readonly CONTEXT_FILE = ".restart_context.json";
private static readonly ROLLBACK_FILE = ".rollback_commit.txt";
static async checkForUpdates(): Promise<{ hasUpdates: boolean; log: string; branch: string }> {
/**
* Check for available updates with detailed commit information
*/
static async checkForUpdates(): Promise<UpdateInfo> {
const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD");
const branch = branchName.trim();
const { stdout: currentCommit } = await execAsync("git rev-parse --short HEAD");
await execAsync("git fetch --all");
const { stdout: logOutput } = await execAsync(`git log HEAD..origin/${branch} --oneline`);
const { stdout: latestCommit } = await execAsync(`git rev-parse --short origin/${branch}`);
// Get commit log with author info
const { stdout: logOutput } = await execAsync(
`git log HEAD..origin/${branch} --format="%h|%s|%an" --no-merges`
);
const commits: CommitInfo[] = logOutput
.trim()
.split("\n")
.filter(line => line.length > 0)
.map(line => {
const [hash, message, author] = line.split("|");
return { hash: hash || "", message: message || "", author: author || "" };
});
return {
hasUpdates: !!logOutput.trim(),
log: logOutput.trim(),
branch
hasUpdates: commits.length > 0,
branch,
currentCommit: currentCommit.trim(),
latestCommit: latestCommit.trim(),
commitCount: commits.length,
commits
};
}
static async performUpdate(branch: string): Promise<void> {
await execAsync(`git reset --hard origin/${branch}`);
}
/**
* Analyze what the update requires
*/
static async checkUpdateRequirements(branch: string): Promise<UpdateCheckResult> {
try {
const { stdout } = await execAsync(`git diff HEAD..origin/${branch} --name-only`);
const changedFiles = stdout.trim().split("\n").filter(f => f.length > 0);
const needsRootInstall = changedFiles.some(file =>
file === "package.json" || file === "bun.lock"
);
const needsWebInstall = changedFiles.some(file =>
file === "src/web/package.json" || file === "src/web/bun.lock"
);
const needsMigrations = changedFiles.some(file =>
file.includes("schema.ts") || file.startsWith("drizzle/")
);
return {
needsInstall: stdout.includes("package.json"),
needsMigrations: stdout.includes("schema.ts") || stdout.includes("drizzle/")
needsRootInstall,
needsWebInstall,
needsMigrations,
changedFiles
};
} catch (e) {
console.error("Failed to check update requirements:", e);
return {
needsInstall: false,
needsRootInstall: false,
needsWebInstall: false,
needsMigrations: false,
changedFiles: [],
error: e instanceof Error ? e : new Error(String(e))
};
}
}
static async installDependencies(): Promise<string> {
const { stdout } = await execAsync("bun install");
return stdout;
/**
* Get a summary of changed file categories
*/
static categorizeChanges(changedFiles: string[]): Record<string, number> {
const categories: Record<string, number> = {};
for (const file of changedFiles) {
let category = "Other";
if (file.startsWith("src/commands/")) category = "Commands";
else if (file.startsWith("src/modules/")) category = "Modules";
else if (file.startsWith("src/web/")) category = "Web Dashboard";
else if (file.startsWith("src/lib/")) category = "Library";
else if (file.startsWith("drizzle/") || file.includes("schema")) category = "Database";
else if (file.endsWith(".test.ts")) category = "Tests";
else if (file.includes("package.json") || file.includes("lock")) category = "Dependencies";
categories[category] = (categories[category] || 0) + 1;
}
return categories;
}
/**
* Save the current commit for potential rollback
*/
static async saveRollbackPoint(): Promise<string> {
const { stdout } = await execAsync("git rev-parse HEAD");
const commit = stdout.trim();
await writeFile(this.ROLLBACK_FILE, commit);
return commit;
}
/**
* Rollback to the previous commit
*/
static async rollback(): Promise<{ success: boolean; message: string }> {
try {
const rollbackCommit = await readFile(this.ROLLBACK_FILE, "utf-8");
await execAsync(`git reset --hard ${rollbackCommit.trim()}`);
await unlink(this.ROLLBACK_FILE);
return { success: true, message: `Rolled back to ${rollbackCommit.trim().substring(0, 7)}` };
} catch (e) {
return {
success: false,
message: e instanceof Error ? e.message : "No rollback point available"
};
}
}
/**
* Check if a rollback point exists
*/
static async hasRollbackPoint(): Promise<boolean> {
try {
await readFile(this.ROLLBACK_FILE, "utf-8");
return true;
} catch {
return false;
}
}
/**
* Perform the git update
*/
static async performUpdate(branch: string): Promise<void> {
await execAsync(`git reset --hard origin/${branch}`);
}
/**
* Install dependencies for specified projects
*/
static async installDependencies(options: { root: boolean; web: boolean }): Promise<string> {
const outputs: string[] = [];
if (options.root) {
const { stdout } = await execAsync("bun install");
outputs.push(`📦 Root: ${stdout.trim() || "Done"}`);
}
if (options.web) {
const { stdout } = await execAsync("cd src/web && bun install");
outputs.push(`🌐 Web: ${stdout.trim() || "Done"}`);
}
return outputs.join("\n");
}
/**
* Prepare restart context with rollback info
*/
static async prepareRestartContext(context: RestartContext): Promise<void> {
await writeFile(this.CONTEXT_FILE, JSON.stringify(context));
}
/**
* Trigger a restart
*/
static async triggerRestart(): Promise<void> {
if (process.env.RESTART_COMMAND) {
// Run without awaiting - it may kill the process immediately
exec(process.env.RESTART_COMMAND).unref();
} else {
// Fallback: exit the process and let Docker/PM2/systemd restart it
// Small delay to allow any pending I/O to complete
setTimeout(() => process.exit(0), 100);
}
}
/**
* Handle post-restart tasks
*/
static async handlePostRestart(client: Client): Promise<void> {
try {
const context = await this.loadRestartContext();
@@ -99,7 +248,7 @@ export class UpdateService {
}
const result = await this.executePostRestartTasks(context, channel);
await this.notifyPostRestartResult(channel, result);
await this.notifyPostRestartResult(channel, result, context);
await this.cleanupContext();
} catch (e) {
console.error("Failed to handle post-restart context:", e);
@@ -143,15 +292,20 @@ export class UpdateService {
migrationSuccess: true,
migrationOutput: "",
ranInstall: context.installDependencies,
ranMigrations: context.runMigrations
ranMigrations: context.runMigrations,
previousCommit: context.previousCommit,
newCommit: context.newCommit
};
// 1. Install Dependencies if needed
if (context.installDependencies) {
try {
await channel.send({ embeds: [getInstallingDependenciesEmbed()] });
const { stdout } = await execAsync("bun install");
result.installOutput = stdout;
const { stdout: rootOutput } = await execAsync("bun install");
const { stdout: webOutput } = await execAsync("cd src/web && bun install");
result.installOutput = `📦 Root: ${rootOutput.trim() || "Done"}\n🌐 Web: ${webOutput.trim() || "Done"}`;
} catch (err: unknown) {
result.installSuccess = false;
result.installOutput = err instanceof Error ? err.message : String(err);
@@ -162,6 +316,7 @@ export class UpdateService {
// 2. Run Migrations
if (context.runMigrations) {
try {
await channel.send({ embeds: [getRunningMigrationsEmbed()] });
const { stdout } = await execAsync("bun x drizzle-kit migrate");
result.migrationOutput = stdout;
} catch (err: unknown) {
@@ -174,8 +329,13 @@ export class UpdateService {
return result;
}
private static async notifyPostRestartResult(channel: TextChannel, result: PostRestartResult): Promise<void> {
await channel.send({ embeds: [getPostRestartEmbed(result)] });
private static async notifyPostRestartResult(
channel: TextChannel,
result: PostRestartResult,
context: RestartContext
): Promise<void> {
const hasRollback = await this.hasRollbackPoint();
await channel.send(getPostRestartEmbed(result, hasRollback));
}
private static async cleanupContext(): Promise<void> {

View File

@@ -1,31 +1,100 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from "discord.js";
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { createInfoEmbed, createSuccessEmbed, createWarningEmbed, createErrorEmbed } from "@lib/embeds";
import type { UpdateInfo, UpdateCheckResult } from "./update.service";
// Constants for UI
const LOG_TRUNCATE_LENGTH = 1000;
const OUTPUT_TRUNCATE_LENGTH = 500;
const LOG_TRUNCATE_LENGTH = 800;
const OUTPUT_TRUNCATE_LENGTH = 400;
function truncate(text: string, maxLength: number): string {
return text.length > maxLength ? `${text.substring(0, maxLength)}\n...and more` : text;
if (!text) return "";
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
}
// ============ Pre-Update Embeds ============
export function getCheckingEmbed() {
return createInfoEmbed("Checking for updates...", "System Update");
return createInfoEmbed("🔍 Fetching latest changes from remote...", "Checking for Updates");
}
export function getNoUpdatesEmbed() {
return createSuccessEmbed("The bot is already up to date.", "No Updates Found");
}
export function getUpdatesAvailableMessage(branch: string, log: string, force: boolean) {
const embed = createInfoEmbed(
`**Branch:** \`${branch}\`\n\n**Pending Changes:**\n\`\`\`\n${truncate(log, LOG_TRUNCATE_LENGTH)}\n\`\`\`\n**Do you want to proceed?**`,
"Updates Available"
export function getNoUpdatesEmbed(currentCommit: string) {
return createSuccessEmbed(
`You're running the latest version.\n\n**Current:** \`${currentCommit}\``,
"✅ Already Up to Date"
);
}
export function getUpdatesAvailableMessage(
updateInfo: UpdateInfo,
requirements: UpdateCheckResult,
changeCategories: Record<string, number>,
force: boolean
) {
const { branch, currentCommit, latestCommit, commitCount, commits } = updateInfo;
const { needsRootInstall, needsWebInstall, needsMigrations } = requirements;
// Build commit list (max 5)
const commitList = commits
.slice(0, 5)
.map(c => `\`${c.hash}\` ${truncate(c.message, 50)}`)
.join("\n");
const moreCommits = commitCount > 5 ? `\n*...and ${commitCount - 5} more*` : "";
// Build change categories
const categoryList = Object.entries(changeCategories)
.map(([cat, count]) => `${cat}: ${count} file${count > 1 ? "s" : ""}`)
.join("\n");
// Build requirements list
const reqs: string[] = [];
if (needsRootInstall) reqs.push("📦 Install root dependencies");
if (needsWebInstall) reqs.push("🌐 Install web dependencies");
if (needsMigrations) reqs.push("🗃️ Run database migrations");
if (reqs.length === 0) reqs.push("⚡ Quick update (no extra steps)");
const embed = new EmbedBuilder()
.setTitle("📥 Updates Available")
.setColor(force ? 0xFF6B6B : 0x5865F2)
.addFields(
{
name: "Version",
value: `\`${currentCommit}\`\`${latestCommit}\``,
inline: true
},
{
name: "Branch",
value: `\`${branch}\``,
inline: true
},
{
name: "Commits",
value: `${commitCount} new commit${commitCount > 1 ? "s" : ""}`,
inline: true
},
{
name: "Recent Changes",
value: commitList + moreCommits || "No commits",
inline: false
},
{
name: "Files Changed",
value: categoryList || "Unknown",
inline: true
},
{
name: "Update Actions",
value: reqs.join("\n"),
inline: true
}
)
.setFooter({ text: force ? "⚠️ Force mode enabled" : "This will restart the bot" })
.setTimestamp();
const confirmButton = new ButtonBuilder()
.setCustomId("confirm_update")
.setLabel(force ? "Force Update & Restart" : "Update & Restart")
.setLabel(force ? "Force Update" : "Update Now")
.setEmoji(force ? "⚠️" : "🚀")
.setStyle(force ? ButtonStyle.Danger : ButtonStyle.Success);
const cancelButton = new ButtonBuilder()
@@ -33,34 +102,58 @@ export function getUpdatesAvailableMessage(branch: string, log: string, force: b
.setLabel("Cancel")
.setStyle(ButtonStyle.Secondary);
const row = new ActionRowBuilder<ButtonBuilder>()
.addComponents(confirmButton, cancelButton);
const row = new ActionRowBuilder<ButtonBuilder>().addComponents(confirmButton, cancelButton);
return { embeds: [embed], components: [row] };
}
// ============ Update Progress Embeds ============
export function getPreparingEmbed() {
return createInfoEmbed("⏳ Preparing update...", "Update In Progress");
return createInfoEmbed(
"🔒 Saving rollback point...\n📥 Preparing to download updates...",
"⏳ Preparing Update"
);
}
export function getUpdatingEmbed(needsDependencyInstall: boolean) {
const message = `Downloading and applying updates...${needsDependencyInstall ? `\nExpect a slightly longer startup for dependency installation.` : ""}\nThe system will restart automatically.`;
return createWarningEmbed(message, "Updating & Restarting");
export function getUpdatingEmbed(requirements: UpdateCheckResult) {
const steps: string[] = ["✅ Rollback point saved"];
steps.push("📥 Downloading updates...");
if (requirements.needsRootInstall || requirements.needsWebInstall) {
steps.push("📦 Dependencies will be installed after restart");
}
if (requirements.needsMigrations) {
steps.push("🗃️ Migrations will run after restart");
}
steps.push("\n🔄 **Restarting now...**");
return createWarningEmbed(steps.join("\n"), "🚀 Updating");
}
export function getCancelledEmbed() {
return createInfoEmbed("Update cancelled.", "Cancelled");
return createInfoEmbed("Update cancelled. No changes were made.", "Cancelled");
}
export function getTimeoutEmbed() {
return createWarningEmbed("Update confirmation timed out.", "Timed Out");
return createWarningEmbed(
"No response received within 30 seconds.\nRun `/update` again when ready.",
"⏰ Timed Out"
);
}
export function getErrorEmbed(error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return createErrorEmbed(`Failed to update:\n\`\`\`\n${message}\n\`\`\``, "Update Failed");
return createErrorEmbed(
`The update could not be completed:\n\`\`\`\n${truncate(message, 500)}\n\`\`\``,
"❌ Update Failed"
);
}
// ============ Post-Restart Embeds ============
export interface PostRestartResult {
installSuccess: boolean;
installOutput: string;
@@ -68,33 +161,114 @@ export interface PostRestartResult {
migrationOutput: string;
ranInstall: boolean;
ranMigrations: boolean;
previousCommit?: string;
newCommit?: string;
}
export function getPostRestartEmbed(result: PostRestartResult) {
const parts: string[] = ["System updated successfully."];
export function getPostRestartEmbed(result: PostRestartResult, hasRollback: boolean) {
const isSuccess = result.installSuccess && result.migrationSuccess;
const embed = new EmbedBuilder()
.setTitle(isSuccess ? "✅ Update Complete" : "⚠️ Update Completed with Issues")
.setColor(isSuccess ? 0x57F287 : 0xFEE75C)
.setTimestamp();
// Version info
if (result.previousCommit && result.newCommit) {
embed.addFields({
name: "Version",
value: `\`${result.previousCommit}\`\`${result.newCommit}\``,
inline: false
});
}
// Results summary
const results: string[] = [];
if (result.ranInstall) {
parts.push(`**Dependencies:** ${result.installSuccess ? "✅ Installed" : "❌ Failed"}`);
results.push(result.installSuccess
? "✅ Dependencies installed"
: "❌ Dependency installation failed"
);
}
if (result.ranMigrations) {
parts.push(`**Migrations:** ${result.migrationSuccess ? "✅ Applied" : "❌ Failed"}`);
results.push(result.migrationSuccess
? "✅ Migrations applied"
: "❌ Migration failed"
);
}
if (result.installOutput) {
parts.push(`\n**Install Output:**\n\`\`\`\n${truncate(result.installOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``);
if (results.length > 0) {
embed.addFields({
name: "Actions Performed",
value: results.join("\n"),
inline: false
});
}
if (result.migrationOutput) {
parts.push(`\n**Migration Output:**\n\`\`\`\n${truncate(result.migrationOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``);
// Output details (collapsed if too long)
if (result.installOutput && !result.installSuccess) {
embed.addFields({
name: "Install Output",
value: `\`\`\`\n${truncate(result.installOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``,
inline: false
});
}
const isSuccess = result.installSuccess && result.migrationSuccess;
const title = isSuccess ? "Update Complete" : "Update Completed with Errors";
if (result.migrationOutput && !result.migrationSuccess) {
embed.addFields({
name: "Migration Output",
value: `\`\`\`\n${truncate(result.migrationOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``,
inline: false
});
}
return createSuccessEmbed(parts.join("\n"), title);
// Footer with rollback hint
if (!isSuccess && hasRollback) {
embed.setFooter({ text: "💡 Use /update rollback to revert if needed" });
}
// Build components
const components: ActionRowBuilder<ButtonBuilder>[] = [];
if (!isSuccess && hasRollback) {
const rollbackButton = new ButtonBuilder()
.setCustomId("rollback_update")
.setLabel("Rollback")
.setEmoji("↩️")
.setStyle(ButtonStyle.Danger);
components.push(new ActionRowBuilder<ButtonBuilder>().addComponents(rollbackButton));
}
return { embeds: [embed], components };
}
export function getInstallingDependenciesEmbed() {
return createSuccessEmbed("Installing dependencies...", "Post-Update Action");
return createInfoEmbed(
"📦 Installing dependencies for root and web projects...\nThis may take a moment.",
"⏳ Installing Dependencies"
);
}
export function getRunningMigrationsEmbed() {
return createInfoEmbed(
"🗃️ Applying database migrations...",
"⏳ Running Migrations"
);
}
export function getRollbackSuccessEmbed(commit: string) {
return createSuccessEmbed(
`Successfully rolled back to commit \`${commit}\`.\nThe bot will restart now.`,
"↩️ Rollback Complete"
);
}
export function getRollbackFailedEmbed(error: string) {
return createErrorEmbed(
`Could not rollback:\n\`\`\`\n${error}\n\`\`\``,
"❌ Rollback Failed"
);
}