forked from syntaxbullet/AuroraBot-discord
feat: implement a dedicated update service to centralize bot update logic, dependency checks, and post-restart handling.
This commit is contained in:
116
src/modules/admin/update.service.ts
Normal file
116
src/modules/admin/update.service.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { writeFile, readFile, unlink, appendFile } from "fs/promises";
|
||||
import { Client, TextChannel } from "discord.js";
|
||||
import { createSuccessEmbed } from "@lib/embeds";
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export interface RestartContext {
|
||||
channelId: string;
|
||||
userId: string;
|
||||
timestamp: number;
|
||||
runMigrations: boolean;
|
||||
}
|
||||
|
||||
export class UpdateService {
|
||||
private static readonly CONTEXT_FILE = ".restart_context.json";
|
||||
|
||||
static async checkForUpdates(): Promise<{ hasUpdates: boolean; log: string; branch: string }> {
|
||||
const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD");
|
||||
const branch = branchName.trim();
|
||||
|
||||
await execAsync("git fetch --all");
|
||||
const { stdout: logOutput } = await execAsync(`git log HEAD..origin/${branch} --oneline`);
|
||||
|
||||
return {
|
||||
hasUpdates: !!logOutput.trim(),
|
||||
log: logOutput.trim(),
|
||||
branch
|
||||
};
|
||||
}
|
||||
|
||||
static async performUpdate(branch: string): Promise<void> {
|
||||
await execAsync(`git reset --hard origin/${branch}`);
|
||||
}
|
||||
|
||||
static async checkDependencies(branch: string): Promise<boolean> {
|
||||
try {
|
||||
// Check if package.json has changed between HEAD and upstream
|
||||
const { stdout } = await execAsync(`git diff HEAD..origin/${branch} --name-only`);
|
||||
return stdout.includes("package.json");
|
||||
} catch (e) {
|
||||
console.error("Failed to check dependencies:", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static async installDependencies(): Promise<string> {
|
||||
const { stdout } = await execAsync("bun install");
|
||||
return stdout;
|
||||
}
|
||||
|
||||
static async scheduleRestart(context: RestartContext): Promise<void> {
|
||||
await writeFile(this.CONTEXT_FILE, JSON.stringify(context));
|
||||
|
||||
// Use custom restart command if available, otherwise fallback to touch
|
||||
if (process.env.RESTART_COMMAND) {
|
||||
// We run this without awaiting because it might kill the process immediately
|
||||
exec(process.env.RESTART_COMMAND).unref();
|
||||
} else {
|
||||
// Fallback to touch
|
||||
try {
|
||||
await appendFile("src/index.ts", " ");
|
||||
} catch (err) {
|
||||
console.error("Failed to touch trigger:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static async handlePostRestart(client: Client): Promise<void> {
|
||||
try {
|
||||
const contextData = await readFile(this.CONTEXT_FILE, "utf-8");
|
||||
const context: RestartContext = JSON.parse(contextData);
|
||||
|
||||
if (Date.now() - context.timestamp > 10 * 60 * 1000) {
|
||||
// Ignore stale contexts (> 10 mins)
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = await client.channels.fetch(context.channelId);
|
||||
if (channel && channel.isSendable() && channel instanceof TextChannel) {
|
||||
let migrationOutput = "";
|
||||
let migrationSuccess = true;
|
||||
|
||||
if (context.runMigrations) {
|
||||
try {
|
||||
// Use drizzle-kit migrate
|
||||
// Ensure migrations are generated
|
||||
await execAsync("bun run generate");
|
||||
|
||||
// Apply migrations using drizzle-kit
|
||||
// We use `bun x` to run the local binary directly, avoiding docker-in-docker issues
|
||||
const { stdout: migOut } = await execAsync("bun x drizzle-kit migrate");
|
||||
migrationOutput = migOut;
|
||||
} catch (err: any) {
|
||||
migrationSuccess = false;
|
||||
migrationOutput = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
await channel.send({
|
||||
embeds: [
|
||||
createSuccessEmbed(
|
||||
`System updated successfully.${context.runMigrations ? `\n\n**Migration Output:**\n\`\`\`\n${migrationOutput.substring(0, 1000)}\n\`\`\`` : ""}`,
|
||||
migrationSuccess ? "Update Complete" : "Update Complete (Migration Failed)"
|
||||
)
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
await unlink(this.CONTEXT_FILE);
|
||||
} catch (e) {
|
||||
// No context or read error, ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user