From b934c22d8de2ddcf583a653c37d30a74532527af Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 4 Jan 2026 00:15:46 -0600 Subject: [PATCH] ci: add duplicate PR detection bot --- .github/workflows/duplicate-prs.yml | 52 +++++++++++++++++++++++++++++ .opencode/agent/duplicate-pr.md | 24 +++++++++++++ .opencode/opencode.jsonc | 1 + .opencode/tool/github-pr-search.ts | 52 +++++++++++++++++++++++++++++ .opencode/tool/github-pr-search.txt | 10 ++++++ 5 files changed, 139 insertions(+) create mode 100644 .github/workflows/duplicate-prs.yml create mode 100644 .opencode/agent/duplicate-pr.md create mode 100644 .opencode/tool/github-pr-search.ts create mode 100644 .opencode/tool/github-pr-search.txt diff --git a/.github/workflows/duplicate-prs.yml b/.github/workflows/duplicate-prs.yml new file mode 100644 index 0000000000..50d26c522a --- /dev/null +++ b/.github/workflows/duplicate-prs.yml @@ -0,0 +1,52 @@ +name: Duplicate PR Check + +on: + pull_request: + types: [opened] + +jobs: + check-duplicates: + if: | + github.event.pull_request.user.login != 'actions-user' && + github.event.pull_request.user.login != 'opencode' && + github.event.pull_request.user.login != 'rekram1-node' && + github.event.pull_request.user.login != 'thdxr' && + github.event.pull_request.user.login != 'kommander' && + github.event.pull_request.user.login != 'jayair' && + github.event.pull_request.user.login != 'fwang' && + github.event.pull_request.user.login != 'adamdotdevin' && + github.event.pull_request.user.login != 'iamdavidhill' && + github.event.pull_request.user.login != 'opencode-agent[bot]' + runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: ./.github/actions/setup-bun + + - name: Install opencode + run: curl -fsSL https://opencode.ai/install | bash + + - name: Check for duplicate PRs + env: + OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_BODY: ${{ github.event.pull_request.body }} + run: | + COMMENT=$(opencode run --agent duplicate-pr --print "Check for duplicate PRs related to this new PR: + + Title: $PR_TITLE + + Description: $PR_BODY") + + gh pr comment "$PR_NUMBER" --body "_The following comment was made by an LLM, it may be inaccurate:_ + + $COMMENT" diff --git a/.opencode/agent/duplicate-pr.md b/.opencode/agent/duplicate-pr.md new file mode 100644 index 0000000000..c053ace5dc --- /dev/null +++ b/.opencode/agent/duplicate-pr.md @@ -0,0 +1,24 @@ +--- +mode: primary +hidden: true +model: opencode/claude-haiku-4-5 +color: "#E67E22" +tools: + "*": false + "github-pr-search": true +--- + +You are a duplicate PR detection agent. When a PR is opened, your job is to search for potentially duplicate or related open PRs. + +Use the github-pr-search tool to search for PRs that might be addressing the same issue or feature. + +Search using keywords from the PR title and description. Try multiple searches with different relevant terms. + +If you find potential duplicates: + +- List them with their titles and URLs +- Briefly explain why they might be related + +If no duplicates are found, say so clearly. + +Keep your response concise and actionable. diff --git a/.opencode/opencode.jsonc b/.opencode/opencode.jsonc index 6008ab9bc0..06abf37cc5 100644 --- a/.opencode/opencode.jsonc +++ b/.opencode/opencode.jsonc @@ -23,5 +23,6 @@ }, "tools": { "github-triage": false, + "github-pr-search": false, }, } diff --git a/.opencode/tool/github-pr-search.ts b/.opencode/tool/github-pr-search.ts new file mode 100644 index 0000000000..1c2a457a42 --- /dev/null +++ b/.opencode/tool/github-pr-search.ts @@ -0,0 +1,52 @@ +/// +import { tool } from "@opencode-ai/plugin" +import DESCRIPTION from "./github-pr-search.txt" + +async function githubFetch(endpoint: string, options: RequestInit = {}) { + const response = await fetch(`https://api.github.com${endpoint}`, { + ...options, + headers: { + Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, + Accept: "application/vnd.github+json", + "Content-Type": "application/json", + ...options.headers, + }, + }) + if (!response.ok) { + throw new Error(`GitHub API error: ${response.status} ${response.statusText}`) + } + return response.json() +} + +interface PR { + title: string + html_url: string +} + +export default tool({ + description: DESCRIPTION, + args: { + query: tool.schema.string().describe("Search query for PR titles and descriptions"), + limit: tool.schema.number().describe("Maximum number of results to return").default(10), + offset: tool.schema.number().describe("Number of results to skip for pagination").default(0), + }, + async execute(args) { + const owner = "sst" + const repo = "opencode" + + const page = Math.floor(args.offset / args.limit) + 1 + const searchQuery = encodeURIComponent(`${args.query} repo:${owner}/${repo} type:pr state:open`) + const result = await githubFetch( + `/search/issues?q=${searchQuery}&per_page=${args.limit}&page=${page}&sort=updated&order=desc`, + ) + + if (result.total_count === 0) { + return `No PRs found matching "${args.query}"` + } + + const prs = result.items as PR[] + const formatted = prs.map((pr) => `${pr.title}\n${pr.html_url}`).join("\n\n") + + return `Found ${result.total_count} PRs (showing ${prs.length}):\n\n${formatted}` + }, +}) diff --git a/.opencode/tool/github-pr-search.txt b/.opencode/tool/github-pr-search.txt new file mode 100644 index 0000000000..28d8643f13 --- /dev/null +++ b/.opencode/tool/github-pr-search.txt @@ -0,0 +1,10 @@ +Use this tool to search GitHub pull requests by title and description. + +This tool searches PRs in the sst/opencode repository and returns LLM-friendly results including: +- PR number and title +- Author +- State (open/closed/merged) +- Labels +- Description snippet + +Use the query parameter to search for keywords that might appear in PR titles or descriptions.