mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-22 20:06:18 +02:00
239 lines
11 KiB
YAML
239 lines
11 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: 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" \
|
|
--jq "map(select(.user.login == \"$ISSUE_AUTHOR\" and .number != ${{ github.event.issue.number }})) | length" \
|
|
--paginate || echo "0")
|
|
|
|
if [ "$ISSUE_COUNT" = "0" ]; then
|
|
echo "is_first_time=true" >> $GITHUB_OUTPUT
|
|
else
|
|
echo "is_first_time=false" >> $GITHUB_OUTPUT
|
|
fi
|
|
|
|
- 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
|
|
|
|
# Write all user content to files to avoid shell escaping issues
|
|
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
|
|
|
|
# Build the JSON payload entirely in jq — never interpolate user content in shell
|
|
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 \
|
|
'{
|
|
model: "z-ai/glm-5",
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: "You are a triage bot for Donut Browser (open-source anti-detect browser, Tauri + Next.js + Rust).\n\nAnalyze the issue and produce a single concise comment. Format:\n\n1. One sentence acknowledging what the user wants.\n2. A short **Action items** list - what specific info is missing or what the user should do next. Only include items that are actually missing. If the issue is complete, say so and skip this section.\n3. Suggest a label at the very end of your response on its own line in the exact format: Label: bug OR Label: enhancement\n\nRules:\n- Be brief. No filler, no generic tips, no templates.\n- If it is a bug report, check for: reproduction steps, OS/version, error messages. Only ask for what is actually missing.\n- If it is a feature request, check for: clear description of desired behavior, use case. Only ask for what is actually missing.\n- If the issue already has everything needed, just acknowledge it.\n- Never exceed 6 items total."
|
|
},
|
|
{
|
|
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
|
|
)
|
|
}
|
|
]
|
|
}')
|
|
|
|
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
|
|
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$PAYLOAD")
|
|
|
|
# Extract the comment using jq — never parse AI output in shell
|
|
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: |
|
|
# Extract and strip the label line before posting
|
|
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: 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: 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
|
|
|
|
# Write all user content to files to avoid shell escaping issues
|
|
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
|
|
|
|
gh api "/repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" \
|
|
--jq '.[] | "- \(.filename) (\(.status)) +\(.additions)/-\(.deletions)"' \
|
|
> /tmp/pr-files.txt
|
|
|
|
# Build the JSON payload entirely in jq — never interpolate user content in shell
|
|
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 greeting /tmp/greeting.txt \
|
|
'{
|
|
model: "z-ai/glm-5",
|
|
max_tokens: 1024,
|
|
messages: [
|
|
{
|
|
role: "system",
|
|
content: "You are a review bot for Donut Browser (open-source anti-detect browser, Tauri + Next.js + Rust).\n\nReview this PR and produce a single concise comment. Format:\n\n1. One sentence summarizing what this PR does.\n2. **Action items** - only list things that actually need to be fixed or addressed. If the PR looks good, say so and skip this section.\n\nRules:\n- Be brief. No filler, no praise padding.\n- Focus on: bugs, security issues, missing edge cases, breaking changes.\n- If the PR touches UI text or adds new strings, remind to update translation files in src/i18n/locales/.\n- If the PR modifies Tauri commands, remind to check the unused-commands test.\n- Do not nitpick style or formatting - the project has automated linting.\n- Never exceed 8 lines total."
|
|
},
|
|
{
|
|
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
|
|
)
|
|
}
|
|
]
|
|
}')
|
|
|
|
RESPONSE=$(curl -fsSL https://openrouter.ai/api/v1/chat/completions \
|
|
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$PAYLOAD")
|
|
|
|
# Extract the comment using jq — never parse AI output in shell
|
|
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
|
|
|
|
- name: Run opencode
|
|
uses: anomalyco/opencode/github@4ee426ba549131c4903a71dfb6259200467aca81 #v1.2.27
|
|
env:
|
|
ZHIPU_API_KEY: ${{ secrets.ZHIPU_API_KEY }}
|
|
TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
with:
|
|
model: zai-coding-plan/glm-4.7
|