diff --git a/.github/scripts/bot.sh b/.github/scripts/bot.sh new file mode 100644 index 0000000..3bedf9a --- /dev/null +++ b/.github/scripts/bot.sh @@ -0,0 +1,122 @@ +#!/usr/bin/env bash +# Lightweight triage and reply bot for issues and pull requests. +# Runs inside GitHub Actions; no server involved. +# +# Triggers (see bot.yml): +# - issue opened: exactly one triage comment per issue +# - comment containing @artex-bot: answers that comment (works in PR +# threads too, since issue_comment fires there) +# - pull request opened: one first-look review comment +# The bot never posts an approving review and never replies to itself, +# to the maintainer, or to other bots (enforced in bot.yml). +set -uo pipefail + +MAX_CHARS=8000 +FOOTER='--- +🤖 automated reply from the repo bot. A human maintainer reads everything. Ping me in any comment with `@artex-bot`.' + +log() { echo "[bot] $*"; } + +[ -z "${DEEPSEEK_API_KEY:-}" ] && { log "DEEPSEEK_API_KEY secret is not set; skipping."; exit 0; } + +ev=$(cat "$GITHUB_EVENT_PATH") +event_name="$GITHUB_EVENT_NAME" +repo="$GITHUB_REPOSITORY" + +ask_model() { # $1 = system prompt, $2 = user payload; prints the answer + local body="${2:0:$MAX_CHARS}" + local req + req=$(jq -n --arg sys "$1" --arg body "$body" \ + '{model: "deepseek-chat", + messages: [{role: "system", content: $sys}, {role: "user", content: $body}], + max_tokens: 900, temperature: 0.2}') + curl -sS --max-time 120 https://api.deepseek.com/chat/completions \ + -H "Authorization: Bearer $DEEPSEEK_API_KEY" \ + -H 'Content-Type: application/json' \ + -d "$req" | jq -r '.choices[0].message.content // empty' +} + +repo_context() { git ls-files | head -120; } + +post_comment() { # $1 = issue or PR number, $2 = markdown text + { echo "$2"; echo; echo "$FOOTER"; } > /tmp/bot-comment.md + if gh issue view "$1" --json pull_request --jq '.pull_request' | grep -q .; then + gh pr comment "$1" --body-file /tmp/bot-comment.md + else + gh issue comment "$1" --body-file /tmp/bot-comment.md + fi +} + +eyes() { # $1 = issue or PR number + gh api -X POST "repos/$repo/issues/$1/reactions" -f content=eyes >/dev/null 2>&1 || true +} + +already_triaged() { # $1 = issue number + gh api "repos/$repo/issues/$1/comments" --jq '.[].body' 2>/dev/null \ + | grep -q 'automated reply from the repo bot' +} + +answer="" + +case "$event_name" in + issues) + number=$(jq -r '.issue.number' <<<"$ev") + title=$(jq -r '.issue.title' <<<"$ev") + body=$(jq -r '.issue.body // ""' <<<"$ev") + if already_triaged "$number"; then log "issue #$number already triaged"; exit 0; fi + eyes "$number" + answer=$(ask_model \ + 'You are the triage bot of an open-source Go + Next.js repository (an autonomous penetration testing agent system; AGPL-3.0). Classify this report as bug, question, or feature request. If it is a question, answer it concisely using the repository file list as context. If it is a bug report, say what reproduction steps or version information are missing. If it is a feature request, restate it in one sentence. Maximum 180 words, plain markdown, reply in the language of the report, no heading.' \ + "Issue title: $title + +Issue body: +$body + +Repository file list: +$(repo_context)") + [ -z "$answer" ] && { log "model returned nothing"; exit 0; } + post_comment "$number" "$answer" + ;; + + issue_comment) + number=$(jq -r '.issue.number' <<<"$ev") + body=$(jq -r '.comment.body' <<<"$ev") + question="${body#*@artex-bot}" # strip up to the mention + echo "$body" | grep -qi '@artex-bot' || { log "no mention; skipping"; exit 0; } + eyes "$number" + answer=$(ask_model \ + 'You are the helper bot of an open-source Go + Next.js repository (an autonomous penetration testing agent system; AGPL-3.0). Someone pinged you with a question about this repository. Answer it concisely and only from what the repository file list suggests; say plainly when you are not sure and suggest opening a proper issue. Maximum 180 words, plain markdown, reply in the language of the question.' \ + "Question: +$question + +Repository file list: +$(repo_context)") + [ -z "$answer" ] && { log "model returned nothing"; exit 0; } + post_comment "$number" "$answer" + ;; + + pull_request) + number=$(jq -r '.pull_request.number' <<<"$ev") + title=$(jq -r '.pull_request.title' <<<"$ev") + body=$(jq -r '.pull_request.body // ""' <<<"$ev") + eyes "$number" + diff=$(gh pr diff "$number" 2>/dev/null | head -c 4000) + answer=$(ask_model \ + 'You are the first-look bot of an open-source Go + Next.js repository (an autonomous penetration testing agent system; AGPL-3.0). Summarize what this pull request changes in two or three sentences, then a short checklist: does it include or update tests, does it touch documentation where needed, does anything look inconsistent with the described goal. You are commenting for information only; you do not approve or request changes. Maximum 180 words, plain markdown.' \ + "Pull request title: $title + +Pull request description: +$body + +Diff (truncated): +$diff") + [ -z "$answer" ] && { log "model returned nothing"; exit 0; } + post_comment "$number" "$answer" + ;; + + *) + log "unhandled event $event_name; skipping" + ;; +esac + +log "done" diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml new file mode 100644 index 0000000..9853b0e --- /dev/null +++ b/.github/workflows/bot.yml @@ -0,0 +1,31 @@ +name: bot + +on: + issues: + types: [opened] + issue_comment: + types: [created] + pull_request: + types: [opened] + +permissions: + issues: write + pull-requests: write + contents: read + +jobs: + reply: + # Serves visitors only: never react to the maintainer, to bots, or to itself. + if: > + github.actor != 'BaaRai' && + github.actor != 'github-actions[bot]' && + github.actor != 'dependabot[bot]' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: triage or reply + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_EVENT_PATH: ${{ github.event_path }} + DEEPSEEK_API_KEY: ${{ secrets.DEEPSEEK_API_KEY }} + run: bash .github/scripts/bot.sh