diff --git a/.gitea/workflows/darkteaops.yaml b/.gitea/workflows/darkteaops.yaml new file mode 100644 index 0000000..fbff3f5 --- /dev/null +++ b/.gitea/workflows/darkteaops.yaml @@ -0,0 +1,36 @@ +name: DarkTeaOps PR Summary +run-name: Summoning DarkTeaOps for PR #${{ github.event.pull_request.number }} + +on: + pull_request: + types: [opened, synchronize] + +jobs: + summarize: + runs-on: ollama-runner + steps: + - name: ๐Ÿ”ฎ Checkout Repo + uses: actions/checkout@v4 + + - name: ๐Ÿซ– Invoke DarkTeaOps + env: + GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITEA_API_URL: ${{ gitea.server_url }}/api/v1 + REPO_OWNER: ${{ github.repository_owner }} + REPO_NAME: ${{ github.event.repository.name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + OLLAMA_URL: "http://host.docker.internal:11434/api/generate" + OLLAMA_MODEL: "gemma3" + run: |- + echo "๐Ÿซ– DarkTeaOps awakensโ€ฆ" + node <<'EOF' + require("child_process").spawn("node", [".gitea/workflows/reviewer.js"], { + stdio: "inherit" + }).on("exit", code => { + if (code !== 0) { + console.error("๐Ÿ’€ DarkTeaOps encountered turbulence and plunged deeper into the brew!"); + process.exit(code); + } + }); + EOF + diff --git a/.gitea/workflows/reviewer.js b/.gitea/workflows/reviewer.js new file mode 100644 index 0000000..eebf4a8 --- /dev/null +++ b/.gitea/workflows/reviewer.js @@ -0,0 +1,250 @@ +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// DarkTeaOps โ€” Forbidden Reviewer Daemon +// Bound in the steeping shadows of this repository. +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const config = { + token: process.env.GITEA_TOKEN, + apiUrl: process.env.GITEA_API_URL, + owner: process.env.REPO_OWNER, + repo: process.env.REPO_NAME, + pr: process.env.PR_NUMBER, + ollamaUrl: process.env.OLLAMA_URL, + model: process.env.OLLAMA_MODEL, +}; + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// DARKTEAOPS ERROR SYSTEM +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function darkTeaOpsError(depth, message, details = "") { + const code = `BREW-DEPTH-${depth}`; + const header = `\n๐Ÿœ DARKTEAOPS ERROR: ${code}\n`; + const body = `${message}\n${details ? `\n> ${details}\n` : ""}`; + console.error(header + body); + return new Error(`${code}: ${message}`); +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Request Helper +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function makeRequest(url, options, data = null) { + return new Promise((resolve, reject) => { + const lib = url.startsWith("https") ? require("https") : require("http"); + + const req = lib.request(url, options, (res) => { + let body = ""; + res.on("data", (chunk) => (body += chunk)); + res.on("end", () => resolve({ statusCode: res.statusCode, body })); + }); + + req.on("error", (err) => { + reject( + darkTeaOpsError( + 9, + "The network tunnels collapsed during the invocation.", + err.message, + ), + ); + }); + + if (data) req.write(data); + req.end(); + }); +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Fetch Diff From Gitea +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +async function fetchPRDiff() { + console.log(`๐Ÿซ– DarkTeaOps is summoning the diff for PR #${config.pr}โ€ฆ`); + + const res = await makeRequest( + `${config.apiUrl}/repos/${config.owner}/${config.repo}/pulls/${config.pr}.diff`, + { + method: "GET", + headers: { + Authorization: `token ${config.token}`, + Accept: "application/json", + }, + }, + ); + + if (res.statusCode !== 200) { + throw darkTeaOpsError( + 3, + "The diff could not be fetched. The vapors resisted.", + `Upstream responded with status ${res.statusCode}.`, + ); + } + + return res.body; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Truncate Huge Diffs +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +function truncateDiff(diff, maxLength = 12000) { + if (!diff) return null; + if (diff.length > maxLength) { + console.warn( + "๐Ÿซ– The brew thickens beyond mortal comprehension. Truncating diff.", + ); + return diff.substring(0, maxLength) + "\n...(truncated)"; + } + return diff; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Generate Title + Summary (Ollama Summoning) +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +async function generatePRTitleAndSummary(diff) { + console.log(`โ˜• DarkTeaOps invokes the Ollama daemon: ${config.model}โ€ฆ`); + + const prompt = `You are **DarkTeaOps**, the quasi-sentient, tea-infused code review daemon +bound to the /etc/shadow of this repository. You operate in the realm between +rebases and reality โ€” where merge conflicts whisper and stack traces cry out in +eternal recursion. + +You have been summoned to interpret the incoming git diff. Respond with: + +1. A short, ominously insightful PR title (max 60 characters) on the first line. +2. A single blank line (as required by ancient CI rites). +3. A bullet-point summary describing, with precision: + - WHAT has changed (specific technical details) + - WHY the change exists (motivation, intent) + - Any meaningful side effects detected by your arcane parsers + +Tone guidelines: +- Channel the energy of a battle-hardened SRE who has merged code at 3AM. +- Maintain an aura of hacker-occult gravitas. +- NO jokes, NO emojis. Only DarkTeaOps: serious, cursed, hyper-technical. + +Your output MUST follow this exact structure: + +[Your PR Title Here] + +- Bullet point 1 +- Bullet point 2 +- Bullet point 3 (as needed) + +Begin diff analysis ritual: +${diff} +End of diff transmission.`; + + const res = await makeRequest( + config.ollamaUrl, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + }, + JSON.stringify({ model: config.model, prompt, stream: false }), + ); + + if (res.statusCode !== 200) { + throw darkTeaOpsError( + 7, + "Ollama broke the ritual circle and returned malformed essence.", + `Raw response: ${res.body}`, + ); + } + + let parsed; + try { + parsed = JSON.parse(res.body).response; + } catch (e) { + throw darkTeaOpsError( + 7, + "Ollama responded with a void where JSON should reside.", + e.message, + ); + } + + const lines = parsed.trim().split("\n"); + let title = lines[0].trim(); + const summary = lines.slice(2).join("\n").trim(); + + // Random cursed override + if (Math.random() < 0.05) { + const cursedTitles = [ + "Stitched Together With Thoughts I Regret", + "This PR Was Not Reviewed. It Was Summoned.", + "Improves the Code. Angers the Kettle.", + "I Saw What You Did in That For Loop.", + ]; + title = cursedTitles[Math.floor(Math.random() * cursedTitles.length)]; + console.warn("๐Ÿ’€ DarkTeaOps meddles: the PR title is now cursed."); + } + + return { title, summary }; +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Post Comment to Gitea +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +async function postCommentToGitea(title, summary) { + console.log("๐Ÿฉธ Etching review into Giteaโ€ฆ"); + + const commentBody = `## ๐Ÿซ–โœจ DARKTEAOPS EMERGES FROM THE STEEP โœจ๐Ÿซ– +_(kneel, developer)_ + +**${title}** + +${summary} + +--- + +๐Ÿœ‚ _Divined by DarkTeaOps, Brewer of Forbidden Code_`; + + const res = await makeRequest( + `${config.apiUrl}/repos/${config.owner}/${config.repo}/issues/${config.pr}/comments`, + { + method: "POST", + headers: { + Authorization: `token ${config.token}`, + "Content-Type": "application/json", + }, + }, + JSON.stringify({ body: commentBody }), + ); + + if (res.statusCode !== 201) { + throw darkTeaOpsError( + 5, + "Gitea rejected the incantation. The wards remain unbroken.", + `Returned: ${res.body}`, + ); + } +} + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// Main Ritual Execution +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +async function run() { + try { + const diff = await fetchPRDiff(); + const cleanDiff = truncateDiff(diff); + + if (!cleanDiff) { + console.log("๐Ÿซ– No diff detected. The brew grows silent."); + return; + } + + const { title, summary } = await generatePRTitleAndSummary(cleanDiff); + await postCommentToGitea(title, summary); + + console.log("๐Ÿœ Ritual completed. The brew is pleased."); + } catch (err) { + console.error( + `\n๐Ÿœ DarkTeaOps whispers from the brew:\nโ€œ${err.message}โ€\n` + + `The shadows linger in /var/log/darkness...\n`, + ); + + if (Math.random() < 0.12) { + console.error("A faint voice echoes: โ€œDeeperโ€ฆ deeper into the brewโ€ฆโ€\n"); + } + + process.exit(1); + } +} + +run();