name: Bot Reply on Mention on: issue_comment: types: [created] jobs: continuous-reply: if: ${{ contains(github.event.comment.body, '@mirrobot') || contains(github.event.comment.body, '@mirrobot-agent') }} runs-on: ubuntu-latest permissions: contents: write issues: write pull-requests: write env: THREAD_NUMBER: ${{ github.event.issue.number }} BOT_NAMES_JSON: '["mirrobot", "mirrobot-agent", "mirrobot-agent[bot]"]' IGNORE_BOT_NAMES_JSON: '["ellipsis-dev"]' COMMENT_FETCH_LIMIT: '20' REVIEW_FETCH_LIMIT: '15' REVIEW_THREAD_FETCH_LIMIT: '20' THREAD_COMMENT_FETCH_LIMIT: '5' steps: - name: Checkout repository uses: actions/checkout@v4 - name: Bot Setup id: setup uses: ./.github/actions/bot-setup with: bot-app-id: ${{ secrets.BOT_APP_ID }} bot-private-key: ${{ secrets.BOT_PRIVATE_KEY }} opencode-api-key: ${{ secrets.OPENCODE_API_KEY }} opencode-model: ${{ secrets.OPENCODE_MODEL }} opencode-fast-model: ${{ secrets.OPENCODE_FAST_MODEL }} custom-providers-json: ${{ secrets.CUSTOM_PROVIDERS_JSON }} - name: Add reaction to comment env: GH_TOKEN: ${{ steps.setup.outputs.token }} run: | gh api \ --method POST \ -H "Accept: application/vnd.github+json" \ /repos/${{ github.repository }}/issues/comments/${{ github.event.comment.id }}/reactions \ -f content='eyes' - name: Gather Full Thread Context id: context env: GH_TOKEN: ${{ steps.setup.outputs.token }} BOT_NAMES_JSON: ${{ env.BOT_NAMES_JSON }} IGNORE_BOT_NAMES_JSON: ${{ env.IGNORE_BOT_NAMES_JSON }} run: | # Common Info echo "NEW_COMMENT_AUTHOR=${{ github.event.comment.user.login }}" >> $GITHUB_ENV # Use a unique delimiter for safety COMMENT_DELIMITER="GH_BODY_DELIMITER_$(openssl rand -hex 8)" { echo "NEW_COMMENT_BODY<<$COMMENT_DELIMITER"; echo "${{ github.event.comment.body }}"; echo "$COMMENT_DELIMITER"; } >> "$GITHUB_ENV" # Determine if PR or Issue if [ -n '${{ github.event.issue.pull_request }}' ]; then IS_PR="true" else IS_PR="false" fi echo "IS_PR=$IS_PR" >> $GITHUB_OUTPUT # Define a unique, random delimiter for the main context block CONTEXT_DELIMITER="GH_CONTEXT_DELIMITER_$(openssl rand -hex 8)" # Fetch and Format Context based on type if [[ "$IS_PR" == "true" ]]; then # Fetch PR data pr_json=$(gh pr view ${{ env.THREAD_NUMBER }} --repo ${{ github.repository }} --json author,title,body,createdAt,state,headRefName,baseRefName,headRefOid,additions,deletions,commits,files,closingIssuesReferences,headRepository) # Debug: Output pr_json and review_comments_json for inspection echo "$pr_json" > pr_json.txt # Fetch timeline data to find cross-references timeline_data=$(gh api "/repos/${{ github.repository }}/issues/${{ env.THREAD_NUMBER }}/timeline") repo_owner="${GITHUB_REPOSITORY%/*}" repo_name="${GITHUB_REPOSITORY#*/}" GRAPHQL_QUERY='query($owner:String!, $name:String!, $number:Int!, $commentLimit:Int!, $reviewLimit:Int!, $threadLimit:Int!, $threadCommentLimit:Int!) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { comments(last: $commentLimit) { nodes { databaseId author { login } body createdAt isMinimized minimizedReason } } reviews(last: $reviewLimit) { nodes { databaseId author { login } body state submittedAt isMinimized minimizedReason } } reviewThreads(last: $threadLimit) { nodes { id isResolved isOutdated comments(last: $threadCommentLimit) { nodes { databaseId author { login } body createdAt path line originalLine diffHunk isMinimized minimizedReason pullRequestReview { databaseId isMinimized minimizedReason } } } } } } } }' discussion_data=$(gh api graphql \ -F owner="$repo_owner" \ -F name="$repo_name" \ -F number=${{ env.THREAD_NUMBER }} \ -F commentLimit=${{ env.COMMENT_FETCH_LIMIT }} \ -F reviewLimit=${{ env.REVIEW_FETCH_LIMIT }} \ -F threadLimit=${{ env.REVIEW_THREAD_FETCH_LIMIT }} \ -F threadCommentLimit=${{ env.THREAD_COMMENT_FETCH_LIMIT }} \ -f query="$GRAPHQL_QUERY") echo "$discussion_data" > discussion_data.txt # For prompt context echo "PR_HEAD_SHA=$(echo "$pr_json" | jq -r .headRefOid)" >> $GITHUB_ENV echo "THREAD_AUTHOR=$(echo "$pr_json" | jq -r .author.login)" >> $GITHUB_ENV echo "BASE_BRANCH=$(echo "$pr_json" | jq -r .baseRefName)" >> $GITHUB_ENV # Prepare all variables from JSON author=$(echo "$pr_json" | jq -r .author.login) created_at=$(echo "$pr_json" | jq -r .createdAt) base_branch=$(echo "$pr_json" | jq -r .baseRefName) head_branch=$(echo "$pr_json" | jq -r .headRefName) state=$(echo "$pr_json" | jq -r .state) additions=$(echo "$pr_json" | jq -r .additions) deletions=$(echo "$pr_json" | jq -r .deletions) total_commits=$(echo "$pr_json" | jq -r '.commits | length') changed_files_count=$(echo "$pr_json" | jq -r '.files | length') title=$(echo "$pr_json" | jq -r .title) body=$(echo "$pr_json" | jq -r '.body // "(No description provided)"') # Prepare changed files list # Build changed files list with correct jq interpolations for additions and deletions # Previous pattern had a missing backslash before the deletions interpolation, leaving a literal '((.deletions))'. changed_files_list=$(echo "$pr_json" | jq -r '.files[] | "- \(.path) (MODIFIED) +\((.additions))/-\((.deletions))"') # Prepare general PR comments (exclude ignored bots) comments=$(echo "$discussion_data" | jq -r --argjson ignored "$IGNORE_BOT_NAMES_JSON" ' ((.data.repository.pullRequest.comments.nodes // []) | map(select((.isMinimized != true) and (((.author.login? // "unknown") as $login | $ignored | index($login)) | not)))) | if length > 0 then map("- " + (.author.login? // "unknown") + " at " + (.createdAt // "N/A") + ":\n" + ((.body // "") | tostring) + "\n") | join("") else "No general comments." end') # ===== ACCURATE FILTERING & COUNTING (Fixed math logic) ===== stats_json=$(echo "$discussion_data" | jq -r --argjson ignored "$IGNORE_BOT_NAMES_JSON" ' # Define filter logic def is_valid_review: (.author.login? // "unknown") as $login | $ignored | index($login) | not and (.isMinimized != true); def is_valid_comment: .isResolved != true and .isOutdated != true and (((.comments.nodes // []) | first | .isMinimized) != true) and ((((.comments.nodes // []) | first | .pullRequestReview.isMinimized) // false) != true); def is_valid_inline: .isMinimized != true and ((.pullRequestReview.isMinimized // false) != true) and (((.author.login? // "unknown") as $login | $ignored | index($login)) | not); # Calculate Reviews def raw_reviews: (.data.repository.pullRequest.reviews.nodes // []); def total_reviews: (raw_reviews | length); def included_reviews: ([raw_reviews[]? | select(is_valid_review)] | length); # Calculate Review Comments def raw_threads: (.data.repository.pullRequest.reviewThreads.nodes // []); def valid_threads: (raw_threads | map(select(is_valid_comment))); def all_valid_comments: (valid_threads | map(.comments.nodes // []) | flatten | map(select(is_valid_inline))); # We count total comments as "active/unresolved threads comments" def total_review_comments: (raw_threads | map(select(.isResolved != true and .isOutdated != true)) | map(.comments.nodes // []) | flatten | length); def included_review_comments: (all_valid_comments | length); { total_reviews: total_reviews, included_reviews: included_reviews, excluded_reviews: (total_reviews - included_reviews), total_review_comments: total_review_comments, included_review_comments: included_review_comments, excluded_comments: (total_review_comments - included_review_comments) } ') # Export stats to env vars filtered_reviews=$(echo "$stats_json" | jq .included_reviews) excluded_reviews=$(echo "$stats_json" | jq .excluded_reviews) filtered_comments=$(echo "$stats_json" | jq .included_review_comments) excluded_comments=$(echo "$stats_json" | jq .excluded_comments) echo "✓ Filtered reviews: $filtered_reviews included, $excluded_reviews excluded (ignored bots/hidden)" echo "✓ Filtered review comments: $filtered_comments included, $excluded_comments excluded (outdated/hidden)" # Reviews Text review_filter_err=$(mktemp 2>/dev/null || echo "/tmp/review_filter_err.log") if reviews=$(echo "$discussion_data" | jq -r --argjson ignored "$IGNORE_BOT_NAMES_JSON" ' if ((((.data.repository.pullRequest.reviews.nodes // []) | length) > 0)) then ((.data.repository.pullRequest.reviews.nodes // [])[]? | select( ((.author.login? // "unknown") as $login | $ignored | index($login) | not) and (.isMinimized != true) ) | "- " + (.author.login? // "unknown") + " at " + (.submittedAt // "N/A") + ":\n - Review body: " + (.body // "(No summary comment)") + "\n - State: " + (.state // "UNKNOWN") + "\n") else "No formal reviews." end' 2>"$review_filter_err"); then if [ -s "$review_filter_err" ]; then echo "::debug::jq stderr (reviews) emitted output:" cat "$review_filter_err" fi else echo "::warning::Review formatting failed, using unfiltered data" reviews="Error processing reviews." echo "FILTER_ERROR_REVIEWS=true" >> $GITHUB_ENV fi rm -f "$review_filter_err" || true # Review Comments Text review_comment_filter_err=$(mktemp 2>/dev/null || echo "/tmp/review_comment_filter_err.log") if review_comments=$(echo "$discussion_data" | jq -r --argjson ignored "$IGNORE_BOT_NAMES_JSON" ' ((.data.repository.pullRequest.reviewThreads.nodes // []) | map(select( .isResolved != true and .isOutdated != true and (((.comments.nodes // []) | first | .isMinimized) != true) and ((((.comments.nodes // []) | first | .pullRequestReview.isMinimized) // false) != true) )) | map(.comments.nodes // []) | flatten | map(select((.isMinimized != true) and ((.pullRequestReview.isMinimized // false) != true) and (((.author.login? // "unknown") as $login | $ignored | index($login)) | not)))) | if length > 0 then map("- " + (.author.login? // "unknown") + " at " + (.createdAt // "N/A") + " (" + (.path // "Unknown file") + ":" + ((.line // .originalLine // "N/A") | tostring) + "):\n " + ((.body // "") | tostring) + "\n") | join("") else "No inline review comments." end' 2>"$review_comment_filter_err"); then if [ -s "$review_comment_filter_err" ]; then echo "::debug::jq stderr (review comments) emitted output:" cat "$review_comment_filter_err" fi else echo "::warning::Review comment formatting failed" review_comments="Error processing review comments." echo "FILTER_ERROR_COMMENTS=true" >> $GITHUB_ENV fi rm -f "$review_comment_filter_err" || true # Store filtering statistics echo "EXCLUDED_REVIEWS=$excluded_reviews" >> $GITHUB_ENV echo "EXCLUDED_COMMENTS=$excluded_comments" >> $GITHUB_ENV # Build filtering summary filter_summary="Context filtering applied: ${excluded_reviews:-0} reviews and ${excluded_comments:-0} review comments excluded from this context." if [ "${FILTER_ERROR_REVIEWS}" = "true" ] || [ "${FILTER_ERROR_COMMENTS}" = "true" ]; then filter_summary="$filter_summary"$'\n'"Warning: Some filtering operations encountered errors. Context may include items that should have been filtered." fi # Prepare linked issues robustly by fetching each one individually. linked_issues_content="" issue_numbers=$(echo "$pr_json" | jq -r '.closingIssuesReferences[].number') if [ -z "$issue_numbers" ]; then linked_issues="No issues are formally linked for closure by this PR." else for number in $issue_numbers; do # Fetch each issue's data separately. This is more reliable for cross-repo issues or permission nuances. issue_details_json=$(gh issue view "$number" --repo "${{ github.repository }}" --json title,body 2>/dev/null || echo "{}") issue_title=$(echo "$issue_details_json" | jq -r '.title // "Title not available"') issue_body=$(echo "$issue_details_json" | jq -r '.body // "Body not available"') linked_issues_content+=$(printf "\n #%s\n %s\n \n%s\n\n\n" "$number" "$issue_title" "$issue_body") done linked_issues=$linked_issues_content fi # Prepare cross-references from timeline data references=$(echo "$timeline_data" | jq -r '.[] | select(.event == "cross-referenced") | .source.issue | "- Mentioned in \(.html_url | if contains("/pull/") then "PR" else "Issue" end): #\(.number) - \(.title)"') if [ -z "$references" ]; then references="This PR has not been mentioned in other issues or PRs."; fi # Step 1: Write the header for the multi-line environment variable echo "THREAD_CONTEXT<<$CONTEXT_DELIMITER" >> "$GITHUB_ENV" # Step 2: Append the content line by line echo "Type: Pull Request" >> "$GITHUB_ENV" echo "PR Number: #${{ env.THREAD_NUMBER }}" >> "$GITHUB_ENV" echo "Title: $title" >> "$GITHUB_ENV" echo "Author: $author" >> "$GITHUB_ENV" echo "Created At: $created_at" >> "$GITHUB_ENV" echo "Base Branch (target): $base_branch" >> "$GITHUB_ENV" echo "Head Branch (source): $head_branch" >> "$GITHUB_ENV" echo "State: $state" >> "$GITHUB_ENV" echo "Additions: $additions" >> "$GITHUB_ENV" echo "Deletions: $deletions" >> "$GITHUB_ENV" echo "Total Commits: $total_commits" >> "$GITHUB_ENV" echo "Changed Files: $changed_files_count files" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$title" >> "$GITHUB_ENV" echo "---" >> "$GITHUB_ENV" echo "$body" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$comments" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$reviews" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$review_comments" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$changed_files_list" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$linked_issues" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" # Step 3: Write the closing delimiter # Add cross-references and filtering summary to the final context echo "" >> "$GITHUB_ENV" echo "$references" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$filter_summary" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$CONTEXT_DELIMITER" >> "$GITHUB_ENV" else # It's an Issue issue_data=$(gh issue view ${{ env.THREAD_NUMBER }} --repo ${{ github.repository }} --json author,title,body,createdAt,state,comments) timeline_data=$(gh api "/repos/${{ github.repository }}/issues/${{ env.THREAD_NUMBER }}/timeline") echo "THREAD_AUTHOR=$(echo "$issue_data" | jq -r .author.login)" >> $GITHUB_ENV # Prepare metadata author=$(echo "$issue_data" | jq -r .author.login) created_at=$(echo "$issue_data" | jq -r .createdAt) state=$(echo "$issue_data" | jq -r .state) title=$(echo "$issue_data" | jq -r .title) body=$(echo "$issue_data" | jq -r '.body // "(No description provided)"') # Prepare comments (exclude ignored bots) comments=$(echo "$issue_data" | jq -r --argjson ignored "$IGNORE_BOT_NAMES_JSON" 'if (((.comments // []) | length) > 0) then ((.comments[]? | select((.author.login as $login | $ignored | index($login)) | not)) | "- " + (.author.login // "unknown") + " at " + (.createdAt // "N/A") + ":\n" + ((.body // "") | tostring) + "\n") else "No comments have been posted yet." end') # Prepare cross-references references=$(echo "$timeline_data" | jq -r '.[] | select(.event == "cross-referenced") | .source.issue | "- Mentioned in \(.html_url | if contains("/pull/") then "PR" else "Issue" end): #\(.number) - \(.title)"') if [ -z "$references" ]; then references="No other issues or PRs have mentioned this thread."; fi # Step 1: Write the header echo "THREAD_CONTEXT<<$CONTEXT_DELIMITER" >> "$GITHUB_ENV" # Step 2: Append the content line by line echo "Type: Issue" >> "$GITHUB_ENV" echo "Issue Number: #${{ env.THREAD_NUMBER }}" >> "$GITHUB_ENV" echo "Title: $title" >> "$GITHUB_ENV" echo "Author: $author" >> "$GITHUB_ENV" echo "Created At: $created_at" >> "$GITHUB_ENV" echo "State: $state" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$body" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$comments" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" echo "$references" >> "$GITHUB_ENV" echo "" >> "$GITHUB_ENV" # Step 3: Write the footer echo "$CONTEXT_DELIMITER" >> "$GITHUB_ENV" fi - name: Clear pending bot review if: steps.context.outputs.IS_PR == 'true' env: GH_TOKEN: ${{ steps.setup.outputs.token }} BOT_NAMES_JSON: ${{ env.BOT_NAMES_JSON }} run: | pending_review_ids=$(gh api --paginate \ "/repos/${GITHUB_REPOSITORY}/pulls/${{ env.THREAD_NUMBER }}/reviews" \ | jq -r --argjson bots "$BOT_NAMES_JSON" '.[]? | select((.state // "") == "PENDING" and (((.user.login // "") as $login | $bots | index($login)))) | .id' \ | sort -u) if [ -z "$pending_review_ids" ]; then echo "No pending bot reviews to clear." exit 0 fi while IFS= read -r review_id; do [ -z "$review_id" ] && continue if gh api \ --method DELETE \ -H "Accept: application/vnd.github+json" \ "/repos/${GITHUB_REPOSITORY}/pulls/${{ env.THREAD_NUMBER }}/reviews/$review_id"; then echo "Cleared pending review $review_id" else echo "::warning::Failed to clear pending review $review_id" fi done <<< "$pending_review_ids" - name: Determine Review Type and Last Reviewed SHA if: steps.context.outputs.IS_PR == 'true' id: review_type env: GH_TOKEN: ${{ steps.setup.outputs.token }} BOT_NAMES_JSON: ${{ env.BOT_NAMES_JSON }} run: | pr_summary_payload=$(gh pr view ${{ env.THREAD_NUMBER }} --repo ${{ github.repository }} --json comments,reviews) detect_json=$(echo "$pr_summary_payload" | jq -c --argjson bots "$BOT_NAMES_JSON" ' def ts(x): if (x//""=="") then null else x end; def items: [ (.comments[]? | select(.author.login as $a | $bots | index($a)) | {type:"comment", body:(.body//""), ts:(.updatedAt // .createdAt // "")} ), (.reviews[]? | select(.author.login as $a | $bots | index($a)) | {type:"review", body:(.body//""), ts:(.submittedAt // .updatedAt // .createdAt // "")} ) ] | sort_by(.ts) | .; def has_phrase: (.body//"") | test("This review was generated by an AI assistant\\.?"); def has_marker: (.body//"") | test(""); { latest_phrase: (items | map(select(has_phrase)) | last // {}), latest_marker: (items | map(select(has_marker)) | last // {}) } ') latest_phrase_ts=$(echo "$detect_json" | jq -r '.latest_phrase.ts // ""') latest_marker_ts=$(echo "$detect_json" | jq -r '.latest_marker.ts // ""') latest_marker_body=$(echo "$detect_json" | jq -r '.latest_marker.body // ""') echo "is_first_review=false" >> $GITHUB_OUTPUT resolved_sha="" if [ -z "$latest_phrase_ts" ] && [ -z "$latest_marker_ts" ]; then echo "is_first_review=true" >> $GITHUB_OUTPUT fi if [ -n "$latest_marker_ts" ] && { [ -z "$latest_phrase_ts" ] || [ "$latest_marker_ts" \> "$latest_phrase_ts" ] || [ "$latest_marker_ts" = "$latest_phrase_ts" ]; }; then resolved_sha=$(printf "%s" "$latest_marker_body" | sed -nE 's/.*.*/\1/p' | head -n1) fi if [ -z "$resolved_sha" ] && [ -n "$latest_phrase_ts" ]; then reviews_json=$(gh api "/repos/${{ github.repository }}/pulls/${{ env.THREAD_NUMBER }}/reviews" || echo '[]') resolved_sha=$(echo "$reviews_json" | jq -r --argjson bots "$BOT_NAMES_JSON" '[.[] | select((.user.login // "") as $u | $bots | index($u)) | .commit_id] | last // ""') fi if [ -n "$resolved_sha" ]; then echo "last_reviewed_sha=$resolved_sha" >> $GITHUB_OUTPUT echo "$resolved_sha" > last_review_sha.txt else echo "last_reviewed_sha=" >> $GITHUB_OUTPUT echo "" > last_review_sha.txt fi - name: Save secure prompt from base branch run: cp .github/prompts/bot-reply.md /tmp/bot-reply.md - name: Checkout PR head if: steps.context.outputs.IS_PR == 'true' uses: actions/checkout@v4 with: ref: ${{ env.PR_HEAD_SHA }} token: ${{ steps.setup.outputs.token }} fetch-depth: 0 # Full history needed for git operations and code analysis - name: Generate PR Diffs (Full and Incremental) if: steps.context.outputs.IS_PR == 'true' id: generate_diffs env: BASE_BRANCH: ${{ env.BASE_BRANCH }} run: | mkdir -p "$GITHUB_WORKSPACE/.mirrobot_files" BASE_BRANCH="${BASE_BRANCH}" CURRENT_SHA="${PR_HEAD_SHA}" LAST_SHA="${{ steps.review_type.outputs.last_reviewed_sha }}" # Always generate full diff against base branch echo "Generating full PR diff against base branch: $BASE_BRANCH" if git fetch origin "$BASE_BRANCH":refs/remotes/origin/"$BASE_BRANCH" 2>/dev/null; then if MERGE_BASE=$(git merge-base origin/"$BASE_BRANCH" "$CURRENT_SHA" 2>/dev/null); then if DIFF_CONTENT=$(git diff --patch "$MERGE_BASE".."$CURRENT_SHA" 2>/dev/null); then DIFF_SIZE=${#DIFF_CONTENT} if [ $DIFF_SIZE -gt 500000 ]; then TRUNCATION_MSG=$'\n\n[DIFF TRUNCATED - PR is very large. Showing first 500KB only. Review scaled to high-impact areas.]' DIFF_CONTENT="${DIFF_CONTENT:0:500000}${TRUNCATION_MSG}" fi echo "$DIFF_CONTENT" > "$GITHUB_WORKSPACE/.mirrobot_files/first_review_diff.txt" echo "Full diff generated ($(echo "$DIFF_CONTENT" | wc -l) lines)" else echo "(Diff generation failed. Please refer to the changed files list above.)" > "$GITHUB_WORKSPACE/.mirrobot_files/first_review_diff.txt" fi else echo "(No common ancestor found. This might be a new branch or orphaned commits.)" > "$GITHUB_WORKSPACE/.mirrobot_files/first_review_diff.txt" fi else echo "(Base branch not available for diff. Please refer to the changed files list above.)" > "$GITHUB_WORKSPACE/.mirrobot_files/first_review_diff.txt" fi # Generate incremental diff if this is a follow-up review if [ -n "$LAST_SHA" ]; then echo "Generating incremental diff from $LAST_SHA to $CURRENT_SHA" if git fetch origin $LAST_SHA 2>/dev/null || git cat-file -e $LAST_SHA^{commit} 2>/dev/null; then if DIFF_CONTENT=$(git diff --patch $LAST_SHA..$CURRENT_SHA 2>/dev/null); then DIFF_SIZE=${#DIFF_CONTENT} if [ $DIFF_SIZE -gt 500000 ]; then TRUNCATION_MSG=$'\n\n[DIFF TRUNCATED - Changes are very large. Showing first 500KB only.]' DIFF_CONTENT="${DIFF_CONTENT:0:500000}${TRUNCATION_MSG}" fi echo "$DIFF_CONTENT" > "$GITHUB_WORKSPACE/.mirrobot_files/incremental_diff.txt" echo "Incremental diff generated ($(echo "$DIFF_CONTENT" | wc -l) lines)" else echo "(Unable to generate incremental diff.)" > "$GITHUB_WORKSPACE/.mirrobot_files/incremental_diff.txt" fi else echo "(Last reviewed SHA not accessible for incremental diff.)" > "$GITHUB_WORKSPACE/.mirrobot_files/incremental_diff.txt" fi else echo "(No previous review - incremental diff not applicable.)" > "$GITHUB_WORKSPACE/.mirrobot_files/incremental_diff.txt" fi - name: Checkout repository (for issues) if: steps.context.outputs.IS_PR == 'false' uses: actions/checkout@v4 with: token: ${{ steps.setup.outputs.token }} fetch-depth: 0 # Full history needed for git operations and code analysis - name: Analyze comment and respond env: GITHUB_TOKEN: ${{ steps.setup.outputs.token }} THREAD_CONTEXT: ${{ env.THREAD_CONTEXT }} NEW_COMMENT_AUTHOR: ${{ env.NEW_COMMENT_AUTHOR }} NEW_COMMENT_BODY: ${{ env.NEW_COMMENT_BODY }} THREAD_NUMBER: ${{ env.THREAD_NUMBER }} GITHUB_REPOSITORY: ${{ github.repository }} THREAD_AUTHOR: ${{ env.THREAD_AUTHOR }} PR_HEAD_SHA: ${{ env.PR_HEAD_SHA }} IS_FIRST_REVIEW: ${{ steps.review_type.outputs.is_first_review }} OPENCODE_PERMISSION: | { "bash": { "gh*": "allow", "git*": "allow", "jq*": "allow" }, "external_directory": "allow", "webfetch": "deny" } run: | # Only substitute the variables we intend; leave example $vars and secrets intact if [ "${{ steps.context.outputs.IS_PR }}" = "true" ]; then FULL_DIFF_PATH="$GITHUB_WORKSPACE/.mirrobot_files/first_review_diff.txt" INCREMENTAL_DIFF_PATH="$GITHUB_WORKSPACE/.mirrobot_files/incremental_diff.txt" LAST_REVIEWED_SHA="${{ steps.review_type.outputs.last_reviewed_sha }}" else FULL_DIFF_PATH="" INCREMENTAL_DIFF_PATH="" LAST_REVIEWED_SHA="" fi VARS='$THREAD_CONTEXT $NEW_COMMENT_AUTHOR $NEW_COMMENT_BODY $THREAD_NUMBER $GITHUB_REPOSITORY $THREAD_AUTHOR $PR_HEAD_SHA $IS_FIRST_REVIEW $FULL_DIFF_PATH $INCREMENTAL_DIFF_PATH $LAST_REVIEWED_SHA' FULL_DIFF_PATH="$FULL_DIFF_PATH" INCREMENTAL_DIFF_PATH="$INCREMENTAL_DIFF_PATH" LAST_REVIEWED_SHA="$LAST_REVIEWED_SHA" envsubst "$VARS" < /tmp/bot-reply.md | opencode run --share -