Files
dependabot[bot] 1bd3a9d123 ci(deps): bump the github-actions group with 3 updates
Bumps the github-actions group with 3 updates: [pnpm/action-setup](https://github.com/pnpm/action-setup), [anomalyco/opencode](https://github.com/anomalyco/opencode) and [crate-ci/typos](https://github.com/crate-ci/typos).


Updates `pnpm/action-setup` from 6.0.0 to 6.0.1
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/08c4be7e2e672a47d11bd04269e27e5f3e8529cb...078e9d416474b29c0c387560859308974f7e9c53)

Updates `anomalyco/opencode` from 1.4.3 to 1.4.11
- [Release notes](https://github.com/anomalyco/opencode/releases)
- [Commits](https://github.com/anomalyco/opencode/compare/877be7e8e04142cd8fbebcb5e6c4b9617bf28cce...a35b8a95c27d28e979a3826e1289d7ee87f40251)

Updates `crate-ci/typos` from 1.45.0 to 1.45.1
- [Release notes](https://github.com/crate-ci/typos/releases)
- [Changelog](https://github.com/crate-ci/typos/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crate-ci/typos/compare/02ea592e44b3a53c302f697cddca7641cd051c3d...cf5f1c29a8ac336af8568821ec41919923b05a83)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: 6.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: anomalyco/opencode
  dependency-version: 1.4.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
- dependency-name: crate-ci/typos
  dependency-version: 1.45.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-18 09:04:48 +00:00

336 lines
17 KiB
YAML

name: Issue & PR Automation
on:
issues:
types: [opened]
pull_request_target:
types: [opened]
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
permissions:
contents: read
issues: write
pull-requests: write
id-token: write
jobs:
analyze-issue:
if: github.repository == 'zhom/donutbrowser' && github.event_name == 'issues'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Check if first-time contributor
id: check-first-time
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
run: |
ISSUE_COUNT=$(gh api "/repos/${{ github.repository }}/issues?state=all&creator=$ISSUE_AUTHOR&per_page=100" \
--jq "[.[] | select(.number != ${{ github.event.issue.number }}) ] | length" \
|| echo "0")
if [ "$ISSUE_COUNT" = "0" ]; then
echo "is_first_time=true" >> $GITHUB_OUTPUT
else
echo "is_first_time=false" >> $GITHUB_OUTPUT
fi
- name: Build repo context and find related files
env:
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
run: |
# Read project guidelines (contains repo structure)
cp CLAUDE.md /tmp/repo-context.txt
printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt
# List all source files for the AI to pick from
find . -type f \( -name "*.rs" -o -name "*.ts" -o -name "*.tsx" \) \
! -path "*/node_modules/*" ! -path "*/target/*" ! -path "*/.next/*" ! -path "*/dist/*" \
! -path "*/.git/*" ! -path "*/gen/*" ! -path "*/data/*" \
| sed 's|^\./||' | sort > /tmp/all-source-files.txt
- name: Select relevant files with AI
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: |
PAYLOAD=$(jq -n \
--rawfile title /tmp/issue-title.txt \
--rawfile body /tmp/issue-body.txt \
--rawfile files /tmp/all-source-files.txt \
'{
model: "anthropic/claude-opus-4.6",
messages: [
{
role: "system",
content: "You are a file selector for Donut Browser (Tauri + Next.js + Rust anti-detect browser). Given an issue and a list of source files, output ONLY the 10 most likely relevant file paths, one per line. No explanations, no numbering, just paths."
},
{
role: "user",
content: ("Issue: " + $title + "\n\n" + $body + "\n\nFiles:\n" + $files)
}
]
}')
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/selected-files.txt
# Read the selected files in full (skip binary files)
echo "" > /tmp/file-contents.txt
while IFS= read -r filepath; do
filepath=$(echo "$filepath" | xargs)
[ -z "$filepath" ] && continue
if [ -f "$filepath" ] && file --mime "$filepath" | grep -q "text/"; then
echo "=== $filepath ===" >> /tmp/file-contents.txt
cat "$filepath" >> /tmp/file-contents.txt
echo "" >> /tmp/file-contents.txt
fi
done < /tmp/selected-files.txt
# Cap total context at 100KB
head -c 100000 /tmp/file-contents.txt > /tmp/file-context.txt
- name: Analyze issue with AI
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_BODY: ${{ github.event.issue.body }}
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
IS_FIRST_TIME: ${{ steps.check-first-time.outputs.is_first_time }}
run: |
GREETING=""
if [ "$IS_FIRST_TIME" = "true" ]; then
GREETING='This is a first-time contributor. Start your comment with: "Thanks for opening your first issue!"'
fi
printf '%s' "$ISSUE_TITLE" > /tmp/issue-title.txt
printf '%s' "${ISSUE_BODY:-}" > /tmp/issue-body.txt
printf '%s' "$ISSUE_AUTHOR" > /tmp/issue-author.txt
printf '%s' "$GREETING" > /tmp/greeting.txt
PAYLOAD=$(jq -n \
--rawfile title /tmp/issue-title.txt \
--rawfile body /tmp/issue-body.txt \
--rawfile author /tmp/issue-author.txt \
--rawfile greeting /tmp/greeting.txt \
--rawfile repo_context /tmp/repo-context.txt \
--rawfile context /tmp/file-context.txt \
'{
model: "anthropic/claude-opus-4.6",
messages: [
{
role: "system",
content: ("You are a triage bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nYou have access to relevant source files for context.\n\nAnalyze the issue and produce a single comment. Your job is to collect missing information needed to diagnose the issue, NOT to guess the cause.\n\nFormat:\n\n1. One sentence acknowledging the issue.\n2. **Missing information** - Ask specific questions about what is missing from the report. Focus on reproducing the issue. Do NOT speculate about root causes or mention internal code/files — you will almost certainly be wrong without logs. Instead, ask for:\n - Exact steps to reproduce (if not provided)\n - Expected vs actual behavior (if unclear)\n - Error messages or screenshots (if not provided)\n - OS and app version (if not provided)\n - For bug reports: if logs are needed, tell the user EXACTLY how to get them:\n - macOS app logs: `~/Library/Logs/Donut Browser/`\n - Linux app logs: `~/.local/share/DonutBrowser/logs/`\n - Windows app logs: `%APPDATA%\\DonutBrowser\\logs\\`\n - Sync server logs: `docker logs <container>` or check the server console\n - Provide a ready-to-run shell command when possible.\n - For self-hosted sync issues: check if the user is using the latest Docker image (`docker pull donutbrowser/donut-sync:latest`).\n - Only ask for information that is actually missing. If the issue is already detailed, just acknowledge it.\n3. Suggest a label: `Label: bug` or `Label: enhancement` on its own line.\n\nRules:\n- Do NOT include a \"Possible cause\" section. Do not speculate about what code might be causing the issue.\n- Be brief and focused on collecting actionable information from the reporter.\n- If the issue already has everything needed (steps to reproduce, logs, version, OS), just acknowledge it.\n- Never exceed 15 lines.")
},
{
role: "user",
content: (
(if ($greeting | length) > 0 then $greeting + "\n\n" else "" end) +
"Analyze this issue:\n\nTitle: " + $title +
"\nAuthor: " + $author +
"\n\nBody:\n" + $body +
"\n\nRelevant source files:\n" + $context
)
}
]
}')
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/ai-comment.txt
if [ ! -s /tmp/ai-comment.txt ]; then
echo "::error::AI response was empty"
echo "Raw response:"
echo "$RESPONSE"
exit 1
fi
- name: Post comment and label
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBER: ${{ github.event.issue.number }}
run: |
LABEL=$(grep -oP '^Label:\s*\K.*' /tmp/ai-comment.txt | tail -1 | tr '[:upper:]' '[:lower:]' | xargs)
sed -i '/^Label:/d' /tmp/ai-comment.txt
gh issue comment "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file /tmp/ai-comment.txt
if [ "$LABEL" = "bug" ]; then
gh issue edit "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "bug" 2>/dev/null || true
elif [ "$LABEL" = "enhancement" ]; then
gh issue edit "$ISSUE_NUMBER" --repo "$GITHUB_REPOSITORY" --add-label "enhancement" 2>/dev/null || true
fi
analyze-pr:
if: github.repository == 'zhom/donutbrowser' && github.event_name == 'pull_request_target' && github.actor != 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Check if first-time contributor
id: check-first-time
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
PR_COUNT=$(gh api "/repos/${{ github.repository }}/pulls?state=all&per_page=100" \
--jq "[.[] | select(.user.login == \"$PR_AUTHOR\" and .number != ${{ github.event.pull_request.number }})] | length" \
|| echo "0")
if [ "$PR_COUNT" = "0" ]; then
echo "is_first_time=true" >> $GITHUB_OUTPUT
else
echo "is_first_time=false" >> $GITHUB_OUTPUT
fi
- name: Gather PR context
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
# Get changed files list
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \
--jq '.[] | "- \(.filename) (\(.status)) +\(.additions)/-\(.deletions)"' \
> /tmp/pr-files.txt
# Get the actual diff
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER" \
--header "Accept: application/vnd.github.diff" \
> /tmp/pr-diff-full.txt 2>/dev/null || true
head -c 20000 /tmp/pr-diff-full.txt > /tmp/pr-diff.txt
# Get CONTRIBUTING.md and README.md for context
cat CONTRIBUTING.md > /tmp/contributing.txt 2>/dev/null || echo "Not found" > /tmp/contributing.txt
head -50 README.md > /tmp/readme.txt 2>/dev/null || echo "Not found" > /tmp/readme.txt
# Read project guidelines (contains repo structure)
cp CLAUDE.md /tmp/repo-context.txt
# Read full contents of all changed files (skip binary)
echo "" > /tmp/related-file-contents.txt
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --jq '.[].filename' | while IFS= read -r filepath; do
if [ -f "$filepath" ] && file --mime "$filepath" | grep -q "text/"; then
echo "=== $filepath (full file) ===" >> /tmp/related-file-contents.txt
cat "$filepath" >> /tmp/related-file-contents.txt
echo "" >> /tmp/related-file-contents.txt
fi
done
head -c 100000 /tmp/related-file-contents.txt > /tmp/pr-file-context.txt
- name: Analyze PR with AI
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_BODY: ${{ github.event.pull_request.body }}
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_BASE: ${{ github.event.pull_request.base.ref }}
PR_HEAD: ${{ github.event.pull_request.head.ref }}
IS_FIRST_TIME: ${{ steps.check-first-time.outputs.is_first_time }}
run: |
GREETING=""
if [ "$IS_FIRST_TIME" = "true" ]; then
GREETING='This is a first-time contributor. Start your comment with: "Thanks for your first PR!"'
fi
printf '%s' "$PR_TITLE" > /tmp/pr-title.txt
printf '%s' "${PR_BODY:-}" > /tmp/pr-body.txt
printf '%s' "$PR_AUTHOR" > /tmp/pr-author.txt
printf '%s' "$PR_BASE" > /tmp/pr-base.txt
printf '%s' "$PR_HEAD" > /tmp/pr-head.txt
printf '%s' "$GREETING" > /tmp/greeting.txt
PAYLOAD=$(jq -n \
--rawfile title /tmp/pr-title.txt \
--rawfile body /tmp/pr-body.txt \
--rawfile author /tmp/pr-author.txt \
--rawfile base /tmp/pr-base.txt \
--rawfile head /tmp/pr-head.txt \
--rawfile files /tmp/pr-files.txt \
--rawfile diff /tmp/pr-diff.txt \
--rawfile greeting /tmp/greeting.txt \
--rawfile repo_context /tmp/repo-context.txt \
--rawfile contributing /tmp/contributing.txt \
--rawfile file_context /tmp/pr-file-context.txt \
'{
model: "anthropic/claude-opus-4.6",
messages: [
{
role: "system",
content: ("You are a code review bot for Donut Browser, an open-source anti-detect browser (Tauri desktop app: Rust backend + Next.js frontend).\n\nProject guidelines and structure:\n" + $repo_context + "\n\nContributing guidelines:\n" + $contributing + "\n\nYou have access to the full changed files and the diff. Use them to give a substantive review.\n\nReview this PR and produce a single comment. Format:\n\n1. One sentence summarizing what this PR does and whether the approach is sound.\n2. **Code review** - Specific observations about the actual code changes. Mention file names and what you see in the diff. Look for:\n - Bugs or logic errors in the changed code\n - Security issues (SQL injection, path traversal, XSS, command injection)\n - Missing error handling or edge cases\n - Breaking changes to existing APIs or behavior\n - If UI text was added/changed, check if all 7 translation files (en, es, fr, ja, pt, ru, zh) in src/i18n/locales/ were updated\n - If Tauri commands were added/removed, the unused-commands test in lib.rs needs updating\n3. **Suggestions** - Concrete improvements if any. Skip if the PR looks good.\n\nRules:\n- Be substantive. Review the actual diff, not just the description.\n- Do NOT nitpick formatting or style — the project has automated linting (biome + clippy + rustfmt).\n- Do NOT just summarize the PR description back to the user — they wrote it, they know what it says.\n- If the PR is good, say so briefly.\n- Never exceed 20 lines.")
},
{
role: "user",
content: (
(if ($greeting | length) > 0 then $greeting + "\n\n" else "" end) +
"Review this PR:\n\nTitle: " + $title +
"\nAuthor: " + $author +
"\nBase: " + $base + " <- Head: " + $head +
"\n\nDescription:\n" + $body +
"\n\nChanged files:\n" + $files +
"\n\nDiff:\n" + $diff +
"\n\nFull file contents:\n" + $file_context
)
}
]
}')
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
jq -r '.choices[0].message.content // empty' <<< "$RESPONSE" > /tmp/ai-comment.txt
if [ ! -s /tmp/ai-comment.txt ]; then
echo "::error::AI response was empty"
echo "Raw response:"
echo "$RESPONSE"
exit 1
fi
- name: Post comment
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
gh pr comment "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --body-file /tmp/ai-comment.txt
opencode-command:
if: |
github.repository == 'zhom/donutbrowser' &&
(github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment') &&
(contains(github.event.comment.body, ' /oc') ||
startsWith(github.event.comment.body, '/oc') ||
contains(github.event.comment.body, ' /opencode') ||
startsWith(github.event.comment.body, '/opencode'))
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
- name: Run opencode
uses: anomalyco/opencode/github@a35b8a95c27d28e979a3826e1289d7ee87f40251 #v1.4.11
env:
ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }}
TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
model: zai-coding-plan/glm-4.7