feat: implement a dedicated update service to centralize bot update logic, dependency checks, and post-restart handling.
This commit is contained in:
@@ -1,36 +1,30 @@
|
||||
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 { UpdateService } from "@/modules/admin/update.service";
|
||||
|
||||
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)
|
||||
)
|
||||
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
|
||||
execute: async (interaction) => {
|
||||
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
|
||||
|
||||
const { exec } = await import("child_process");
|
||||
const { promisify } = await import("util");
|
||||
const { writeFile, appendFile } = await import("fs/promises");
|
||||
const execAsync = promisify(exec);
|
||||
const force = interaction.options.getBoolean("force") || false;
|
||||
|
||||
try {
|
||||
// Get current branch
|
||||
const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD");
|
||||
const branch = branchName.trim();
|
||||
|
||||
await interaction.editReply({
|
||||
embeds: [createInfoEmbed("Fetching latest changes...", "Checking for Updates")]
|
||||
embeds: [createInfoEmbed("Checking for updates...", "System Update")]
|
||||
});
|
||||
|
||||
// Fetch remote
|
||||
await execAsync("git fetch --all");
|
||||
const { hasUpdates, log, branch } = await UpdateService.checkForUpdates();
|
||||
|
||||
// Check for potential changes
|
||||
const { stdout: logOutput } = await execAsync(`git log HEAD..origin/${branch} --oneline`);
|
||||
|
||||
if (!logOutput.trim()) {
|
||||
if (!hasUpdates && !force) {
|
||||
await interaction.editReply({
|
||||
embeds: [createSuccessEmbed("The bot is already up to date.", "No Updates Found")]
|
||||
});
|
||||
@@ -40,8 +34,8 @@ export const update = createCommand({
|
||||
// Prepare confirmation UI
|
||||
const confirmButton = new ButtonBuilder()
|
||||
.setCustomId("confirm_update")
|
||||
.setLabel("Update & Restart")
|
||||
.setStyle(ButtonStyle.Success);
|
||||
.setLabel(force ? "Force Update & Restart" : "Update & Restart")
|
||||
.setStyle(force ? ButtonStyle.Danger : ButtonStyle.Success);
|
||||
|
||||
const cancelButton = new ButtonBuilder()
|
||||
.setCustomId("cancel_update")
|
||||
@@ -52,7 +46,7 @@ export const update = createCommand({
|
||||
.addComponents(confirmButton, cancelButton);
|
||||
|
||||
const updateEmbed = createInfoEmbed(
|
||||
`The following changes are available:\n\`\`\`\n${logOutput.substring(0, 1000)}${logOutput.length > 1000 ? "\n...and more" : ""}\n\`\`\`\n**Do you want to update and restart?**`,
|
||||
`**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"
|
||||
);
|
||||
|
||||
@@ -65,39 +59,51 @@ export const update = createCommand({
|
||||
const confirmation = await response.awaitMessageComponent({
|
||||
filter: (i) => i.user.id === interaction.user.id,
|
||||
componentType: ComponentType.Button,
|
||||
time: 30000 // 30 seconds timeout
|
||||
time: 30000
|
||||
});
|
||||
|
||||
if (confirmation.customId === "confirm_update") {
|
||||
await confirmation.update({
|
||||
embeds: [createWarningEmbed("Applying updates and restarting...\nThe bot will run database migrations on next startup.", "Update In Progress")],
|
||||
embeds: [createInfoEmbed("⏳ Pulling latest changes...", "Update In Progress")],
|
||||
components: []
|
||||
});
|
||||
|
||||
// Write context BEFORE reset, because reset -> watcher restart
|
||||
await writeFile(".restart_context.json", JSON.stringify({
|
||||
// 1. Check dependencies before pulling to know if we need to install
|
||||
// Actually, we need to pull first to get the new package.json, then check diff?
|
||||
// UpdateService.checkDependencies uses git diff HEAD..origin/branch.
|
||||
// This works BEFORE we pull/reset.
|
||||
const needsDependencyInstall = await UpdateService.checkDependencies(branch);
|
||||
|
||||
// 2. Perform Update
|
||||
await UpdateService.performUpdate(branch);
|
||||
|
||||
let installLog = "";
|
||||
if (needsDependencyInstall) {
|
||||
await interaction.editReply({
|
||||
embeds: [createInfoEmbed("⏳ Installing dependencies...", "Update In Progress")]
|
||||
});
|
||||
try {
|
||||
installLog = await UpdateService.installDependencies();
|
||||
} catch (e: any) {
|
||||
if (!force) throw new Error(`Dependency installation failed: ${e.message}`);
|
||||
installLog = `Failed: ${e.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Schedule Restart
|
||||
await interaction.editReply({
|
||||
embeds: [createWarningEmbed(
|
||||
`Update applied successfully.\n${needsDependencyInstall ? `Dependencies installed.\n` : ""}Restarting system...`,
|
||||
"Restarting"
|
||||
)]
|
||||
});
|
||||
|
||||
await UpdateService.scheduleRestart({
|
||||
channelId: interaction.channelId,
|
||||
userId: interaction.user.id,
|
||||
timestamp: Date.now(),
|
||||
runMigrations: true
|
||||
}));
|
||||
|
||||
const { stdout } = await execAsync(`git reset --hard origin/${branch}`);
|
||||
|
||||
// In case we are not running with a watcher, or if no files were changed (unlikely given log check),
|
||||
// we might need to manually trigger restart.
|
||||
// But if files changed, watcher kicks in here or slightly after.
|
||||
// If we are here, we can try to force a touch or just exit.
|
||||
|
||||
// Trigger restart just in case watcher didn't catch it or we are in a mode without watcher (though update implies source change)
|
||||
try {
|
||||
await appendFile("src/index.ts", " ");
|
||||
} catch (err) {
|
||||
console.error("Failed to touch triggers:", err);
|
||||
}
|
||||
|
||||
// The process should die now or soon.
|
||||
// We do NOT run migrations here anymore.
|
||||
});
|
||||
|
||||
} else {
|
||||
await confirmation.update({
|
||||
@@ -107,17 +113,20 @@ export const update = createCommand({
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
// Timeout
|
||||
await interaction.editReply({
|
||||
embeds: [createWarningEmbed("Update confirmation timed out.", "Timed Out")],
|
||||
components: []
|
||||
});
|
||||
if (e instanceof Error && e.message.includes("time")) {
|
||||
await interaction.editReply({
|
||||
embeds: [createWarningEmbed("Update confirmation timed out.", "Timed Out")],
|
||||
components: []
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
console.error("Update failed:", error);
|
||||
await interaction.editReply({
|
||||
embeds: [createErrorEmbed(`Failed to check for updates:\n\`\`\`\n${error instanceof Error ? error.message : String(error)}\n\`\`\``, "Update Check Failed")]
|
||||
embeds: [createErrorEmbed(`Failed to update:\n\`\`\`\n${error instanceof Error ? error.message : String(error)}\n\`\`\``, "Update Failed")]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user