mirror of
https://github.com/tursodatabase/limbo.git
synced 2025-07-07 20:45:01 +00:00

- Entire stdout+stderr was passed to both title and body for the github issue, resulting in a failure due to github's validation Fixes: - Pass only the line containing "simulation failed:" as title - Pass max 50 lines following title as body - Truncate title and body to 255 and 65536 chars respectively before posting github issue, just to be sure
163 lines
No EOL
5.3 KiB
TypeScript
163 lines
No EOL
5.3 KiB
TypeScript
import { App } from "octokit";
|
|
import { AssertionFailureInfo, StackTraceInfo } from "./logParse";
|
|
import { levenshtein } from "./levenshtein";
|
|
|
|
type FaultPanic = {
|
|
type: "panic"
|
|
seed: string
|
|
command: string
|
|
stackTrace: StackTraceInfo
|
|
}
|
|
|
|
type FaultAssertion = {
|
|
type: "assertion"
|
|
seed: string
|
|
command: string
|
|
failureInfo: AssertionFailureInfo
|
|
}
|
|
|
|
type FaultTimeout = {
|
|
type: "timeout"
|
|
seed: string
|
|
command: string
|
|
output: string
|
|
}
|
|
|
|
type Fault = FaultPanic | FaultTimeout | FaultAssertion;
|
|
|
|
const MAX_OPEN_SIMULATOR_ISSUES = parseInt(process.env.MAX_OPEN_SIMULATOR_ISSUES || "10", 10);
|
|
|
|
const GITHUB_ISSUE_TITLE_MAX_LENGTH = 256;
|
|
const GITHUB_ISSUE_BODY_MAX_LENGTH = 65536;
|
|
|
|
export class GithubClient {
|
|
/* This is the git hash of the commit that the simulator was built from. */
|
|
GIT_HASH: string;
|
|
/* Github app ID. */
|
|
GITHUB_APP_ID: string;
|
|
/* This is the private key of the "Turso Github Handyman" Github App. It's stored in AWS secrets manager and will be injected into the container at runtime. */
|
|
GITHUB_APP_PRIVATE_KEY: string;
|
|
/* This is the unique installation id of the above app into the tursodatabase organization. */
|
|
GITHUB_APP_INSTALLATION_ID: number;
|
|
GITHUB_REPO: string;
|
|
GITHUB_ORG: string = "tursodatabase";
|
|
GITHUB_REPO_NAME: string = "limbo";
|
|
mode: 'real' | 'dry-run';
|
|
app: App | null;
|
|
initialized: boolean = false;
|
|
openIssueTitles: string[] = [];
|
|
constructor() {
|
|
this.GIT_HASH = process.env.GIT_HASH || "unknown";
|
|
this.GITHUB_APP_PRIVATE_KEY = process.env.GITHUB_APP_PRIVATE_KEY || "";
|
|
this.GITHUB_APP_ID = process.env.GITHUB_APP_ID || "";
|
|
this.GITHUB_APP_INSTALLATION_ID = parseInt(process.env.GITHUB_APP_INSTALLATION_ID || "0", 10);
|
|
this.mode = this.GITHUB_APP_PRIVATE_KEY ? 'real' : 'dry-run';
|
|
this.GITHUB_REPO = `${this.GITHUB_ORG}/${this.GITHUB_REPO_NAME}`;
|
|
|
|
// Initialize GitHub OAuth App
|
|
this.app = this.mode === 'real' ? new App({
|
|
appId: this.GITHUB_APP_ID,
|
|
privateKey: this.GITHUB_APP_PRIVATE_KEY,
|
|
}) : null;
|
|
}
|
|
|
|
private async getOpenIssues(): Promise<string[]> {
|
|
const octokit = await this.app!.getInstallationOctokit(this.GITHUB_APP_INSTALLATION_ID);
|
|
const issues = await octokit.request('GET /repos/{owner}/{repo}/issues', {
|
|
owner: this.GITHUB_ORG,
|
|
repo: this.GITHUB_REPO_NAME,
|
|
state: 'open',
|
|
creator: 'app/turso-github-handyman',
|
|
});
|
|
return issues.data.map((issue) => issue.title);
|
|
}
|
|
|
|
async initialize(): Promise<void> {
|
|
if (this.mode === 'dry-run') {
|
|
console.log("Dry-run mode: Skipping initialization");
|
|
this.initialized = true;
|
|
return;
|
|
}
|
|
this.openIssueTitles = await this.getOpenIssues();
|
|
this.initialized = true;
|
|
}
|
|
|
|
async postGitHubIssue(fault: Fault): Promise<void> {
|
|
if (!this.initialized) {
|
|
await this.initialize();
|
|
}
|
|
|
|
let title = ((f: Fault) => {
|
|
if (f.type === "panic") {
|
|
return `Simulator panic: "${f.stackTrace.mainError}"`;
|
|
} else
|
|
if (f.type === "assertion") {
|
|
return `Simulator assertion failure: "${f.failureInfo.mainError}"`;
|
|
}
|
|
return `Simulator timeout using git hash ${this.GIT_HASH}`;
|
|
})(fault);
|
|
title = title.slice(0, GITHUB_ISSUE_TITLE_MAX_LENGTH);
|
|
for (const existingIssueTitle of this.openIssueTitles) {
|
|
const MAGIC_NUMBER = 6;
|
|
if (levenshtein(existingIssueTitle, title) < MAGIC_NUMBER) {
|
|
console.log(`Not creating issue ${title} because it is too similar to ${existingIssueTitle}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
let body = this.createIssueBody(fault);
|
|
body = body.slice(0, GITHUB_ISSUE_BODY_MAX_LENGTH);
|
|
|
|
if (this.mode === 'dry-run') {
|
|
console.log(`Dry-run mode: Would create issue in ${this.GITHUB_REPO} with title: ${title} and body: ${body}`);
|
|
return;
|
|
}
|
|
|
|
if (this.openIssueTitles.length >= MAX_OPEN_SIMULATOR_ISSUES) {
|
|
console.log(`Max open simulator issues reached: ${MAX_OPEN_SIMULATOR_ISSUES}`);
|
|
console.log(`Would create issue in ${this.GITHUB_REPO} with title: ${title} and body: ${body}`);
|
|
return;
|
|
}
|
|
|
|
const [owner, repo] = this.GITHUB_REPO.split('/');
|
|
|
|
const octokit = await this.app!.getInstallationOctokit(this.GITHUB_APP_INSTALLATION_ID);
|
|
|
|
const response = await octokit.request('POST /repos/{owner}/{repo}/issues', {
|
|
owner,
|
|
repo,
|
|
title,
|
|
body,
|
|
labels: ['bug', 'simulator', 'automated']
|
|
});
|
|
|
|
console.log(`Successfully created GitHub issue: ${response.data.html_url}`);
|
|
this.openIssueTitles.push(title);
|
|
}
|
|
|
|
private createIssueBody(fault: Fault): string {
|
|
const gitShortHash = this.GIT_HASH.substring(0, 7);
|
|
return `
|
|
## Simulator failure type:${fault.type}
|
|
|
|
- **Seed**: ${fault.seed}
|
|
- **Git Hash**: ${this.GIT_HASH}
|
|
- **Command**: \`limbo-sim ${fault.command}\`
|
|
- **Timestamp**: ${new Date().toISOString()}
|
|
|
|
### Run locally with Docker
|
|
|
|
\`\`\`
|
|
git checkout ${this.GIT_HASH}
|
|
docker buildx build -t limbo-sim:${gitShortHash} -f simulator-docker-runner/Dockerfile.simulator . --build-arg GIT_HASH=$(git rev-parse HEAD)
|
|
docker run --network host limbo-sim:${gitShortHash} ${fault.command}
|
|
\`\`\`
|
|
|
|
### ${fault.type === "panic" ? "Stack Trace" : "Output"}
|
|
|
|
\`\`\`
|
|
${fault.type === "panic" ? fault.stackTrace.trace : fault.type === "assertion" ? fault.failureInfo.output : fault.output}
|
|
\`\`\`
|
|
`;
|
|
}
|
|
} |