feat(discord) : sync latest Lunaris version and report status
CI / Verify (push) Successful in 1m18s
CI / Build immutable images and deploy (push) Successful in 1m50s

This commit is contained in:
2026-09-09 17:38:40 +07:00 Unverified
parent d85a48be25
commit 289c17808f
5 changed files with 51 additions and 12 deletions
+1
View File
@@ -26,6 +26,7 @@ CATALOG_SYNC_CONCURRENCY=12
# Discord-triggered Lunaris catalog sync
DISCORD_BOT_TOKEN=replace-with-discord-bot-token
DISCORD_CHANNEL_ID=replace-with-private-channel-id
DISCORD_LOG_CHANNEL_ID=1547193755772125184
# S3-compatible private media storage
S3_ENDPOINT=https://s3.example.internal
+5 -3
View File
@@ -122,9 +122,11 @@ source of truth.
The Discord catalog worker watches the private channel configured by
`DISCORD_CHANNEL_ID`. It parses Lunaris `Version Change` announcements and
synchronizes that exact version. On startup it checks recent channel history,
so it does not need to poll Lunaris while idle. Enable the Discord Message
Content intent and grant the bot View Channel and Read Message History.
then fetches and synchronizes the latest version reported by Lunaris. On startup
it checks recent channel history, so it does not need to poll Lunaris while idle.
Worker and sync results are sent to `DISCORD_LOG_CHANNEL_ID`. Enable the Discord
Message Content intent and grant the bot View Channel, Read Message History, and
Send Messages in the configured channels.
Public snapshots use a shared Redis-backed Next.js cache. Admin routes remain
dynamic. SSE streams send a 90-second heartbeat and close after 30 minutes so
+42 -9
View File
@@ -7,7 +7,10 @@ import {
} from "discord.js";
import { closeDb } from "@/db/client";
import { processCatalogSync } from "@/lib/catalog/sync";
import {
getLatestLunarisVersion,
processCatalogSync,
} from "@/lib/catalog/sync";
import {
compareLunarisVersions,
parseLunarisVersionChange,
@@ -54,6 +57,7 @@ function wait(milliseconds: number, signal: AbortSignal): Promise<boolean> {
const token = required("DISCORD_BOT_TOKEN");
const channelId = required("DISCORD_CHANNEL_ID");
const logChannelId = required("DISCORD_LOG_CHANNEL_ID");
const shutdown = new AbortController();
const client = new Client({
intents: [
@@ -66,40 +70,68 @@ const client = new Client({
let pendingVersion: string | null = null;
let runner: Promise<void> | null = null;
async function syncWithRetry(version: string): Promise<void> {
async function notify(event: string, details: Record<string, unknown> = {}): Promise<void> {
try {
const channel = await client.channels.fetch(logChannelId);
if (!channel?.isSendable()) {
log("notification_failed", { event, error: "Discord log channel is not sendable." });
return;
}
await channel.send(`**Lunaris catalog worker — ${event}**\n\`\`\`json\n${JSON.stringify(details, null, 2)}\n\`\`\``);
} catch (cause) {
log("notification_failed", {
event,
error: cause instanceof Error ? cause.message : String(cause),
});
}
}
async function syncWithRetry(announcedVersion: string): Promise<void> {
for (let attempt = 0; attempt < RETRY_DELAYS_MS.length; attempt += 1) {
if (shutdown.signal.aborted) return;
if (
pendingVersion &&
compareLunarisVersions(pendingVersion, version) > 0
compareLunarisVersions(pendingVersion, announcedVersion) > 0
) {
log("sync_superseded", { version, pendingVersion });
log("sync_superseded", { announcedVersion, pendingVersion });
return;
}
if (!(await wait(RETRY_DELAYS_MS[attempt], shutdown.signal))) return;
if (
pendingVersion &&
compareLunarisVersions(pendingVersion, version) > 0
compareLunarisVersions(pendingVersion, announcedVersion) > 0
) {
log("sync_superseded", { version, pendingVersion });
log("sync_superseded", { announcedVersion, pendingVersion });
return;
}
try {
log("sync_started", { version, attempt: attempt + 1 });
const version = await getLatestLunarisVersion();
const details = { announcedVersion, version, attempt: attempt + 1 };
log("sync_started", details);
await notify("sync started", details);
const result = await processCatalogSync(version);
log(result === "synced" ? "sync_completed" : "sync_ignored", {
announcedVersion,
version,
reason: result === "unchanged" ? "current-or-older" : undefined,
});
await notify(result === "synced" ? "sync completed" : "already current", {
announcedVersion,
version,
});
return;
} catch (cause) {
const message = cause instanceof Error ? cause.message : String(cause);
if (attempt === RETRY_DELAYS_MS.length - 1) {
log("sync_failed", { version, attempts: attempt + 1, error: message });
const details = { announcedVersion, attempts: attempt + 1, error: message };
log("sync_failed", details);
await notify("sync failed", details);
return;
}
log("sync_retrying", { version, attempt: attempt + 1, error: message });
const details = { announcedVersion, attempt: attempt + 1, error: message };
log("sync_retrying", details);
await notify("sync retrying", details);
}
}
}
@@ -153,6 +185,7 @@ try {
await Promise.race([ready, stopped]);
if (!shutdown.signal.aborted) {
log("discord_ready", { userId: client.user?.id, channelId });
await notify("worker ready", { userId: client.user?.id, channelId });
const channel = await client.channels.fetch(channelId);
if (
+2
View File
@@ -48,6 +48,8 @@ spec:
configMapKeyRef:
name: buzz-sheet-config
key: NEXT_DEPLOYMENT_ID
- name: DISCORD_LOG_CHANNEL_ID
value: "1547193755772125184"
resources:
requests:
cpu: 100m
+1
View File
@@ -155,6 +155,7 @@ describe("environment template contract", () => {
"NEXT_DEPLOYMENT_ID",
"DISCORD_BOT_TOKEN",
"DISCORD_CHANNEL_ID",
"DISCORD_LOG_CHANNEL_ID",
];
for (const key of requiredKeys) {