UNPKG

buddy-bot

Version:
1,239 lines (1,212 loc) 6.51 MB
// @bun var __create = Object.create; var __getProtoOf = Object.getPrototypeOf; var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __toESM = (mod, isNodeMode, target) => { target = mod != null ? __create(__getProtoOf(mod)) : {}; const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target; for (let key of __getOwnPropNames(mod)) if (!__hasOwnProp.call(to, key)) __defProp(to, key, { get: () => mod[key], enumerable: true }); return to; }; var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports); var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true, configurable: true, set: (newValue) => all[name] = () => newValue }); }; var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res); var __require = import.meta.require; // src/dashboard/dashboard-generator.ts class DashboardGenerator { generateDashboard(data, options2 = {}) { const { showOpenPRs = true, showDetectedDependencies = true, showDeprecatedDependencies = true, bodyTemplate } = options2; const title = "Dependency Dashboard"; if (bodyTemplate) { return { title, body: this.applyTemplate(bodyTemplate, data) }; } let body = this.generateDefaultHeader(data); if (showDeprecatedDependencies && data.deprecatedDependencies && data.deprecatedDependencies.length > 0) { body += this.generateDeprecatedDependenciesSection(data.deprecatedDependencies); } if (showOpenPRs && data.openPRs.length > 0) { body += this.generateOpenPRsSection(data.openPRs); } if (showDetectedDependencies) { body += this.generateDetectedDependenciesSection(data.detectedDependencies); } body += this.generateFooter(); return { title, body }; } generateDefaultHeader(_data) { return `This issue lists Buddy Bot updates and detected dependencies. Read the [Dependency Dashboard](https://buddy-bot.sh/features/dependency-dashboard) docs to learn more. `; } generateOpenPRsSection(openPRs) { let section = `## Open The following updates have all been created. To force a retry/rebase of any, click on a checkbox below. `; for (const pr of openPRs) { const packageInfo = this.extractPackageInfo(pr); const rebaseBranch = pr.head; const relativeUrl = pr.url.includes("/pull/") && pr.url.includes("github.com") ? `../pull/${pr.number}` : pr.url; section += ` - [ ] <!-- rebase-branch=${rebaseBranch} -->[${pr.title}](${relativeUrl})`; if (packageInfo.length > 0) { section += ` (\`${packageInfo.join("`, `")}\`)`; } section += ` `; } section += ` - [ ] <!-- rebase-all-open-prs -->**Click on this checkbox to rebase all open PRs at once** `; section += ` `; return section; } generateDetectedDependenciesSection(dependencies) { let section = `## Detected dependencies `; if (dependencies.packageJson.length > 0) { section += this.generatePackageJsonSection(dependencies.packageJson); } const composerFiles = dependencies.dependencyFiles.filter((file) => file.path === "composer.json" || file.path.endsWith("/composer.json")); const rootComposerFiles = composerFiles.filter((file) => file.path === "composer.json"); const vendorComposerFiles = composerFiles.filter((file) => file.path !== "composer.json"); if (rootComposerFiles.length > 0) { section += this.generateComposerSection(rootComposerFiles); } if (dependencies.githubActions.length > 0) { section += this.generateGitHubActionsSection(dependencies.githubActions); } const otherDependencyFiles = dependencies.dependencyFiles.filter((file) => !file.path.endsWith("/composer.json") && file.path !== "composer.json"); if (otherDependencyFiles.length > 0 || vendorComposerFiles.length > 0) { section += this.generateDependencyFilesSection([...otherDependencyFiles, ...vendorComposerFiles]); } return section; } generatePackageJsonSection(packageFiles) { let section = `<details><summary>npm</summary> <blockquote> `; for (const file of packageFiles) { const fileName = file.path.split("/").pop() || file.path; section += `<details><summary>${fileName}</summary> `; const depsByType = { dependencies: file.dependencies.filter((d) => d.type === "dependencies"), devDependencies: file.dependencies.filter((d) => d.type === "devDependencies"), peerDependencies: file.dependencies.filter((d) => d.type === "peerDependencies"), optionalDependencies: file.dependencies.filter((d) => d.type === "optionalDependencies") }; for (const [_type, deps] of Object.entries(depsByType)) { if (deps.length > 0) { for (const dep of deps) { section += ` - \`${dep.name} ${dep.currentVersion}\` `; } } } section += ` </details> `; } section += `</blockquote> </details> `; return section; } generateGitHubActionsSection(actionFiles) { let section = `<details><summary>github-actions</summary> <blockquote> `; for (const file of actionFiles) { section += `<details><summary>${file.path}</summary> `; const uniqueActions = new Map; for (const action of file.dependencies) { const key = `${action.name}@${action.currentVersion}`; if (!uniqueActions.has(key)) { uniqueActions.set(key, { name: action.name, currentVersion: action.currentVersion }); } } for (const action of uniqueActions.values()) { section += ` - \`${action.name} ${action.currentVersion}\` `; } section += ` </details> `; } section += `</blockquote> </details> `; return section; } generateDependencyFilesSection(dependencyFiles) { let section = `<details><summary>dependency-files</summary> <blockquote> `; for (const file of dependencyFiles) { section += `<details><summary>${file.path}</summary> `; for (const dep of file.dependencies) { section += ` - \`${dep.name} ${dep.currentVersion}\` `; } section += ` </details> `; } section += `</blockquote> </details> `; return section; } generateComposerSection(composerFiles) { let section = `<details><summary>composer</summary> <blockquote> `; for (const file of composerFiles) { const fileName = file.path.split("/").pop() || file.path; section += `<details><summary>${fileName}</summary> `; const depsByType = { require: file.dependencies.filter((d) => d.type === "require"), "require-dev": file.dependencies.filter((d) => d.type === "require-dev") }; for (const [_type, deps] of Object.entries(depsByType)) { if (deps.length > 0) { for (const dep of deps) { section += ` - \`${dep.name} ${dep.currentVersion}\` `; } } } section += ` </details> `; } section += `</blockquote> </details> `; return section; } generateDeprecatedDependenciesSection(deprecatedDependencies) { let section = `> [!WARNING] > These dependencies are deprecated and should be updated to avoid potential security risks and compatibility issues. | Datasource | Name | Replacement PR? | |------------|------|-----------------| `; for (const dep of deprecatedDependencies) { const nameDisplay = `\`${dep.name}\``; const replacementStatus = dep.replacementAvailable ? "available" : "unavailable"; const replacementBadge = replacementStatus === "available" ? "![available](https://img.shields.io/badge/available-green)" : "![unavailable](https://img.shields.io/badge/unavailable-orange)"; section += `| ${dep.datasource} | ${nameDisplay} | ${replacementBadge} | `; } section += ` `; return section; } generateFooter() { return `--- - [ ] <!-- manual job -->Check this box to trigger a request for Buddy Bot to run again on this repository `; } extractPackageInfo(pr) { const packages = []; const titlePatterns = [ /update.*?dependency\s+(\S+)/i, /update\s+require(?:-dev)?\s+(\S+)\s+to\s+v?\d+/i, /update\s+(\S+)\s+to\s+v?\d+/i, /bump\s+(\S+)\s+from/i, /chore\(deps\):\s*update\s+dependency\s+(\S+)/i ]; for (const pattern of titlePatterns) { const match = pr.title.match(pattern); if (match && match[1] && !packages.includes(match[1])) { packages.push(match[1]); } } const tableSections = [ { name: "npm", pattern: /### npm Dependencies[\s\S]*?(?=###|\n\n---|z)/i }, { name: "pkgx", pattern: /### Launchpad\/pkgx Dependencies[\s\S]*?(?=###|\n\n---|z)/i }, { name: "actions", pattern: /### GitHub Actions[\s\S]*?(?=###|\n\n---|z)/i } ]; for (const section of tableSections) { const sectionMatch = pr.body.match(section.pattern); if (sectionMatch) { const sectionContent = sectionMatch[0]; const tableRowMatches = sectionContent.match(/\|\s*\[([^\]]+)\]\([^)]+\)\s*\|/g); if (tableRowMatches) { for (const match of tableRowMatches) { const packageMatch = match.match(/\|\s*\[([^\]]+)\]/); if (packageMatch && packageMatch[1]) { const packageName = packageMatch[1].trim(); if (packageName.includes("`") && packageName.includes("->")) { const urlMatch = match.match(/\]\(([^)]+)\)/); if (urlMatch && urlMatch[1]) { const url = urlMatch[1]; const diffUrlMatch = url.match(/\/diffs\/npm\/([^/]+)\//); if (diffUrlMatch && diffUrlMatch[1]) { const extractedPackage = decodeURIComponent(diffUrlMatch[1]); if (extractedPackage && extractedPackage.length > 1 && !packages.includes(extractedPackage)) { packages.push(extractedPackage); } } } continue; } if (!packageName.includes("://") && !packageName.includes("Compare Source") && !packageName.includes("badge") && !packageName.includes("!") && !packageName.startsWith("[![") && !packageName.includes("`") && !packageName.includes("->") && !packageName.includes(" -> ") && !packageName.match(/^\d+\.\d+/) && !packageName.includes(" ") && packageName.length > 0 && !packages.includes(packageName)) { packages.push(packageName); } } } } } } if (packages.length < 3) { const bodyMatches = pr.body.match(/`([^`]+)`/g); if (bodyMatches) { for (const match of bodyMatches) { let packageName = match.replace(/`/g, "").trim(); if (packageName.includes("->") || packageName.includes(" -> ") || packageName.includes("` -> `") || packageName.match(/^\d+\.\d+/) || packageName.match(/^v\d+/) || packageName.match(/[\d.]+\s*->\s*[\d.]+/) || packageName.match(/^[\d.]+$/) || packageName.match(/^\d+\.\d+\.\d+/) || packageName.match(/^\d+\.\d+\.\d+\./) || packageName.match(/^\^?\d+\.\d+/) || packageName.match(/^~\d+\.\d+/) || packageName.includes("://") || packageName.includes("Compare Source") || packageName.includes("badge") || packageName.includes(" ")) { continue; } packageName = packageName.split(",")[0].trim(); if (packageName && packageName.length > 1 && !packages.includes(packageName) && (packageName.startsWith("@") || packageName.includes("/") || packageName.match(/^[a-z][a-z0-9.-]*$/i))) { packages.push(packageName); } } } } return packages; } applyTemplate(template, data) { return template.replace(/\{\{repository\.owner\}\}/g, data.repository.owner).replace(/\{\{repository\.name\}\}/g, data.repository.name).replace(/\{\{openPRs\.count\}\}/g, data.openPRs.length.toString()).replace(/\{\{lastUpdated\}\}/g, data.lastUpdated.toISOString()).replace(/\{\{detectedDependencies\.packageJson\.count\}\}/g, data.detectedDependencies.packageJson.length.toString()).replace(/\{\{detectedDependencies\.githubActions\.count\}\}/g, data.detectedDependencies.githubActions.length.toString()).replace(/\{\{detectedDependencies\.dependencyFiles\.count\}\}/g, data.detectedDependencies.dependencyFiles.length.toString()); } } // src/git/github-provider.ts var exports_github_provider = {}; __export(exports_github_provider, { GitHubProvider: () => GitHubProvider }); import { Buffer as Buffer2 } from "buffer"; import { spawn } from "child_process"; import process2 from "process"; class GitHubProvider { token; owner; repo; hasWorkflowPermissions; apiUrl = "https://api.github.com"; constructor(token, owner, repo, hasWorkflowPermissions = false) { this.token = token; this.owner = owner; this.repo = repo; this.hasWorkflowPermissions = hasWorkflowPermissions; } async createBranch(branchName, baseBranch) { try { const baseRef = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/git/ref/heads/${baseBranch}`); const baseSha = baseRef.object.sha; await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/git/refs`, { ref: `refs/heads/${branchName}`, sha: baseSha }); console.log(`\u2705 Created branch ${branchName} from ${baseBranch}`); } catch (error) { console.error(`\u274C Failed to create branch ${branchName}:`, error); throw error; } } async commitChanges(branchName, message, files) { try { await this.commitChangesWithGit(branchName, message, files); } catch (gitError) { console.warn(`\u26A0\uFE0F Git CLI commit failed, falling back to GitHub API: ${gitError}`); await this.commitChangesWithAPI(branchName, message, files); } } async commitChangesWithGit(branchName, message, files) { try { const workflowFiles = files.filter((f) => f.path.includes(".github/workflows/")); const nonWorkflowFiles = files.filter((f) => !f.path.includes(".github/workflows/")); if (workflowFiles.length > 0 && !this.hasWorkflowPermissions) { console.warn(`\u26A0\uFE0F Detected ${workflowFiles.length} workflow file(s). These require elevated permissions.`); console.warn(`\u26A0\uFE0F Workflow files: ${workflowFiles.map((f) => f.path).join(", ")}`); console.warn(`\u2139\uFE0F Workflow files will be skipped in this commit. BUDDY_BOT_TOKEN not detected or lacks workflow permissions.`); if (nonWorkflowFiles.length > 0) { console.log(`\uD83D\uDCDD Committing ${nonWorkflowFiles.length} non-workflow files...`); files = nonWorkflowFiles; } else { console.warn(`\u26A0\uFE0F All files are workflow files. No files will be committed in this PR.`); console.warn(`\uD83D\uDCA1 To update workflow files, ensure BUDDY_BOT_TOKEN is set with workflow:write permissions.`); console.log(`\uD83D\uDCDD Creating empty commit to avoid "No commits between branches" error...`); try { await this.runCommand("git", ["commit", "--allow-empty", "-m", "Workflow files require elevated permissions - no changes committed"]); console.log(`\u2705 Created empty commit for workflow-only PR`); } catch (error) { console.warn(`\u26A0\uFE0F Failed to create empty commit: ${error}`); try { const readmePath = "README.md"; const fs = await import("fs"); if (fs.existsSync(readmePath)) { const content = fs.readFileSync(readmePath, "utf-8"); const updatedContent = `${content} <!-- Updated by Buddy Bot --> `; fs.writeFileSync(readmePath, updatedContent); await this.runCommand("git", ["add", readmePath]); await this.runCommand("git", ["commit", "-m", "Update README for workflow-only PR"]); console.log(`\u2705 Created README update for workflow-only PR`); } } catch (readmeError) { console.error(`\u274C Failed to create any commit: ${readmeError}`); } } return; } } else if (workflowFiles.length > 0) { console.log(`\u2705 Including ${workflowFiles.length} workflow file(s) with elevated permissions`); } try { await this.runCommand("git", ["config", "user.name", "github-actions[bot]"]); await this.runCommand("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"]); console.log("\u2705 Git identity configured for github-actions[bot]"); } catch (error) { console.warn("\u26A0\uFE0F Failed to configure Git identity:", error); } await this.runCommand("git", ["fetch", "origin"]); console.log(`\uD83D\uDD04 Resetting ${branchName} to main for clean rebase...`); await this.runCommand("git", ["checkout", "main"]); await this.runCommand("git", ["reset", "--hard", "HEAD"]); await this.runCommand("git", ["clean", "-fd"]); try { await this.runCommand("git", ["branch", "-D", branchName]); } catch {} await this.runCommand("git", ["checkout", "-b", branchName]); for (const file of files) { const cleanPath = file.path.replace(/^\.\//, "").replace(/^\/+/, ""); if (file.type === "delete") { try { await this.runCommand("git", ["rm", cleanPath]); } catch {} } else { const fs = await import("fs"); const path = await import("path"); const dir = path.dirname(cleanPath); if (dir !== ".") { fs.mkdirSync(dir, { recursive: true }); } fs.writeFileSync(cleanPath, file.content, "utf8"); await this.runCommand("git", ["add", cleanPath]); } } const status = await this.runCommand("git", ["status", "--porcelain"]); if (status.trim()) { await this.runCommand("git", ["commit", "-m", message]); await this.runCommand("git", ["push", "origin", branchName, "--force"]); console.log(`\u2705 Successfully rebased ${branchName} with fresh changes: ${message}`); } else { console.log(`\u2139\uFE0F No changes to commit for ${branchName}`); } } catch (error) { console.error(`\u274C Failed to commit changes to ${branchName} with Git CLI:`, error); throw error; } } async commitChangesWithAPI(branchName, message, files) { try { const workflowFiles = files.filter((f) => f.path.includes(".github/workflows/")); const nonWorkflowFiles = files.filter((f) => !f.path.includes(".github/workflows/")); if (workflowFiles.length > 0 && !this.hasWorkflowPermissions) { console.warn(`\u26A0\uFE0F Detected ${workflowFiles.length} workflow file(s). These require elevated permissions.`); console.warn(`\u26A0\uFE0F Workflow files: ${workflowFiles.map((f) => f.path).join(", ")}`); console.warn(`\u2139\uFE0F Workflow files will be skipped in this commit. Consider using a GitHub App with workflow permissions for workflow updates.`); if (nonWorkflowFiles.length > 0) { console.log(`\uD83D\uDCDD Committing ${nonWorkflowFiles.length} non-workflow files...`); files = nonWorkflowFiles; } else { console.warn(`\u26A0\uFE0F All files are workflow files. No files will be committed in this PR.`); console.warn(`\uD83D\uDCA1 To update workflow files, consider using a GitHub App with appropriate permissions.`); return; } } else if (workflowFiles.length > 0) { console.log(`\u2705 Including ${workflowFiles.length} workflow file(s) with elevated permissions`); } const branchRef = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/git/ref/heads/${branchName}`); const currentSha = branchRef.object.sha; const currentCommit = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/git/commits/${currentSha}`); const currentTreeSha = currentCommit.tree.sha; const tree = []; for (const file of files) { const cleanPath = file.path.replace(/^\.\//, "").replace(/^\/+/, ""); if (file.type === "delete") { tree.push({ path: cleanPath, mode: "100644", type: "blob", sha: null }); } else { const blob = await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/git/blobs`, { content: Buffer2.from(file.content).toString("base64"), encoding: "base64" }); tree.push({ path: cleanPath, mode: "100644", type: "blob", sha: blob.sha }); } } const newTree = await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/git/trees`, { base_tree: currentTreeSha, tree }); const newCommit = await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/git/commits`, { message, tree: newTree.sha, parents: [currentSha], author: { name: "github-actions[bot]", email: "41898282+github-actions[bot]@users.noreply.github.com" }, committer: { name: "github-actions[bot]", email: "41898282+github-actions[bot]@users.noreply.github.com" } }); await this.apiRequest(`PATCH /repos/${this.owner}/${this.repo}/git/refs/heads/${branchName}`, { sha: newCommit.sha }); console.log(`\u2705 Committed changes to ${branchName}: ${message}`); } catch (error) { console.error(`\u274C Failed to commit changes to ${branchName}:`, error); throw error; } } async createPullRequest(options2) { try { return await this.createPullRequestWithCLI(options2); } catch (cliError) { console.warn(`\u26A0\uFE0F GitHub CLI failed, falling back to API: ${cliError}`); return await this.createPullRequestWithAPI(options2); } } async createPullRequestWithCLI(options2) { try { const args = [ "pr", "create", "--title", options2.title, "--body", options2.body, "--head", options2.head, "--base", options2.base ]; if (options2.draft) { args.push("--draft"); } if (options2.reviewers && options2.reviewers.length > 0) { console.log(`\uD83D\uDD0D Adding reviewers via CLI: ${options2.reviewers.join(", ")}`); args.push("--reviewer", options2.reviewers.join(",")); } if (options2.assignees && options2.assignees.length > 0) { args.push("--assignee", options2.assignees.join(",")); } const result = await this.runCommand("gh", args); const prUrlMatch = result.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/); if (!prUrlMatch) { throw new Error("Failed to parse PR number from GitHub CLI output"); } const prNumber = Number.parseInt(prUrlMatch[1]); const prUrl = prUrlMatch[0]; console.log(`\u2705 Created PR #${prNumber}: ${options2.title}`); if (options2.labels && options2.labels.length > 0) { try { await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues/${prNumber}/labels`, { labels: options2.labels }); console.log(`\u2705 Added labels to PR #${prNumber}: ${options2.labels.join(", ")}`); } catch (labelError) { console.warn(`\u26A0\uFE0F Failed to add labels: ${labelError}`); for (const label of options2.labels) { try { await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues/${prNumber}/labels`, { labels: [label] }); } catch (singleLabelError) { console.warn(`\u26A0\uFE0F Failed to add label '${label}': ${singleLabelError}`); } } } } return { number: prNumber, title: options2.title, body: options2.body, head: options2.head, base: options2.base, state: "open", url: prUrl, createdAt: new Date, updatedAt: new Date, author: "github-actions[bot]", reviewers: options2.reviewers || [], assignees: options2.assignees || [], labels: options2.labels || [], draft: options2.draft || false }; } catch (error) { console.error(`\u274C Failed to create PR with GitHub CLI: ${options2.title}`, error); throw error; } } async createPullRequestWithAPI(options2) { try { const response = await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/pulls`, { title: options2.title, body: options2.body, head: options2.head, base: options2.base, draft: options2.draft || false }); if (options2.reviewers && options2.reviewers.length > 0) { try { console.log(`\uD83D\uDD0D Adding reviewers to PR #${response.number}: ${options2.reviewers.join(", ")}`); await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/pulls/${response.number}/requested_reviewers`, { reviewers: options2.reviewers, team_reviewers: options2.teamReviewers || [] }); console.log(`\u2705 Successfully added reviewers: ${options2.reviewers.join(", ")}`); } catch (reviewerError) { console.error(`\u274C Failed to add reviewers: ${reviewerError}`); console.error(` Reviewers: ${options2.reviewers.join(", ")}`); console.error(` Repository: ${this.owner}/${this.repo}`); console.error(` PR: #${response.number}`); } } if (options2.assignees && options2.assignees.length > 0) { try { await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues/${response.number}/assignees`, { assignees: options2.assignees }); } catch (assigneeError) { console.warn(`\u26A0\uFE0F Failed to add assignees: ${assigneeError}`); } } if (options2.labels && options2.labels.length > 0) { try { await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues/${response.number}/labels`, { labels: options2.labels }); } catch (labelError) { console.warn(`\u26A0\uFE0F Failed to add labels: ${labelError}`); for (const label of options2.labels) { try { await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues/${response.number}/labels`, { labels: [label] }); } catch (singleLabelError) { console.warn(`\u26A0\uFE0F Failed to add label '${label}': ${singleLabelError}`); } } } } console.log(`\u2705 Created PR #${response.number}: ${options2.title}`); return { number: response.number, title: response.title, body: response.body || "", head: response.head.ref, base: response.base.ref, state: response.state, url: response.html_url, createdAt: new Date(response.created_at), updatedAt: new Date(response.updated_at), author: response.user.login, reviewers: options2.reviewers || [], assignees: options2.assignees || [], labels: options2.labels || [], draft: response.draft }; } catch (error) { console.error(`\u274C Failed to create PR with API: ${options2.title}`, error); throw error; } } async runCommand(command, args) { return new Promise((resolve, reject) => { const child = spawn(command, args, { stdio: "pipe", env: { ...process2.env, GITHUB_TOKEN: this.token, GH_TOKEN: this.token } }); let stdout = ""; let stderr = ""; child.stdout?.on("data", (data) => { stdout += data.toString(); }); child.stderr?.on("data", (data) => { stderr += data.toString(); }); child.on("close", (code) => { if (code === 0) { resolve(stdout); } else { reject(new Error(`Command failed with code ${code}: ${stderr}`)); } }); child.on("error", (error) => { reject(error); }); }); } async getPullRequests(state = "open") { try { const response = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/pulls?state=${state}`); return response.map((pr) => ({ number: pr.number, title: pr.title, body: pr.body || "", head: pr.head.ref, base: pr.base.ref, state: pr.state, url: pr.html_url, createdAt: new Date(pr.created_at), updatedAt: new Date(pr.updated_at), mergedAt: pr.merged_at ? new Date(pr.merged_at) : undefined, author: pr.user.login, reviewers: pr.requested_reviewers?.map((r) => r.login) || [], assignees: pr.assignees?.map((a) => a.login) || [], labels: pr.labels?.map((l) => l.name) || [], draft: pr.draft })); } catch (error) { console.error(`\u274C Failed to get PRs:`, error); throw error; } } async updatePullRequest(prNumber, options2) { try { const updateData = {}; if (options2.title) updateData.title = options2.title; if (options2.body) updateData.body = options2.body; if (options2.base) updateData.base = options2.base; if (options2.draft !== undefined) updateData.draft = options2.draft; const response = await this.apiRequest(`PATCH /repos/${this.owner}/${this.repo}/pulls/${prNumber}`, updateData); if (options2.labels && options2.labels.length > 0) { try { await this.apiRequest(`PUT /repos/${this.owner}/${this.repo}/issues/${prNumber}/labels`, { labels: options2.labels }); console.log(`\u2705 Updated labels for PR #${prNumber}: ${options2.labels.join(", ")}`); } catch (labelError) { console.warn(`\u26A0\uFE0F Failed to update labels for PR #${prNumber}: ${labelError}`); for (const label of options2.labels) { try { await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues/${prNumber}/labels`, { labels: [label] }); } catch (singleLabelError) { console.warn(`\u26A0\uFE0F Failed to add label '${label}' to PR #${prNumber}: ${singleLabelError}`); } } } } if (options2.reviewers && options2.reviewers.length > 0) { try { console.log(`\uD83D\uDD0D Adding reviewers to existing PR #${prNumber}: ${options2.reviewers.join(", ")}`); await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/pulls/${prNumber}/requested_reviewers`, { reviewers: options2.reviewers, team_reviewers: options2.teamReviewers || [] }); console.log(`\u2705 Updated reviewers for PR #${prNumber}: ${options2.reviewers.join(", ")}`); } catch (reviewerError) { console.error(`\u274C Failed to update reviewers for PR #${prNumber}: ${reviewerError}`); console.error(` Reviewers: ${options2.reviewers.join(", ")}`); console.error(` Repository: ${this.owner}/${this.repo}`); } } if (options2.assignees && options2.assignees.length > 0) { try { await this.runCommand("gh", ["issue", "edit", prNumber.toString(), "--add-assignee", options2.assignees.join(",")]); console.log(`\u2705 Updated assignees for PR #${prNumber}: ${options2.assignees.join(", ")}`); } catch (assigneeError) { console.warn(`\u26A0\uFE0F Failed to update assignees for PR #${prNumber}: ${assigneeError}`); } } console.log(`\u2705 Updated PR #${prNumber}`); return { number: response.number, title: response.title, body: response.body || "", head: response.head.ref, base: response.base.ref, state: response.state, url: response.html_url, createdAt: new Date(response.created_at), updatedAt: new Date(response.updated_at), author: response.user.login, reviewers: [], assignees: [], labels: options2.labels || [], draft: response.draft }; } catch (error) { console.error(`\u274C Failed to update PR #${prNumber}:`, error); throw error; } } async closePullRequest(prNumber) { try { const prDetails = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/pulls/${prNumber}`); const branchName = prDetails.head.ref; await this.apiRequest(`PATCH /repos/${this.owner}/${this.repo}/pulls/${prNumber}`, { state: "closed" }); console.log(`\u2705 Closed PR #${prNumber}`); try { await this.deleteBranch(branchName); console.log(`\uD83E\uDDF9 Cleaned up branch ${branchName} after close`); } catch (cleanupError) { console.warn(`\u26A0\uFE0F Failed to clean up branch ${branchName}:`, cleanupError); } } catch (error) { console.error(`\u274C Failed to close PR #${prNumber}:`, error); throw error; } } async mergePullRequest(prNumber, strategy = "merge") { try { const mergeMethod = strategy === "rebase" ? "rebase" : strategy === "squash" ? "squash" : "merge"; const prDetails = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/pulls/${prNumber}`); const branchName = prDetails.head.ref; await this.apiRequest(`PUT /repos/${this.owner}/${this.repo}/pulls/${prNumber}/merge`, { merge_method: mergeMethod }); console.log(`\u2705 Merged PR #${prNumber} using ${strategy}`); try { await this.deleteBranch(branchName); console.log(`\uD83E\uDDF9 Cleaned up branch ${branchName} after merge`); } catch (cleanupError) { console.warn(`\u26A0\uFE0F Failed to clean up branch ${branchName}:`, cleanupError); } } catch (error) { console.error(`\u274C Failed to merge PR #${prNumber}:`, error); throw error; } } async deleteBranch(branchName) { try { try { await this.runCommand("gh", ["api", "repos", this.owner, this.repo, "git/refs/heads", branchName, "-X", "DELETE"]); console.log(`\u2705 Deleted branch ${branchName} via CLI`); return; } catch (cliError) { console.log(`\u26A0\uFE0F CLI branch deletion failed, trying API: ${cliError}`); } await this.apiRequest(`DELETE /repos/${this.owner}/${this.repo}/git/refs/heads/${branchName}`); console.log(`\u2705 Deleted branch ${branchName} via API`); } catch (error) { console.warn(`\u26A0\uFE0F Failed to delete branch ${branchName}:`, error); } } async apiRequest(endpoint, data) { const [method, path] = endpoint.split(" "); const url = `${this.apiUrl}${path}`; const options2 = { method, headers: { Authorization: `Bearer ${this.token}`, Accept: "application/vnd.github.v3+json", "Content-Type": "application/json", "User-Agent": "buddy-bot" } }; if (data && (method === "POST" || method === "PATCH" || method === "PUT")) { options2.body = JSON.stringify(data); } const response = await fetch(url, options2); if (!response.ok) { const errorBody = await response.text(); throw new Error(`GitHub API error: ${response.status} ${response.statusText} ${errorBody}`); } if (response.headers.get("content-type")?.includes("application/json")) { return response.json(); } return response.text(); } async createIssue(options2) { try { const response = await this.apiRequest(`POST /repos/${this.owner}/${this.repo}/issues`, { title: options2.title, body: options2.body, assignees: options2.assignees || [], labels: options2.labels || [], milestone: options2.milestone }); console.log(`\u2705 Created issue #${response.number}: ${options2.title}`); return { number: response.number, title: response.title, body: response.body, state: response.state, url: response.html_url, createdAt: new Date(response.created_at), updatedAt: new Date(response.updated_at), closedAt: response.closed_at ? new Date(response.closed_at) : undefined, author: response.user.login, assignees: response.assignees?.map((a) => a.login) || [], labels: response.labels?.map((l) => typeof l === "string" ? l : l.name) || [], pinned: false }; } catch (error) { console.error(`\u274C Failed to create issue: ${options2.title}`, error); throw error; } } async getIssues(state = "open") { try { const response = await this.apiRequest(`GET /repos/${this.owner}/${this.repo}/issues?state=${state}&sort=updated&direction=desc`); return response.filter((issue) => !issue.pull_request).map((issue) => ({ number: issue.number, title: issue.title, body: issue.body || "", state: issue.state, url: issue.html_url, createdAt: new Date(issue.created_at), updatedAt: new Date(issue.updated_at), closedAt: issue.closed_at ? new Date(issue.closed_at) : undefined, author: issue.user.login, assignees: issue.assignees?.map((a) => a.login) || [], labels: issue.labels?.map((l) => typeof l === "string" ? l : l.name) || [], pinned: false })); } catch (error) { console.error("\u274C Failed to get issues:", error); throw error; } } async updateIssue(issueNumber, options2) { try { const updateData = {}; if (options2.title !== undefined) updateData.title = options2.title; if (options2.body !== undefined) updateData.body = options2.body; if (options2.assignees !== undefined) updateData.assignees = options2.assignees; if (options2.labels !== undefined) updateData.labels = options2.labels; if (options2.milestone !== undefined) updateData.milestone = options2.milestone; const response = await this.apiRequest(`PATCH /repos/${this.owner}/${this.repo}/issues/${issueNumber}`, updateData); console.log(`\u2705 Updated issue #${issueNumber}: ${response.title}`); return { number: response.number, title: response.title, body: response.body, state: response.state, url: response.html_url, createdAt: new Date(response.created_at), updatedAt: new Date(response.updated_at), closedAt: response.closed_at ? new Date(response.closed_at) : undefined, author: response.user.login, assignees: response.assignees?.map((a) => a.login) || [], labels: response.labels?.map((l) => typeof l === "string" ? l : l.name) || [], pinned: false }; } catch (error) { console.error(`\u274C Failed to update issue #${issueNumber}:`, error); throw error; } } async closeIssue(issueNumber) { try { await this.apiRequest(`PATCH /repos/${this.owner}/${this.repo}/issues/${issueNumber}`, { state: "closed" }); console.log(`\u2705 Closed issue #${issueNumber}`); } catch (error) { console.error(`\u274C Failed to close issue #${issueNumber}:`, error); throw error; } } async unpinIssue(issueNumber) { try { await this.apiRequest(`DELETE /repos/${this.owner}/${this.repo}/issues/${issueNumber}/pin`, undefined); } catch (error) { console.log(`\u26A0\uFE0F Failed to unpin issue #${issueNumber}:`, error); } } } var init_github_provider = () => {}; // src/services/release-notes-fetcher.ts class ReleaseNotesFetcher { userAgent = "Buddy-Bot/1.0.0 (https://github.com/stacksjs/buddy)"; async fetchPackageInfo(packageName, currentVersion, newVersion) { try { const packageInfo = await this.fetchNpmPackageInfo(packageName); let releaseNotes = []; let changelog = []; let compareUrl; if (packageInfo.repository?.url) { const githubInfo = this.parseGitHubUrl(packageInfo.repository.url); if (githubInfo) { releaseNotes = await this.fetchGitHubReleases(githubInfo.owner, githubInfo.repo, currentVersion, newVersion); compareUrl = this.generateCompareUrl(githubInfo.owner, githubInfo.repo, currentVersion, newVersion); changelog = await this.fetchChangelog(githubInfo.owner, githubInfo.repo); } } return { packageInfo, releaseNotes, changelog, compareUrl }; } catch (error) { console.error(`\u274C Failed to fetch package info for ${packageName}:`, error); return { packageInfo: { name: packageName, description: `Package ${packageName} - see npm for details`, repository: { type: "git", url: `https://github.com/search?q=${encodeURIComponent(packageName)}&type=repositories` } }, releaseNotes: [], changelog: [] }; } } async fetchNpmPackageInfo(packageName) { try { const response = await fetch(`https://registry.npmjs.org/${encodeURIComponent(packageName)}`, { headers: { "User-Agent": this.userAgent } }); if (!response.ok) { throw new Error(`NPM registry responded with ${response.status}`); } const data = await response.json(); const latest = data["dist-tags"]?.latest; let weeklyDownloads; try { const downloadsResponse = await fetch(`https://api.npmjs.org/downloads/point/last-week/${encodeURIComponent(packageName)}`, { headers: { "User-Agent": this.userAgent } }); if (downloadsResponse.ok) { const downloadsData = await downloadsResponse.json(); weeklyDownloads = downloadsData.downloads; } } catch {} return { name: packageName, description: data.description, homepage: data.homepage, repository: data.repository, license: data.license, author: data.author, keywords: data.keywords, weeklyDownloads, lastPublish: data.time?.[latest], maintainers: data.maintainers }; } catch (error) { console.warn(`\u26A0\uFE0F Failed to fetch npm info for ${packageName}:`, error); return { name: packageName, description: `NPM package ${packageName}`, homepage: `https://www.npmjs.com/package/${encodeURIComponent(packageName)}` }; } } parseGitHubUrl(repositoryUrl) { try { const cleanUrl = repositoryUrl.replace(/^git\+/, "").replace(/\.git$/, "").replace(/^git:\/\//, "https://").replace(/^ssh:\/\/git@/, "https://").replace(/^git@github\.com:/, "https://github.com/"); const url = new URL(cleanUrl); if (url.hostname !== "github.com") { return null; } const pathParts = url.pathname.split("/").filter(Boolean); if (pathParts.length >= 2) { return { owner: pathParts[0], repo: pathParts[1] }; } return null; } catch { return null; } } async fetchGitHubReleases(owner, repo, currentVersion, newVersion) { try { const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/releases?per_page=50`, { headers: { "User-Agent": this.userAgent } }); if (!response.ok) { return []; } const releases = await response.json(); return releases.filter((release) => { const releaseVersion = release.tag_name.replace(/^v/, ""); return this.isVersionBetween(releaseVersion, currentVersion, newVersion); }).map((release) => ({ version: release.tag_name, date: release.published_at, title: release.name || release.tag_name, body: release.body || "", htmlUrl: release.html_url, compareUrl: this.generateCompareUrl(owner, repo, currentVersion, release.tag_name), author: release.author?.login, isPrerelease: release.prerelease, assets: release.assets?.map((asset) => ({ name: asset.name, downloadUrl: asset.browser_download_url, size: asset.size, contentType: asset.content_type })) || [] })); } catch (error) { console.warn(`Failed to fetch GitHub releases for ${owner}/${repo}:`, error); return []; } } async fetchChangelog(owner, repo) { const changelogFiles = ["CHANGELOG.md", "CHANGELOG.rst", "HISTORY.md", "RELEASES.md"]; for (const filename of changelogFiles) { try { const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/contents/${filename}`, { headers: { "User-Agent": this.userAgent } }); if (response.ok) { const data = await response.json(); if (data.content) { const content = atob(data.content); return this.parseChangelog(content); } } } catch {} } return []; } parseChangelog(content) { const entries = []; const lines = content.split(` `); let currentEntry = null; let currentSection = null; for (const line of lines) { const versionMatch = line.match(/^##\s*\[?([^\]]+)\]?\s*-?\s*(.*)/); if (versionMatch) { if (currentEntry) { entries.push(currentEntry); } currentEntry = { version: versionMatch[1], date: versionMatch[2].trim(), changes: {} }; currentSection = null; continue; } const sectionMatch = line.match(/^###\s*(.+)/); if (sectionMatch && currentEntry) { currentSection = sectionMatch[1].toLowerCase(); continue; } const itemMatch = line.match(/^[-*]\s*(.+)/); if (itemMatch && currentEntry && currentSection) { if (!currentEntry.changes) currentEntry.changes = {}; const sectionKey = currentSection; if (!currentEntry.changes[sectionKey]) { currentEntry.changes[sectionKey] = []; } currentEntry.changes[sectionKey].push(itemMatch[1]); } } if (currentEntry) { entries.push(currentEntry); } return entries.slice(0, 10); } generateCompareUrl(owner, repo, fromVersion, toVersion) { const cleanFrom = fromVersion.startsWith("v") ? fromVersion : `v${fromVersion}`; const cleanTo = toVersion.startsWith("v") ? toVersion : `v${toVersion}`; return `https://github.com/${owner}/${repo}/compare/${cleanFrom}...${cleanTo}`; } isVersionBetween(version, current, target) { const cleanVersion = version.replace(/^v/, ""); const cleanCurrent = current.replace(/^v/, ""); const cleanTarget = target.replace(/^v/, ""); return cleanVersion === cleanTarget || cleanVersion > cleanCurrent; } generatePackageBadges(packageInfo, currentVersion, newVersion) { const packageName = encodeURIComponent(packageInfo.name); const normalizedCurrent = this.normalizeVersionForBadges(currentVersion); const normalizedNew = this.normalizeVersionForBadges(newVersion); const encodedCurrent = encodeURIComponent(normalizedCurrent); const encodedNew = encodeURIComponent(normalizedNew); return { age: `[![age](https://developer.mend.io/api/mc/badges/age/npm/${packageName}/${encodedNew}?slim=true)](https://docs.renovatebot.com/merge-confidence/)`, adoption: `[![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/${packageName}/${encodedNew}?slim=true)](https://docs.renovatebot.com/merge-confidence/)`, passing: `[![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/${packageName}/${encodedCurrent}/${encodedNew}?slim=true)](https://docs.renovatebot.com/merge-confidence/)`, confidence: `[![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/${packageName}/${encodedCurrent}/${encodedNew}?slim=true)](https://docs.renovatebot.com/merge-confidence/)` }; } async fetchComposerPackageInfo(packageName) { try { const response = await fetch(`https://packagist.org/packages/${encodeURIComponent(packageName)}.json`, { headers: { "User-Agent": this.userAgent } }); if (!response.ok) { throw new Error(`Packagist responded with ${response.status}`); } const data = await response.json(); const packageData = data.package; if (!packageData) { return { name: packageName }; } const versions = Object.keys(packageData.versions || {}); const latestVersion = versions.find((v) => !v.includes("dev") && !v.includes("alpha") && !v.includes("beta")) || versions[0]; const versionData = packageData.versions[latestVersion] || {}; return { name: packageData.name, description: versionData.description, homepage: versionData.homepage, repository: versionData.source ? { type: "git", url: versionData.source.url } : undefined, license: versionData.license?.[0] || versionData.license, author: versionData.authors?.[0], keywords: versionData.keywords, lastPublish: versionData.time }; } catch (erro