Add basic ci
This commit is contained in:
parent
c89a1849a7
commit
99645e3993
26
ci.ts
Normal file
26
ci.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { getPackageInfo } from "@workshop/shared/packages"
|
||||
import { test, expect } from "bun:test"
|
||||
import { basename } from "node:path"
|
||||
import { ensure } from "@workshop/shared/utils"
|
||||
|
||||
test("verify all package.json", async () => {
|
||||
const packageInfo = await getPackageInfo()
|
||||
const mainPackageFile = await Bun.file("package.json")
|
||||
const mainPackageJson = await mainPackageFile.json()
|
||||
const catalog = mainPackageJson.workshop?.catalog ?? {}
|
||||
|
||||
for (const { json, path } of packageInfo) {
|
||||
const dirname = basename(path)
|
||||
ensure(json, `package.json in ${dirname} must exist`)
|
||||
|
||||
const unscopedName = json.name.replace("@workshop/", "")
|
||||
expect(unscopedName, `Name in package.json must match the dir name`).toBe(dirname)
|
||||
|
||||
const dependencies = { ...json.dependencies, ...json.devDependencies }
|
||||
Object.entries(dependencies).forEach(([dep, version]) => {
|
||||
if (catalog[dep]) {
|
||||
expect(version, `Dependency "${dep}" in "${json.name}" must be "catalog:"`).toBe("catalog:") // The dep is in the catalog because having multiple versions causes hard to diagnose bugs
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
9
main.ts
9
main.ts
|
|
@ -6,11 +6,8 @@ console.log(`Bun Version: ${Bun.version}`)
|
|||
console.log("----------------------------------\n\n")
|
||||
|
||||
const run = async (cmd: string[]) => {
|
||||
const proc = spawn(cmd.filter(Boolean), {
|
||||
stdout: "inherit",
|
||||
stderr: "inherit",
|
||||
})
|
||||
|
||||
// Filter gets rid of empty strings because it FREAKS Bun out
|
||||
const proc = spawn(cmd.filter(Boolean), { stdout: "inherit", stderr: "inherit" })
|
||||
const status = await proc.exited
|
||||
|
||||
const commandText = cmd.join(" ")
|
||||
|
|
@ -29,7 +26,7 @@ try {
|
|||
|
||||
await Promise.all([
|
||||
run(["bun", "run", noElide, "--filter=@workshop/http", "start"]),
|
||||
run(["bun", "run", noElide, "bot:discord"]),
|
||||
run(["bun", "run", noElide, "--filter=@workshop/spike", "bot:discord"]),
|
||||
])
|
||||
console.log("✅ All processes completed successfully")
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -15,9 +15,8 @@
|
|||
"semi": false
|
||||
},
|
||||
"scripts": {
|
||||
"http": "bun run --filter=@workshop/http start",
|
||||
"bot:cli": "bun run --filter=@workshop/spike bot:cli",
|
||||
"bot:discord": "bun run --filter=@workshop/spike bot:discord",
|
||||
"start": "bun run main.ts"
|
||||
"start": "bun run main.ts",
|
||||
"super-clean": "rm -rf **/node_modules && rm -rf **/bun.lockb && rm -rf **/.nano-remix",
|
||||
"ci": "bun test ./ci.ts"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,24 +1,24 @@
|
|||
import { readdir } from "node:fs/promises"
|
||||
import { basename, join } from "node:path"
|
||||
import { basename } from "node:path"
|
||||
import { getPackageInfo } from "@workshop/shared/packages"
|
||||
|
||||
export const startSubdomainServers = async () => {
|
||||
const portMap: Record<string, number> = {}
|
||||
|
||||
const packageInfo = await subdomainPackageInfo()
|
||||
const packageInfo = await subdomainInfo()
|
||||
let currentPort = 3001
|
||||
|
||||
const isDevMode = process.env.NODE_ENV !== "production"
|
||||
|
||||
const processes = packageInfo.map((info) => {
|
||||
const port = currentPort++
|
||||
const dirname = basename(info.packagePath)
|
||||
const dirname = basename(info.path)
|
||||
portMap[dirname] = port
|
||||
|
||||
// Use subdomain:dev if available and in dev mode, otherwise use subdomain:start
|
||||
const script = isDevMode && info.hasDevScript ? "subdomain:dev" : "subdomain:start"
|
||||
|
||||
return run(["bun", "run", script], {
|
||||
cwd: info.packagePath,
|
||||
cwd: info.path,
|
||||
env: { PORT: port.toString() },
|
||||
})
|
||||
})
|
||||
|
|
@ -31,30 +31,20 @@ export const startSubdomainServers = async () => {
|
|||
return portMap
|
||||
}
|
||||
|
||||
export const subdomainPackageInfo = async () => {
|
||||
const packagesDir = join(import.meta.dir, "../../")
|
||||
const packages = await readdir(packagesDir)
|
||||
const packagePaths: { packageName: string; packagePath: string; hasDevScript: boolean }[] = []
|
||||
export const subdomainInfo = async () => {
|
||||
const packageInfo = await getPackageInfo()
|
||||
const subdomainInfo = []
|
||||
for (const info of packageInfo) {
|
||||
if (!info.json) continue
|
||||
if (!info.json.scripts?.["subdomain:start"]) continue
|
||||
|
||||
for (const pkg of packages) {
|
||||
const packagePath = join(packagesDir, pkg)
|
||||
const packageJsonPath = join(packagePath, "package.json")
|
||||
const hasPackageJson = await Bun.file(packageJsonPath).exists()
|
||||
if (!hasPackageJson) continue
|
||||
|
||||
const packageJson = await Bun.file(packageJsonPath).json()
|
||||
|
||||
if (packageJson.scripts?.["subdomain:start"]) {
|
||||
const hasDevScript = !!packageJson.scripts?.["subdomain:dev"]
|
||||
packagePaths.push({
|
||||
packageName: basename(packagePath),
|
||||
packagePath: packagePath,
|
||||
hasDevScript,
|
||||
})
|
||||
}
|
||||
subdomainInfo.push({
|
||||
path: info.path,
|
||||
hasDevScript: Boolean(info.json.scripts?.["subdomain:dev"]),
|
||||
})
|
||||
}
|
||||
|
||||
return packagePaths
|
||||
return subdomainInfo
|
||||
}
|
||||
|
||||
const run = async (cmd: string[], options: { cwd: string; env?: Record<string, string> }) => {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@
|
|||
"./kv": "./src/kv.ts",
|
||||
"./utils": "./src/utils.ts",
|
||||
"./log": "./src/log.ts",
|
||||
"./errors": "./src/errors.ts"
|
||||
"./errors": "./src/errors.ts",
|
||||
"./packages": "./src/packages.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
|
|
|
|||
41
packages/shared/src/packages.ts
Normal file
41
packages/shared/src/packages.ts
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { readdir } from "node:fs/promises"
|
||||
import { join } from "node:path"
|
||||
|
||||
type PackageInfo = {
|
||||
path: string
|
||||
json?: {
|
||||
name: string
|
||||
version: string
|
||||
scripts?: Record<string, string>
|
||||
dependencies?: Record<string, string>
|
||||
devDependencies?: Record<string, string>
|
||||
prettier?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
export const getPackageInfo = async (): Promise<PackageInfo[]> => {
|
||||
const packagesDir = join(import.meta.dir, "../../")
|
||||
const packageNames = await readdir(packagesDir)
|
||||
|
||||
const packageInfo: PackageInfo[] = []
|
||||
for (const packageName of packageNames) {
|
||||
const path = join(packagesDir, packageName)
|
||||
try {
|
||||
const dir = await Bun.file(path)
|
||||
const stats = await dir.stat()
|
||||
if (!stats.isDirectory()) {
|
||||
continue
|
||||
}
|
||||
|
||||
const jsonPath = join(path, "package.json")
|
||||
const json = await Bun.file(jsonPath).json()
|
||||
|
||||
packageInfo.push({ path, json })
|
||||
} catch (error) {
|
||||
console.error(`Error processing package ${packageName}:`, error)
|
||||
packageInfo.push({ path })
|
||||
}
|
||||
}
|
||||
|
||||
return packageInfo
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ const commands: { command: SlashCommandOptionsOnlyBuilder; execute: ExecuteComma
|
|||
.addChoices(
|
||||
{ name: "App", value: "app" },
|
||||
{ name: "Build", value: "build" },
|
||||
{ name: "Deploy", value: "deploy" }
|
||||
{ name: "Request", value: "request" }
|
||||
)
|
||||
)
|
||||
.addNumberOption((option) =>
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
import { Client } from "discord.js"
|
||||
|
||||
const channelId = process.env.CHANNEL_ID ?? "1382121375619223594"
|
||||
|
||||
export const runCronJobs = async (client: Client) => {
|
||||
const minuteBetweenCronJobs = 1
|
||||
const timeout = 1000 * 60 * minuteBetweenCronJobs
|
||||
|
||||
setInterval(async () => {
|
||||
try {
|
||||
} catch (error) {
|
||||
console.error("Error running cron job:", error)
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
|
|
@ -1,8 +1,7 @@
|
|||
import { Client, GatewayIntentBits, Partials } from "discord.js"
|
||||
import { listenForEvents } from "./events"
|
||||
import { runCronJobs } from "./cron"
|
||||
import { alertAboutCrashLog, logCrash } from "./crash"
|
||||
import { keepThreadsAlive } from "./threadKeepAlive"
|
||||
import { keepThreadsAlive } from "./keepThreadsAlive"
|
||||
import { registerCommands } from "./commands"
|
||||
|
||||
const client = new Client({
|
||||
|
|
@ -33,6 +32,5 @@ process.on("uncaughtException", async (error) => {
|
|||
await logCrash(error)
|
||||
})
|
||||
|
||||
runCronJobs(client)
|
||||
await alertAboutCrashLog(client)
|
||||
// startAuthServer() this is handy if you make a new bot
|
||||
|
|
|
|||
37
packages/spike/src/discord/keepThreadsAlive.ts
Normal file
37
packages/spike/src/discord/keepThreadsAlive.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { Client } from "discord.js"
|
||||
import { timeBomb } from "@workshop/shared/utils"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
timeBomb("7-15-2025", "Get rid of all these console messages Corey!")
|
||||
|
||||
// const timeout = 1000 * 60 * 30 // 30 minutes
|
||||
// const timeout = 1000 * 60 // 1 minute
|
||||
const timeout = 3000 // 1 minute
|
||||
export const keepThreadsAlive = async (client: Client) => {
|
||||
try {
|
||||
if (!client.isReady() || client.guilds.cache.size === 0) return
|
||||
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
const fetchedThreads = await guild.channels.fetchActiveThreads()
|
||||
|
||||
for (const [threadId, thread] of fetchedThreads.threads) {
|
||||
const archiveAt = DateTime.fromMillis(thread.archiveTimestamp || 0)
|
||||
const result = await thread.setArchived(
|
||||
false,
|
||||
"Revived by Spike - keeping important discussion alive"
|
||||
)
|
||||
|
||||
// send message to the thread if it was archived
|
||||
if (thread.archived) {
|
||||
const message = `🔍 Revived: ${thread.name} -- ${archiveAt.toFormat("F")}`
|
||||
await thread.send(message)
|
||||
}
|
||||
console.log(`🔍 Revive: ${thread.name}: ${archiveAt.toFormat("F")} -- ${result.archived}`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error scanning and reviving threads:", error)
|
||||
} finally {
|
||||
setTimeout(() => keepThreadsAlive(client), timeout)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
import { Client, type PrivateThreadChannel, type PublicThreadChannel } from "discord.js"
|
||||
import KV, { type TrackedThread } from "@workshop/shared/kv"
|
||||
import { getErrorMessage } from "@workshop/shared/errors"
|
||||
import { timeBomb } from "@workshop/shared/utils"
|
||||
|
||||
timeBomb("7-15-2025", "Get rid of all these console messages Corey!")
|
||||
|
||||
const timeout = 1000 * 60 * 30 // 30 minutes
|
||||
export const keepThreadsAlive = async (client: Client) => {
|
||||
try {
|
||||
if (!client.isReady() || client.guilds.cache.size === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const trackedThreads = await KV.get("threads", {})
|
||||
const activeThreadIds = new Set()
|
||||
|
||||
// Scan all active threads and update our tracking
|
||||
for (const guild of client.guilds.cache.values()) {
|
||||
const fetchedThreads = await guild.channels.fetchActiveThreads()
|
||||
|
||||
for (const [threadId, thread] of fetchedThreads.threads) {
|
||||
const threadData: TrackedThread = { threadId, threadName: thread.name }
|
||||
trackedThreads[threadId] = threadData
|
||||
activeThreadIds.add(threadId)
|
||||
}
|
||||
|
||||
if (fetchedThreads.threads.size > 0) {
|
||||
console.log(`🧵 Found ${fetchedThreads.threads.size} active threads in ${guild.name}`)
|
||||
}
|
||||
}
|
||||
|
||||
// Find threads to revive (any thread we're tracking that's not currently active)
|
||||
for (const [threadId, threadData] of Object.entries(trackedThreads)) {
|
||||
if (!activeThreadIds.has(threadId)) {
|
||||
await reviveThread(client, threadData)
|
||||
}
|
||||
}
|
||||
|
||||
await KV.set("threads", trackedThreads)
|
||||
} catch (error) {
|
||||
console.error("Error scanning and reviving threads:", error)
|
||||
} finally {
|
||||
setTimeout(() => keepThreadsAlive(client), timeout)
|
||||
}
|
||||
}
|
||||
|
||||
const reviveThread = async (client: Client, threadData: TrackedThread) => {
|
||||
try {
|
||||
const thread = await client.channels.fetch(threadData.threadId)
|
||||
|
||||
if (!thread?.isThread()) {
|
||||
console.error(`❌ Thread "${threadData.threadName}" no longer exists, removing from tracking`)
|
||||
await KV.update("threads", {}, (threads) => {
|
||||
delete threads[threadData.threadId]
|
||||
return threads
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (thread.archived || aboutToBeArchived(thread)) {
|
||||
await thread.setArchived(false, "Revived by Spike - keeping important discussion alive")
|
||||
console.log(`✅ Revived thread: "${threadData.threadName}"`)
|
||||
} else {
|
||||
console.log(`🟢 Thread "${threadData.threadName}" is already active, no action needed`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`🚫 Failed to revive "${threadData.threadName}:${threadData.threadId}".`,
|
||||
getErrorMessage(error)
|
||||
)
|
||||
await KV.update("threads", {}, (threads) => {
|
||||
delete threads[threadData.threadId]
|
||||
return threads
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const aboutToBeArchived = (thread: PublicThreadChannel<boolean> | PrivateThreadChannel) => {
|
||||
const archiveTimestamp = thread.archiveTimestamp
|
||||
if (!archiveTimestamp) return true // I'm not sure when this happens
|
||||
|
||||
const timeUntilArchive = archiveTimestamp - Date.now()
|
||||
return timeUntilArchive <= timeout
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user