name: Probe chat Github Action
on:
workflow_call:
inputs:
command_prefix:
description: "The prefix required on comments to trigger the AI (e.g., /probe, /ai, etc)"
default: "/probe"
required: true
type: string
default_probe_chat_command:
description: "The default probe-chat command if PROBE_CHAT_COMMAND secret is not set"
required: false
default: "npx -y @buger/probe-chat@latest"
type: string
git_user_name:
description: "Git user name for commits (default: GitHub Actions)"
required: false
type: string
default: "github-actions[bot]"
git_user_email:
description: "Git user email for commits (default: GitHub Actions bot email)"
required: false
type: string
default: "41898282+github-actions[bot]@users.noreply.github.com"
prompt:
description: "Custom prompt to use (values: architect, code-review, support, path to a file, or arbitrary string)"
required: false
type: string
allow_edit:
description: "Enable the implement tool for editing files"
required: false
type: boolean
default: false
allow_suggestions:
description: "Enable the implement tool for suggesting file changes via reviewdog"
required: false
type: boolean
default: false
update_existing_comment:
description: "If 'true', Probe will try to update the previous comment identified by update_comment_marker."
required: false
default: false
type: boolean
update_comment_marker:
description: "Hidden marker injected into the comment to locate it later."
required: false
default: "<!-- probe-bot -->"
type: string
failure_tag:
description: "Tag to detect in AI response that triggers job failure (default: <fail>)"
required: false
type: string
default: "<fail>"
failure_message:
description: "Message to prepend when failure tag is detected"
required: false
type: string
default: "๐ด **CHECK FAILED:** This review has identified issues that require attention."
enable_tracing:
description: "Enable OpenTelemetry tracing for AI model calls"
required: false
type: boolean
default: false
tracing_url:
description: "Remote OpenTelemetry collector endpoint for tracing (e.g., http://localhost:4318/v1/traces)"
required: false
type: string
secrets:
PROBE_CHAT_COMMAND:
required: false
description: "Optional command for probe chat"
ANTHROPIC_API_KEY:
required: false
description: "API key for Anthropic service"
OPENAI_API_KEY:
required: false
description: "API key for OpenAI service"
GOOGLE_API_KEY:
required: false
description: "API key for Google service"
ANTHROPIC_API_URL:
required: false
description: "Custom API URL for Anthropic service"
OPENAI_API_URL:
required: false
description: "Custom API URL for OpenAI service"
GOOGLE_API_URL:
required: false
description: "Custom API URL for Google service"
LLM_BASE_URL:
required: false
description: "Base URL for the LLM service"
MODEL_NAME:
required: false
description: "Name of the model to use"
FORCE_PROVIDER:
required: false
description: "Force the use of a specific provider"
APP_ID:
required: false
description: "The GitHub App ID (if using App auth)"
APP_PRIVATE_KEY:
required: false
description: "The GitHub App's private key (if using App auth)"
MAX_TOOL_ITERATIONS:
required: false
description: "Max tool iterations. Default to 30"
DEBUG_CHAT:
required: false
description: "Enable debug logging for chat interactions (set to '1' to enable)"
jobs:
process_comment:
runs-on: ubuntu-latest
if: (github.event_name != 'issue_comment') || (github.event_name == 'issue_comment' && !contains(github.event.comment.user.login, '[bot]') && contains(github.event.comment.body, inputs.command_prefix))
outputs:
response_body_b64: ${{ steps.read_response.outputs.response_body_b64 }}
pr_issue_num: ${{ steps.set_context.outputs.pr_issue_num }}
probe_succeeded: ${{ steps.probe.outcome == 'success' }}
context_type: ${{ steps.format.outputs.context_type }}
steps:
- name: Validate Input Parameters
run: |
if [[ "${{ inputs.allow_edit }}" == "true" && "${{ inputs.allow_suggestions }}" == "true" ]]; then
echo "::error::Cannot enable both 'allow_edit' and 'allow_suggestions' simultaneously. Please choose one mode."
exit 1
fi
echo "Input validation passed."
- name: Check for App Credentials
id: check_app_secrets run: |
if [[ -n "${{ secrets.APP_ID }}" && -n "${{ secrets.APP_PRIVATE_KEY }}" ]]; then
echo "App credentials provided."
echo "use_app_token=true" >> $GITHUB_OUTPUT
else
echo "App credentials not provided."
echo "use_app_token=false" >> $GITHUB_OUTPUT
fi
- name: Generate GitHub App Token
id: generate_app_token
if: steps.check_app_secrets.outputs.use_app_token == 'true'
uses: actions/create-github-app-token@v1 with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Determine Workflow Token
id: set_token
run: |
if [[ -n "${{ steps.generate_app_token.outputs.token }}" ]]; then
echo "Using GitHub App token for authentication."
echo "WORKFLOW_TOKEN=${{ steps.generate_app_token.outputs.token }}" >> $GITHUB_ENV
else
echo "Using default GITHUB_TOKEN for authentication."
echo "WORKFLOW_TOKEN=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
fi
# only mask if we actually have a value
if [[ -n "${{ env.WORKFLOW_TOKEN }}" ]]; then
echo "::add-mask::${{ env.WORKFLOW_TOKEN }}"
fi
- name: Set Context Variables
id: set_context
run: |
EVENT_NAME="${{ github.event_name }}"
echo "Event name: $EVENT_NAME"
echo "skip=false" >> "$GITHUB_OUTPUT" # Default; overridden if skipping
if [[ "$EVENT_NAME" == "issue_comment" ]]; then
# Filter out bot comments and those without the required prefix
if [[ "${{ github.event.comment.user.login }}" == *"[bot]"* ]] || [[ "${{ github.event.comment.body }}" != ${{ inputs.command_prefix }}* ]]; then
echo "::notice::Skipping: Bot comment or missing prefix."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "pr_issue_num=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
echo "comment_id=${{ github.event.comment.id }}" >> "$GITHUB_OUTPUT"
USER_REQUEST="${{ github.event.comment.body }}"
echo "user_request<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "$USER_REQUEST" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "context_type=to_determine" >> "$GITHUB_OUTPUT"
echo "head_ref=" >> "$GITHUB_OUTPUT" # Determined later via API
elif [[ "$EVENT_NAME" == "pull_request" ]]; then
PR_BODY="${{ github.event.pull_request.body }}"
if [[ -z "$PR_BODY" ]]; then
echo "::notice::PR body is empty โ nothing for Probe AI to process, skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "pr_issue_num=${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
echo "comment_id=" >> "$GITHUB_OUTPUT"
echo "user_request<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "$PR_BODY" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "context_type=pr" >> "$GITHUB_OUTPUT"
echo "head_ref=${{ github.event.pull_request.head.ref }}" >> "$GITHUB_OUTPUT"
elif [[ "$EVENT_NAME" == "issues" ]]; then
ISSUE_BODY="${{ github.event.issue.body }}"
if [[ -z "$ISSUE_BODY" ]]; then
echo "::notice::Issue body is empty โ nothing for Probe AI to process, skipping."
echo "skip=true" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "pr_issue_num=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT"
echo "comment_id=" >> "$GITHUB_OUTPUT"
echo "user_request<<EOF" >> "$GITHUB_OUTPUT"
printf '%s\n' "$ISSUE_BODY" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
echo "context_type=issue" >> "$GITHUB_OUTPUT"
echo "head_ref=" >> "$GITHUB_OUTPUT" # N/A for issues
else
echo "::error::Unsupported event: $EVENT_NAME"
exit 1
fi
- name: Checkout repository
if: steps.set_context.outputs.skip != 'true'
uses: actions/checkout@v4
with:
token: ${{ env.WORKFLOW_TOKEN }}
fetch-depth: 0
- name: Install jq, perl and Verify gh
if: steps.set_context.outputs.skip != 'true'
run: |
sudo apt-get update && sudo apt-get install -y jq perl --no-install-recommends
gh --version
- name: Detect Project Languages
id: detect_languages
if: steps.set_context.outputs.skip != 'true'
run: |
# (Script remains the same)
# Initialize flags
NODE_FOUND=false
GO_FOUND=false
RUST_FOUND=false
PYTHON_FOUND=false
# Check for dependency files
if [ -f "package.json" ]; then
echo "Detected Node.js (package.json)"
NODE_FOUND=true
fi
if [ -f "go.mod" ]; then
echo "Detected Go (go.mod)"
GO_FOUND=true
fi
if [ -f "Cargo.toml" ]; then
echo "Detected Rust (Cargo.toml)"
RUST_FOUND=true
fi
if [ -f "requirements.txt" ]; then
echo "Detected Python (requirements.txt)"
PYTHON_FOUND=true
fi
# Set outputs for use in later steps
echo "node_found=$NODE_FOUND" >> $GITHUB_OUTPUT
echo "go_found=$GO_FOUND" >> $GITHUB_OUTPUT
echo "rust_found=$RUST_FOUND" >> $GITHUB_OUTPUT
echo "python_found=$PYTHON_FOUND" >> $GITHUB_OUTPUT
- name: Set up Node.js (Project Deps)
if: steps.detect_languages.outputs.node_found == 'true'
uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
- name: Set up Go (Project Deps)
if: steps.detect_languages.outputs.go_found == 'true'
uses: actions/setup-go@v5
with:
go-version: "1.21"
- name: Set up Rust (Project Deps)
if: steps.detect_languages.outputs.rust_found == 'true'
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Cache Rust dependencies (Project Deps)
if: steps.detect_languages.outputs.rust_found == 'true'
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Set up Python (Project Deps)
if: steps.detect_languages.outputs.python_found == 'true'
uses: actions/setup-python@v5
with:
python-version: "3.x"
cache: "pip"
cache-dependency-path: "**/requirements.txt"
- name: Install Project Dependencies
if: steps.set_context.outputs.skip != 'true'
run: |
# (Script remains the same)
# Node.js - Check for package.json
if [ "${{ steps.detect_languages.outputs.node_found }}" == "true" ]; then
echo "Found package.json - Installing Node.js dependencies..."
npm install || echo "::warning::npm install failed, continuing..."
fi
# Go - Check for go.mod
if [ "${{ steps.detect_languages.outputs.go_found }}" == "true" ]; then
echo "Found go.mod - Installing Go dependencies..."
go mod download || echo "::warning::go mod download failed, continuing..."
fi
# Rust - Check for Cargo.toml
if [ "${{ steps.detect_languages.outputs.rust_found }}" == "true" ]; then
echo "Found Cargo.toml - Building Rust dependencies..."
cargo build --quiet || echo "::warning::cargo build failed, continuing..."
fi
# Python - Check for requirements.txt
if [ "${{ steps.detect_languages.outputs.python_found }}" == "true" ]; then
echo "Found requirements.txt - Installing Python dependencies..."
pip install -r requirements.txt || echo "::warning::pip install failed, continuing..."
fi
- name: Set up Node.js (for probe-chat command)
uses: actions/setup-node@v4
if: steps.set_context.outputs.skip != 'true'
with:
node-version: "20"
- name: Add 'eyes' reaction to comment
id: add_reaction
if: steps.set_context.outputs.comment_id != ''
env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }} REPO: ${{ github.repository }}
COMMENT_ID: ${{ steps.set_context.outputs.comment_id }}
run: |
echo "Adding ๐ reaction to comment ID ${COMMENT_ID} in repo ${REPO}..."
gh api --method POST -H "Accept: application/vnd.github+json" "/repos/${REPO}/issues/comments/${COMMENT_ID}/reactions" -f content='eyes' --silent || echo "::warning::Failed to add 'eyes' reaction."
- name: Format - Initialize and Detect Context
id: format_init
if: steps.set_context.outputs.skip != 'true'
env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }} COMMAND_PREFIX: ${{ inputs.command_prefix }}
REPO: ${{ github.repository }}
USER_REQUEST: ${{ steps.set_context.outputs.user_request }}
INITIAL_CONTEXT_TYPE: ${{ steps.set_context.outputs.context_type }}
run: |
# (Updated script: Handle prefix only for comments, use initial context_type if set)
shopt -s extglob
set -e
echo "::group::Initialization and User Request Extraction"
RAW_COMMENT_BODY="$USER_REQUEST"
if [[ -z "$RAW_COMMENT_BODY" ]]; then
echo "::notice::No request text found โ skipping Probe run."
exit 0
fi
if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
USER_REQUEST_BODY_RAW="${RAW_COMMENT_BODY#$COMMAND_PREFIX}"
USER_REQUEST_BODY_RAW="${USER_REQUEST_BODY_RAW##*( )}"
USER_REQUEST_BODY_RAW="${USER_REQUEST_BODY_RAW%%*( )}"
else
USER_REQUEST_BODY_RAW="$RAW_COMMENT_BODY" # No prefix for creation events
fi
echo "USER_REQUEST_BODY<<EOF_USER_REQUEST" >> "$GITHUB_ENV"
printf '%s\n' "$USER_REQUEST_BODY_RAW" >> "$GITHUB_ENV"
echo "EOF_USER_REQUEST" >> "$GITHUB_ENV"
echo "User request extracted (first 100 chars): [${USER_REQUEST_BODY_RAW:0:100}]"
echo "::endgroup::"
echo "::group::Determine Context Type"
CONTEXT_TYPE="$INITIAL_CONTEXT_TYPE"
if [[ "$CONTEXT_TYPE" == "to_determine" ]]; then
CONTEXT_TYPE="unknown"
ISSUE_OR_PR_NUMBER=${{ steps.set_context.outputs.pr_issue_num }}
echo "Detecting context for #${ISSUE_OR_PR_NUMBER} in $REPO using token type: ${{ env.WORKFLOW_TOKEN != secrets.GITHUB_TOKEN && 'App Token' || 'Default GITHUB_TOKEN' }}" # Log token type
# Try viewing as PR first
echo "Checking if #${ISSUE_OR_PR_NUMBER} is a Pull Request using 'gh pr view'..."
set +e
gh pr view "$ISSUE_OR_PR_NUMBER" --repo "$REPO" --json id > /dev/null 2>&1
PR_VIEW_EXIT_CODE=$?
set -e
if [[ $PR_VIEW_EXIT_CODE -eq 0 ]]; then
CONTEXT_TYPE="pr"
echo "Context detected: PR (gh pr view succeeded)"
else
echo "'gh pr view' failed or returned no JSON (exit code $PR_VIEW_EXIT_CODE). Checking if it's an Issue..."
set +e
gh issue view "$ISSUE_OR_PR_NUMBER" --repo "$REPO" --json id > /dev/null 2>&1
ISSUE_VIEW_EXIT_CODE=$?
set -e
if [[ $ISSUE_VIEW_EXIT_CODE -eq 0 ]]; then
CONTEXT_TYPE="issue"
echo "Context confirmed: Issue (gh issue view succeeded)"
else
echo "::error::Failed to determine context for #${ISSUE_OR_PR_NUMBER}. 'gh pr view' failed (code $PR_VIEW_EXIT_CODE) AND 'gh issue view' failed (code $ISSUE_VIEW_EXIT_CODE)."
echo "::error::Please check the token permissions (needs issues:read and pull-requests:read) and if the item #${ISSUE_OR_PR_NUMBER} actually exists and is accessible."
CONTEXT_TYPE="issue"
echo "::warning::Proceeding with 'issue' context as a fallback despite view errors."
fi
fi
if [[ "$CONTEXT_TYPE" == "unknown" ]]; then
echo "::error::Context type could not be determined after checks. Defaulting to 'issue'."
CONTEXT_TYPE="issue"
fi
else
echo "Using pre-determined context type: $CONTEXT_TYPE"
fi
echo "Final Context Type: $CONTEXT_TYPE"
echo "FINAL_CONTEXT_TYPE=$CONTEXT_TYPE" >> $GITHUB_ENV
echo "ISSUE_OR_PR_NUMBER=${{ steps.set_context.outputs.pr_issue_num }}" >> $GITHUB_ENV
echo "::endgroup::"
- name: Format - Fetch Standard Comments
id: format_fetch_comments
if: steps.set_context.outputs.skip != 'true'
env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }} REPO: ${{ github.repository }}
ISSUE_OR_PR_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }}
run: |
# (Script remains the same)
echo "::group::Fetch Standard Comments"
set -e
GITHUB_API_ARGS_VERBOSE=(-H "Accept: application/vnd.github+json")
COMMENTS_XML=""
echo "Fetching standard comments for #${ISSUE_OR_PR_NUMBER}..."
STD_COMMENTS_JSON=$(gh api "${GITHUB_API_ARGS_VERBOSE[@]}" "/repos/${REPO}/issues/$ISSUE_OR_PR_NUMBER/comments" --paginate || echo "FETCH_FAILED")
if [[ "$STD_COMMENTS_JSON" == "FETCH_FAILED" || -z "$STD_COMMENTS_JSON" ]]; then
echo "::warning::Failed to fetch standard comments JSON or received empty response."
else
if echo "$STD_COMMENTS_JSON" | jq -e '. | type == "array"' > /dev/null 2>&1; then
TSV_OUTPUT=$(echo "$STD_COMMENTS_JSON" | jq -r '.[] | select(.body != null) | [.user.login // "unknown", .created_at // "N/A", .body] | @tsv' 2> jq_std_error.log)
JQ_EXIT_CODE=$?
if [[ $JQ_EXIT_CODE -eq 0 ]]; then
if [[ -n "$TSV_OUTPUT" ]]; then
echo "Processing standard comments..."
while IFS=$'\t' read -r login created_at body; do
[[ -n "$login" || -n "$created_at" || -n "$body" ]] || continue
COMMENTS_XML="${COMMENTS_XML}<comment type=\"issue\"><author>$login</author><timestamp>$created_at</timestamp><content><![CDATA[$body]]></content></comment>"
done <<< "$TSV_OUTPUT"
else
echo "Standard comments JSON valid, but no comments found or jq produced empty TSV."
fi
else
echo "::warning::jq failed processing standard comments (exit code $JQ_EXIT_CODE). Error log:"
cat jq_std_error.log
fi
else
echo "::warning::Fetched standard comments data is not a valid JSON array."
fi
fi
echo "Standard comments processed. Current XML length: ${#COMMENTS_XML}"
echo "COMMENTS_XML<<EOF_COMMENTS_XML" >> $GITHUB_ENV
echo "$COMMENTS_XML" >> $GITHUB_ENV
echo "EOF_COMMENTS_XML" >> $GITHUB_ENV
echo "::endgroup::"
- name: Format - Fetch PR Details (Title, Body, SHAs, RefName)
id: format_pr_details
if: env.FINAL_CONTEXT_TYPE == 'pr' && steps.set_context.outputs.skip != 'true' env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }}
REPO: ${{ github.repository }}
ISSUE_OR_PR_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }}
EVENT_HEAD_REF: ${{ steps.set_context.outputs.head_ref }} run: |
echo "::group::Fetch PR Details (Title, Body, SHAs, RefName)"
set -e
echo "Fetching PR specific data for #${ISSUE_OR_PR_NUMBER}..."
echo "Fetching PR base SHA, head SHA, head ref name, and head repo..."
# FIX: Also fetch headRepositoryOwner.login and headRepository.name to handle forks
if [[ -n "$EVENT_HEAD_REF" ]]; then
# For pull_request event, fetch via API but use head ref from event
PR_REFS_JSON=$(gh pr view "$ISSUE_OR_PR_NUMBER" --json baseRefOid,headRefOid,headRepositoryOwner,headRepository --repo "${REPO}" 2> pr_refs_stderr.log || echo "FETCH_FAILED")
HEAD_REF_NAME="$EVENT_HEAD_REF"
else
PR_REFS_JSON=$(gh pr view "$ISSUE_OR_PR_NUMBER" --json baseRefOid,headRefOid,headRefName,headRepositoryOwner,headRepository --repo "${REPO}" 2> pr_refs_stderr.log || echo "FETCH_FAILED")
HEAD_REF_NAME=""
fi
BASE_SHA=""
HEAD_SHA=""
HEAD_REPO_OWNER=""
HEAD_REPO_NAME=""
if [[ "$PR_REFS_JSON" == "FETCH_FAILED" ]]; then
echo "::error::Failed to fetch PR SHAs/RefName/Repo."
cat pr_refs_stderr.log >&2
else
BASE_SHA=$(echo "$PR_REFS_JSON" | jq -r .baseRefOid)
HEAD_SHA=$(echo "$PR_REFS_JSON" | jq -r .headRefOid)
if [[ -z "$HEAD_REF_NAME" ]]; then
HEAD_REF_NAME=$(echo "$PR_REFS_JSON" | jq -r .headRefName)
fi
HEAD_REPO_OWNER=$(echo "$PR_REFS_JSON" | jq -r .headRepositoryOwner.login)
HEAD_REPO_NAME=$(echo "$PR_REFS_JSON" | jq -r .headRepository.name)
if [[ -z "$BASE_SHA" || "$BASE_SHA" == "null" || \
-z "$HEAD_SHA" || "$HEAD_SHA" == "null" || \
-z "$HEAD_REF_NAME" || "$HEAD_REF_NAME" == "null" || \
-z "$HEAD_REPO_OWNER" || "$HEAD_REPO_OWNER" == "null" || \
-z "$HEAD_REPO_NAME" || "$HEAD_REPO_NAME" == "null" ]]; then
echo "::error::Could not extract valid base SHA, head SHA, head ref name, or head repo details from JSON: $PR_REFS_JSON"
BASE_SHA=""
HEAD_SHA=""
HEAD_REF_NAME=""
HEAD_REPO_OWNER=""
HEAD_REPO_NAME=""
else
echo "Base SHA: $BASE_SHA"
echo "Head SHA: $HEAD_SHA"
echo "Head Ref Name: $HEAD_REF_NAME"
echo "Head Repo Owner: $HEAD_REPO_OWNER"
echo "Head Repo Name: $HEAD_REPO_NAME"
echo "HEAD_REF_NAME=$HEAD_REF_NAME" >> $GITHUB_ENV
echo "HEAD_REPO_OWNER=$HEAD_REPO_OWNER" >> $GITHUB_ENV
echo "HEAD_REPO_NAME=$HEAD_REPO_NAME" >> $GITHUB_ENV
fi
fi
echo "BASE_SHA=$BASE_SHA" >> $GITHUB_ENV
echo "HEAD_SHA=$HEAD_SHA" >> $GITHUB_ENV
echo "Fetching PR Title and Body..."
PR_DATA=$(gh pr view "$ISSUE_OR_PR_NUMBER" --json title,body --repo "${REPO}" 2>/dev/null || echo 'FETCH_FAILED')
PR_TITLE_DEFAULT="Error fetching title"
PR_BODY_DEFAULT="Error fetching body"
if [[ "$PR_DATA" == "FETCH_FAILED" ]]; then
echo "::warning::Failed PR details fetch (gh pr view command failed)."
PR_TITLE="$PR_TITLE_DEFAULT"
PR_BODY="$PR_BODY_DEFAULT"
else
echo "Parsing PR Title and Body..."
PR_TITLE=$(echo "$PR_DATA" | jq -r --arg default "$PR_TITLE_DEFAULT" '.title // $default')
PR_BODY=$(echo "$PR_DATA" | jq -r --arg default "$PR_BODY_DEFAULT" '.body // $default')
fi
echo "PR_TITLE<<EOF_PR_TITLE" >> $GITHUB_ENV
echo "$PR_TITLE" >> $GITHUB_ENV
echo "EOF_PR_TITLE" >> $GITHUB_ENV
echo "PR_BODY<<EOF_PR_BODY" >> $GITHUB_ENV
echo "$PR_BODY" >> $GITHUB_ENV
echo "EOF_PR_BODY" >> $GITHUB_ENV
echo "::endgroup::"
- name: Format - Fetch & Filter PR Diff
id: format_pr_diff
if: env.FINAL_CONTEXT_TYPE == 'pr'
env:
BASE_SHA: ${{ env.BASE_SHA }}
HEAD_SHA: ${{ env.HEAD_SHA }}
run: |
# (Script remains the same)
echo "::group::Fetch & Filter PR Diff"
set -e
FILTERED_PR_DIFF="<!-- Diff generation skipped or failed -->" # Default value
if [[ -z "$BASE_SHA" || -z "$HEAD_SHA" ]]; then
echo "::error::Cannot run git diff without valid base/head SHAs. Skipping diff generation."
RAW_DIFF_CONTENT="FETCH_FAILED"
else
ALLOWED_PATTERNS=(
'*.go' '*.js' '*.ts' '*.jsx' '*.tsx' '*.rs' '*.java' '*.c' '*.h' '*.cpp' '*.hpp' '*.py'
'*.cs' '*.php' '*.rb' '*.swift' '*.kt' '*.kts' '*.scala' '*.sh' '*.pl' '*.pm' '*.lua' '*.sql'
'*.md' '*.yaml' '*.yml' '*.json'
'Dockerfile' 'Makefile' '.dockerignore' '.gitignore'
'go.mod' 'go.sum' 'package.json' 'package-lock.json' 'yarn.lock' 'pnpm-lock.yaml'
'requirements.txt' 'Pipfile' 'Pipfile.lock' 'pyproject.toml' 'poetry.lock'
'Cargo.toml' 'Cargo.lock' 'pom.xml' 'build.gradle' 'settings.gradle' 'build.gradle.kts' 'settings.gradle.kts'
'composer.json' 'composer.lock' 'Gemfile' 'Gemfile.lock'
'.*rc' '*.conf' '*.cfg' '*.ini' '*.toml' '*.properties'
'README.*' 'LICENSE*' 'CONTRIBUTING.*' 'CHANGELOG.*' '*.rst' '*.adoc'
)
RAW_DIFF_CONTENT=""
GIT_DIFF_EXIT_CODE=1
echo "Running: git diff '${BASE_SHA}...${HEAD_SHA}' -- ${ALLOWED_PATTERNS[*]}"
set +e
git diff "${BASE_SHA}...${HEAD_SHA}" -- "${ALLOWED_PATTERNS[@]}" > raw_diff_output.txt 2> git_diff_stderr.log
GIT_DIFF_EXIT_CODE=$?
set -e
RAW_DIFF_CONTENT=$(cat raw_diff_output.txt)
if [[ $GIT_DIFF_EXIT_CODE -ne 0 ]]; then
echo "::warning::git diff command exited with code $GIT_DIFF_EXIT_CODE."
if [[ -s git_diff_stderr.log ]]; then echo "Stderr from git diff:"; cat git_diff_stderr.log; fi
fi
if [[ -z "$RAW_DIFF_CONTENT" ]]; then
if [[ $GIT_DIFF_EXIT_CODE -eq 0 || $GIT_DIFF_EXIT_CODE -eq 1 ]]; then
echo "git diff produced no output for the specified patterns."
FILTERED_PR_DIFF="<!-- No relevant file changes found for specified patterns -->"
else
echo "::error::git diff failed (Exit: $GIT_DIFF_EXIT_CODE) and produced no output."
RAW_DIFF_CONTENT="FETCH_FAILED"
fi
else
echo "git diff successful. Raw diff size: ${#RAW_DIFF_CONTENT} bytes."
fi
fi
if [[ "$RAW_DIFF_CONTENT" != "FETCH_FAILED" && -n "$RAW_DIFF_CONTENT" ]]; then
echo "Filtering suspected minified files..."
set +e
FILTERED_PR_DIFF_CONTENT=$(echo "$RAW_DIFF_CONTENT" | perl -ne '
BEGIN { $chunk = ""; $print_chunk = 1; $max_len = 500; }
if (/^diff --git a\/(.+)\s+b\/(.+)$/) {
print $chunk if $chunk ne "" && $print_chunk;
$chunk = $_; $print_chunk = 1; my $b_path = $2;
if ($b_path eq "/dev/null") { $print_chunk = 1; }
elsif ($b_path =~ m/\.(lock|sum|mod|toml|cfg|ini|properties|yaml|yml|json|md|rst|adoc|txt|conf)$/i ||
$b_path =~ m/(Makefile|Dockerfile|LICENSE|README|CONTRIBUTING|CHANGELOG)/i ||
$b_path =~ m/\.(gitignore|dockerignore|.*rc)$/ ) { $print_chunk = 1; }
elsif (! -e $b_path) { warn "Warning: File $b_path from diff not found at ./$b_path, including chunk."; $print_chunk = 1; }
else {
if (open my $fh, "<", $b_path) {
my $lines_read = 0;
while (my $line = <$fh>) {
$lines_read++; chomp $line;
if (length($line) > $max_len && $line !~ m{(https?://\S+|/\S+|[a-zA-Z0-9+/=]{20,}|d="M[\d\.,\sA-Za-z-]+"})}) {
warn "Info: Filtering chunk for $b_path (line $lines_read length > $max_len)"; $print_chunk = 0; last;
}
last if $lines_read >= 3;
}
close $fh;
} else { warn "Warning: Could not open $b_path to check, including chunk."; $print_chunk = 1; }
}
} else { $chunk .= $_; }
END { print $chunk if $chunk ne "" && $print_chunk; }
' 2> filter_stderr.log)
PERL_PIPE_STATUS=${PIPESTATUS[1]}
set -e
if [[ $PERL_PIPE_STATUS -ne 0 ]]; then echo "::warning::Perl filter script exited with status $PERL_PIPE_STATUS."; fi
if [[ -s filter_stderr.log ]]; then echo "Perl filter script warnings/info:"; cat filter_stderr.log; fi
if [[ -z "$FILTERED_PR_DIFF_CONTENT" ]] && [[ -n "$RAW_DIFF_CONTENT" ]]; then
echo "All diff chunks were filtered out by Perl script."
FILTERED_PR_DIFF="<!-- Diff contained only files filtered out by heuristic -->"
elif [[ -z "$FILTERED_PR_DIFF_CONTENT" ]]; then
echo "Filtered diff is empty (remained empty after Perl filter)."
else
echo "Perl filtering complete. Final diff size: ${#FILTERED_PR_DIFF_CONTENT} bytes."
FILTERED_PR_DIFF="$FILTERED_PR_DIFF_CONTENT"
fi
elif [[ "$RAW_DIFF_CONTENT" == "FETCH_FAILED" ]]; then
FILTERED_PR_DIFF="<!-- Error fetching or generating diff -->"
fi
echo "FILTERED_PR_DIFF<<EOF_PR_DIFF" >> $GITHUB_ENV
echo "$FILTERED_PR_DIFF" >> $GITHUB_ENV
echo "EOF_PR_DIFF" >> $GITHUB_ENV
echo "::endgroup::"
- name: Format - Fetch PR Review Comments & Bodies
id: format_pr_reviews
if: env.FINAL_CONTEXT_TYPE == 'pr'
env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }} REPO: ${{ github.repository }}
ISSUE_OR_PR_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }}
COMMENTS_XML: ${{ env.COMMENTS_XML }}
run: |
# (Script remains the same)
echo "::group::Fetch PR Review Comments & Bodies"
set -e
GITHUB_API_ARGS_VERBOSE=(-H "Accept: application/vnd.github+json")
CURRENT_COMMENTS_XML="$COMMENTS_XML"
# --- PR Review Comments ---
echo "Fetching PR review comments..."
REVIEW_COMMENTS_JSON=$(gh api "${GITHUB_API_ARGS_VERBOSE[@]}" "/repos/${REPO}/pulls/$ISSUE_OR_PR_NUMBER/comments" --paginate || echo "FETCH_FAILED")
if [[ "$REVIEW_COMMENTS_JSON" == "FETCH_FAILED" || -z "$REVIEW_COMMENTS_JSON" ]]; then
echo "::warning::Failed to fetch PR review comments JSON or received empty response."
else
if echo "$REVIEW_COMMENTS_JSON" | jq -e '. | type == "array"' > /dev/null 2>&1; then
TSV_OUTPUT=$(echo "$REVIEW_COMMENTS_JSON" | jq -r '.[] | select(.body != null) | [.user.login // "unknown", .created_at // "N/A", .body, .path // "unknown", .diff_hunk // "", (.line // .original_line // "N/A")] | @tsv' 2> jq_rev_com_error.log)
JQ_EXIT_CODE=$?
if [[ $JQ_EXIT_CODE -eq 0 ]]; then
if [[ -n "$TSV_OUTPUT" ]]; then
echo "Processing review comments..."
while IFS=$'\t' read -r login created_at body path diff_hunk line; do
[[ -n "$login" || -n "$created_at" || -n "$body" ]] || continue
CURRENT_COMMENTS_XML="${CURRENT_COMMENTS_XML}<comment type=\"review_comment\" file=\"$path\" line=\"$line\"><author>$login</author><timestamp>$created_at</timestamp><diff_hunk><![CDATA[$diff_hunk]]></diff_hunk><content><![CDATA[$body]]></content></comment>"
done <<< "$TSV_OUTPUT"
else
echo "Review comments JSON valid, but no comments found or jq produced empty TSV."
fi
else
echo "::warning::jq failed processing review comments (exit code $JQ_EXIT_CODE). Error log:"
cat jq_rev_com_error.log
fi
else
echo "::warning::Fetched review comments data is not a valid JSON array."
fi
fi
echo "Review comments processed. Current XML length: ${#CURRENT_COMMENTS_XML}"
# --- PR Reviews (Bodies) ---
echo "Fetching PR reviews (bodies)..."
REVIEWS_JSON=$(gh api "${GITHUB_API_ARGS_VERBOSE[@]}" "/repos/${REPO}/pulls/$ISSUE_OR_PR_NUMBER/reviews" --paginate || echo "FETCH_FAILED")
if [[ "$REVIEWS_JSON" == "FETCH_FAILED" || -z "$REVIEWS_JSON" ]]; then
echo "::warning::Failed to fetch PR reviews JSON or received empty response."
else
if echo "$REVIEWS_JSON" | jq -e '. | type == "array"' > /dev/null 2>&1; then
TSV_OUTPUT=$(echo "$REVIEWS_JSON" | jq -r '.[] | select(.body != null and .body != "") | [.user.login // "unknown", .submitted_at // "N/A", .body, .state // "N/A"] | @tsv' 2> jq_rev_error.log)
JQ_EXIT_CODE=$?
if [[ $JQ_EXIT_CODE -eq 0 ]]; then
if [[ -n "$TSV_OUTPUT" ]]; then
echo "Processing review bodies..."
while IFS=$'\t' read -r login submitted_at body state; do
[[ -n "$login" || -n "$submitted_at" || -n "$body" ]] || continue
CURRENT_COMMENTS_XML="${CURRENT_COMMENTS_XML}<comment type=\"review_body\" state=\"$state\"><author>$login</author><timestamp>$submitted_at</timestamp><content><![CDATA[$body]]></content></comment>"
done <<< "$TSV_OUTPUT"
else
echo "Review JSON valid, but no review bodies with content found or jq produced empty TSV."
fi
else
echo "::warning::jq failed processing review bodies (exit code $JQ_EXIT_CODE). Error log:"
cat jq_rev_error.log
fi
else
echo "::warning::Fetched reviews data is not a valid JSON array."
fi
fi
echo "Review bodies processed. Final XML length: ${#CURRENT_COMMENTS_XML}"
echo "COMMENTS_XML<<EOF_COMMENTS_XML_UPDATED" >> $GITHUB_ENV
echo "$CURRENT_COMMENTS_XML" >> $GITHUB_ENV
echo "EOF_COMMENTS_XML_UPDATED" >> $GITHUB_ENV
echo "::endgroup::"
- name: Format - Fetch Issue Details (Title, Body)
id: format_issue_details
if: env.FINAL_CONTEXT_TYPE == 'issue'
env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }} REPO: ${{ github.repository }}
ISSUE_OR_PR_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }}
run: |
# (Script remains the same)
echo "::group::Fetch Issue Details (Title, Body)"
set -e
echo "Fetching Issue specific data for #${ISSUE_OR_PR_NUMBER}..."
ISSUE_DATA_JSON=$(gh issue view "$ISSUE_OR_PR_NUMBER" --json title,body --repo "${REPO}" 2>/dev/null || echo 'FETCH_FAILED')
ISSUE_TITLE="Error fetching title"
ISSUE_BODY="Error fetching body"
if [[ "$ISSUE_DATA_JSON" != "FETCH_FAILED" && -n "$ISSUE_DATA_JSON" ]]; then
echo "Issue data fetched successfully. Parsing title and body..."
EXTRACTED_TITLE=$(printf "%s" "$ISSUE_DATA_JSON" | jq -e -r '.title // ""')
JQ_TITLE_EXIT=$?
if [[ $JQ_TITLE_EXIT -eq 0 && -n "$EXTRACTED_TITLE" ]]; then ISSUE_TITLE="$EXTRACTED_TITLE";
elif [[ $JQ_TITLE_EXIT -eq 0 && -z "$EXTRACTED_TITLE" ]]; then echo "::debug::Issue title is null/empty."; ISSUE_TITLE="";
else echo "::warning::jq failed to extract issue title (exit $JQ_TITLE_EXIT) or value was null."; fi
EXTRACTED_BODY=$(printf "%s" "$ISSUE_DATA_JSON" | jq -e -r '.body // ""')
JQ_BODY_EXIT=$?
if [[ $JQ_BODY_EXIT -eq 0 && -n "$EXTRACTED_BODY" ]]; then ISSUE_BODY="$EXTRACTED_BODY";
elif [[ $JQ_BODY_EXIT -eq 0 && -z "$EXTRACTED_BODY" ]]; then echo "::debug::Issue body is null/empty."; ISSUE_BODY="";
else echo "::warning::jq failed to extract issue body (exit $JQ_BODY_EXIT) or value was null."; fi
else
echo "::warning::Failed Issue details fetch (gh command failed or returned empty)."
fi
echo "ISSUE_TITLE<<EOF_ISSUE_TITLE" >> $GITHUB_ENV
echo "$ISSUE_TITLE" >> $GITHUB_ENV
echo "EOF_ISSUE_TITLE" >> $GITHUB_ENV
echo "ISSUE_BODY<<EOF_ISSUE_BODY" >> $GITHUB_ENV
echo "$ISSUE_BODY" >> $GITHUB_ENV
echo "EOF_ISSUE_BODY" >> $GITHUB_ENV
echo "::endgroup::"
- name: Format - Assemble Final Prompt
id: format env:
FINAL_CONTEXT_TYPE: ${{ env.FINAL_CONTEXT_TYPE }}
ISSUE_OR_PR_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }}
USER_REQUEST_BODY: ${{ env.USER_REQUEST_BODY }}
COMMENTS_XML: ${{ env.COMMENTS_XML }}
UPDATED_COMMENTS_XML: ${{ env.COMMENTS_XML_UPDATED }}
PR_TITLE: ${{ env.PR_TITLE }}
PR_BODY: ${{ env.PR_BODY }}
FILTERED_PR_DIFF: ${{ env.FILTERED_PR_DIFF }}
ISSUE_TITLE: ${{ env.ISSUE_TITLE }}
ISSUE_BODY: ${{ env.ISSUE_BODY }}
run: |
# (Script remains the same)
echo "::group::Assemble Final Prompt & Set Outputs"
set -e
CONTEXT_DETAILS_XML=""
DIFF_XML=""
PROMPT_INSTRUCTION=""
FINAL_COMMENTS_XML="${UPDATED_COMMENTS_XML:-$COMMENTS_XML}"
if [[ "$FINAL_CONTEXT_TYPE" == "pr" ]]; then
CONTEXT_DETAILS_XML="<details><title><![CDATA[$PR_TITLE]]></title><body><![CDATA[$PR_BODY]]></body></details>"
DIFF_XML="<diff><![CDATA[$FILTERED_PR_DIFF]]></diff>"
PROMPT_INSTRUCTION="You are an AI assistant analyzing a GitHub Pull Request..."
elif [[ "$FINAL_CONTEXT_TYPE" == "issue" ]]; then
CONTEXT_DETAILS_XML="<details><title><![CDATA[$ISSUE_TITLE]]></title><body><![CDATA[$ISSUE_BODY]]></body></details>"
DIFF_XML=""
PROMPT_INSTRUCTION="You are an AI assistant analyzing a GitHub Issue..."
fi
if [[ "${{ github.event_name }}" == "issue_comment" ]]; then
USER_LOGIN="${{ github.event.comment.user.login }}"
TIMESTAMP="${{ github.event.comment.created_at }}"
else
USER_LOGIN="${{ github.actor }}" # Fallback to actor for creation events
TIMESTAMP="$(date -u +%Y-%m-%dT%H:%M:%SZ)" # Current time
fi
FORMATTED_PROMPT="<github_context type=\"$FINAL_CONTEXT_TYPE\" number=\"$ISSUE_OR_PR_NUMBER\">
${CONTEXT_DETAILS_XML}
${DIFF_XML}
<comments>${FINAL_COMMENTS_XML}</comments>
<user_request author=\"${USER_LOGIN}\" timestamp=\"${TIMESTAMP}\"><![CDATA[${USER_REQUEST_BODY}]]></user_request>
</github_context>
<instructions><![CDATA[${PROMPT_INSTRUCTION}]]></instructions>"
if [ ${#FORMATTED_PROMPT} -lt 200 ]; then
echo "::warning::Formatted prompt seems very short (${#FORMATTED_PROMPT} bytes)."
fi
echo "Final prompt assembled. Length: ${#FORMATTED_PROMPT} bytes."
PROMPT_FILENAME="formatted_prompt.txt"
echo "$FORMATTED_PROMPT" > "$PROMPT_FILENAME"
echo "Prompt written to $PROMPT_FILENAME"
echo "Setting step outputs..."
echo "context_type=${FINAL_CONTEXT_TYPE}" >> "$GITHUB_OUTPUT"
echo "formatted_prompt_file=${PROMPT_FILENAME}" >> "$GITHUB_OUTPUT"
echo "Outputs set."
echo "::endgroup::"
- name: Determine probe-chat command
id: determine_command
env:
PROBE_CHAT_COMMAND_SECRET: ${{ secrets.PROBE_CHAT_COMMAND }}
run: |
# (Script remains the same)
COMMAND_VAR=""
if [[ -n "${PROBE_CHAT_COMMAND_SECRET}" ]]; then
echo "Using PROBE_CHAT_COMMAND secret."
COMMAND_VAR="${PROBE_CHAT_COMMAND_SECRET}"
else
echo "Using default_probe_chat_command input: ${{ inputs.default_probe_chat_command }}"
COMMAND_VAR="${{ inputs.default_probe_chat_command }}"
fi
if [[ -z "$COMMAND_VAR" ]]; then
echo "::error::Command is empty after evaluation. Check secrets/inputs."
exit 1
fi
echo "Determined command base: $COMMAND_VAR"
echo "command=$COMMAND_VAR" >> "$GITHUB_OUTPUT"
- name: Set up Python and Install Aider
if: inputs.allow_edit
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Install Aider
if: inputs.allow_edit
run: |
# (Script remains the same)
echo "Installing aider for code editing capabilities..."
python -m pip install aider-install
aider-install
echo "Aider installation completed."
- name: Switch to PR Branch (if applicable)
if: env.FINAL_CONTEXT_TYPE == 'pr' && env.HEAD_REF_NAME != '' && steps.set_context.outputs.skip != 'true'
env:
PR_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }}
PR_BRANCH_NAME: ${{ env.HEAD_REF_NAME }}
run: |
echo "::group::Switching to PR branch '$PR_BRANCH_NAME'"
set -e
# FIX: Fetch the PR head ref instead of the branch ref (works even if branch deleted, and for forks)
echo "Fetching PR #${PR_NUMBER} head commit to local branch '$PR_BRANCH_NAME' from origin..."
git fetch origin "refs/pull/${PR_NUMBER}/head:refs/heads/${PR_BRANCH_NAME}"
if [[ $? -ne 0 ]]; then
echo "::error::Failed to fetch PR #${PR_NUMBER} head."
exit 1
fi
echo "Checking out local branch '$PR_BRANCH_NAME'..."
git checkout "$PR_BRANCH_NAME"
if [[ $? -ne 0 ]]; then
echo "::error::Failed to checkout '$PR_BRANCH_NAME' even after fetch."
exit 1
fi
echo "Successfully checked out branch: $(git rev-parse --abbrev-ref HEAD)"
echo "::endgroup::"
- name: Run probe-chat
id: probe
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
ANTHROPIC_API_URL: ${{ secrets.ANTHROPIC_API_URL }}
ANTHROPIC_BASE_URL: ${{ secrets.ANTHROPIC_API_URL }} OPENAI_API_URL: ${{ secrets.OPENAI_API_URL }}
GOOGLE_API_URL: ${{ secrets.GOOGLE_API_URL }}
LLM_BASE_URL: ${{ secrets.LLM_BASE_URL }}
MODEL_NAME: ${{ secrets.MODEL_NAME }}
FORCE_PROVIDER: ${{ secrets.FORCE_PROVIDER }}
ALLOW_EDIT: ${{ inputs.allow_edit == 'true' && '1' || '0' }}
ALLOW_SUGGESTIONS: ${{ inputs.allow_suggestions == 'true' && '1' || '0' }}
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }}
MAX_TOOL_ITERATIONS: ${{ secrets.MAX_TOOL_ITERATIONS }}
DEBUG_CHAT: ${{ secrets.DEBUG_CHAT }}
run: |
# (Script remains the same)
# Check and modify ANTHROPIC_API_URL to set ANTHROPIC_BASE_URL
if [[ -n "$ANTHROPIC_API_URL" ]]; then
if [[ "$ANTHROPIC_API_URL" == */v1 ]]; then
export ANTHROPIC_BASE_URL="${ANTHROPIC_API_URL%/v1}"
echo "ANTHROPIC_BASE_URL set to $ANTHROPIC_BASE_URL (removed /v1 from ANTHROPIC_API_URL)"
else
export ANTHROPIC_BASE_URL="$ANTHROPIC_API_URL"
echo "ANTHROPIC_BASE_URL set to $ANTHROPIC_BASE_URL (same as ANTHROPIC_API_URL)"
fi
fi
set -o pipefail
PROMPT_FILE="${{ steps.format.outputs.formatted_prompt_file }}"
COMMAND_BASE="${{ steps.determine_command.outputs.command }}"
RESPONSE_FILE="response.txt"
ERROR_LOG="error.log"
COMMAND_TO_RUN="$COMMAND_BASE"
if [[ -n "${{ inputs.prompt }}" ]]; then
PROMPT_VALUE="${{ inputs.prompt }}"
COMMAND_TO_RUN="$COMMAND_TO_RUN --prompt '$PROMPT_VALUE'"
echo "Using custom prompt from workflow input (properly quoted for multiline support)"
else
CONTEXT_TYPE="${{ steps.format.outputs.context_type }}"
if [[ "$CONTEXT_TYPE" == "pr" ]]; then
COMMAND_TO_RUN="$COMMAND_TO_RUN --prompt code-review"
echo "Using code-review prompt for pull request context"
elif [[ "$CONTEXT_TYPE" == "issue" ]]; then
COMMAND_TO_RUN="$COMMAND_TO_RUN --prompt support"
echo "Using support prompt for issue context"
else
echo "::warning:: Unknown context type '$CONTEXT_TYPE', not adding specific prompt flag."
fi
fi
if [[ "${{ inputs.allow_edit }}" == "true" ]]; then
COMMAND_TO_RUN="$COMMAND_TO_RUN --allow-edit"
echo "Enabling implement tool with --allow-edit flag"
fi
if [[ "${{ inputs.allow_suggestions }}" == "true" ]]; then
COMMAND_TO_RUN="$COMMAND_TO_RUN --allow-edit"
echo "Enabling implement tool with --allow-edit flag (suggestions mode)"
fi
# Add tracing options
if [[ "${{ inputs.enable_tracing }}" == "true" ]]; then
if [[ -n "${{ inputs.tracing_url }}" ]]; then
COMMAND_TO_RUN="$COMMAND_TO_RUN --trace-remote '${{ inputs.tracing_url }}'"
echo "Enabling remote tracing to: ${{ inputs.tracing_url }}"
else
COMMAND_TO_RUN="$COMMAND_TO_RUN --trace-file ./probe-traces.jsonl"
echo "Enabling file tracing to: ./probe-traces.jsonl"
fi
fi
if [[ -z "$COMMAND_TO_RUN" ]]; then
echo "::error::COMMAND_TO_RUN is unexpectedly empty after building!" >&2
echo "๐ค **Error:** Internal configuration error - AI command is missing." > "$RESPONSE_FILE"
exit 1
fi
if [ ! -s "$PROMPT_FILE" ]; then
echo "::error::Prompt file '$PROMPT_FILE' not found or is empty. Check 'format' step logs." >&2
echo "๐ค **Error:** Internal error - prompt file missing or empty." > "$RESPONSE_FILE"
exit 1
fi
echo "Prompt file: $PROMPT_FILE"
echo "Prompt file size: $(wc -c < "$PROMPT_FILE") bytes"
echo "Command to run: $COMMAND_TO_RUN"
echo "Running probe-chat..."
cat "$PROMPT_FILE" | $COMMAND_TO_RUN > "$RESPONSE_FILE" 2> >(tee "$ERROR_LOG" >&2)
EXIT_CODE=${PIPESTATUS[1]}
if [ $EXIT_CODE -ne 0 ]; then
echo "::error::probe-chat command failed with exit code $EXIT_CODE." >&2
if [ -s "$ERROR_LOG" ]; then
echo "--- probe-chat stderr ---" >&2
cat "$ERROR_LOG" >&2
echo "--- end probe-chat stderr ---" >&2
fi
if [ ! -s "$RESPONSE_FILE" ]; then
echo "๐ค **Error:** AI command failed (Exit code: $EXIT_CODE). Check Action logs." > "$RESPONSE_FILE"
fi
else
echo "probe-chat command finished successfully (Exit code: 0)."
fi
if [ $EXIT_CODE -eq 0 ] && [ ! -s "$RESPONSE_FILE" ]; then
echo "::warning::probe-chat command succeeded but produced an empty response."
echo "๐ค AI command ran successfully but generated no response." > "$RESPONSE_FILE"
fi
- name: Upload Debug Files as Artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: probe-debug-files
path: |
error.log
formatted_prompt.txt
jq_std_error.log
jq_rev_com_error.log
jq_rev_error.log
pr_refs_stderr.log
raw_diff_output.txt
git_diff_stderr.log
filter_stderr.log
response.txt
if-no-files-found: ignore
retention-days: 7
- name: Upload Trace Files as Artifacts
if: always() && inputs.enable_tracing && inputs.tracing_url == ''
uses: actions/upload-artifact@v4
with:
name: probe-traces
path: |
probe-traces.jsonl
if-no-files-found: ignore
retention-days: 30
- name: Handle Git Changes
id: handle_git
if: inputs.allow_edit && steps.probe.outcome == 'success'
env:
GH_TOKEN: ${{ env.WORKFLOW_TOKEN }}
CONTEXT_TYPE: ${{ env.FINAL_CONTEXT_TYPE }} ISSUE_NUMBER: ${{ env.ISSUE_OR_PR_NUMBER }} run: |
# (Script remains the same, but uses CHECKED_OUT_BRANCH for push target)
echo "::group::Handling Git Changes"
git config --global user.name "${{ inputs.git_user_name }}"
git config --global user.email "${{ inputs.git_user_email }}"
echo "Using git identity: ${{ inputs.git_user_name }} <${{ inputs.git_user_email }}>"
# Get the branch that is ACTUALLY checked out now (SHOULD be the PR branch)
CHECKED_OUT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
echo "Working directory is on branch: $CHECKED_OUT_BRANCH"
INITIAL_COMMIT=$(git rev-parse HEAD)
echo "Commit SHA before AI changes: $INITIAL_COMMIT" # More accurate description
echo "Checking for file changes made by AI..."
git status --porcelain
if [[ -z $(git status --porcelain) ]]; then
echo "No file changes detected by AI. Nothing to commit."
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "::endgroup::"
exit 0
fi
echo "File changes detected."
echo "has_changes=true" >> $GITHUB_OUTPUT
# Extract a short summary from the AI response for the commit message
RESPONSE_SUMMARY=$(head -n 1 response.txt 2>/dev/null || echo "AI code modification")
COMMIT_MSG=$(printf "AI: %s\n\nGenerated by Probe AI for %s #%s" "${RESPONSE_SUMMARY:0:100}" "$CONTEXT_TYPE" "$ISSUE_NUMBER")
PR_URL=""
# === Stage, Commit the changes WHERE WE ARE (on the checked-out branch) ===
echo "Staging all detected changes..."
git add --all
echo "Unstaging workflow artifacts..."
git reset HEAD -- error.log formatted_prompt.txt jq_std_error.log jq_rev_com_error.log jq_rev_error.log pr_refs_stderr.log raw_diff_output.txt git_diff_stderr.log filter_stderr.log response.txt || echo "::warning::Failed to unstage some artifacts, continuing..."
echo "Checking for staged code changes after unstaging artifacts..."
git status --porcelain
if [[ -z $(git status --porcelain | grep -v '??') ]]; then
echo "No actual code changes left after unstaging artifacts. Nothing to commit."
echo "has_changes=false" >> $GITHUB_OUTPUT
echo "::endgroup::"
exit 0
fi
echo "Committing staged changes locally..."
git commit -m "$COMMIT_MSG"
if [[ $? -ne 0 ]]; then
echo "::error::Git commit failed."
git status; git diff --staged
exit 1
fi
NEW_COMMIT=$(git rev-parse HEAD)
echo "Created local commit: $NEW_COMMIT on branch $CHECKED_OUT_BRANCH"
# === Push the commit to the appropriate destination ===
if [[ "$CONTEXT_TYPE" == "pr" ]]; then
# Target the currently checked-out branch (which should be the PR branch)
TARGET_BRANCH_NAME="$CHECKED_OUT_BRANCH"
echo "Context is PR #${ISSUE_NUMBER}. Attempting to push commit $NEW_COMMIT to branch '${TARGET_BRANCH_NAME}'."
if [[ "$TARGET_BRANCH_NAME" == "HEAD" || -z "$TARGET_BRANCH_NAME" ]]; then
echo "::error::Cannot push because checked out branch is '$TARGET_BRANCH_NAME' (HEAD or empty). Check 'Switch to PR Branch' step."
exit 1
fi
TARGET_REF="refs/heads/${TARGET_BRANCH_NAME}"
echo "Executing: git push origin HEAD:${TARGET_REF}"
git push origin "HEAD:${TARGET_REF}"
PUSH_EXIT_CODE=$?
if [[ $PUSH_EXIT_CODE -ne 0 ]]; then
echo "::error::Git push failed (Exit code: $PUSH_EXIT_CODE). The remote branch '${TARGET_BRANCH_NAME}' might have new changes (non-fast-forward). Manual intervention required."
exit 1 # Fail the step
fi
echo "Changes successfully pushed to the PR branch '${TARGET_BRANCH_NAME}'."
echo "pr_url=" >> $GITHUB_OUTPUT
elif [[ "$CONTEXT_TYPE" == "issue" ]]; then
# Issue context - create a NEW branch from the current HEAD (which should be 'main' or default)
echo "Context is Issue #${ISSUE_NUMBER}. Creating a new branch and PR based on commit $NEW_COMMIT from branch $CHECKED_OUT_BRANCH."
TIMESTAMP=$(date +%Y%m%d%H%M%S)
BRANCH_NAME="probe-ai/issue-${ISSUE_NUMBER}-${TIMESTAMP}"
echo "Creating new branch '$BRANCH_NAME' pointing to current commit $NEW_COMMIT..."
git branch "$BRANCH_NAME" HEAD
if [[ $? -ne 0 ]]; then
echo "::error::Failed create branch '$BRANCH_NAME'."
exit 1
fi
echo "Pushing new branch '$BRANCH_NAME' to origin..."
echo "Executing: git push origin ${NEW_COMMIT}:refs/heads/${BRANCH_NAME}"
git push origin "${NEW_COMMIT}:refs/heads/${BRANCH_NAME}"
PUSH_EXIT_CODE=$?
if [[ $PUSH_EXIT_CODE -ne 0 ]]; then
echo "::error::Git push failed for new branch '$BRANCH_NAME' (Exit code: $PUSH_EXIT_CODE)."
exit 1
fi
PR_TITLE="AI Changes for Issue #${ISSUE_NUMBER}"
PR_BODY_CONTENT=$(head -c 50000 response.txt 2>/dev/null || echo "AI generated changes.")
if [[ ${#PR_BODY_CONTENT} -ge 50000 ]]; then PR_BODY_CONTENT+=$'\n\n...(truncated)'; fi
PR_BODY=$(printf "This PR was automatically created by Probe AI in response to issue #%s.\n\n**AI Response Summary:**\n\n%s" "$ISSUE_NUMBER" "$PR_BODY_CONTENT")
echo "Creating Pull Request..."
DEFAULT_BRANCH=$(gh repo view $GITHUB_REPOSITORY --json defaultBranchRef -q .defaultBranchRef.name || echo "main")
echo "Using base branch '$DEFAULT_BRANCH' for PR."
PR_URL=$(gh pr create --title "$PR_TITLE" --body "$PR_BODY" --base "$DEFAULT_BRANCH" --head "$BRANCH_NAME" --repo "$GITHUB_REPOSITORY")
if [[ $? -ne 0 || -z "$PR_URL" ]]; then
echo "::warning::Failed to create PR using 'gh pr create'. PR URL will be empty."
PR_URL=""
else
echo "Successfully created PR: $PR_URL"
fi
echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT
else
echo "::error::Unhandled context type '$CONTEXT_TYPE' for pushing changes. Commit $NEW_COMMIT was created locally but not pushed."
exit 1
fi
echo "::endgroup::"
- name: Handle Suggestions with reviewdog
id: handle_suggestions
if: inputs.allow_suggestions && steps.probe.outcome == 'success' && env.FINAL_CONTEXT_TYPE == 'pr'
uses: reviewdog/action-suggester@v1.21.0
with:
github_token: ${{ env.WORKFLOW_TOKEN }}
tool_name: 'Probe AI Code Suggestions' level: 'warning' filter_mode: 'added' fail_level: 'none' fail_on_error: 'false' reviewdog_flags: '--reporter=github-pr-review' cleanup: 'true'
- name: Read Response or Error and Format Output
id: read_response
env:
COMMAND_PREFIX: ${{ inputs.command_prefix }}
HAS_CHANGES: ${{ steps.handle_git.outputs.has_changes }}
PR_URL: ${{ steps.handle_git.outputs.pr_url }}
CONTEXT_TYPE: ${{ steps.format.outputs.context_type }}
run: |
set -e
RESPONSE_CONTENT=$(cat response.txt 2>/dev/null || echo "")
ERROR_LOG_CONTENT=$(cat error.log 2>/dev/null || echo "")
# Check for failure tag and process it
FAILURE_TAG="${{ inputs.failure_tag }}"
FAILURE_MESSAGE="${{ inputs.failure_message }}"
SHOULD_FAIL_JOB=false
if [[ -n "$RESPONSE_CONTENT" && "$RESPONSE_CONTENT" == *"$FAILURE_TAG"* ]]; then
echo "::notice::Failure tag '$FAILURE_TAG' detected in AI response. Job will fail after posting comment."
SHOULD_FAIL_JOB=true
# Strip the failure tag from the response
RESPONSE_CONTENT=$(echo "$RESPONSE_CONTENT" | sed "s|$FAILURE_TAG||g")
# Prepend failure message to the response
RESPONSE_CONTENT="${FAILURE_MESSAGE}\n\n${RESPONSE_CONTENT}"
fi
FINAL_BODY=""
if [[ "$HAS_CHANGES" == "true" ]]; then
if [[ "$CONTEXT_TYPE" == "pr" ]]; then
RESPONSE_CONTENT="${RESPONSE_CONTENT}\n\n**Changes have been applied and pushed to the current PR branch.**"
elif [[ "$CONTEXT_TYPE" == "issue" && -n "$PR_URL" ]]; then
RESPONSE_CONTENT="${RESPONSE_CONTENT}\n\n**Changes have been applied and a PR has been created: ${PR_URL}**"
else
RESPONSE_CONTENT="${RESPONSE_CONTENT}\n\n**Changes have been applied and committed to the current branch.**"
fi
fi
if [[ "${{ steps.handle_suggestions.outcome }}" == "success" && "${{ inputs.allow_suggestions }}" == "true" ]]; then
if [[ "$CONTEXT_TYPE" == "pr" ]]; then
RESPONSE_CONTENT="${RESPONSE_CONTENT}\n\n---\n\n### ๐ก Code Suggestions Available\n\n**AI-generated code suggestions have been added to this PR as review comments.** You can:\n\n- ๐ Review each suggestion in the \"Files changed\" tab\n- โ
Accept suggestions by clicking \"Apply suggestion\" or \"Add suggestion to batch\"\n- โ Dismiss suggestions that don't apply\n- ๐ Request modifications by replying to suggestion comments"
fi
elif [[ "${{ steps.handle_suggestions.outcome }}" == "failure" && "${{ inputs.allow_suggestions }}" == "true" ]]; then
if [[ "$CONTEXT_TYPE" == "pr" ]]; then
RESPONSE_CONTENT="${RESPONSE_CONTENT}\n\n**Note:** Failed to create suggestions via reviewdog. Check workflow logs for details."
fi
fi
if [[ "${{ steps.probe.outcome }}" == "success" ]]; then
FINAL_BODY="${RESPONSE_CONTENT}"
else
ERROR_MESSAGE="๐ค **Error:** AI interaction failed."
if [[ "${{ steps.format.outcome }}" == "failure" ]]; then
ERROR_MESSAGE="๐ค **Error:** Failed during context preparation. Check 'Format Input' logs."
elif [[ "${{ steps.probe.outcome }}" == "failure" ]]; then
if [[ -n "${RESPONSE_CONTENT// }" ]] && echo "$RESPONSE_CONTENT" | grep -q -E "(Error:|ERROR:|Failed|failed|Cannot|Could not)"; then
ERROR_MESSAGE="${RESPONSE_CONTENT}"
else
ERROR_MESSAGE="๐ค **Error:** The AI command failed to execute or returned an error."
if [[ -n "${ERROR_LOG_CONTENT// }" ]]; then
ERROR_DETAILS=$(head -c 1000 <<< "$ERROR_LOG_CONTENT")
if [[ ${#ERROR_LOG_CONTENT} -gt 1000 ]]; then ERROR_DETAILS="${ERROR_DETAILS}\n...(truncated)"; fi
ERROR_MESSAGE="${ERROR_MESSAGE}\n\n**Details (stderr):**\n\`\`\`\n${ERROR_DETAILS}\n\`\`\`"
else
ERROR_MESSAGE="${ERROR_MESSAGE} Check 'Run probe-chat' logs."
fi
fi
else
ERROR_MESSAGE="๐ค **Error:** Workflow failed before AI interaction. Check logs."
fi
FINAL_BODY="${ERROR_MESSAGE}"
fi
printf -v FOOTER "\n\n-----\n*Tip: Mention me again using \`%s <request>\`.*\n*Powered by [Probe AI](https://probeai.dev)*" "$COMMAND_PREFIX"
if [[ "${{ inputs.update_existing_comment }}" == "true" ]]; then
FOOTER+="\n${{ inputs.update_comment_marker }}"
fi
FINAL_BODY="${FINAL_BODY:-๐ค **Error:** AI interaction failed.}${FOOTER}"
echo "Final response body prepared. Length: ${#FINAL_BODY}"
# write to file for debugging
printf '%s' "$FINAL_BODY" > full_response.md
# **baseโ64 encode** so GH will not think it contains a secret
ENCODED=$(printf '%s' "$FINAL_BODY" | base64 -w0)
echo "response_body_b64=$ENCODED" >> "$GITHUB_OUTPUT"
echo "should_fail_job=$SHOULD_FAIL_JOB" >> "$GITHUB_OUTPUT"
- name: Fail job if failure tag detected
if: steps.read_response.outputs.should_fail_job == 'true'
run: |
echo "::error::Failure tag was detected in AI response. Failing the job as requested."
exit 1
post_response:
if: always() && needs.process_comment.result != 'skipped'
runs-on: ubuntu-latest
needs: [process_comment]
steps:
- name: Check for App Credentials Post
id: check_app_secrets_post run: |
if [[ -n "${{ secrets.APP_ID }}" && -n "${{ secrets.APP_PRIVATE_KEY }}" ]]; then
echo "App credentials provided for posting."
echo "use_app_token=true" >> $GITHUB_OUTPUT
else
echo "App credentials not provided for posting."
echo "use_app_token=false" >> $GITHUB_OUTPUT
fi
- name: Generate GitHub App Token Post
id: generate_app_token_post
if: steps.check_app_secrets_post.outputs.use_app_token == 'true'
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Determine Workflow Token
id: set_token_post
run: |
if [[ -n "${{ steps.generate_app_token_post.outputs.token }}" ]]; then
echo "WORKFLOW_TOKEN_POST=${{ steps.generate_app_token_post.outputs.token }}" >> $GITHUB_ENV
else
echo "WORKFLOW_TOKEN_POST=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV
fi
if [[ -n "${{ env.WORKFLOW_TOKEN_POST }}" ]]; then
echo "::add-mask::${{ env.WORKFLOW_TOKEN_POST }}"
fi
- name: Prepare workspace for artifact download
run: mkdir -p /tmp/artifacts
- name: Decode AI response
id: decode
run: |
printf '%s' "${{ needs.process_comment.outputs.response_body_b64 }}" | base64 -d > /tmp/artifacts/comment_body.md
echo "body_path=/tmp/artifacts/comment_body.md" >> $GITHUB_OUTPUT
- name: Find Comment
id: find_comment
if: inputs.update_existing_comment == 'true'
uses: peter-evans/find-comment@v3
with:
token: ${{ env.WORKFLOW_TOKEN_POST }}
repository: ${{ github.repository }}
issue-number: ${{ needs.process_comment.outputs.pr_issue_num }}
body-includes: ${{ inputs.update_comment_marker }}
- name: Post Response Comment
if: inputs.update_existing_comment != 'true' || steps.find_comment.outputs.comment-id == ''
uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ env.WORKFLOW_TOKEN_POST }}
repository: ${{ github.repository }}
issue-number: ${{ needs.process_comment.outputs.pr_issue_num }}
body-path: ${{ steps.decode.outputs.body_path }}
reactions: ${{ needs.process_comment.outputs.probe_succeeded == 'true' && '+1' || '-1' }}
- name: Update Comment
if: inputs.update_existing_comment == 'true' && steps.find_comment.outputs.comment-id != ''
uses: peter-evans/create-or-update-comment@v4
with:
token: ${{ env.WORKFLOW_TOKEN_POST }}
repository: ${{ github.repository }}
issue-number: ${{ needs.process_comment.outputs.pr_issue_num }}
comment-id: ${{ steps.find_comment.outputs.comment-id }}
body-path: ${{ steps.decode.outputs.body_path }}
reactions: ${{ needs.process_comment.outputs.probe_succeeded == 'true' && '+1' || '-1' }}
edit-mode: replace