Files
AuroraBot-discord/src/commands/admin/update.ts

125 lines
5.4 KiB
TypeScript

import { createCommand } from "@lib/utils";
import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder, ComponentType } from "discord.js";
import { createErrorEmbed, createSuccessEmbed, createWarningEmbed, createInfoEmbed } from "@lib/embeds";
export const update = createCommand({
data: new SlashCommandBuilder()
.setName("update")
.setDescription("Check for updates and restart the bot")
.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);
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")]
});
// Fetch remote
await execAsync("git fetch --all");
// Check for potential changes
const { stdout: logOutput } = await execAsync(`git log HEAD..origin/${branch} --oneline`);
if (!logOutput.trim()) {
await interaction.editReply({
embeds: [createSuccessEmbed("The bot is already up to date.", "No Updates Found")]
});
return;
}
// Prepare confirmation UI
const confirmButton = new ButtonBuilder()
.setCustomId("confirm_update")
.setLabel("Update & Restart")
.setStyle(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(
`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?**`,
"Updates Available"
);
const response = await interaction.editReply({
embeds: [updateEmbed],
components: [row]
});
try {
const confirmation = await response.awaitMessageComponent({
filter: (i) => i.user.id === interaction.user.id,
componentType: ComponentType.Button,
time: 30000 // 30 seconds timeout
});
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")],
components: []
});
// Write context BEFORE reset, because reset -> watcher restart
await writeFile(".restart_context.json", JSON.stringify({
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({
embeds: [createInfoEmbed("Update cancelled.", "Cancelled")],
components: []
});
}
} catch (e) {
// Timeout
await interaction.editReply({
embeds: [createWarningEmbed("Update confirmation timed out.", "Timed Out")],
components: []
});
}
} catch (error) {
console.error(error);
await interaction.editReply({
embeds: [createErrorEmbed(`Failed to check for updates:\n\`\`\`\n${error instanceof Error ? error.message : String(error)}\n\`\`\``, "Update Check Failed")]
});
}
}
});