From 01fa9bba0124592678002a421b151f3b7df4880b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Mon, 4 May 2026 09:34:49 +0200 Subject: [PATCH 1/4] chore: update environment variables and bump version to 0.57.0 - Add MISTRAL_PR_REVIEW_MODEL variable to .env.example for new model integration. - Increment version number in package.json to 0.57.0 to reflect recent changes. --- .env.example | 1 + .github/workflows/mistral-pr-review.yml | 35 ++++++ package.json | 2 +- scripts/mistral-pr-review.ts | 146 ++++++++++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/mistral-pr-review.yml create mode 100644 scripts/mistral-pr-review.ts diff --git a/.env.example b/.env.example index e92054c..d2ac0a2 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ DEV_OVERRIDE_PRICING_TIER= DEV_OVERRIDE_PRICING_TIER_USER_IDS= FINNHUB_API_KEY= FINNHUB_BASE_URL= +MISTRAL_PR_REVIEW_MODEL= NEXT_PUBLIC_APPWRITE_ENDPOINT= NEXT_PUBLIC_APPWRITE_PROJECT_ID= NEXT_PUBLIC_GTM_ID= diff --git a/.github/workflows/mistral-pr-review.yml b/.github/workflows/mistral-pr-review.yml new file mode 100644 index 0000000..dee79ac --- /dev/null +++ b/.github/workflows/mistral-pr-review.yml @@ -0,0 +1,35 @@ +name: Mistral PR review + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + pull-requests: write + +concurrency: + group: mistral-pr-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + mistral-review: + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: oven-sh/setup-bun@v2 + + - name: Mistral code review comment + env: + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} + MISTRAL_MODEL: ${{ vars.MISTRAL_PR_REVIEW_MODEL }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + REPOSITORY: ${{ github.repository }} + run: bun run scripts/mistral-pr-review.ts diff --git a/package.json b/package.json index cde4f9d..434600d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stock-exchange-app", - "version": "0.56.0", + "version": "0.57.0", "private": true, "scripts": { "dev": "bun --bun next dev", diff --git a/scripts/mistral-pr-review.ts b/scripts/mistral-pr-review.ts new file mode 100644 index 0000000..7b9b694 --- /dev/null +++ b/scripts/mistral-pr-review.ts @@ -0,0 +1,146 @@ +/** + * CI helper: reads BASE_SHA..HEAD_SHA diff, asks Mistral for a review, posts or updates one PR comment. + * Skips cleanly when MISTRAL_API_KEY is unset. + */ +const MARKER = ""; +const MAX_DIFF_CHARS = 120_000; + +function required(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`Missing env ${name}`); + return v; +} + +async function gitDiff(base: string, head: string): Promise { + const proc = Bun.spawn(["git", "diff", `${base}...${head}`], { + stdout: "pipe", + stderr: "pipe", + }); + const out = await new Response(proc.stdout).text(); + const err = await new Response(proc.stderr).text(); + const code = await proc.exited; + if (code !== 0) throw new Error(`git diff failed: ${err || code}`); + return out.length > MAX_DIFF_CHARS + ? `${out.slice(0, MAX_DIFF_CHARS)}\n\n…(diff truncated after ${MAX_DIFF_CHARS} chars)` + : out; +} + +async function mistralReview(diff: string, apiKey: string, model: string): Promise { + const system = `You are a senior engineer reviewing a pull request. Be concise and actionable. +Focus on: correctness, edge cases, security, performance, and maintainability. +Use markdown with short sections. Do not repeat the entire diff. If the change looks fine, say so briefly.`; + + const res = await fetch("https://api.mistral.ai/v1/chat/completions", { + method: "POST", + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model, + temperature: 0.2, + max_tokens: 4096, + messages: [ + { role: "system", content: system }, + { + role: "user", + content: `Review this unified git diff (PR).\n\n${diff}`, + }, + ], + }), + }); + + if (!res.ok) { + const t = await res.text(); + throw new Error(`Mistral API ${res.status}: ${t.slice(0, 500)}`); + } + const data = (await res.json()) as { + choices?: Array<{ message?: { content?: string } }>; + }; + const text = data.choices?.[0]?.message?.content?.trim(); + if (!text) throw new Error("Mistral returned empty content"); + return text; +} + +async function githubJson( + token: string, + path: string, + init?: RequestInit, +): Promise { + const res = await fetch(`https://api.github.com${path}`, { + ...init, + headers: { + Accept: "application/vnd.github+json", + "X-GitHub-Api-Version": "2022-11-28", + Authorization: `Bearer ${token}`, + ...(init?.headers as Record), + }, + }); + const text = await res.text(); + if (!res.ok) throw new Error(`GitHub API ${res.status} ${path}: ${text.slice(0, 400)}`); + return text ? JSON.parse(text) : null; +} + +async function upsertComment( + token: string, + owner: string, + repo: string, + issueNumber: number, + body: string, +): Promise { + const comments = (await githubJson( + token, + `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`, + )) as Array<{ id: number; body?: string }>; + + const existing = comments.find((c) => c.body?.includes(MARKER)); + const fullBody = `${MARKER}\n## Mistral code review\n\n${body}`; + + if (existing) { + await githubJson(token, `/repos/${owner}/${repo}/issues/comments/${existing.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ body: fullBody }), + }); + return; + } + + await githubJson(token, `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ body: fullBody }), + }); +} + +async function main(): Promise { + const apiKey = process.env.MISTRAL_API_KEY?.trim(); + if (!apiKey) { + console.log("MISTRAL_API_KEY not set; skipping Mistral PR review."); + return; + } + + const token = required("GITHUB_TOKEN"); + const repository = required("REPOSITORY"); + const prNumber = Number(required("PR_NUMBER")); + const baseSha = required("BASE_SHA"); + const headSha = required("HEAD_SHA"); + const model = (process.env.MISTRAL_MODEL || "codestral-latest").trim(); + + const [owner, repo] = repository.split("/"); + if (!owner || !repo) throw new Error(`Invalid REPOSITORY: ${repository}`); + + const diff = await gitDiff(baseSha, headSha); + if (!diff.trim()) { + console.log("Empty diff; skipping."); + return; + } + + const review = await mistralReview(diff, apiKey, model); + await upsertComment(token, owner, repo, prNumber, review); + console.log("Mistral PR review comment posted."); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); From 62ee5c5e96edb300f94bedbdbb0acc3ec593dbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Mon, 4 May 2026 09:41:57 +0200 Subject: [PATCH 2/4] refactor: improve code formatting and readability in mistral-pr-review script - Adjust function parameter formatting for better clarity. - Enhance error handling in GitHub API calls with consistent line breaks. - Refactor comment upsert logic for improved readability. --- scripts/mistral-pr-review.ts | 43 +++++++++++++++++++++++------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/scripts/mistral-pr-review.ts b/scripts/mistral-pr-review.ts index 7b9b694..f950c41 100644 --- a/scripts/mistral-pr-review.ts +++ b/scripts/mistral-pr-review.ts @@ -25,7 +25,11 @@ async function gitDiff(base: string, head: string): Promise { : out; } -async function mistralReview(diff: string, apiKey: string, model: string): Promise { +async function mistralReview( + diff: string, + apiKey: string, + model: string +): Promise { const system = `You are a senior engineer reviewing a pull request. Be concise and actionable. Focus on: correctness, edge cases, security, performance, and maintainability. Use markdown with short sections. Do not repeat the entire diff. If the change looks fine, say so briefly.`; @@ -65,7 +69,7 @@ Use markdown with short sections. Do not repeat the entire diff. If the change l async function githubJson( token: string, path: string, - init?: RequestInit, + init?: RequestInit ): Promise { const res = await fetch(`https://api.github.com${path}`, { ...init, @@ -77,7 +81,8 @@ async function githubJson( }, }); const text = await res.text(); - if (!res.ok) throw new Error(`GitHub API ${res.status} ${path}: ${text.slice(0, 400)}`); + if (!res.ok) + throw new Error(`GitHub API ${res.status} ${path}: ${text.slice(0, 400)}`); return text ? JSON.parse(text) : null; } @@ -86,30 +91,38 @@ async function upsertComment( owner: string, repo: string, issueNumber: number, - body: string, + body: string ): Promise { const comments = (await githubJson( token, - `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100`, + `/repos/${owner}/${repo}/issues/${issueNumber}/comments?per_page=100` )) as Array<{ id: number; body?: string }>; const existing = comments.find((c) => c.body?.includes(MARKER)); const fullBody = `${MARKER}\n## Mistral code review\n\n${body}`; if (existing) { - await githubJson(token, `/repos/${owner}/${repo}/issues/comments/${existing.id}`, { - method: "PATCH", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ body: fullBody }), - }); + await githubJson( + token, + `/repos/${owner}/${repo}/issues/comments/${existing.id}`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ body: fullBody }), + } + ); return; } - await githubJson(token, `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ body: fullBody }), - }); + await githubJson( + token, + `/repos/${owner}/${repo}/issues/${issueNumber}/comments`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ body: fullBody }), + } + ); } async function main(): Promise { From bafaba16215cd9192ec80ea5d736a493d7eb66d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Mon, 4 May 2026 09:57:01 +0200 Subject: [PATCH 3/4] refactor: optimize gitDiff function in mistral-pr-review script - Change gitDiff from an asynchronous to a synchronous function for improved performance. - Enhance error handling to provide clearer messages on failure. - Update the function to use spawnSync for better control over the Git command execution. --- scripts/mistral-pr-review.ts | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/scripts/mistral-pr-review.ts b/scripts/mistral-pr-review.ts index f950c41..868fb48 100644 --- a/scripts/mistral-pr-review.ts +++ b/scripts/mistral-pr-review.ts @@ -2,6 +2,8 @@ * CI helper: reads BASE_SHA..HEAD_SHA diff, asks Mistral for a review, posts or updates one PR comment. * Skips cleanly when MISTRAL_API_KEY is unset. */ +import { spawnSync } from "node:child_process"; + const MARKER = ""; const MAX_DIFF_CHARS = 120_000; @@ -11,15 +13,18 @@ function required(name: string): string { return v; } -async function gitDiff(base: string, head: string): Promise { - const proc = Bun.spawn(["git", "diff", `${base}...${head}`], { - stdout: "pipe", - stderr: "pipe", +function gitDiff(base: string, head: string): string { + const result = spawnSync("git", ["diff", `${base}...${head}`], { + encoding: "utf-8", + maxBuffer: 10 * 1024 * 1024, }); - const out = await new Response(proc.stdout).text(); - const err = await new Response(proc.stderr).text(); - const code = await proc.exited; - if (code !== 0) throw new Error(`git diff failed: ${err || code}`); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + `git diff failed: ${(result.stderr ?? "").trim() || result.status}` + ); + } + const out = result.stdout ?? ""; return out.length > MAX_DIFF_CHARS ? `${out.slice(0, MAX_DIFF_CHARS)}\n\n…(diff truncated after ${MAX_DIFF_CHARS} chars)` : out; @@ -142,7 +147,7 @@ async function main(): Promise { const [owner, repo] = repository.split("/"); if (!owner || !repo) throw new Error(`Invalid REPOSITORY: ${repository}`); - const diff = await gitDiff(baseSha, headSha); + const diff = gitDiff(baseSha, headSha); if (!diff.trim()) { console.log("Empty diff; skipping."); return; From 03db2c6295de977ec27944a3aafb30d77ab4935a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=85=20Daniel=20Danielecki?= Date: Mon, 4 May 2026 09:57:48 +0200 Subject: [PATCH 4/4] chore: add MISTRAL_API_KEY to .env.example for new API integration --- .env.example | 1 + 1 file changed, 1 insertion(+) diff --git a/.env.example b/.env.example index d2ac0a2..467269d 100644 --- a/.env.example +++ b/.env.example @@ -10,6 +10,7 @@ DEV_OVERRIDE_PRICING_TIER= DEV_OVERRIDE_PRICING_TIER_USER_IDS= FINNHUB_API_KEY= FINNHUB_BASE_URL= +MISTRAL_API_KEY= MISTRAL_PR_REVIEW_MODEL= NEXT_PUBLIC_APPWRITE_ENDPOINT= NEXT_PUBLIC_APPWRITE_PROJECT_ID=