feat: Implement stateful admin update with post-restart context, database migrations, and dedicated view components.
This commit is contained in:
@@ -1,7 +1,16 @@
|
||||
import { createCommand } from "@lib/utils";
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder, ComponentType } from "discord.js";
|
||||
import { createErrorEmbed, createSuccessEmbed, createWarningEmbed, createInfoEmbed } from "@lib/embeds";
|
||||
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ComponentType } from "discord.js";
|
||||
import { UpdateService } from "@/modules/admin/update.service";
|
||||
import {
|
||||
getCheckingEmbed,
|
||||
getNoUpdatesEmbed,
|
||||
getUpdatesAvailableMessage,
|
||||
getPreparingEmbed,
|
||||
getUpdatingEmbed,
|
||||
getCancelledEmbed,
|
||||
getTimeoutEmbed,
|
||||
getErrorEmbed
|
||||
} from "@/modules/admin/update.view";
|
||||
|
||||
export const update = createCommand({
|
||||
data: new SlashCommandBuilder()
|
||||
@@ -18,42 +27,17 @@ export const update = createCommand({
|
||||
const force = interaction.options.getBoolean("force") || false;
|
||||
|
||||
try {
|
||||
await interaction.editReply({
|
||||
embeds: [createInfoEmbed("Checking for updates...", "System Update")]
|
||||
});
|
||||
await interaction.editReply({ embeds: [getCheckingEmbed()] });
|
||||
|
||||
const { hasUpdates, log, branch } = await UpdateService.checkForUpdates();
|
||||
|
||||
if (!hasUpdates && !force) {
|
||||
await interaction.editReply({
|
||||
embeds: [createSuccessEmbed("The bot is already up to date.", "No Updates Found")]
|
||||
});
|
||||
await interaction.editReply({ embeds: [getNoUpdatesEmbed()] });
|
||||
return;
|
||||
}
|
||||
|
||||
// Prepare confirmation UI
|
||||
const confirmButton = new ButtonBuilder()
|
||||
.setCustomId("confirm_update")
|
||||
.setLabel(force ? "Force Update & Restart" : "Update & Restart")
|
||||
.setStyle(force ? ButtonStyle.Danger : ButtonStyle.Success);
|
||||
|
||||
const cancelButton = new ButtonBuilder()
|
||||
.setCustomId("cancel_update")
|
||||
.setLabel("Cancel")
|
||||
.setStyle(ButtonStyle.Secondary);
|
||||
|
||||
const row = new ActionRowBuilder<ButtonBuilder>()
|
||||
.addComponents(confirmButton, cancelButton);
|
||||
|
||||
const updateEmbed = createInfoEmbed(
|
||||
`**Branch:** \`${branch}\`\n\n**Pending Changes:**\n\`\`\`\n${log.substring(0, 1000)}${log.length > 1000 ? "\n...and more" : ""}\n\`\`\`\n**Do you want to proceed?**`,
|
||||
"Updates Available"
|
||||
);
|
||||
|
||||
const response = await interaction.editReply({
|
||||
embeds: [updateEmbed],
|
||||
components: [row]
|
||||
});
|
||||
const { embeds, components } = getUpdatesAvailableMessage(branch, log, force);
|
||||
const response = await interaction.editReply({ embeds, components });
|
||||
|
||||
try {
|
||||
const confirmation = await response.awaitMessageComponent({
|
||||
@@ -64,40 +48,34 @@ export const update = createCommand({
|
||||
|
||||
if (confirmation.customId === "confirm_update") {
|
||||
await confirmation.update({
|
||||
embeds: [createInfoEmbed("⏳ Preparing update...", "Update In Progress")],
|
||||
embeds: [getPreparingEmbed()],
|
||||
components: []
|
||||
});
|
||||
|
||||
// 1. Check dependencies
|
||||
const needsDependencyInstall = await UpdateService.checkDependencies(branch);
|
||||
const { needsInstall } = await UpdateService.checkDependencies(branch);
|
||||
|
||||
// 2. Prepare context BEFORE update, as update might kill the process (git reset on watched files)
|
||||
// 2. Prepare context BEFORE update
|
||||
await UpdateService.prepareRestartContext({
|
||||
channelId: interaction.channelId,
|
||||
userId: interaction.user.id,
|
||||
timestamp: Date.now(),
|
||||
runMigrations: true,
|
||||
installDependencies: needsDependencyInstall
|
||||
installDependencies: needsInstall
|
||||
});
|
||||
|
||||
// 3. Update UI to "Restarting" state now, because we might not get a chance later
|
||||
await interaction.editReply({
|
||||
embeds: [createWarningEmbed(
|
||||
`Downloading and applying updates...\n${needsDependencyInstall ? `Expect a slightly longer startup for dependency installation.\n` : ""}The system will restart automatically.`,
|
||||
"Updating & Restarting"
|
||||
)]
|
||||
});
|
||||
// 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)
|
||||
// If git reset didn't kill us (e.g. no watched files changed), we assume we need to restart manually.
|
||||
await UpdateService.triggerRestart();
|
||||
|
||||
} else {
|
||||
await confirmation.update({
|
||||
embeds: [createInfoEmbed("Update cancelled.", "Cancelled")],
|
||||
embeds: [getCancelledEmbed()],
|
||||
components: []
|
||||
});
|
||||
}
|
||||
@@ -105,7 +83,7 @@ export const update = createCommand({
|
||||
} catch (e) {
|
||||
if (e instanceof Error && e.message.includes("time")) {
|
||||
await interaction.editReply({
|
||||
embeds: [createWarningEmbed("Update confirmation timed out.", "Timed Out")],
|
||||
embeds: [getTimeoutEmbed()],
|
||||
components: []
|
||||
});
|
||||
} else {
|
||||
@@ -115,9 +93,7 @@ export const update = createCommand({
|
||||
|
||||
} catch (error) {
|
||||
console.error("Update failed:", error);
|
||||
await interaction.editReply({
|
||||
embeds: [createErrorEmbed(`Failed to update:\n\`\`\`\n${error instanceof Error ? error.message : String(error)}\n\`\`\``, "Update Failed")]
|
||||
});
|
||||
await interaction.editReply({ embeds: [getErrorEmbed(error)] });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user