chore: import upstream snapshot with attribution
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
Upgrade checks / Notify on failure (push) Has been cancelled
Upgrade checks / Close issue on success (push) Has been cancelled
Schema Crash Test / Real-world schema crash test (232K schemas) (push) Has been cancelled
Run static analysis / static_analysis (push) Has been cancelled
Tests / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Tests / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Tests / Tests with lowest-direct dependencies (push) Has been cancelled
Tests / MCP conformance tests (push) Has been cancelled
Tests / Integration tests (push) Has been cancelled
Tests / Package install smoke (push) Has been cancelled
Upgrade checks / Static analysis (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.13 on ubuntu-latest (push) Has been cancelled
Upgrade checks / Tests: Python 3.10 on windows-latest (push) Has been cancelled
Upgrade checks / Integration tests (push) Has been cancelled
Update MCPServerConfig Schema / update-config-schema (push) Has been cancelled
Update SDK Documentation / update-sdk-docs (push) Has been cancelled
This commit is contained in:
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Get PR review threads with comments via GitHub GraphQL API
|
||||
#
|
||||
# Usage:
|
||||
# gh-get-review-threads.sh [FILTER]
|
||||
#
|
||||
# Arguments:
|
||||
# FILTER - Optional: filter for unresolved threads from specific author
|
||||
#
|
||||
# Environment (set by composite action):
|
||||
# MENTION_REPO - Repository (owner/repo format)
|
||||
# MENTION_PR_NUMBER - Pull request number
|
||||
# GITHUB_TOKEN - GitHub API token
|
||||
#
|
||||
# Output:
|
||||
# JSON array of review threads with nested comments
|
||||
|
||||
# Parse OWNER and REPO from MENTION_REPO
|
||||
REPO_FULL="${MENTION_REPO:?MENTION_REPO environment variable is required}"
|
||||
OWNER="${REPO_FULL%/*}"
|
||||
REPO="${REPO_FULL#*/}"
|
||||
PR_NUMBER="${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}"
|
||||
FILTER="${1:-}"
|
||||
|
||||
gh api graphql -f query='
|
||||
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
isResolved
|
||||
isOutdated
|
||||
path
|
||||
line
|
||||
comments(first: 50) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
author { login }
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -F owner="$OWNER" \
|
||||
-F repo="$REPO" \
|
||||
-F prNumber="$PR_NUMBER" \
|
||||
--jq '.data.repository.pullRequest.reviewThreads.nodes' | \
|
||||
if [ -n "$FILTER" ]; then
|
||||
jq --arg author "$FILTER" '
|
||||
map(select(
|
||||
.isResolved == false and
|
||||
.comments.nodes | any(.author.login == $author)
|
||||
))'
|
||||
else
|
||||
cat
|
||||
fi
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Resolve a GitHub PR review thread, optionally posting a comment first
|
||||
#
|
||||
# Usage:
|
||||
# gh-resolve-review-thread.sh THREAD_ID [COMMENT]
|
||||
#
|
||||
# Arguments:
|
||||
# THREAD_ID - The GraphQL node ID of the review thread to resolve
|
||||
# COMMENT - Optional: Comment body to post before resolving
|
||||
#
|
||||
# Environment (set by composite action):
|
||||
# MENTION_REPO - Repository (owner/repo format)
|
||||
# MENTION_PR_NUMBER - Pull request number
|
||||
# GITHUB_TOKEN - GitHub API token
|
||||
#
|
||||
# Behavior:
|
||||
# 1. If COMMENT is provided, posts it as a reply to the thread
|
||||
# 2. Resolves the thread
|
||||
|
||||
# Validate required environment variables
|
||||
: "${MENTION_REPO:?MENTION_REPO environment variable is required}"
|
||||
: "${MENTION_PR_NUMBER:?MENTION_PR_NUMBER environment variable is required}"
|
||||
THREAD_ID="${1:?Thread ID required}"
|
||||
COMMENT="${2:-}"
|
||||
|
||||
# Step 1: Post comment if provided
|
||||
if [ -n "$COMMENT" ]; then
|
||||
echo "Posting comment to thread..." >&2
|
||||
COMMENT_RESULT=$(gh api graphql -f query='
|
||||
mutation($threadId: ID!, $body: String!) {
|
||||
addPullRequestReviewThreadReply(input: {
|
||||
pullRequestReviewThreadId: $threadId,
|
||||
body: $body
|
||||
}) {
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}' -f threadId="$THREAD_ID" -f body="$COMMENT")
|
||||
if echo "$COMMENT_RESULT" | jq -e '.errors' > /dev/null 2>&1; then
|
||||
echo "Error posting comment: $COMMENT_RESULT" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Step 2: Resolve the thread
|
||||
echo "Resolving thread..." >&2
|
||||
RESOLVE_RESULT=$(gh api graphql -f query='
|
||||
mutation($threadId: ID!) {
|
||||
resolveReviewThread(input: {threadId: $threadId}) {
|
||||
thread {
|
||||
id
|
||||
isResolved
|
||||
}
|
||||
}
|
||||
}' -f threadId="$THREAD_ID" --jq '.data.resolveReviewThread.thread')
|
||||
|
||||
echo "$RESOLVE_RESULT"
|
||||
echo "✓ Thread resolved" >&2
|
||||
Executable
+251
@@ -0,0 +1,251 @@
|
||||
#!/bin/bash
|
||||
# pr-comment.sh - Queue a structured inline review comment for the PR review
|
||||
#
|
||||
# Usage:
|
||||
# pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> [suggestion via stdin]
|
||||
# pr-comment.sh <file> <line> --severity <level> --title <description> --why <reason> --no-suggestion
|
||||
#
|
||||
# Arguments:
|
||||
# file File path (required)
|
||||
# line Line number (required)
|
||||
# --severity Severity level: critical, high, medium, low, nitpick (required)
|
||||
# --title Brief description for comment heading (required)
|
||||
# --why One sentence explaining the risk/impact (required)
|
||||
# --no-suggestion Explicitly skip suggestion (use for architectural issues)
|
||||
#
|
||||
# The suggestion code is read from stdin (use heredoc). If no stdin and no --no-suggestion, errors.
|
||||
#
|
||||
# Examples:
|
||||
# # With suggestion (preferred)
|
||||
# pr-comment.sh src/main.go 42 --severity high --title "Missing error check" --why "Errors are silently ignored" <<'EOF'
|
||||
# if err != nil {
|
||||
# return fmt.Errorf("operation failed: %w", err)
|
||||
# }
|
||||
# EOF
|
||||
#
|
||||
# # Without suggestion (for issues requiring broader changes)
|
||||
# pr-comment.sh src/main.go 42 --severity medium --title "Consider extracting to function" \
|
||||
# --why "This logic is duplicated in 3 places" --no-suggestion
|
||||
#
|
||||
# Environment variables (set by the composite action):
|
||||
# PR_REVIEW_REPO - Repository (owner/repo)
|
||||
# PR_REVIEW_PR_NUMBER - Pull request number
|
||||
# PR_REVIEW_COMMENTS_DIR - Directory to cache comments (default: /tmp/pr-review-comments)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration from environment
|
||||
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
|
||||
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
|
||||
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
|
||||
|
||||
# Severity emoji mapping
|
||||
declare -A SEVERITY_EMOJI=(
|
||||
[critical]="🔴 CRITICAL"
|
||||
[high]="🟠 HIGH"
|
||||
[medium]="🟡 MEDIUM"
|
||||
[low]="⚪ LOW"
|
||||
[nitpick]="💬 NITPICK"
|
||||
)
|
||||
|
||||
# Parse arguments
|
||||
FILE=""
|
||||
LINE=""
|
||||
SEVERITY=""
|
||||
TITLE=""
|
||||
WHY=""
|
||||
NO_SUGGESTION=false
|
||||
|
||||
# First two positional args are file and line
|
||||
if [ $# -lt 2 ]; then
|
||||
echo "Error: file and line are required"
|
||||
echo "Usage: pr-comment.sh <file> <line> --severity <level> --title <desc> --why <reason> [<<'EOF' ... EOF]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
FILE="$1"
|
||||
LINE="$2"
|
||||
shift 2
|
||||
|
||||
# Parse named arguments
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--severity)
|
||||
SEVERITY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--title)
|
||||
TITLE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--why)
|
||||
WHY="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-suggestion)
|
||||
NO_SUGGESTION=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown argument: $1"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Read suggestion from stdin if available
|
||||
SUGGESTION=""
|
||||
if [ ! -t 0 ]; then
|
||||
SUGGESTION=$(cat)
|
||||
fi
|
||||
|
||||
# Validate required arguments
|
||||
if [ -z "$SEVERITY" ]; then
|
||||
echo "Error: --severity is required (critical, high, medium, low, nitpick)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$TITLE" ]; then
|
||||
echo "Error: --title is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$WHY" ]; then
|
||||
echo "Error: --why is required"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate severity level
|
||||
if [ -z "${SEVERITY_EMOJI[$SEVERITY]}" ]; then
|
||||
echo "Error: Invalid severity '$SEVERITY'. Must be one of: critical, high, medium, low, nitpick"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Require either suggestion or explicit --no-suggestion
|
||||
if [ -z "$SUGGESTION" ] && [ "$NO_SUGGESTION" = false ]; then
|
||||
echo "Error: Suggestion required. Provide code via stdin (heredoc) or use --no-suggestion"
|
||||
echo ""
|
||||
echo "Example with suggestion:"
|
||||
echo " pr-comment.sh file.go 42 --severity high --title \"desc\" --why \"reason\" <<'EOF'"
|
||||
echo " fixed code here"
|
||||
echo " EOF"
|
||||
echo ""
|
||||
echo "Example without suggestion:"
|
||||
echo " pr-comment.sh file.go 42 --severity medium --title \"desc\" --why \"reason\" --no-suggestion"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate line is a positive integer (>= 1)
|
||||
if ! [[ "$LINE" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Error: Line number must be a positive integer (>= 1), got: $LINE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get the diff for this file to validate the comment location
|
||||
DIFF_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate | jq --arg f "$FILE" '.[] | select(.filename==$f)')
|
||||
|
||||
if [ -z "$DIFF_DATA" ]; then
|
||||
echo "Error: File '${FILE}' not found in PR diff"
|
||||
echo ""
|
||||
echo "Files changed in this PR:"
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PATCH=$(echo "$DIFF_DATA" | jq -r '.patch // empty')
|
||||
|
||||
if [ -z "$PATCH" ]; then
|
||||
echo "Error: No patch data for file '${FILE}' (file may be binary or too large)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify the line exists in the diff
|
||||
LINE_IN_DIFF=$(echo "$PATCH" | awk -v target_line="$LINE" '
|
||||
BEGIN { current_line = 0; found = 0 }
|
||||
/^@@/ {
|
||||
line = $0
|
||||
gsub(/.*\+/, "", line)
|
||||
gsub(/[^0-9].*/, "", line)
|
||||
current_line = line - 1
|
||||
next
|
||||
}
|
||||
{
|
||||
if (substr($0, 1, 1) != "-") {
|
||||
current_line++
|
||||
if (current_line == target_line) {
|
||||
found = 1
|
||||
exit
|
||||
}
|
||||
}
|
||||
}
|
||||
END { if (found) print "1"; else print "0" }
|
||||
')
|
||||
|
||||
if [ "$LINE_IN_DIFF" != "1" ]; then
|
||||
echo "Error: Line ${LINE} not found in the diff for '${FILE}'"
|
||||
echo ""
|
||||
echo "Note: You can only comment on lines that appear in the diff (added, modified, or context lines)"
|
||||
echo ""
|
||||
echo "First 50 lines of diff for this file:"
|
||||
echo "$PATCH" | head -50
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create comments directory if it doesn't exist
|
||||
mkdir -p "${COMMENTS_DIR}"
|
||||
|
||||
# Assemble the comment body
|
||||
SEVERITY_LABEL="${SEVERITY_EMOJI[$SEVERITY]}"
|
||||
|
||||
BODY="**${SEVERITY_LABEL}** ${TITLE}
|
||||
|
||||
Why: ${WHY}"
|
||||
|
||||
# Add suggestion block if provided
|
||||
if [ -n "$SUGGESTION" ]; then
|
||||
BODY="${BODY}
|
||||
|
||||
\`\`\`suggestion
|
||||
${SUGGESTION}
|
||||
\`\`\`"
|
||||
fi
|
||||
|
||||
# Append standard footer
|
||||
FOOTER='
|
||||
|
||||
---
|
||||
Marvin Context Protocol | Type `/marvin` to interact further
|
||||
|
||||
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'
|
||||
|
||||
BODY_WITH_FOOTER="${BODY}${FOOTER}"
|
||||
|
||||
# Generate unique comment ID
|
||||
COMMENT_ID="comment-$(date +%s)-$(od -An -N4 -tu4 /dev/urandom | tr -d ' ')"
|
||||
COMMENT_FILE="${COMMENTS_DIR}/${COMMENT_ID}.json"
|
||||
|
||||
# Create the comment JSON object
|
||||
jq -n \
|
||||
--arg path "$FILE" \
|
||||
--argjson line "$LINE" \
|
||||
--arg side "RIGHT" \
|
||||
--arg body "$BODY_WITH_FOOTER" \
|
||||
--arg id "$COMMENT_ID" \
|
||||
'{
|
||||
path: $path,
|
||||
line: $line,
|
||||
side: $side,
|
||||
body: $body,
|
||||
_meta: {
|
||||
id: $id,
|
||||
file: $path,
|
||||
line: $line
|
||||
}
|
||||
}' > "${COMMENT_FILE}"
|
||||
|
||||
echo "✓ Queued review comment for ${FILE}:${LINE}"
|
||||
echo " Severity: ${SEVERITY_LABEL}"
|
||||
echo " Title: ${TITLE}"
|
||||
echo " Comment ID: ${COMMENT_ID}"
|
||||
echo " Comment will be submitted with pr-review.sh"
|
||||
echo " Remove with: pr-remove-comment.sh ${FILE} ${LINE}"
|
||||
Executable
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
# pr-diff.sh - Show changed files or diff for a specific file
|
||||
#
|
||||
# Usage:
|
||||
# pr-diff.sh - List all changed files (shows full diff if small enough)
|
||||
# pr-diff.sh <file> - Show diff for a specific file with line numbers
|
||||
#
|
||||
# Environment variables (set by the composite action):
|
||||
# PR_REVIEW_REPO - Repository (owner/repo)
|
||||
# PR_REVIEW_PR_NUMBER - Pull request number
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration from environment
|
||||
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
|
||||
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
|
||||
EXPECTED_HEAD="${PR_REVIEW_HEAD_SHA:-}"
|
||||
|
||||
# Check if HEAD has changed since review started (race condition detection)
|
||||
if [ -n "$EXPECTED_HEAD" ]; then
|
||||
CURRENT_HEAD=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
|
||||
if [ "$CURRENT_HEAD" != "$EXPECTED_HEAD" ]; then
|
||||
echo "⚠️ WARNING: PR head has changed since review started!"
|
||||
echo " Review started at: ${EXPECTED_HEAD:0:7}"
|
||||
echo " Current head: ${CURRENT_HEAD:0:7}"
|
||||
echo " Line numbers below may not match the commit being reviewed."
|
||||
echo ""
|
||||
fi
|
||||
fi
|
||||
|
||||
# Thresholds for "too big" - show file list only if exceeded
|
||||
MAX_FILES=25
|
||||
MAX_TOTAL_LINES=1500
|
||||
|
||||
FILE="$1"
|
||||
|
||||
# Function to add line numbers to a patch
|
||||
# Format: [LINE] +added | [LINE] context | [----] -deleted
|
||||
add_line_numbers() {
|
||||
awk '
|
||||
BEGIN { new_line = 0 }
|
||||
/^@@/ {
|
||||
# Parse hunk header: @@ -old_start,old_count +new_start,new_count @@
|
||||
match($0, /\+([0-9]+)/)
|
||||
new_line = substr($0, RSTART+1, RLENGTH-1) - 1
|
||||
print ""
|
||||
print $0
|
||||
next
|
||||
}
|
||||
/^-/ {
|
||||
# Deleted line - cannot comment on these
|
||||
printf "[----] %s\n", $0
|
||||
next
|
||||
}
|
||||
/^\+/ {
|
||||
# Added line - can comment, show line number
|
||||
new_line++
|
||||
printf "[%4d] %s\n", new_line, $0
|
||||
next
|
||||
}
|
||||
{
|
||||
# Context line (space prefix) - can comment, show line number
|
||||
new_line++
|
||||
printf "[%4d] %s\n", new_line, $0
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
if [ -z "$FILE" ]; then
|
||||
# Get file list with stats
|
||||
FILES_DATA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate)
|
||||
|
||||
FILE_COUNT=$(echo "$FILES_DATA" | jq 'length')
|
||||
TOTAL_ADDITIONS=$(echo "$FILES_DATA" | jq '[.[].additions] | add // 0')
|
||||
TOTAL_DELETIONS=$(echo "$FILES_DATA" | jq '[.[].deletions] | add // 0')
|
||||
TOTAL_LINES=$((TOTAL_ADDITIONS + TOTAL_DELETIONS))
|
||||
|
||||
echo "PR #${PR_NUMBER} Summary: ${FILE_COUNT} files changed (+${TOTAL_ADDITIONS}/-${TOTAL_DELETIONS})"
|
||||
echo ""
|
||||
|
||||
# Check if diff is too large
|
||||
if [ "$FILE_COUNT" -gt "$MAX_FILES" ] || [ "$TOTAL_LINES" -gt "$MAX_TOTAL_LINES" ]; then
|
||||
echo "⚠️ Large diff detected (>${MAX_FILES} files or >${MAX_TOTAL_LINES} lines changed)"
|
||||
echo " Review files individually using: pr-diff.sh <filename>"
|
||||
echo ""
|
||||
echo "Files changed:"
|
||||
echo "$FILES_DATA" | jq -r '.[] | " \(.filename) (+\(.additions)/-\(.deletions))"'
|
||||
else
|
||||
# Small enough - show all diffs with line numbers
|
||||
echo "Files changed:"
|
||||
echo "$FILES_DATA" | jq -r '.[] | " \(.filename) (+\(.additions)/-\(.deletions))"'
|
||||
echo ""
|
||||
echo "─────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
|
||||
# Show each file's diff by iterating over indices
|
||||
for i in $(seq 0 $((FILE_COUNT - 1))); do
|
||||
FNAME=$(echo "$FILES_DATA" | jq -r ".[$i].filename")
|
||||
PATCH=$(echo "$FILES_DATA" | jq -r ".[$i].patch // empty")
|
||||
|
||||
if [ -n "$PATCH" ]; then
|
||||
echo "## ${FNAME}"
|
||||
echo "Use: pr-comment.sh ${FNAME} <LINE> --severity <level> --title \"desc\" --why \"reason\" <<'EOF' ... EOF"
|
||||
echo "Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)"
|
||||
echo "$PATCH" | add_line_numbers
|
||||
echo ""
|
||||
echo "─────────────────────────────────────────────────────────────────────"
|
||||
echo ""
|
||||
fi
|
||||
done
|
||||
fi
|
||||
else
|
||||
# Show specific file diff
|
||||
PATCH=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq --arg file "$FILE" '.[] | select(.filename==$file) | .patch')
|
||||
|
||||
if [ -z "$PATCH" ]; then
|
||||
echo "Error: File '${FILE}' not found in PR diff"
|
||||
echo ""
|
||||
echo "Files changed in this PR:"
|
||||
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate --jq '.[].filename'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "## ${FILE}"
|
||||
echo "Use: pr-comment.sh ${FILE} <LINE> --severity <level> --title \"desc\" --why \"reason\" <<'EOF' ... EOF"
|
||||
echo "Format: [LINE] +added | [LINE] context | [----] -deleted (can't comment)"
|
||||
echo "$PATCH" | add_line_numbers
|
||||
fi
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
#!/bin/bash
|
||||
# pr-existing-comments.sh - Fetch existing review threads on a PR
|
||||
#
|
||||
# Usage:
|
||||
# pr-existing-comments.sh - Show all review threads with full details
|
||||
# pr-existing-comments.sh --summary - Show per-file summary only (for large PRs)
|
||||
# pr-existing-comments.sh --unresolved - Show only unresolved threads
|
||||
# pr-existing-comments.sh --file <path> - Show threads for a specific file
|
||||
# pr-existing-comments.sh --full - Show full comment text (no truncation)
|
||||
#
|
||||
# Output: Formatted summary of existing review threads grouped by file,
|
||||
# showing thread status, comments, and whether issues were addressed.
|
||||
#
|
||||
# For large PRs, use --summary first to see the overview, then --file <path>
|
||||
# to get full thread details when reviewing each file.
|
||||
#
|
||||
# Environment variables (set by the composite action):
|
||||
# PR_REVIEW_REPO - Repository (owner/repo)
|
||||
# PR_REVIEW_PR_NUMBER - Pull request number
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration from environment
|
||||
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
|
||||
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
|
||||
|
||||
OWNER="${REPO%/*}"
|
||||
REPO_NAME="${REPO#*/}"
|
||||
|
||||
# Parse arguments
|
||||
FILTER_UNRESOLVED=false
|
||||
FILTER_FILE=""
|
||||
SUMMARY_ONLY=false
|
||||
FULL_TEXT=false
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--unresolved)
|
||||
FILTER_UNRESOLVED=true
|
||||
shift
|
||||
;;
|
||||
--file)
|
||||
FILTER_FILE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--summary)
|
||||
SUMMARY_ONLY=true
|
||||
shift
|
||||
;;
|
||||
--full)
|
||||
FULL_TEXT=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Usage: pr-existing-comments.sh [--summary] [--unresolved] [--file <path>] [--full]"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Fetch review threads via GraphQL
|
||||
THREADS=$(gh api graphql -f query='
|
||||
query($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||
repository(owner: $owner, name: $repo) {
|
||||
pullRequest(number: $prNumber) {
|
||||
reviewThreads(first: 100) {
|
||||
nodes {
|
||||
id
|
||||
isResolved
|
||||
isOutdated
|
||||
path
|
||||
line
|
||||
originalLine
|
||||
startLine
|
||||
originalStartLine
|
||||
diffSide
|
||||
comments(first: 50) {
|
||||
nodes {
|
||||
id
|
||||
body
|
||||
author { login }
|
||||
createdAt
|
||||
originalCommit { abbreviatedOid }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}' -F owner="$OWNER" \
|
||||
-F repo="$REPO_NAME" \
|
||||
-F prNumber="$PR_NUMBER" \
|
||||
--jq '.data.repository.pullRequest.reviewThreads.nodes')
|
||||
|
||||
if [ -z "$THREADS" ] || [ "$THREADS" = "null" ]; then
|
||||
echo "No existing review threads found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Apply filters
|
||||
FILTERED="$THREADS"
|
||||
|
||||
if [ "$FILTER_UNRESOLVED" = true ]; then
|
||||
FILTERED=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == false)]')
|
||||
fi
|
||||
|
||||
if [ -n "$FILTER_FILE" ]; then
|
||||
FILTERED=$(echo "$FILTERED" | jq --arg file "$FILTER_FILE" '[.[] | select(.path == $file)]')
|
||||
fi
|
||||
|
||||
THREAD_COUNT=$(echo "$FILTERED" | jq 'length')
|
||||
|
||||
if [ "$THREAD_COUNT" -eq 0 ]; then
|
||||
if [ "$FILTER_UNRESOLVED" = true ]; then
|
||||
echo "No unresolved review threads found."
|
||||
elif [ -n "$FILTER_FILE" ]; then
|
||||
echo "No review threads found for ${FILTER_FILE}."
|
||||
else
|
||||
echo "No existing review threads found."
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Count resolved vs unresolved
|
||||
RESOLVED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == true)] | length')
|
||||
UNRESOLVED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isResolved == false)] | length')
|
||||
OUTDATED_COUNT=$(echo "$FILTERED" | jq '[.[] | select(.isOutdated == true)] | length')
|
||||
|
||||
echo "Existing review threads: ${THREAD_COUNT} total (${UNRESOLVED_COUNT} unresolved, ${RESOLVED_COUNT} resolved, ${OUTDATED_COUNT} outdated)"
|
||||
echo ""
|
||||
|
||||
# Summary mode: show per-file counts only
|
||||
if [ "$SUMMARY_ONLY" = true ]; then
|
||||
echo "Threads by file:"
|
||||
echo "$FILTERED" | jq -r '
|
||||
group_by(.path) | .[] |
|
||||
. as $threads |
|
||||
($threads | length) as $total |
|
||||
([$threads[] | select(.isResolved == false)] | length) as $unresolved |
|
||||
([$threads[] | select(.isResolved == true)] | length) as $resolved |
|
||||
([$threads[] | select(.isOutdated == true)] | length) as $outdated |
|
||||
([$threads[] | select(.comments.nodes | length > 1)] | length) as $has_replies |
|
||||
" " + $threads[0].path +
|
||||
" — " + ($total | tostring) + " threads" +
|
||||
" (" + ($unresolved | tostring) + " unresolved, " + ($resolved | tostring) + " resolved" +
|
||||
(if $outdated > 0 then ", " + ($outdated | tostring) + " outdated" else "" end) +
|
||||
")" +
|
||||
(if $has_replies > 0 then " ⚠️ " + ($has_replies | tostring) + " with replies" else "" end)
|
||||
'
|
||||
echo ""
|
||||
echo "Use: pr-existing-comments.sh --file <path> to see full thread details for a file"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Full detail mode: output threads grouped by file
|
||||
# Show full conversation for threads with replies
|
||||
FIRST_LIMIT=200
|
||||
REPLY_LIMIT=300
|
||||
if [ "$FULL_TEXT" = true ]; then
|
||||
FIRST_LIMIT=999999
|
||||
REPLY_LIMIT=999999
|
||||
fi
|
||||
|
||||
echo "$FILTERED" | jq -r --argjson first_limit "$FIRST_LIMIT" --argjson reply_limit "$REPLY_LIMIT" '
|
||||
group_by(.path) | .[] |
|
||||
"## " + .[0].path + " (" + (length | tostring) + " threads)\n" +
|
||||
([.[] |
|
||||
" " +
|
||||
(if .isResolved then "✅ RESOLVED" elif .isOutdated then "⚠️ OUTDATED" else "🔴 UNRESOLVED" end) +
|
||||
" (line " + (if .line then (.line | tostring) elif .startLine then (.startLine | tostring) elif .originalLine then ("~" + (.originalLine | tostring)) elif .originalStartLine then ("~" + (.originalStartLine | tostring)) else "?" end) + ")" +
|
||||
# Show the commit the comment was originally made on
|
||||
(if .comments.nodes[0].originalCommit.abbreviatedOid then " [" + .comments.nodes[0].originalCommit.abbreviatedOid + "]" else "" end) +
|
||||
# Flag threads with replies — indicates a conversation happened
|
||||
(if (.comments.nodes | length) > 1 then " ← has replies" else "" end) +
|
||||
"\n" +
|
||||
([.comments.nodes | to_entries[] |
|
||||
.value as $comment |
|
||||
.key as $idx |
|
||||
($comment.body | gsub("\n"; " ")) as $flat |
|
||||
if $idx == 0 then
|
||||
" @" + ($comment.author.login // "unknown") + ": " + $flat[0:$first_limit] +
|
||||
(if ($flat | length) > $first_limit then " [truncated]" else "" end)
|
||||
else
|
||||
" ↳ @" + ($comment.author.login // "unknown") + ": " + $flat[0:$reply_limit] +
|
||||
(if ($flat | length) > $reply_limit then " [truncated]" else "" end)
|
||||
end
|
||||
] | join("\n")) +
|
||||
"\n"
|
||||
] | join("\n"))
|
||||
'
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
# pr-remove-comment.sh - Remove a queued review comment
|
||||
#
|
||||
# Usage:
|
||||
# pr-remove-comment.sh <file> <line-number>
|
||||
# pr-remove-comment.sh <comment-id>
|
||||
#
|
||||
# Examples:
|
||||
# pr-remove-comment.sh src/main.go 42
|
||||
# pr-remove-comment.sh comment-1234567890-1234567890
|
||||
#
|
||||
# This script removes a previously queued comment before it's submitted.
|
||||
# Useful if the agent realizes it made a mistake or wants to update a comment.
|
||||
#
|
||||
# Environment variables (set by the composite action):
|
||||
# PR_REVIEW_COMMENTS_DIR - Directory containing comment files (default: /tmp/pr-review-comments)
|
||||
|
||||
set -e
|
||||
|
||||
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
|
||||
|
||||
if [ ! -d "${COMMENTS_DIR}" ]; then
|
||||
echo "No comments directory found: ${COMMENTS_DIR}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Check if first argument looks like a comment ID
|
||||
if [[ "$1" =~ ^comment- ]]; then
|
||||
COMMENT_ID="$1"
|
||||
COMMENT_FILE="${COMMENTS_DIR}/${COMMENT_ID}.json"
|
||||
|
||||
if [ -f "${COMMENT_FILE}" ]; then
|
||||
FILE=$(jq -r '._meta.file // .path' "${COMMENT_FILE}")
|
||||
LINE=$(jq -r '._meta.line // .line' "${COMMENT_FILE}")
|
||||
rm -f "${COMMENT_FILE}"
|
||||
echo "✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}"
|
||||
else
|
||||
echo "Comment not found: ${COMMENT_ID}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
# Treat as file and line number
|
||||
FILE="$1"
|
||||
LINE="$2"
|
||||
|
||||
if [ -z "$FILE" ] || [ -z "$LINE" ]; then
|
||||
echo "Usage:"
|
||||
echo " pr-remove-comment.sh <file> <line-number>"
|
||||
echo " pr-remove-comment.sh <comment-id>"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " pr-remove-comment.sh src/main.go 42"
|
||||
echo " pr-remove-comment.sh comment-1234567890-1234567890"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate line is a positive integer (>= 1)
|
||||
if ! [[ "$LINE" =~ ^[1-9][0-9]*$ ]]; then
|
||||
echo "Error: Line number must be a positive integer (>= 1), got: $LINE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find and remove matching comment files
|
||||
# Use nullglob to handle case where no files match
|
||||
shopt -s nullglob
|
||||
REMOVED=0
|
||||
for COMMENT_FILE in "${COMMENTS_DIR}"/comment-*.json; do
|
||||
|
||||
COMMENT_FILE_PATH=$(jq -r '._meta.file // .path' "${COMMENT_FILE}")
|
||||
COMMENT_LINE=$(jq -r '._meta.line // .line' "${COMMENT_FILE}")
|
||||
|
||||
if [ "$COMMENT_FILE_PATH" = "$FILE" ] && [ "$COMMENT_LINE" = "$LINE" ]; then
|
||||
COMMENT_ID=$(basename "${COMMENT_FILE}" .json)
|
||||
rm -f "${COMMENT_FILE}"
|
||||
echo "✓ Removed comment ${COMMENT_ID} for ${FILE}:${LINE}"
|
||||
REMOVED=$((REMOVED + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$REMOVED" -eq 0 ]; then
|
||||
echo "No comment found for ${FILE}:${LINE}"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
Executable
+143
@@ -0,0 +1,143 @@
|
||||
#!/bin/bash
|
||||
# pr-review.sh - Submit a PR review (approve, request changes, or comment)
|
||||
#
|
||||
# Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]
|
||||
# Example: pr-review.sh REQUEST_CHANGES "Please fix the issues noted above"
|
||||
#
|
||||
# This script creates and submits a review with any queued inline comments.
|
||||
# Comments are read from individual files in PR_REVIEW_COMMENTS_DIR (created by pr-comment.sh).
|
||||
#
|
||||
# The review body can contain special characters (backticks, dollar signs, etc.)
|
||||
# and will be safely passed to the GitHub API without shell interpretation.
|
||||
#
|
||||
# Environment variables (set by the composite action):
|
||||
# PR_REVIEW_REPO - Repository (owner/repo)
|
||||
# PR_REVIEW_PR_NUMBER - Pull request number
|
||||
# PR_REVIEW_HEAD_SHA - HEAD commit SHA
|
||||
# PR_REVIEW_COMMENTS_DIR - Directory containing queued comment files (default: /tmp/pr-review-comments)
|
||||
|
||||
set -e
|
||||
|
||||
# Configuration from environment
|
||||
REPO="${PR_REVIEW_REPO:?PR_REVIEW_REPO environment variable is required}"
|
||||
PR_NUMBER="${PR_REVIEW_PR_NUMBER:?PR_REVIEW_PR_NUMBER environment variable is required}"
|
||||
HEAD_SHA="${PR_REVIEW_HEAD_SHA:?PR_REVIEW_HEAD_SHA environment variable is required}"
|
||||
COMMENTS_DIR="${PR_REVIEW_COMMENTS_DIR:-/tmp/pr-review-comments}"
|
||||
|
||||
# Arguments
|
||||
EVENT="$1"
|
||||
shift 2>/dev/null || true
|
||||
|
||||
# Read body from remaining arguments
|
||||
# Join all remaining arguments with spaces, preserving the string as-is
|
||||
BODY="$*"
|
||||
|
||||
if [ -z "$EVENT" ]; then
|
||||
echo "Usage: pr-review.sh <APPROVE|REQUEST_CHANGES|COMMENT> [review-body]"
|
||||
echo "Example: pr-review.sh REQUEST_CHANGES 'Please fix the issues noted in the inline comments'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate event type
|
||||
case "$EVENT" in
|
||||
APPROVE|REQUEST_CHANGES|COMMENT)
|
||||
;;
|
||||
*)
|
||||
echo "Error: Invalid event type '${EVENT}'"
|
||||
echo "Must be one of: APPROVE, REQUEST_CHANGES, COMMENT"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# Read queued comments from individual files
|
||||
COMMENTS="[]"
|
||||
COMMENT_COUNT=0
|
||||
|
||||
if [ -d "${COMMENTS_DIR}" ]; then
|
||||
# Collect all comment files and merge into a single JSON array
|
||||
# Remove _meta fields before submitting (they're only for internal use)
|
||||
COMMENT_FILES=("${COMMENTS_DIR}"/comment-*.json)
|
||||
|
||||
if [ -f "${COMMENT_FILES[0]}" ]; then
|
||||
# Use jq to read all comment files, extract the comment data (without _meta), and combine
|
||||
COMMENTS=$(jq -s '[.[] | del(._meta)]' "${COMMENTS_DIR}"/comment-*.json)
|
||||
COMMENT_COUNT=$(echo "$COMMENTS" | jq 'length')
|
||||
if [ "$COMMENT_COUNT" -gt 0 ]; then
|
||||
echo "Found ${COMMENT_COUNT} queued inline comment(s)"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Append standard footer to the review body (if body is provided)
|
||||
FOOTER='
|
||||
|
||||
---
|
||||
Marvin Context Protocol | Type `/marvin` to interact further
|
||||
|
||||
Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.'
|
||||
|
||||
if [ -n "$BODY" ]; then
|
||||
BODY_WITH_FOOTER="${BODY}${FOOTER}"
|
||||
else
|
||||
BODY_WITH_FOOTER=""
|
||||
fi
|
||||
|
||||
# Build the review request JSON
|
||||
# Use jq to safely construct the JSON with all special characters handled
|
||||
REVIEW_JSON=$(jq -n \
|
||||
--arg commit_id "$HEAD_SHA" \
|
||||
--arg event "$EVENT" \
|
||||
--arg body "$BODY_WITH_FOOTER" \
|
||||
--argjson comments "$COMMENTS" \
|
||||
'{
|
||||
commit_id: $commit_id,
|
||||
event: $event,
|
||||
comments: $comments
|
||||
} + (if $body != "" then {body: $body} else {} end)')
|
||||
|
||||
# Check if HEAD has changed since review started (race condition detection)
|
||||
CURRENT_HEAD=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq '.head.sha')
|
||||
if [ "$CURRENT_HEAD" != "$HEAD_SHA" ]; then
|
||||
echo "⚠️ WARNING: PR head has changed since review started!"
|
||||
echo " Review started at: ${HEAD_SHA:0:7}"
|
||||
echo " Current head: ${CURRENT_HEAD:0:7}"
|
||||
echo ""
|
||||
echo " New commits may have shifted line numbers. Review will be submitted"
|
||||
echo " against the original commit (${HEAD_SHA:0:7}) but comments may be outdated."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo "Submitting ${EVENT} review for commit ${HEAD_SHA:0:7}..."
|
||||
|
||||
# Create and submit the review in one API call
|
||||
# Use a temp file to safely pass the JSON body
|
||||
TEMP_JSON=$(mktemp)
|
||||
trap "rm -f ${TEMP_JSON}" EXIT
|
||||
echo "$REVIEW_JSON" > "${TEMP_JSON}"
|
||||
|
||||
RESPONSE=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \
|
||||
-X POST \
|
||||
--input "${TEMP_JSON}" 2>&1) || {
|
||||
echo "Error submitting review:"
|
||||
echo "$RESPONSE"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Clean up the comments directory after successful submission
|
||||
if [ -d "${COMMENTS_DIR}" ] && [ "$COMMENT_COUNT" -gt 0 ]; then
|
||||
rm -f "${COMMENTS_DIR}"/comment-*.json
|
||||
# Remove directory if empty
|
||||
rmdir "${COMMENTS_DIR}" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
REVIEW_URL=$(echo "$RESPONSE" | jq -r '.html_url // empty')
|
||||
REVIEW_STATE=$(echo "$RESPONSE" | jq -r '.state // empty')
|
||||
|
||||
if [ -n "$REVIEW_URL" ]; then
|
||||
echo "✓ Review submitted (${REVIEW_STATE}): ${REVIEW_URL}"
|
||||
if [ "$COMMENT_COUNT" -gt 0 ]; then
|
||||
echo " Included ${COMMENT_COUNT} inline comment(s)"
|
||||
fi
|
||||
else
|
||||
echo "✓ Review submitted successfully"
|
||||
fi
|
||||
Reference in New Issue
Block a user