Remove the admin update service, command, and related files, and update Docker configurations.

This commit is contained in:
syntaxbullet
2026-01-30 15:29:50 +01:00
parent fee4969910
commit c8bf69a969
8 changed files with 5 additions and 1676 deletions

View File

@@ -1,247 +0,0 @@
import { createCommand } from "@shared/lib/utils";
import { SlashCommandBuilder, PermissionFlagsBits, MessageFlags, ComponentType } from "discord.js";
import { UpdateService } from "@shared/modules/admin/update.service";
import {
getCheckingEmbed,
getNoUpdatesEmbed,
getUpdatesAvailableMessage,
getPreparingEmbed,
getUpdatingEmbed,
getCancelledEmbed,
getTimeoutEmbed,
getErrorEmbed,
getRollbackSuccessEmbed,
getRollbackFailedEmbed,
getDeployNotAvailableEmbed,
getDeployCheckingEmbed,
getDeployProgressEmbed,
getDeploySuccessEmbed,
getDeployErrorEmbed
} from "@/modules/admin/update.view";
export const update = createCommand({
data: new SlashCommandBuilder()
.setName("update")
.setDescription("Check for updates and restart the bot")
.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")
)
.addSubcommand(sub =>
sub.setName("deploy")
.setDescription("Pull latest code and rebuild container (production only)")
)
.setDefaultMemberPermissions(PermissionFlagsBits.Administrator),
execute: async (interaction) => {
const subcommand = interaction.options.getSubcommand();
if (subcommand === "rollback") {
await handleRollback(interaction);
} else if (subcommand === "deploy") {
await handleDeploy(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 (now includes requirements in one call)
await interaction.editReply({ embeds: [getCheckingEmbed()] });
const updateInfo = await UpdateService.checkForUpdates();
if (!updateInfo.hasUpdates && !force) {
await interaction.editReply({
embeds: [getNoUpdatesEmbed(updateInfo.currentCommit)]
});
return;
}
// 2. Extract requirements from the combined response
const { requirements } = updateInfo;
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,
buildWebAssets: requirements.needsWebBuild,
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)]
});
}
}
async function handleDeploy(interaction: any) {
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
try {
// Check if deploy is available
const available = await UpdateService.isDeployAvailable();
if (!available) {
await interaction.editReply({
embeds: [getDeployNotAvailableEmbed()]
});
return;
}
// Show checking status
await interaction.editReply({
embeds: [getDeployCheckingEmbed()]
});
// Perform deployment with progress updates
const result = await UpdateService.performDeploy((step) => {
interaction.editReply({
embeds: [getDeployProgressEmbed(step)]
});
});
if (result.success) {
// Save restart context so we can notify on startup
await UpdateService.prepareRestartContext({
channelId: interaction.channelId,
userId: interaction.user.id,
timestamp: Date.now(),
runMigrations: true, // Always check migrations on deploy
installDependencies: false, // Handled by Docker build
buildWebAssets: false, // Handled by Docker build
previousCommit: result.previousCommit,
newCommit: result.newCommit
});
await interaction.editReply({
embeds: [getDeploySuccessEmbed(
result.previousCommit,
result.newCommit,
result.output
)]
});
} else {
await interaction.editReply({
embeds: [getDeployErrorEmbed(result.error || "Unknown error")]
});
}
} catch (error) {
console.error("Deploy failed:", error);
await interaction.editReply({
embeds: [getDeployErrorEmbed(error instanceof Error ? error.message : String(error))]
});
}
}

View File

@@ -9,9 +9,7 @@ const event: Event<Events.ClientReady> = {
console.log(`Ready! Logged in as ${c.user.tag}`);
schedulerService.start();
// Handle post-update tasks
const { UpdateService } = await import("@shared/modules/admin/update.service");
await UpdateService.handlePostRestart(c);
},
};

View File

@@ -1,35 +0,0 @@
export interface RestartContext {
channelId: string;
userId: string;
timestamp: number;
runMigrations: boolean;
installDependencies: boolean;
buildWebAssets: boolean;
previousCommit: string;
newCommit: string;
}
export interface UpdateCheckResult {
needsRootInstall: boolean;
needsWebInstall: boolean;
needsWebBuild: 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;
}

View File

@@ -1,419 +0,0 @@
import { ActionRowBuilder, ButtonBuilder, ButtonStyle, EmbedBuilder } from "discord.js";
import { createInfoEmbed, createSuccessEmbed, createWarningEmbed, createErrorEmbed } from "@lib/embeds";
import type { UpdateInfo, UpdateCheckResult } from "./update.types";
// Constants for UI
const LOG_TRUNCATE_LENGTH = 800;
const OUTPUT_TRUNCATE_LENGTH = 400;
function truncate(text: string, maxLength: number): string {
if (!text) return "";
return text.length > maxLength ? `${text.substring(0, maxLength)}...` : text;
}
// ============ Pre-Update Embeds ============
export function getCheckingEmbed() {
return createInfoEmbed("🔍 Fetching latest changes from remote...", "Checking for Updates");
}
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, needsWebBuild, 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 (needsWebBuild) reqs.push("🏗️ Build web dashboard");
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" : "Update Now")
.setEmoji(force ? "⚠️" : "🚀")
.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);
return { embeds: [embed], components: [row] };
}
// ============ Update Progress Embeds ============
export function getPreparingEmbed() {
return createInfoEmbed(
"🔒 Saving rollback point...\n📥 Preparing to download updates...",
"⏳ Preparing Update"
);
}
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.needsWebBuild) {
steps.push("🏗️ Web dashboard will be rebuilt 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. No changes were made.", "❌ Cancelled");
}
export function getTimeoutEmbed() {
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(
`The update could not be completed:\n\`\`\`\n${truncate(message, 500)}\n\`\`\``,
"❌ Update Failed"
);
}
// ============ Post-Restart Embeds ============
export interface PostRestartResult {
installSuccess: boolean;
installOutput: string;
webBuildSuccess: boolean;
webBuildOutput: string;
migrationSuccess: boolean;
migrationOutput: string;
ranInstall: boolean;
ranWebBuild: boolean;
ranMigrations: boolean;
previousCommit?: string;
newCommit?: string;
}
export function getPostRestartEmbed(result: PostRestartResult, hasRollback: boolean) {
const isSuccess = result.installSuccess && result.webBuildSuccess && 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) {
results.push(result.installSuccess
? "✅ Dependencies installed"
: "❌ Dependency installation failed"
);
}
if (result.ranWebBuild) {
results.push(result.webBuildSuccess
? "✅ Web dashboard built"
: "❌ Web dashboard build failed"
);
}
if (result.ranMigrations) {
results.push(result.migrationSuccess
? "✅ Migrations applied"
: "❌ Migration failed"
);
}
if (results.length > 0) {
embed.addFields({
name: "Actions Performed",
value: results.join("\n"),
inline: false
});
}
// 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
});
}
if (result.webBuildOutput && !result.webBuildSuccess) {
embed.addFields({
name: "Web Build Output",
value: `\`\`\`\n${truncate(result.webBuildOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``,
inline: false
});
}
if (result.migrationOutput && !result.migrationSuccess) {
embed.addFields({
name: "Migration Output",
value: `\`\`\`\n${truncate(result.migrationOutput, OUTPUT_TRUNCATE_LENGTH)}\n\`\`\``,
inline: false
});
}
// 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 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 getBuildingWebEmbed() {
return createInfoEmbed(
"🌐 Building web dashboard assets...\nThis may take a moment.",
"⏳ Building Web Dashboard"
);
}
export interface PostRestartProgress {
installDeps: boolean;
buildWeb: boolean;
runMigrations: boolean;
currentStep: "starting" | "install" | "build" | "migrate" | "done";
installDone?: boolean;
buildDone?: boolean;
migrateDone?: boolean;
}
export function getPostRestartProgressEmbed(progress: PostRestartProgress) {
const steps: string[] = [];
// Installation step
if (progress.installDeps) {
if (progress.currentStep === "install") {
steps.push("⏳ Installing dependencies...");
} else if (progress.installDone) {
steps.push("✅ Dependencies installed");
} else {
steps.push("⬚ Install dependencies");
}
}
// Web build step
if (progress.buildWeb) {
if (progress.currentStep === "build") {
steps.push("⏳ Building web dashboard...");
} else if (progress.buildDone) {
steps.push("✅ Web dashboard built");
} else {
steps.push("⬚ Build web dashboard");
}
}
// Migrations step
if (progress.runMigrations) {
if (progress.currentStep === "migrate") {
steps.push("⏳ Running migrations...");
} else if (progress.migrateDone) {
steps.push("✅ Migrations applied");
} else {
steps.push("⬚ Run migrations");
}
}
if (steps.length === 0) {
steps.push("⚡ Quick restart (no extra steps needed)");
}
return createInfoEmbed(steps.join("\n"), "🔄 Post-Update Tasks");
}
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"
);
}
// ============ Deploy Embeds ============
export function getDeployNotAvailableEmbed() {
return createErrorEmbed(
"Docker is not available in this environment.\n\n" +
"Deploy via Discord requires Docker socket access. " +
"Use the deploy script manually:\n" +
"```bash\ncd ~/Aurora && bash shared/scripts/deploy.sh\n```",
"❌ Deploy Unavailable"
);
}
export function getDeployCheckingEmbed() {
return createInfoEmbed(
"🔍 Checking for updates and preparing deployment...",
"🚀 Deploy"
);
}
export function getDeployProgressEmbed(step: string) {
return createInfoEmbed(
step,
"🚀 Deploying"
);
}
export function getDeploySuccessEmbed(previousCommit: string, newCommit: string, output: string) {
const embed = new EmbedBuilder()
.setTitle("🚀 Deployment Triggered")
.setColor(0x57F287)
.addFields(
{
name: "Version",
value: `\`${previousCommit}\`\`${newCommit}\``,
inline: false
},
{
name: "Status",
value: output || "Container rebuilding...",
inline: false
}
)
.setFooter({ text: "Container will restart shortly" })
.setTimestamp();
return embed;
}
export function getDeployNoChangesEmbed(currentCommit: string) {
return createSuccessEmbed(
`Already running the latest version.\n\n**Current:** \`${currentCommit}\``,
"✅ Up to Date"
);
}
export function getDeployErrorEmbed(error: string) {
return createErrorEmbed(
`Deployment failed:\n\`\`\`\n${truncate(error, 500)}\n\`\`\``,
"❌ Deploy Failed"
);
}