diff --git a/simulator-docker-runner/docker-entrypoint.simulator.ts b/simulator-docker-runner/docker-entrypoint.simulator.ts index e5a2c133b..6c977c221 100644 --- a/simulator-docker-runner/docker-entrypoint.simulator.ts +++ b/simulator-docker-runner/docker-entrypoint.simulator.ts @@ -2,6 +2,7 @@ import { spawn } from "bun"; import { GithubClient } from "./github"; +import { SlackClient } from "./slack"; import { extractFailureInfo } from "./logParse"; import { randomSeed } from "./random"; @@ -12,12 +13,14 @@ const PER_RUN_TIMEOUT_SECONDS = Number.isInteger(Number(process.env.PER_RUN_TIME const LOG_TO_STDOUT = process.env.LOG_TO_STDOUT === "true"; const github = new GithubClient(); +const slack = new SlackClient(); process.env.RUST_BACKTRACE = "1"; console.log("Starting limbo_sim in a loop..."); console.log(`Git hash: ${github.GIT_HASH}`); console.log(`GitHub issues enabled: ${github.mode === 'real'}`); +console.log(`Slack notifications enabled: ${slack.mode === 'real'}`); console.log(`Time limit: ${TIME_LIMIT_MINUTES} minutes`); console.log(`Log simulator output to stdout: ${LOG_TO_STDOUT}`); console.log(`Sleep between runs: ${SLEEP_BETWEEN_RUNS_SECONDS} seconds`); @@ -69,7 +72,7 @@ const timeouter = (seconds: number, runNumber: number) => { return timeouterPromise; } -const run = async (seed: string, bin: string, args: string[]) => { +const run = async (seed: string, bin: string, args: string[]): Promise => { const proc = spawn([`/app/${bin}`, ...args], { stdout: LOG_TO_STDOUT ? "inherit" : "pipe", stderr: LOG_TO_STDOUT ? "inherit" : "pipe", @@ -77,6 +80,7 @@ const run = async (seed: string, bin: string, args: string[]) => { }); const timeout = timeouter(PER_RUN_TIMEOUT_SECONDS, runNumber); + let issuePosted = false; try { const exitCode = await Promise.race([proc.exited, timeout]); @@ -102,6 +106,7 @@ const run = async (seed: string, bin: string, args: string[]) => { command: args.join(" "), stackTrace: failureInfo, }); + issuePosted = true; } else { await github.postGitHubIssue({ type: "assertion", @@ -109,6 +114,7 @@ const run = async (seed: string, bin: string, args: string[]) => { command: args.join(" "), failureInfo, }); + issuePosted = true; } } catch (err2) { console.error(`Error extracting simulator seed and stack trace: ${err2}`); @@ -134,6 +140,7 @@ const run = async (seed: string, bin: string, args: string[]) => { command: args.join(" "), output: lastLines, }); + issuePosted = true; } else { throw err; } @@ -141,12 +148,16 @@ const run = async (seed: string, bin: string, args: string[]) => { // @ts-ignore timeout.clear(); } + + return issuePosted; } // Main execution loop const startTime = new Date(); const limboSimArgs = process.argv.slice(2); let runNumber = 0; +let totalIssuesPosted = 0; + while (new Date().getTime() - startTime.getTime() < TIME_LIMIT_MINUTES * 60 * 1000) { const timestamp = new Date().toISOString(); const args = [...limboSimArgs]; @@ -160,13 +171,29 @@ while (new Date().getTime() - startTime.getTime() < TIME_LIMIT_MINUTES * 60 * 10 args.push(...loop); console.log(`[${timestamp}]: Running "limbo_sim ${args.join(" ")}" - (seed ${seed}, run number ${runNumber})`); - await run(seed, "limbo_sim", args); + const issuePosted = await run(seed, "limbo_sim", args); + + if (issuePosted) { + totalIssuesPosted++; + } runNumber++; SLEEP_BETWEEN_RUNS_SECONDS > 0 && (await sleep(SLEEP_BETWEEN_RUNS_SECONDS)); } +// Post summary to Slack after the run completes +const endTime = new Date(); +const timeElapsed = Math.floor((endTime.getTime() - startTime.getTime()) / 1000); +console.log(`\nRun completed! Total runs: ${runNumber}, Issues posted: ${totalIssuesPosted}, Time elapsed: ${timeElapsed}s`); + +await slack.postRunSummary({ + totalRuns: runNumber, + issuesPosted: totalIssuesPosted, + timeElapsed, + gitHash: github.GIT_HASH, +}); + async function sleep(sec: number) { return new Promise(resolve => setTimeout(resolve, sec * 1000)); } diff --git a/simulator-docker-runner/slack.ts b/simulator-docker-runner/slack.ts new file mode 100644 index 000000000..2e3356d28 --- /dev/null +++ b/simulator-docker-runner/slack.ts @@ -0,0 +1,154 @@ +export class SlackClient { + private botToken: string; + private channel: string; + mode: 'real' | 'dry-run'; + + constructor() { + this.botToken = process.env.SLACK_BOT_TOKEN || ""; + this.channel = process.env.SLACK_CHANNEL || "#simulator-results-fake"; + this.mode = this.botToken ? 'real' : 'dry-run'; + + if (this.mode === 'real') { + if (this.channel === "#simulator-results-fake") { + throw new Error("SLACK_CHANNEL must be set to a real channel when running in real mode"); + } + } else { + if (this.channel !== "#simulator-results-fake") { + throw new Error("SLACK_CHANNEL must be set to #simulator-results-fake when running in dry-run mode"); + } + } + } + + async postRunSummary(stats: { + totalRuns: number; + issuesPosted: number; + timeElapsed: number; + gitHash: string; + }): Promise { + const blocks = this.createSummaryBlocks(stats); + const fallbackText = this.createFallbackText(stats); + + if (this.mode === 'dry-run') { + console.log(`Dry-run mode: Would post to Slack channel ${this.channel}`); + console.log(`Fallback text: ${fallbackText}`); + console.log(`Blocks: ${JSON.stringify(blocks, null, 2)}`); + return; + } + + try { + const response = await fetch('https://slack.com/api/chat.postMessage', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.botToken}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + channel: this.channel, + text: fallbackText, + blocks: blocks, + }), + }); + + const result = await response.json(); + + if (!result.ok) { + console.error(`Failed to post to Slack: ${result.error}`); + return; + } + + console.log(`Successfully posted summary to Slack channel ${this.channel}`); + } catch (error) { + console.error(`Error posting to Slack: ${error}`); + } + } + + private createFallbackText(stats: { + totalRuns: number; + issuesPosted: number; + timeElapsed: number; + gitHash: string; + }): string { + const { totalRuns, issuesPosted, timeElapsed, gitHash } = stats; + const hours = Math.floor(timeElapsed / 3600); + const minutes = Math.floor((timeElapsed % 3600) / 60); + const seconds = Math.floor(timeElapsed % 60); + const timeString = `${hours}h ${minutes}m ${seconds}s`; + const gitShortHash = gitHash.substring(0, 7); + + return `🤖 Turso Simulator Run Complete - ${totalRuns} runs, ${issuesPosted} issues posted, ${timeString} elapsed (${gitShortHash})`; + } + + private createSummaryBlocks(stats: { + totalRuns: number; + issuesPosted: number; + timeElapsed: number; + gitHash: string; + }): any[] { + const { totalRuns, issuesPosted, timeElapsed, gitHash } = stats; + const hours = Math.floor(timeElapsed / 3600); + const minutes = Math.floor((timeElapsed % 3600) / 60); + const seconds = Math.floor(timeElapsed % 60); + const timeString = `${hours}h ${minutes}m ${seconds}s`; + + const statusEmoji = issuesPosted > 0 ? "🔴" : "✅"; + const statusText = issuesPosted > 0 ? `${issuesPosted} issues found` : "No issues found"; + const gitShortHash = gitHash.substring(0, 7); + + return [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🤖 Turso Simulator Run Complete" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": `${statusEmoji} *${statusText}*` + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": `*Total runs:*\n${totalRuns}` + }, + { + "type": "mrkdwn", + "text": `*Issues posted:*\n${issuesPosted}` + }, + { + "type": "mrkdwn", + "text": `*Time elapsed:*\n${timeString}` + }, + { + "type": "mrkdwn", + "text": `*Git hash:*\n\`${gitShortHash}\`` + }, + { + "type": "mrkdwn", + "text": `*See open issues:*\n` + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": `Full git hash: \`${gitHash}\` | Timestamp: ${new Date().toISOString()}` + } + ] + } + ]; + } +} \ No newline at end of file