From c8ba923830c4febad6b06f2d5db9b9a95f7443f1 Mon Sep 17 00:00:00 2001 From: akash-deriv Date: Fri, 6 Feb 2026 17:02:41 +0400 Subject: [PATCH 1/4] DocSync AI - actions and workflow --- .github/actions/docsync_ai/action.yml | 634 ++++++++++++++++++ .github/actions/docsync_ai_comment/action.yml | 549 +++++++++++++++ .github/workflows/docsync-ai.yml | 258 +++++++ 3 files changed, 1441 insertions(+) create mode 100644 .github/actions/docsync_ai/action.yml create mode 100644 .github/actions/docsync_ai_comment/action.yml create mode 100644 .github/workflows/docsync-ai.yml diff --git a/.github/actions/docsync_ai/action.yml b/.github/actions/docsync_ai/action.yml new file mode 100644 index 0000000..6365d7a --- /dev/null +++ b/.github/actions/docsync_ai/action.yml @@ -0,0 +1,634 @@ +name: DocSync AI - Documentation Sync +description: Automatically update documentation based on PR merges using Claude AI + +inputs: + github_token: + description: "GitHub token for repository operations and PR creation" + required: true + anthropic_api_key: + description: "Anthropic API key for Claude AI" + required: true + repository: + description: "Repository name (owner/repo)" + required: true + pr_number: + description: "Merged PR number that triggered this workflow" + required: true + pr_title: + description: "Merged PR title" + required: true + pr_body: + description: "Merged PR body/description" + required: false + default: "" + base_branch: + description: "Base branch for documentation PR (e.g., master or main)" + required: false + default: "master" + pr_labels: + description: "Comma-separated labels for the documentation PR" + required: false + default: "documentation,automated" + +outputs: + doc_pr_created: + description: "Whether a documentation PR was created" + value: ${{ steps.create-doc-pr.outputs.pr_created }} + doc_pr_number: + description: "Documentation PR number if created" + value: ${{ steps.create-doc-pr.outputs.pr_number }} + doc_pr_url: + description: "Documentation PR URL if created" + value: ${{ steps.create-doc-pr.outputs.pr_url }} + +runs: + using: composite + steps: + - name: đŸ“Ĩ Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ inputs.github_token }} + ref: ${{ inputs.base_branch }} + fetch-depth: 0 + + - name: 🔍 Debug - Repository State + shell: bash + run: | + echo "## 🔍 Repository State" >> $GITHUB_STEP_SUMMARY + echo "Working directory: $(pwd)" >> $GITHUB_STEP_SUMMARY + echo "Current branch: $(git branch --show-current)" >> $GITHUB_STEP_SUMMARY + echo "Latest commit: $(git log -1 --oneline)" >> $GITHUB_STEP_SUMMARY + + # Check for existing documentation files (case-insensitive) + find . -maxdepth 1 -iname "readme.md" | while read f; do echo "✅ Found ${f#./}" >> $GITHUB_STEP_SUMMARY; done + find . -maxdepth 1 -iname "claude.md" | while read f; do echo "✅ Found ${f#./}" >> $GITHUB_STEP_SUMMARY; done + + - name: 📋 Fetch Merged PR Details + id: pr-details + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + PR_NUMBER: ${{ inputs.pr_number }} + REPO_NAME: ${{ inputs.repository }} + run: | + set -e + + # Validate PR number (numeric, reasonable range) + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid PR number: $PR_NUMBER" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ "$PR_NUMBER" -lt 1 ] || [ "$PR_NUMBER" -gt 999999 ]; then + echo "❌ PR number out of valid range: $PR_NUMBER" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Validate repository format (owner/repo) + if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$ ]]; then + echo "❌ Invalid repository format: $REPO_NAME" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ ${#REPO_NAME} -gt 100 ]; then + echo "❌ Repository name too long" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + echo "## 📋 Merged PR Details" >> $GITHUB_STEP_SUMMARY + echo "Repository: $REPO_NAME" >> $GITHUB_STEP_SUMMARY + echo "PR #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY + + # Create secure temporary directory + TEMP_DIR=$(mktemp -d -t docsync-XXXXXXXXXX) + echo "temp_dir=$TEMP_DIR" >> $GITHUB_OUTPUT + + # Get PR diff and files changed with size limit + MAX_DIFF_SIZE=1048576 # 1MB + PR_DIFF=$(gh api \ + -H "Accept: application/vnd.github.v3.diff" \ + "/repos/$REPO_NAME/pulls/$PR_NUMBER" 2>&1 | head -c $MAX_DIFF_SIZE) + + # Sanitize and validate diff content + PR_DIFF=$(echo "$PR_DIFF" | tr -d '\000' | iconv -c -t UTF-8//IGNORE) + + # Save PR diff to secure temp file + PR_DIFF_FILE="$TEMP_DIR/pr_diff.txt" + echo "$PR_DIFF" > "$PR_DIFF_FILE" + chmod 600 "$PR_DIFF_FILE" + + # Get list of changed files + CHANGED_FILES=$(gh api \ + -H "Accept: application/vnd.github+json" \ + "/repos/$REPO_NAME/pulls/$PR_NUMBER/files" \ + --jq '.[].filename' 2>&1 || echo "") + + # Sanitize changed files list (truncate for summary) + CHANGED_FILES_SUMMARY=$(echo "$CHANGED_FILES" | head -n 50) + + echo "### Changed Files:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$CHANGED_FILES_SUMMARY" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + echo "changed_files<> $GITHUB_OUTPUT + echo "$CHANGED_FILES" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: 🤖 Run Claude 3.7 Sonnet Documentation Analysis + id: claude-analysis + shell: bash + env: + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + REPO_NAME: ${{ inputs.repository }} + PR_NUMBER: ${{ inputs.pr_number }} + PR_TITLE: ${{ inputs.pr_title }} + PR_BODY: ${{ inputs.pr_body }} + BASE_BRANCH: ${{ inputs.base_branch }} + TEMP_DIR: ${{ steps.pr-details.outputs.temp_dir }} + run: | + set -e + + echo "## 🤖 Claude 3.7 Sonnet Analysis" >> $GITHUB_STEP_SUMMARY + echo "Model: claude-3-7-sonnet-latest" >> $GITHUB_STEP_SUMMARY + + # Sanitize user inputs (remove null bytes, control characters, limit size) + PR_TITLE_CLEAN=$(echo "$PR_TITLE" | tr -d '\000-\037' | head -c 1000) + PR_BODY_CLEAN=$(echo "$PR_BODY" | tr -d '\000' | iconv -c -t UTF-8//IGNORE | head -c 10000) + + # Validate inputs for suspicious patterns + if echo "$PR_TITLE_CLEAN$PR_BODY_CLEAN" | grep -qE '\$\(|\`|;[[:space:]]*curl|;[[:space:]]*wget|eval|exec'; then + echo "âš ī¸ Warning: Suspicious pattern detected in PR inputs" >> $GITHUB_STEP_SUMMARY + # Continue but log the warning + fi + + # === PHASE 1: DETECT ALL DOCUMENTATION FILES === + # Check for BOTH files before updating anything + HAS_README=false + HAS_CLAUDE=false + + # Validate file paths helper function + validate_doc_file() { + local file=$1 + # Must be in current directory, not a symlink, and match expected names + if [[ ! -f "$file" ]] || [[ -L "$file" ]]; then + return 1 + fi + local basename=$(basename "$file") + local basename_lower=$(echo "$basename" | tr '[:upper:]' '[:lower:]') + if [[ "$basename_lower" != "readme.md" ]] && [[ "$basename_lower" != "claude.md" ]]; then + return 1 + fi + # Ensure file is in repo root (no directory traversal) + local realpath=$(realpath "$file" 2>/dev/null || echo "") + local workdir=$(realpath . 2>/dev/null || echo "") + if [[ ! "$realpath" == "$workdir"/* ]]; then + return 1 + fi + return 0 + } + + # Case-insensitive check for README.md + README_FILE=$(find . -maxdepth 1 -type f -iname "readme.md" -print -quit 2>/dev/null) + if [ -n "$README_FILE" ]; then + README_FILE="${README_FILE#./}" + if validate_doc_file "$README_FILE"; then + HAS_README=true + else + echo "âš ī¸ Invalid README file detected, skipping" >> $GITHUB_STEP_SUMMARY + README_FILE="" + fi + fi + + # Case-insensitive check for CLAUDE.md + CLAUDE_FILE=$(find . -maxdepth 1 -type f -iname "claude.md" -print -quit 2>/dev/null) + if [ -n "$CLAUDE_FILE" ]; then + CLAUDE_FILE="${CLAUDE_FILE#./}" + if validate_doc_file "$CLAUDE_FILE"; then + HAS_CLAUDE=true + else + echo "âš ī¸ Invalid CLAUDE file detected, skipping" >> $GITHUB_STEP_SUMMARY + CLAUDE_FILE="" + fi + fi + + # Log detection results for both files + echo "### 📄 Documentation File Detection:" >> $GITHUB_STEP_SUMMARY + echo "- README.md: $([ "$HAS_README" = true ] && echo "✅ Found ($README_FILE)" || echo '❌ Not found')" >> $GITHUB_STEP_SUMMARY + echo "- CLAUDE.md: $([ "$HAS_CLAUDE" = true ] && echo "✅ Found ($CLAUDE_FILE)" || echo '❌ Not found')" >> $GITHUB_STEP_SUMMARY + + if [ "$HAS_README" = false ] && [ "$HAS_CLAUDE" = false ]; then + echo "❌ No documentation file found (README.md or CLAUDE.md)" >> $GITHUB_STEP_SUMMARY + echo "skip_analysis=true" >> $GITHUB_OUTPUT + rm -rf "$TEMP_DIR" + exit 0 + fi + + # Read PR diff from secure temp file + PR_DIFF_FILE="$TEMP_DIR/pr_diff.txt" + if [ -f "$PR_DIFF_FILE" ]; then + PR_DIFF=$(cat "$PR_DIFF_FILE") + else + PR_DIFF="No diff available" + fi + + # Validate diff size + DIFF_SIZE=${#PR_DIFF} + MAX_DIFF_SIZE=1048576 # 1MB + if [ $DIFF_SIZE -gt $MAX_DIFF_SIZE ]; then + echo "âš ī¸ PR diff too large, truncating..." >> $GITHUB_STEP_SUMMARY + PR_DIFF="${PR_DIFF:0:$MAX_DIFF_SIZE}" + fi + + # Get repository structure (more restrictive search) + REPO_STRUCTURE=$(find . -maxdepth 3 -type f \ + \( -name "*.js" -o -name "*.ts" -o -name "*.jsx" -o -name "*.tsx" -o -name "*.py" -o -name "*.go" -o -name "*.java" -o -name "*.yml" -o -name "*.yaml" \) \ + -not -path "*/node_modules/*" \ + -not -path "*/.git/*" \ + -not -path "*/vendor/*" \ + -not -path "*/dist/*" \ + -not -path "*/build/*" \ + -not -name "*secret*" \ + -not -name "*credential*" \ + -not -name "*.env*" \ + 2>/dev/null | head -30 | sed 's|^\./||' || echo "") + + ANY_UPDATES=false + + # Function to call Claude API for documentation update + update_doc_file() { + local DOC_FILE=$1 + local DOC_FORMAT=$2 + local OTHER_FILES_NOTE=$3 + + echo "Processing $DOC_FILE..." >> $GITHUB_STEP_SUMMARY + + # Validate doc file path + if ! validate_doc_file "$DOC_FILE"; then + echo " ❌ Invalid doc file: $DOC_FILE" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Read current documentation + DOC_CONTENT=$(cat "$DOC_FILE") + + # Validate content size + CONTENT_SIZE=${#DOC_CONTENT} + MAX_CONTENT_SIZE=524288 # 512KB + if [ $CONTENT_SIZE -gt $MAX_CONTENT_SIZE ]; then + echo " âš ī¸ Doc file too large: $CONTENT_SIZE bytes" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Prepare the prompt based on file type + if [ "$DOC_FORMAT" = "README" ]; then + FORMAT_INSTRUCTION="This is a README.md file. Maintain standard README format with clear sections, proper markdown formatting, badges, installation instructions, usage examples, and project overview. Keep the professional, user-facing documentation style." + else + FORMAT_INSTRUCTION="This is a CLAUDE.md file for AI assistance. Maintain the technical, detailed format suitable for AI context with implementation details, architecture notes, and development guidelines. Keep the detailed, technical documentation style." + fi + + # Create secure prompt file + PROMPT_FILE="$TEMP_DIR/claude_prompt_$(openssl rand -hex 8).txt" + + # Build prompt with safe variable substitution + cat > "$PROMPT_FILE" << 'EOF' + You are a technical documentation expert. Analyze the PR changes and update the documentation if needed. + + # Context + Repository: '"$REPO_NAME"' + PR #'"$PR_NUMBER"': '"$PR_TITLE_CLEAN"' + PR Description: '"$PR_BODY_CLEAN"' + + # Current '"$DOC_FILE"': + '"$DOC_CONTENT"' + + # PR Changes (Diff): + '"$PR_DIFF"' + + # Repository Structure (sample): + '"$REPO_STRUCTURE"' + + # Format Requirements: + '"$FORMAT_INSTRUCTION"' + + # Documentation Context: + '"$OTHER_FILES_NOTE"' + + # Your Task + Update the '"$DOC_FILE"' based on the PR changes. + + ## CRITICAL RULES - MUST FOLLOW: + 1. **Direct output only**: Output ONLY the updated documentation content. NO acknowledgments like "I will analyze", "I understand", "Here's the updated", or any explanatory text. + 2. **Maintain format**: Keep the exact same documentation style and structure as the current file. + 3. **Update relevant sections**: Only modify sections that relate to the PR changes. Keep everything else exactly as is. + 4. **Significance check**: Only update if changes are significant (new features, major fixes, API changes, workflow updates). Skip minor changes (typos, formatting tweaks, small refactors). + 5. **Complete file output**: Provide the COMPLETE updated file content, not just the changed sections. + 6. **No meta-commentary**: No phrases like "I've updated", "Changes made", or section summaries. + + ## Decision: + - If significant updates are needed: Output the COMPLETE updated '"$DOC_FILE"' content immediately, with no preamble + - If no significant updates needed: Output exactly: NO_UPDATES_NEEDED + + Start your response immediately with either the updated documentation content or "NO_UPDATES_NEEDED". Nothing else. + EOF + + chmod 600 "$PROMPT_FILE" + FULL_PROMPT=$(cat "$PROMPT_FILE") + + # Call Claude API with secure header handling + echo " Calling Claude API for $DOC_FILE..." >> $GITHUB_STEP_SUMMARY + + # Create secure request body file + REQUEST_FILE="$TEMP_DIR/request_$(openssl rand -hex 8).json" + jq -n \ + --arg model "claude-3-7-sonnet-latest" \ + --argjson max_tokens 8192 \ + --arg content "$FULL_PROMPT" \ + '{ + model: $model, + max_tokens: $max_tokens, + messages: [{ + role: "user", + content: $content + }] + }' > "$REQUEST_FILE" + + chmod 600 "$REQUEST_FILE" + + # Call API + RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ + -H "content-type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d @"$REQUEST_FILE") + + # Clean up request file + rm -f "$REQUEST_FILE" + + # Validate response is valid JSON + if ! echo "$RESPONSE" | jq empty 2>/dev/null; then + echo " ❌ Invalid JSON response from API" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Check for API errors + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_TYPE=$(echo "$RESPONSE" | jq -r '.error.type // "unknown"') + ERROR_CODE=$(echo "$RESPONSE" | jq -r '.error.code // "unknown"') + echo " ❌ Claude API Error for $DOC_FILE: type=$ERROR_TYPE, code=$ERROR_CODE" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Validate response structure + if ! echo "$RESPONSE" | jq -e '.content[0].text' > /dev/null 2>&1; then + echo " ❌ API response missing expected content field" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Extract the response content + CLAUDE_OUTPUT=$(echo "$RESPONSE" | jq -r '.content[0].text') + + # Validate output size + OUTPUT_SIZE=${#CLAUDE_OUTPUT} + MAX_OUTPUT_SIZE=524288 # 512KB + if [ $OUTPUT_SIZE -gt $MAX_OUTPUT_SIZE ]; then + echo " ❌ Claude output too large: $OUTPUT_SIZE bytes" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Sanitize output (remove null bytes, validate UTF-8) + CLAUDE_OUTPUT=$(echo "$CLAUDE_OUTPUT" | tr -d '\000' | iconv -c -t UTF-8//IGNORE) + + # Check if updates are needed + if echo "$CLAUDE_OUTPUT" | grep -q "NO_UPDATES_NEEDED"; then + echo " â„šī¸ No updates needed for $DOC_FILE" >> $GITHUB_STEP_SUMMARY + return 0 + else + echo " ✅ Updating $DOC_FILE" >> $GITHUB_STEP_SUMMARY + # Write the updated documentation + echo "$CLAUDE_OUTPUT" > "$DOC_FILE" + ANY_UPDATES=true + return 0 + fi + } + + # === PHASE 2: UPDATE ALL DETECTED DOCUMENTATION FILES === + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📝 Updating Documentation Files:" >> $GITHUB_STEP_SUMMARY + + if [ "$HAS_README" = true ] && [ "$HAS_CLAUDE" = true ]; then + echo "Both $README_FILE and $CLAUDE_FILE detected — updating both" >> $GITHUB_STEP_SUMMARY + update_doc_file "$README_FILE" "README" "$CLAUDE_FILE also exists in this repository and is being updated separately for AI/Claude Code context. Focus this $README_FILE update on user-facing documentation only." || echo " âš ī¸ Failed to update $README_FILE" >> $GITHUB_STEP_SUMMARY + update_doc_file "$CLAUDE_FILE" "CLAUDE" "$README_FILE also exists in this repository and is being updated separately for user-facing documentation. Focus this $CLAUDE_FILE update on AI/Claude Code technical context only." || echo " âš ī¸ Failed to update $CLAUDE_FILE" >> $GITHUB_STEP_SUMMARY + elif [ "$HAS_README" = true ]; then + echo "Only $README_FILE detected — updating $README_FILE" >> $GITHUB_STEP_SUMMARY + update_doc_file "$README_FILE" "README" "This is the only documentation file in the repository." || echo " âš ī¸ Failed to update $README_FILE" >> $GITHUB_STEP_SUMMARY + elif [ "$HAS_CLAUDE" = true ]; then + echo "Only $CLAUDE_FILE detected — updating $CLAUDE_FILE" >> $GITHUB_STEP_SUMMARY + update_doc_file "$CLAUDE_FILE" "CLAUDE" "This is the only documentation file in the repository." || echo " âš ī¸ Failed to update $CLAUDE_FILE" >> $GITHUB_STEP_SUMMARY + fi + + # Set final output + if [ "$ANY_UPDATES" = true ]; then + echo "skip_analysis=false" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_STEP_SUMMARY + echo "📝 Documentation files have been updated" >> $GITHUB_STEP_SUMMARY + else + echo "skip_analysis=true" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ No documentation updates needed" >> $GITHUB_STEP_SUMMARY + fi + + # Clean up temporary directory + rm -rf "$TEMP_DIR" + + - name: 🔍 Check for Documentation Changes + id: check-changes + if: steps.claude-analysis.outputs.skip_analysis != 'true' + shell: bash + run: | + # Check if documentation files were modified + git add -A + + if git diff --cached --quiet; then + echo "has_changes=false" >> $GITHUB_OUTPUT + echo "✅ No documentation updates needed" >> $GITHUB_STEP_SUMMARY + else + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "📝 Documentation updates detected" >> $GITHUB_STEP_SUMMARY + + # Show what changed + echo "### Changed Files:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + git diff --cached --name-only >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + # Verify only documentation files changed + CHANGED=$(git diff --cached --name-only) + INVALID_FILES="" + + for file in $CHANGED; do + FILE_LOWER=$(echo "$file" | tr '[:upper:]' '[:lower:]') + if [[ "$FILE_LOWER" != "readme.md" ]] && [[ "$FILE_LOWER" != "claude.md" ]]; then + INVALID_FILES="$INVALID_FILES $file" + fi + done + + if [ -n "$INVALID_FILES" ]; then + echo "❌ ERROR: Non-documentation files were modified:$INVALID_FILES" >> $GITHUB_STEP_SUMMARY + echo "Only README.md and CLAUDE.md (any casing) should be updated." >> $GITHUB_STEP_SUMMARY + git reset --hard + echo "has_changes=false" >> $GITHUB_OUTPUT + exit 1 + fi + fi + + - name: đŸ—‘ī¸ Close Existing DocSync PRs + if: steps.check-changes.outputs.has_changes == 'true' + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + run: | + echo "Checking for existing DocSync AI PRs..." + + # Find and close any existing documentation update PRs + EXISTING_PRS=$(gh pr list \ + --search "DocSync AI: Update Documentation" \ + --state open \ + --json number \ + --jq '.[].number' 2>/dev/null || echo "") + + if [ -n "$EXISTING_PRS" ]; then + echo "Found existing PRs to close:" + for pr in $EXISTING_PRS; do + echo " Closing PR #$pr..." + gh pr close "$pr" \ + --comment "🔄 Closing stale documentation PR. A fresh update will be created." \ + --delete-branch 2>/dev/null || echo " Could not close PR #$pr" + done + echo "đŸ—‘ī¸ Closed $(echo "$EXISTING_PRS" | wc -w | tr -d ' ') existing PR(s)" >> $GITHUB_STEP_SUMMARY + else + echo "No existing DocSync AI PRs found" + fi + + - name: 🔀 Create Documentation PR + id: create-doc-pr + if: steps.check-changes.outputs.has_changes == 'true' + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + BASE_BRANCH: ${{ inputs.base_branch }} + PR_LABELS: ${{ inputs.pr_labels }} + MERGED_PR_NUMBER: ${{ inputs.pr_number }} + MERGED_PR_TITLE: ${{ inputs.pr_title }} + run: | + set -e + + # Use local git config only + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + + # Verify configuration was set + CONFIGURED_NAME=$(git config --local user.name) + if [ "$CONFIGURED_NAME" != "github-actions[bot]" ]; then + echo "❌ Failed to set git user configuration" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Generate secure branch name + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + RANDOM_SUFFIX=$(openssl rand -hex 4) + BRANCH="docs/auto-update-${TIMESTAMP}-${RANDOM_SUFFIX}" + + # Validate branch name + if ! [[ "$BRANCH" =~ ^[a-zA-Z0-9/_-]+$ ]]; then + echo "❌ Generated invalid branch name" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Check branch doesn't already exist + if git rev-parse --verify "$BRANCH" 2>/dev/null; then + echo "âš ī¸ Branch $BRANCH already exists, using alternative" >> $GITHUB_STEP_SUMMARY + BRANCH="${BRANCH}-$(openssl rand -hex 4)" + fi + + git checkout -b "$BRANCH" + + # Add only documentation files (case-insensitive match, type f only) + find . -maxdepth 1 -type f \( -iname "readme.md" -o -iname "claude.md" \) -print0 | xargs -0 git add 2>/dev/null || true + + # Sanitize PR title for commit message (remove control chars, limit size) + MERGED_PR_TITLE_CLEAN=$(echo "$MERGED_PR_TITLE" | tr -d '\000-\037' | head -c 200) + + # Create commit with safe message (using heredoc to avoid injection) + git commit -m "$(cat <<'EOF' + 📚 DocSync AI: Update documentation + + Automatically updated documentation based on merged PR changes. + + Generated by DocSync AI + EOF + )" + + git push origin "$BRANCH" + + # Create PR with sanitized content + # Sanitize PR number for display + if ! [[ "$MERGED_PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid PR number for display" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Create PR body safely + PR_BODY="## 📚 Automated Documentation Update + + This PR updates the documentation based on changes merged in a recent PR. + + ### 🔄 Source PR + - **PR**: #$MERGED_PR_NUMBER + + ### 📝 Changes + Documentation has been automatically updated to reflect the latest changes in the codebase. + + ### ✅ Review Checklist + - [ ] Documentation accurately reflects the code changes + - [ ] No unrelated sections were modified + - [ ] Formatting and style are consistent + - [ ] Examples and code snippets are up to date + + --- + 🤖 *Automated by DocSync AI*" + + PR_URL=$(gh pr create \ + --title "📚 DocSync AI: Update Documentation" \ + --body "$PR_BODY" \ + --base "$BASE_BRANCH" \ + --head "$BRANCH") + + # Add labels (validate label format) + PR_NUMBER=$(echo "$PR_URL" | grep -oE '[0-9]+$') + if [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + IFS=',' read -ra LABEL_ARRAY <<< "$PR_LABELS" + for label in "${LABEL_ARRAY[@]}"; do + # Trim whitespace and validate + label=$(echo "$label" | xargs) + if [[ "$label" =~ ^[a-zA-Z0-9_-]+$ ]]; then + gh pr edit "$PR_NUMBER" --add-label "$label" 2>/dev/null || echo "Label '$label' not found, skipping" + fi + done + fi + + echo "pr_created=true" >> $GITHUB_OUTPUT + echo "pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + echo "pr_url=$PR_URL" >> $GITHUB_OUTPUT + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔀 Documentation PR Created: #$PR_NUMBER" >> $GITHUB_STEP_SUMMARY + echo "$PR_URL" >> $GITHUB_STEP_SUMMARY + + - name: ✅ No Updates Needed + if: steps.claude-analysis.outputs.skip_analysis == 'true' || steps.check-changes.outputs.has_changes == 'false' + shell: bash + run: | + echo "pr_created=false" >> $GITHUB_OUTPUT + echo "✅ Documentation is up to date - no changes needed" >> $GITHUB_STEP_SUMMARY diff --git a/.github/actions/docsync_ai_comment/action.yml b/.github/actions/docsync_ai_comment/action.yml new file mode 100644 index 0000000..1fb2620 --- /dev/null +++ b/.github/actions/docsync_ai_comment/action.yml @@ -0,0 +1,549 @@ +name: "DocSync AI - Update from Comment" +description: "Update documentation in existing DocSync PR based on user comment suggestions" + +inputs: + github_token: + description: "GitHub token for repository operations and PR creation" + required: true + anthropic_api_key: + description: "Anthropic API key for Claude AI" + required: true + repository: + description: "Repository name (owner/repo)" + required: true + pr_number: + description: "PR number where comment was made" + required: true + comment_body: + description: "Comment body with user suggestion (must start with 'docsync:')" + required: true + base_branch: + description: "Base branch for documentation PR (e.g., master or main)" + required: false + default: "master" + +outputs: + updated: + description: "Whether documentation was updated" + value: "${{ steps.commit-changes.outputs.updated }}" + suggestion_valid: + description: "Whether the comment had valid docsync prefix" + value: "${{ steps.validate.outputs.valid }}" + is_docsync_pr: + description: "Whether the PR is a DocSync PR" + value: "${{ steps.verify-pr.outputs.is_docsync_pr }}" + +runs: + using: composite + steps: + - name: ✅ Validate Comment Prefix + id: validate + shell: bash + env: + COMMENT_BODY: ${{ inputs.comment_body }} + run: | + set -e + + echo "## ✅ Validating Comment" >> $GITHUB_STEP_SUMMARY + + # Sanitize comment body (remove null bytes, limit size) + COMMENT_BODY_CLEAN=$(echo "$COMMENT_BODY" | tr -d '\000' | iconv -c -t UTF-8//IGNORE | head -c 10000) + + # Validate for suspicious patterns + if echo "$COMMENT_BODY_CLEAN" | grep -qE '\$\(|\`|;[[:space:]]*curl|;[[:space:]]*wget|eval|exec'; then + echo "âš ī¸ Warning: Suspicious pattern detected in comment" >> $GITHUB_STEP_SUMMARY + # Continue but log warning + fi + + # Check if comment starts with "docsync:" (case-insensitive) + if echo "$COMMENT_BODY_CLEAN" | grep -qiE "^docsync:[[:space:]]"; then + echo "valid=true" >> $GITHUB_OUTPUT + echo "✅ Valid docsync comment detected" >> $GITHUB_STEP_SUMMARY + + # Extract suggestion safely (everything after "docsync:") + SUGGESTION=$(echo "$COMMENT_BODY_CLEAN" | sed -E 's/^[Dd][Oo][Cc][Ss][Yy][Nn][Cc]:[[:space:]]*//') + + # Validate suggestion is not empty + if [ -z "$SUGGESTION" ]; then + echo "valid=false" >> $GITHUB_OUTPUT + echo "❌ Comment suggestion is empty" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Limit suggestion size for output + SUGGESTION_DISPLAY=$(echo "$SUGGESTION" | head -c 500) + + echo "suggestion<> $GITHUB_OUTPUT + echo "$SUGGESTION" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + echo "### User Suggestion:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "$SUGGESTION_DISPLAY" >> $GITHUB_STEP_SUMMARY + if [ ${#SUGGESTION} -gt 500 ]; then + echo "... (truncated for display)" >> $GITHUB_STEP_SUMMARY + fi + echo '```' >> $GITHUB_STEP_SUMMARY + else + echo "valid=false" >> $GITHUB_OUTPUT + echo "❌ Comment does not start with 'docsync:' - skipping" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + - name: 🔍 Verify DocSync PR + id: verify-pr + if: steps.validate.outputs.valid == 'true' + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + PR_NUMBER: ${{ inputs.pr_number }} + REPO_NAME: ${{ inputs.repository }} + run: | + set -e + + echo "## 🔍 Verifying DocSync PR" >> $GITHUB_STEP_SUMMARY + + # Validate PR number + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid PR number: $PR_NUMBER" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + if [ "$PR_NUMBER" -lt 1 ] || [ "$PR_NUMBER" -gt 999999 ]; then + echo "❌ PR number out of valid range: $PR_NUMBER" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Validate repository format + if ! [[ "$REPO_NAME" =~ ^[a-zA-Z0-9_-]+/[a-zA-Z0-9_.-]+$ ]]; then + echo "❌ Invalid repository format: $REPO_NAME" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Get PR details including labels and title + PR_DATA=$(gh api \ + -H "Accept: application/vnd.github+json" \ + "/repos/$REPO_NAME/pulls/$PR_NUMBER") + + # Validate response is valid JSON + if ! echo "$PR_DATA" | jq empty 2>/dev/null; then + echo "❌ Invalid response from GitHub API" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Extract labels + PR_LABELS=$(echo "$PR_DATA" | jq -r '.labels[].name' 2>&1 || echo "") + + # Extract PR title and sanitize + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title' 2>&1 || echo "") + PR_TITLE=$(echo "$PR_TITLE" | tr -d '\000-\037' | head -c 200) + + # Extract PR branch and validate + PR_BRANCH=$(echo "$PR_DATA" | jq -r '.head.ref' 2>&1 || echo "") + + # Validate branch name format + if ! [[ "$PR_BRANCH" =~ ^[a-zA-Z0-9/_.-]+$ ]]; then + echo "❌ Invalid branch name format" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Sanitize labels for display + PR_LABELS_DISPLAY=$(echo "$PR_LABELS" | head -n 10) + + echo "### PR Information:" >> $GITHUB_STEP_SUMMARY + echo "- **Title**: ${PR_TITLE:0:100}" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: $PR_BRANCH" >> $GITHUB_STEP_SUMMARY + echo "- **Labels**: ${PR_LABELS_DISPLAY:-'(none)'}" >> $GITHUB_STEP_SUMMARY + + # Check if PR has docsync-ai or automated label, OR if title contains "DocSync" + IS_DOCSYNC_PR=false + + if echo "$PR_LABELS" | grep -qE "(docsync-ai|automated)"; then + IS_DOCSYNC_PR=true + echo "✅ PR has DocSync label" >> $GITHUB_STEP_SUMMARY + elif echo "$PR_TITLE" | grep -qi "docsync"; then + IS_DOCSYNC_PR=true + echo "✅ PR title contains 'DocSync'" >> $GITHUB_STEP_SUMMARY + fi + + if [ "$IS_DOCSYNC_PR" = true ]; then + echo "is_docsync_pr=true" >> $GITHUB_OUTPUT + echo "pr_branch=$PR_BRANCH" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_STEP_SUMMARY + echo "✅ **Verified as DocSync PR**" >> $GITHUB_STEP_SUMMARY + else + echo "is_docsync_pr=false" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_STEP_SUMMARY + echo "❌ **Not a DocSync PR**" >> $GITHUB_STEP_SUMMARY + echo "This feature only works on DocSync PRs (with 'docsync-ai'/'automated' labels or 'DocSync' in title)" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + - name: đŸ“Ĩ Checkout PR Branch + if: steps.verify-pr.outputs.is_docsync_pr == 'true' + uses: actions/checkout@v4 + with: + token: ${{ inputs.github_token }} + ref: ${{ steps.verify-pr.outputs.pr_branch }} + fetch-depth: 0 + + - name: 🤖 Run Claude 3.7 Sonnet Documentation Analysis + id: claude-analysis + if: steps.verify-pr.outputs.is_docsync_pr == 'true' + shell: bash + env: + ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} + REPO_NAME: ${{ inputs.repository }} + PR_NUMBER: ${{ inputs.pr_number }} + USER_SUGGESTION: ${{ steps.validate.outputs.suggestion }} + run: | + set -e + + echo "## 🤖 Claude 3.7 Sonnet Analysis" >> $GITHUB_STEP_SUMMARY + echo "Model: claude-3-7-sonnet-latest" >> $GITHUB_STEP_SUMMARY + + # Create secure temporary directory + TEMP_DIR=$(mktemp -d -t docsync-comment-XXXXXXXXXX) + trap "rm -rf '$TEMP_DIR'" EXIT + + # Sanitize user suggestion + USER_SUGGESTION_CLEAN=$(echo "$USER_SUGGESTION" | tr -d '\000' | iconv -c -t UTF-8//IGNORE | head -c 10000) + + # Validate file paths helper function + validate_doc_file() { + local file=$1 + # Must be in current directory, not a symlink, and match expected names + if [[ ! -f "$file" ]] || [[ -L "$file" ]]; then + return 1 + fi + local basename=$(basename "$file") + local basename_lower=$(echo "$basename" | tr '[:upper:]' '[:lower:]') + if [[ "$basename_lower" != "readme.md" ]] && [[ "$basename_lower" != "claude.md" ]]; then + return 1 + fi + # Ensure file is in repo root (no directory traversal) + local realpath=$(realpath "$file" 2>/dev/null || echo "") + local workdir=$(realpath . 2>/dev/null || echo "") + if [[ ! "$realpath" == "$workdir"/* ]]; then + return 1 + fi + return 0 + } + + # === PHASE 1: DETECT ALL DOCUMENTATION FILES === + # Check for BOTH files before updating anything + HAS_README=false + HAS_CLAUDE=false + + # Case-insensitive check for README.md + README_FILE=$(find . -maxdepth 1 -type f -iname "readme.md" -print -quit 2>/dev/null) + if [ -n "$README_FILE" ]; then + README_FILE="${README_FILE#./}" + if validate_doc_file "$README_FILE"; then + HAS_README=true + else + echo "âš ī¸ Invalid README file detected, skipping" >> $GITHUB_STEP_SUMMARY + README_FILE="" + fi + fi + + # Case-insensitive check for CLAUDE.md + CLAUDE_FILE=$(find . -maxdepth 1 -type f -iname "claude.md" -print -quit 2>/dev/null) + if [ -n "$CLAUDE_FILE" ]; then + CLAUDE_FILE="${CLAUDE_FILE#./}" + if validate_doc_file "$CLAUDE_FILE"; then + HAS_CLAUDE=true + else + echo "âš ī¸ Invalid CLAUDE file detected, skipping" >> $GITHUB_STEP_SUMMARY + CLAUDE_FILE="" + fi + fi + + # Log detection results for both files + echo "### 📄 Documentation File Detection:" >> $GITHUB_STEP_SUMMARY + echo "- README.md: $([ "$HAS_README" = true ] && echo "✅ Found ($README_FILE)" || echo '❌ Not found')" >> $GITHUB_STEP_SUMMARY + echo "- CLAUDE.md: $([ "$HAS_CLAUDE" = true ] && echo "✅ Found ($CLAUDE_FILE)" || echo '❌ Not found')" >> $GITHUB_STEP_SUMMARY + + if [ "$HAS_README" = false ] && [ "$HAS_CLAUDE" = false ]; then + echo "❌ No documentation file found (README.md or CLAUDE.md)" >> $GITHUB_STEP_SUMMARY + echo "skip_analysis=true" >> $GITHUB_OUTPUT + exit 0 + fi + + ANY_UPDATES=false + + # Function to call Claude API for documentation update + update_doc_file() { + local DOC_FILE=$1 + local DOC_FORMAT=$2 + local OTHER_FILES_NOTE=$3 + + echo "Processing $DOC_FILE..." >> $GITHUB_STEP_SUMMARY + + # Validate doc file path + if ! validate_doc_file "$DOC_FILE"; then + echo " ❌ Invalid doc file: $DOC_FILE" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Read current documentation + DOC_CONTENT=$(cat "$DOC_FILE") + + # Validate content size + CONTENT_SIZE=${#DOC_CONTENT} + MAX_CONTENT_SIZE=524288 # 512KB + if [ $CONTENT_SIZE -gt $MAX_CONTENT_SIZE ]; then + echo " âš ī¸ Doc file too large: $CONTENT_SIZE bytes" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Prepare the prompt based on file type + if [ "$DOC_FORMAT" = "README" ]; then + FORMAT_INSTRUCTION="This is a README.md file. Maintain standard README format with clear sections, proper markdown formatting, badges, installation instructions, usage examples, and project overview. Keep the professional, user-facing documentation style." + else + FORMAT_INSTRUCTION="This is a CLAUDE.md file for AI assistance. Maintain the technical, detailed format suitable for AI context with implementation details, architecture notes, and development guidelines. Keep the detailed, technical documentation style." + fi + + # Create secure prompt file + PROMPT_FILE="$TEMP_DIR/claude_prompt_$(openssl rand -hex 8).txt" + + # Build prompt with safe variable substitution + cat > "$PROMPT_FILE" << 'EOF' + You are a technical documentation expert. A user has provided a suggestion for improving the documentation in a DocSync PR. + + # Context + Repository: '"$REPO_NAME"' + PR #'"$PR_NUMBER"' (DocSync Documentation PR) + + # Current '"$DOC_FILE"': + '"$DOC_CONTENT"' + + # User Suggestion: + '"$USER_SUGGESTION_CLEAN"' + + # Format Requirements: + '"$FORMAT_INSTRUCTION"' + + # Documentation Context: + '"$OTHER_FILES_NOTE"' + + # Your Task + Apply the user'\''s suggestion to update the '"$DOC_FILE"'. + + ## CRITICAL RULES - MUST FOLLOW: + 1. **Direct output only**: Output ONLY the updated documentation content. NO acknowledgments like "I will apply", "I understand", "Here'\''s the updated", or any explanatory text. + 2. **Maintain format**: Keep the exact same documentation style and structure as the current file. + 3. **Follow user intent**: Carefully interpret and apply the user'\''s suggestion. If the suggestion is vague, make reasonable improvements. + 4. **Update relevant sections**: Only modify sections relevant to the suggestion. Keep everything else exactly as is. + 5. **Complete file output**: Provide the COMPLETE updated file content, not just the changed sections. + 6. **No meta-commentary**: No phrases like "I'\''ve updated", "Changes made", or section summaries. + 7. **Quality standards**: Ensure updates are accurate, clear, and improve documentation quality. + + Start your response immediately with the complete updated '"$DOC_FILE"' content. Nothing else. + 'EOF' + + chmod 600 "$PROMPT_FILE" + FULL_PROMPT=$(cat "$PROMPT_FILE") + + # Call Claude API with secure header handling + echo " Calling Claude API for $DOC_FILE..." >> $GITHUB_STEP_SUMMARY + + # Create secure request body file + REQUEST_FILE="$TEMP_DIR/request_$(openssl rand -hex 8).json" + jq -n \ + --arg model "claude-3-7-sonnet-latest" \ + --argjson max_tokens 8192 \ + --arg content "$FULL_PROMPT" \ + '{ + model: $model, + max_tokens: $max_tokens, + messages: [{ + role: "user", + content: $content + }] + }' > "$REQUEST_FILE" + + chmod 600 "$REQUEST_FILE" + + # Call API + RESPONSE=$(curl -s https://api.anthropic.com/v1/messages \ + -H "content-type: application/json" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-version: 2023-06-01" \ + -d @"$REQUEST_FILE") + + # Clean up request file + rm -f "$REQUEST_FILE" + + # Validate response is valid JSON + if ! echo "$RESPONSE" | jq empty 2>/dev/null; then + echo " ❌ Invalid JSON response from API" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Check for API errors + if echo "$RESPONSE" | jq -e '.error' > /dev/null 2>&1; then + ERROR_TYPE=$(echo "$RESPONSE" | jq -r '.error.type // "unknown"') + ERROR_CODE=$(echo "$RESPONSE" | jq -r '.error.code // "unknown"') + echo " ❌ Claude API Error for $DOC_FILE: type=$ERROR_TYPE, code=$ERROR_CODE" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Validate response structure + if ! echo "$RESPONSE" | jq -e '.content[0].text' > /dev/null 2>&1; then + echo " ❌ API response missing expected content field" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Extract the response content + CLAUDE_OUTPUT=$(echo "$RESPONSE" | jq -r '.content[0].text') + + # Validate output size + OUTPUT_SIZE=${#CLAUDE_OUTPUT} + MAX_OUTPUT_SIZE=524288 # 512KB + if [ $OUTPUT_SIZE -gt $MAX_OUTPUT_SIZE ]; then + echo " ❌ Claude output too large: $OUTPUT_SIZE bytes" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Sanitize output (remove null bytes, validate UTF-8) + CLAUDE_OUTPUT=$(echo "$CLAUDE_OUTPUT" | tr -d '\000' | iconv -c -t UTF-8//IGNORE) + + echo " ✅ Updating $DOC_FILE" >> $GITHUB_STEP_SUMMARY + # Write the updated documentation + echo "$CLAUDE_OUTPUT" > "$DOC_FILE" + ANY_UPDATES=true + return 0 + } + + # === PHASE 2: UPDATE ALL DETECTED DOCUMENTATION FILES === + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📝 Updating Documentation Files:" >> $GITHUB_STEP_SUMMARY + + if [ "$HAS_README" = true ] && [ "$HAS_CLAUDE" = true ]; then + echo "Both $README_FILE and $CLAUDE_FILE detected — updating both" >> $GITHUB_STEP_SUMMARY + update_doc_file "$README_FILE" "README" "$CLAUDE_FILE also exists in this repository and is being updated separately for AI/Claude Code context. Focus this $README_FILE update on user-facing documentation only." || echo " âš ī¸ Failed to update $README_FILE" >> $GITHUB_STEP_SUMMARY + update_doc_file "$CLAUDE_FILE" "CLAUDE" "$README_FILE also exists in this repository and is being updated separately for user-facing documentation. Focus this $CLAUDE_FILE update on AI/Claude Code technical context only." || echo " âš ī¸ Failed to update $CLAUDE_FILE" >> $GITHUB_STEP_SUMMARY + elif [ "$HAS_README" = true ]; then + echo "Only $README_FILE detected — updating $README_FILE" >> $GITHUB_STEP_SUMMARY + update_doc_file "$README_FILE" "README" "This is the only documentation file in the repository." || echo " âš ī¸ Failed to update $README_FILE" >> $GITHUB_STEP_SUMMARY + elif [ "$HAS_CLAUDE" = true ]; then + echo "Only $CLAUDE_FILE detected — updating $CLAUDE_FILE" >> $GITHUB_STEP_SUMMARY + update_doc_file "$CLAUDE_FILE" "CLAUDE" "This is the only documentation file in the repository." || echo " âš ī¸ Failed to update $CLAUDE_FILE" >> $GITHUB_STEP_SUMMARY + fi + + # Set final output + if [ "$ANY_UPDATES" = true ]; then + echo "skip_analysis=false" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_STEP_SUMMARY + echo "📝 Documentation files have been updated" >> $GITHUB_STEP_SUMMARY + else + echo "skip_analysis=true" >> $GITHUB_OUTPUT + echo "" >> $GITHUB_STEP_SUMMARY + echo "âš ī¸ No documentation updates were applied" >> $GITHUB_STEP_SUMMARY + fi + + - name: 💾 Commit Changes to PR + id: commit-changes + if: steps.claude-analysis.outputs.skip_analysis != 'true' + shell: bash + env: + GH_TOKEN: ${{ inputs.github_token }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + set -e + + # Check if documentation files were modified + git add -A + + if git diff --cached --quiet; then + echo "updated=false" >> $GITHUB_OUTPUT + echo "✅ No documentation updates needed" >> $GITHUB_STEP_SUMMARY + else + echo "updated=true" >> $GITHUB_OUTPUT + echo "📝 Documentation updates detected" >> $GITHUB_STEP_SUMMARY + + # Show what changed + echo "### Changed Files:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + git diff --cached --name-only >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + # Verify only documentation files changed + CHANGED=$(git diff --cached --name-only) + INVALID_FILES="" + + for file in $CHANGED; do + FILE_LOWER=$(echo "$file" | tr '[:upper:]' '[:lower:]') + if [[ "$FILE_LOWER" != "readme.md" ]] && [[ "$FILE_LOWER" != "claude.md" ]]; then + INVALID_FILES="$INVALID_FILES $file" + fi + done + + if [ -n "$INVALID_FILES" ]; then + echo "❌ ERROR: Non-documentation files were modified:$INVALID_FILES" >> $GITHUB_STEP_SUMMARY + echo "Only README.md and CLAUDE.md (any casing) should be updated." >> $GITHUB_STEP_SUMMARY + git reset --hard + echo "updated=false" >> $GITHUB_OUTPUT + exit 1 + fi + + # Commit changes with secure git config + git config --local user.name "github-actions[bot]" + git config --local user.email "github-actions[bot]@users.noreply.github.com" + + # Verify configuration was set + CONFIGURED_NAME=$(git config --local user.name) + if [ "$CONFIGURED_NAME" != "github-actions[bot]" ]; then + echo "❌ Failed to set git user configuration" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Validate PR number for commit message + if ! [[ "$PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "❌ Invalid PR number for commit" >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + # Create commit with safe message (using printf to avoid injection) + git commit -m "📚 DocSync AI: Apply user suggestion from comment + + Applied documentation updates based on user comment. + + Generated by DocSync AI" + + git push origin HEAD + + # Get list of changed files for comment + CHANGED_FILES=$(git diff HEAD~1 --name-only | head -10 | sed 's/^/- /') + + # Add confirmation comment to PR using safe format + COMMENT_BODY="✅ **DocSync AI Update Applied** + + Your documentation suggestion has been applied and committed to this PR. + + 📝 **Updated Files:** + $CHANGED_FILES + + --- + 🤖 *Automated by DocSync AI*" + + gh pr comment "$PR_NUMBER" --body "$COMMENT_BODY" + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Changes Committed and Comment Added" >> $GITHUB_STEP_SUMMARY + fi + + - name: 📊 Summary + if: always() + shell: bash + env: + VALID: ${{ steps.validate.outputs.valid }} + IS_DOCSYNC: ${{ steps.verify-pr.outputs.is_docsync_pr }} + UPDATED: ${{ steps.commit-changes.outputs.updated }} + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "## 📊 Action Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Comment Valid**: ${VALID:-false}" >> $GITHUB_STEP_SUMMARY + echo "- **Is DocSync PR**: ${IS_DOCSYNC:-false}" >> $GITHUB_STEP_SUMMARY + echo "- **Documentation Updated**: ${UPDATED:-false}" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/docsync-ai.yml b/.github/workflows/docsync-ai.yml new file mode 100644 index 0000000..3e05353 --- /dev/null +++ b/.github/workflows/docsync-ai.yml @@ -0,0 +1,258 @@ +name: 📚 DocSync AI - Documentation Sync + +on: + workflow_call: + inputs: + base_branch: + description: 'Base branch for documentation PR (e.g., master or main)' + required: false + default: 'master' + type: string + pr_labels: + description: 'Comma-separated labels for the documentation PR' + required: false + default: 'documentation,automated' + type: string + slack_webhook_url: + description: 'Slack webhook URL for notifications (optional, can use secret instead)' + required: false + default: '' + type: string + repository_owner: + description: 'Repository owner name for Slack notifications' + required: false + default: '' + type: string + trigger_type: + description: 'Trigger type: "merge" for PR merge, "comment" for PR comment' + required: false + default: 'merge' + type: string + comment_body: + description: 'Comment body (only for comment trigger, must start with "docsync:")' + required: false + default: '' + type: string + comment_pr_number: + description: 'PR number where comment was made (only for comment trigger)' + required: false + default: '' + type: string + secrets: + DOCSYNC_GITHUB_TOKEN: + description: 'GitHub PAT for PR creation (repo-specific, needs repo and pull_requests permissions)' + required: true + DOCSYNC_ANTHROPIC_API_KEY: + description: 'Anthropic API key for Claude AI' + required: true + DOCSYNC_SLACK_WEBHOOK: + description: 'Slack webhook URL for notifications (optional if provided via input)' + required: false + +concurrency: + group: docsync-ai-${{ github.repository }} + cancel-in-progress: false + +jobs: + sync-documentation: + name: 📚 Sync Documentation + runs-on: ubuntu-latest + + # Only run when a PR is merged to the base branch + if: github.event.pull_request.merged == true + + permissions: + contents: write + pull-requests: write + + outputs: + doc_pr_created: ${{ steps.docsync.outputs.doc_pr_created }} + doc_pr_number: ${{ steps.docsync.outputs.doc_pr_number }} + doc_pr_url: ${{ steps.docsync.outputs.doc_pr_url }} + + env: + BASE_BRANCH: ${{ inputs.base_branch }} + PR_LABELS: ${{ inputs.pr_labels }} + SLACK_WEBHOOK: ${{ inputs.slack_webhook_url || secrets.DOCSYNC_SLACK_WEBHOOK }} + REPO_OWNER: ${{ inputs.repository_owner }} + + steps: + - name: 🔍 Debug - Workflow Trigger Info + run: | + echo "## 🔍 Workflow Trigger Info" >> $GITHUB_STEP_SUMMARY + echo "Event: ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY + echo "Action: ${{ github.event.action }}" >> $GITHUB_STEP_SUMMARY + echo "PR Merged: ${{ github.event.pull_request.merged }}" >> $GITHUB_STEP_SUMMARY + echo "PR Number: ${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY + echo "PR Title: ${{ github.event.pull_request.title }}" >> $GITHUB_STEP_SUMMARY + echo "Base Branch: ${{ inputs.base_branch }}" >> $GITHUB_STEP_SUMMARY + echo "Repository: ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY + + - name: đŸ“Ĩ Checkout Repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.DOCSYNC_GITHUB_TOKEN }} + ref: ${{ inputs.base_branch }} + + - name: 🤖 Run DocSync AI Action + id: docsync + uses: akash-deriv/shared-actions/.github/actions/docsync_ai@master + with: + github_token: ${{ secrets.DOCSYNC_GITHUB_TOKEN }} + anthropic_api_key: ${{ secrets.DOCSYNC_ANTHROPIC_API_KEY }} + repository: ${{ github.repository }} + pr_number: ${{ github.event.pull_request.number }} + pr_title: ${{ github.event.pull_request.title }} + pr_body: ${{ github.event.pull_request.body }} + base_branch: ${{ inputs.base_branch }} + pr_labels: ${{ inputs.pr_labels }} + + - name: đŸ“ĸ Send Slack Notification - Documentation PR Created + if: steps.docsync.outputs.doc_pr_created == 'true' && env.SLACK_WEBHOOK != '' + env: + DOC_PR_NUMBER: ${{ steps.docsync.outputs.doc_pr_number }} + DOC_PR_URL: ${{ steps.docsync.outputs.doc_pr_url }} + MERGED_PR_NUMBER: ${{ github.event.pull_request.number }} + MERGED_PR_TITLE: ${{ github.event.pull_request.title }} + run: | + set -e + + # Validate webhook URL format + if [[ ! "$SLACK_WEBHOOK" =~ ^https://hooks\.slack\.com/services/ ]]; then + echo "âš ī¸ Warning: Invalid Slack webhook URL format, skipping notification" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Sanitize inputs for Slack message + MERGED_PR_TITLE_CLEAN=$(echo "$MERGED_PR_TITLE" | tr -d '\000-\037' | head -c 200) + DOC_PR_URL_CLEAN=$(echo "$DOC_PR_URL" | head -c 500) + + # Validate PR numbers + if ! [[ "$DOC_PR_NUMBER" =~ ^[0-9]+$ ]] || ! [[ "$MERGED_PR_NUMBER" =~ ^[0-9]+$ ]]; then + echo "âš ī¸ Warning: Invalid PR numbers, skipping notification" >> $GITHUB_STEP_SUMMARY + exit 0 + fi + + # Format repository owner mention for Slack + OWNER_MENTION="" + if [ -n "$REPO_OWNER" ]; then + # Sanitize owner mention + REPO_OWNER_CLEAN=$(echo "$REPO_OWNER" | tr -d '\000-\037' | head -c 50) + OWNER_MENTION="<@$REPO_OWNER_CLEAN> " + fi + + # Create Slack payload safely using jq + SLACK_PAYLOAD=$(jq -n \ + --arg owner "$OWNER_MENTION" \ + --arg repo "${{ github.repository }}" \ + --arg doc_pr_url "$DOC_PR_URL_CLEAN" \ + --arg doc_pr_num "$DOC_PR_NUMBER" \ + --arg merged_pr_num "$MERGED_PR_NUMBER" \ + --arg merged_pr_title "$MERGED_PR_TITLE_CLEAN" \ + '{ + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "\($owner):book: *Documentation Update Ready*\n*<\($doc_pr_url)|PR #\($doc_pr_num)>* in `\($repo)`" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Documentation has been automatically updated based on:\n- *PR #\($merged_pr_num)*: \($merged_pr_title)" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "🤖 DocSync AI | <\($doc_pr_url)|Review documentation updates>" + } + ] + } + ] + }') + + # Create temporary file for response + TEMP_RESPONSE=$(mktemp) + trap "rm -f '$TEMP_RESPONSE'" EXIT + + # Send Slack notification with error handling + HTTP_CODE=$(curl -s -w "%{http_code}" -o "$TEMP_RESPONSE" \ + -X POST "$SLACK_WEBHOOK" \ + -H "Content-Type: application/json" \ + -d "$SLACK_PAYLOAD") + + if [ "$HTTP_CODE" = "200" ]; then + echo "đŸ“ĸ Slack notification sent successfully" >> $GITHUB_STEP_SUMMARY + else + echo "âš ī¸ Warning: Failed to send Slack notification (HTTP $HTTP_CODE)" >> $GITHUB_STEP_SUMMARY + # Don't fail the workflow on notification failure + fi + + - name: ✅ Summary + if: always() + env: + JOB_STATUS: ${{ job.status }} + DOC_PR_CREATED: ${{ steps.docsync.outputs.doc_pr_created }} + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Source PR | #${{ github.event.pull_request.number }} |" >> $GITHUB_STEP_SUMMARY + echo "| Documentation PR Created | ${DOC_PR_CREATED:-false} |" >> $GITHUB_STEP_SUMMARY + echo "| Status | $JOB_STATUS |" >> $GITHUB_STEP_SUMMARY + echo "| Base Branch | $BASE_BRANCH |" >> $GITHUB_STEP_SUMMARY + + update-from-comment: + name: đŸ’Ŧ Update Documentation from Comment + runs-on: ubuntu-latest + + # Only run when trigger_type is 'comment' + if: inputs.trigger_type == 'comment' + + permissions: + contents: write + pull-requests: write + + steps: + - name: 🔍 Debug - Comment Trigger Info + run: | + echo "## 🔍 Comment Trigger Info" >> $GITHUB_STEP_SUMMARY + echo "Trigger Type: ${{ inputs.trigger_type }}" >> $GITHUB_STEP_SUMMARY + echo "PR Number: ${{ inputs.comment_pr_number }}" >> $GITHUB_STEP_SUMMARY + echo "Repository: ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY + + - name: 🤖 Run DocSync AI Comment Action + id: docsync-comment + uses: akash-deriv/shared-actions/.github/actions/docsync_ai_comment@master + with: + github_token: ${{ secrets.DOCSYNC_GITHUB_TOKEN }} + anthropic_api_key: ${{ secrets.DOCSYNC_ANTHROPIC_API_KEY }} + repository: ${{ github.repository }} + pr_number: ${{ inputs.comment_pr_number }} + comment_body: ${{ inputs.comment_body }} + base_branch: ${{ inputs.base_branch }} + + - name: ✅ Summary + if: always() + env: + JOB_STATUS: ${{ job.status }} + UPDATED: ${{ steps.docsync-comment.outputs.updated }} + VALID: ${{ steps.docsync-comment.outputs.suggestion_valid }} + IS_DOCSYNC_PR: ${{ steps.docsync-comment.outputs.is_docsync_pr }} + run: | + echo "" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "| Metric | Value |" >> $GITHUB_STEP_SUMMARY + echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| PR Number | #${{ inputs.comment_pr_number }} |" >> $GITHUB_STEP_SUMMARY + echo "| Valid Comment | ${VALID:-false} |" >> $GITHUB_STEP_SUMMARY + echo "| Is DocSync PR | ${IS_DOCSYNC_PR:-false} |" >> $GITHUB_STEP_SUMMARY + echo "| Documentation Updated | ${UPDATED:-false} |" >> $GITHUB_STEP_SUMMARY + echo "| Status | $JOB_STATUS |" >> $GITHUB_STEP_SUMMARY From 218437ac1a16c65a32d3e020ff26070f905f2dc6 Mon Sep 17 00:00:00 2001 From: akash-deriv Date: Mon, 9 Feb 2026 11:27:24 +0400 Subject: [PATCH 2/4] changed repo reference from personal to organization --- .github/workflows/docsync-ai.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docsync-ai.yml b/.github/workflows/docsync-ai.yml index 3e05353..c0e4866 100644 --- a/.github/workflows/docsync-ai.yml +++ b/.github/workflows/docsync-ai.yml @@ -96,7 +96,7 @@ jobs: - name: 🤖 Run DocSync AI Action id: docsync - uses: akash-deriv/shared-actions/.github/actions/docsync_ai@master + uses: deriv-com/shared-actions/.github/actions/docsync_ai@master with: github_token: ${{ secrets.DOCSYNC_GITHUB_TOKEN }} anthropic_api_key: ${{ secrets.DOCSYNC_ANTHROPIC_API_KEY }} @@ -230,7 +230,7 @@ jobs: - name: 🤖 Run DocSync AI Comment Action id: docsync-comment - uses: akash-deriv/shared-actions/.github/actions/docsync_ai_comment@master + uses: deriv-com/shared-actions/.github/actions/docsync_ai_comment@master with: github_token: ${{ secrets.DOCSYNC_GITHUB_TOKEN }} anthropic_api_key: ${{ secrets.DOCSYNC_ANTHROPIC_API_KEY }} From 59b34efe329c42b7c3d64204dd67b5f7ee107a06 Mon Sep 17 00:00:00 2001 From: akash-deriv Date: Mon, 9 Feb 2026 13:08:29 +0400 Subject: [PATCH 3/4] workflow update --- .github/actions/docsync_ai/action.yml | 175 +++++++++++++++--- .github/actions/docsync_ai_comment/action.yml | 152 ++++++++++++--- 2 files changed, 275 insertions(+), 52 deletions(-) diff --git a/.github/actions/docsync_ai/action.yml b/.github/actions/docsync_ai/action.yml index 6365d7a..d08291e 100644 --- a/.github/actions/docsync_ai/action.yml +++ b/.github/actions/docsync_ai/action.yml @@ -135,7 +135,7 @@ runs: echo "$CHANGED_FILES" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - - name: 🤖 Run Claude 3.7 Sonnet Documentation Analysis + - name: 🤖 Run Claude Sonnet 4.5 Documentation Analysis id: claude-analysis shell: bash env: @@ -149,8 +149,8 @@ runs: run: | set -e - echo "## 🤖 Claude 3.7 Sonnet Analysis" >> $GITHUB_STEP_SUMMARY - echo "Model: claude-3-7-sonnet-latest" >> $GITHUB_STEP_SUMMARY + echo "## 🤖 Claude Sonnet 4.5 Analysis" >> $GITHUB_STEP_SUMMARY + echo "Model: claude-sonnet-4-5-latest" >> $GITHUB_STEP_SUMMARY # Sanitize user inputs (remove null bytes, control characters, limit size) PR_TITLE_CLEAN=$(echo "$PR_TITLE" | tr -d '\000-\037' | head -c 1000) @@ -272,6 +272,17 @@ runs: # Read current documentation DOC_CONTENT=$(cat "$DOC_FILE") + # Extract project title/name for validation (from first heading) + PROJECT_TITLE=$(echo "$DOC_CONTENT" | head -n 30 | grep -E "^#\s+" | head -n 1 | sed 's/^#*\s*//' | tr -d '\000-\037' | head -c 200) + if [ -z "$PROJECT_TITLE" ]; then + PROJECT_TITLE="Unknown Project" + fi + + # Extract key terms from existing documentation for validation + KEY_TERMS=$(echo "$DOC_CONTENT" | head -n 100 | grep -oE '\b[A-Z][A-Za-z0-9]{3,}\b' | sort -u | head -n 20 | tr '\n' ',' | sed 's/,$//') + + echo " 📌 Project: $PROJECT_TITLE" >> $GITHUB_STEP_SUMMARY + # Validate content size CONTENT_SIZE=${#DOC_CONTENT} MAX_CONTENT_SIZE=524288 # 512KB @@ -292,44 +303,89 @@ runs: # Build prompt with safe variable substitution cat > "$PROMPT_FILE" << 'EOF' - You are a technical documentation expert. Analyze the PR changes and update the documentation if needed. + You are a technical documentation expert tasked with updating documentation based on recent code changes. - # Context + ## IMPORTANT: PROJECT IDENTITY + This documentation is for: '"$PROJECT_TITLE"' Repository: '"$REPO_NAME"' + + **CRITICAL**: You MUST maintain the project identity. This is NOT a different project. Do NOT replace this documentation with content about any other project, library, or tool. + + ## Context PR #'"$PR_NUMBER"': '"$PR_TITLE_CLEAN"' PR Description: '"$PR_BODY_CLEAN"' - # Current '"$DOC_FILE"': + ## Current Documentation Content: + ``` '"$DOC_CONTENT"' + ``` - # PR Changes (Diff): + ## PR Changes (Code Diff): + ```diff '"$PR_DIFF"' + ``` - # Repository Structure (sample): + ## Repository Structure: + ``` '"$REPO_STRUCTURE"' + ``` - # Format Requirements: + ## Format Requirements: '"$FORMAT_INSTRUCTION"' - # Documentation Context: + ## Additional Context: '"$OTHER_FILES_NOTE"' - # Your Task - Update the '"$DOC_FILE"' based on the PR changes. + ## Your Task + Analyze the PR changes and determine if the '"$DOC_FILE"' needs updates. + + ## ABSOLUTE REQUIREMENTS - VIOLATION WILL BE REJECTED: + + 1. **PRESERVE PROJECT IDENTITY**: + - The project name "'"$PROJECT_TITLE"'" MUST remain unchanged + - Do NOT replace this with documentation for a different project + - Maintain all existing project-specific information + + 2. **ONLY ADD, NEVER REMOVE**: + - You may ONLY add new information based on PR changes + - NEVER remove existing sections, features, or content + - NEVER replace existing content with unrelated information + - Keep all existing headings, sections, and structure intact + + 3. **STAY RELEVANT TO PR CHANGES**: + - Only update sections directly related to the code changes in the PR diff + - If the PR adds a feature, document that feature + - If the PR changes behavior, update that specific behavior section + - Do NOT make unrelated changes - ## CRITICAL RULES - MUST FOLLOW: - 1. **Direct output only**: Output ONLY the updated documentation content. NO acknowledgments like "I will analyze", "I understand", "Here's the updated", or any explanatory text. - 2. **Maintain format**: Keep the exact same documentation style and structure as the current file. - 3. **Update relevant sections**: Only modify sections that relate to the PR changes. Keep everything else exactly as is. - 4. **Significance check**: Only update if changes are significant (new features, major fixes, API changes, workflow updates). Skip minor changes (typos, formatting tweaks, small refactors). - 5. **Complete file output**: Provide the COMPLETE updated file content, not just the changed sections. - 6. **No meta-commentary**: No phrases like "I've updated", "Changes made", or section summaries. + 4. **VERIFY PROJECT MATCH**: + - Cross-reference the PR diff with the current documentation + - Ensure the technologies, frameworks, and tools mentioned in the PR match those in the documentation + - If there's a mismatch, output NO_UPDATES_NEEDED - ## Decision: - - If significant updates are needed: Output the COMPLETE updated '"$DOC_FILE"' content immediately, with no preamble - - If no significant updates needed: Output exactly: NO_UPDATES_NEEDED + 5. **OUTPUT FORMAT**: + - Output ONLY the complete updated documentation content + - NO preambles, acknowledgments, or explanatory text + - NO phrases like "Here's the updated", "I've analyzed", etc. + - Start directly with the documentation content - Start your response immediately with either the updated documentation content or "NO_UPDATES_NEEDED". Nothing else. + 6. **SIGNIFICANCE CHECK**: + - Only update for significant changes: new features, major fixes, API changes, new dependencies, workflow changes + - Skip minor changes: typos, code formatting, small refactors, comment updates + + ## Decision Process: + + 1. Verify this PR is for the project "'"$PROJECT_TITLE"'" in repository "'"$REPO_NAME"'" + 2. Analyze what changed in the PR diff + 3. Determine if those changes require documentation updates + 4. If yes: Add new information to relevant sections WITHOUT removing existing content + 5. If no: Output exactly "NO_UPDATES_NEEDED" + + ## Response: + - If significant, relevant updates are needed: Output the COMPLETE updated documentation (with additions only) + - If no updates needed OR project mismatch detected: Output exactly: NO_UPDATES_NEEDED + + Begin your response now: EOF chmod 600 "$PROMPT_FILE" @@ -341,7 +397,7 @@ runs: # Create secure request body file REQUEST_FILE="$TEMP_DIR/request_$(openssl rand -hex 8).json" jq -n \ - --arg model "claude-3-7-sonnet-latest" \ + --arg model "claude-sonnet-4-5-latest" \ --argjson max_tokens 8192 \ --arg content "$FULL_PROMPT" \ '{ @@ -403,13 +459,72 @@ runs: if echo "$CLAUDE_OUTPUT" | grep -q "NO_UPDATES_NEEDED"; then echo " â„šī¸ No updates needed for $DOC_FILE" >> $GITHUB_STEP_SUMMARY return 0 - else - echo " ✅ Updating $DOC_FILE" >> $GITHUB_STEP_SUMMARY - # Write the updated documentation - echo "$CLAUDE_OUTPUT" > "$DOC_FILE" - ANY_UPDATES=true - return 0 fi + + # ===== HALLUCINATION PREVENTION VALIDATION ===== + echo " 🔍 Validating output for hallucination..." >> $GITHUB_STEP_SUMMARY + + # Validation 1: Check project title is preserved + if [ "$PROJECT_TITLE" != "Unknown Project" ]; then + PROJECT_NAME_CORE=$(echo "$PROJECT_TITLE" | cut -d: -f1 | cut -d- -f1 | xargs) + if ! echo "$CLAUDE_OUTPUT" | head -n 50 | grep -qi "$PROJECT_NAME_CORE"; then + echo " ❌ VALIDATION FAILED: Project title '$PROJECT_TITLE' not found in output" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Possible hallucination detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + fi + + # Validation 2: Check output length isn't drastically different (hallucination indicator) + INPUT_LENGTH=${#DOC_CONTENT} + OUTPUT_LENGTH=${#CLAUDE_OUTPUT} + + # Allow output to be 50%-200% of input length (prevent complete rewrites) + MIN_LENGTH=$((INPUT_LENGTH * 50 / 100)) + MAX_LENGTH=$((INPUT_LENGTH * 200 / 100)) + + if [ $OUTPUT_LENGTH -lt $MIN_LENGTH ]; then + echo " ❌ VALIDATION FAILED: Output too short ($OUTPUT_LENGTH vs $INPUT_LENGTH chars)" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Possible content removal detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + if [ $OUTPUT_LENGTH -gt $MAX_LENGTH ]; then + echo " ❌ VALIDATION FAILED: Output too long ($OUTPUT_LENGTH vs $INPUT_LENGTH chars)" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Possible hallucination detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Validation 3: Check that first heading is preserved + ORIGINAL_HEADING=$(echo "$DOC_CONTENT" | grep -E "^#\s+" | head -n 1 | sed 's/^#*\s*//') + NEW_HEADING=$(echo "$CLAUDE_OUTPUT" | grep -E "^#\s+" | head -n 1 | sed 's/^#*\s*//') + + if [ -n "$ORIGINAL_HEADING" ] && [ "$ORIGINAL_HEADING" != "$NEW_HEADING" ]; then + echo " âš ī¸ WARNING: First heading changed from '$ORIGINAL_HEADING' to '$NEW_HEADING'" >> $GITHUB_STEP_SUMMARY + echo " ❌ VALIDATION FAILED: Project heading must not change - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Validation 4: Check key structural elements are preserved + ORIGINAL_HEADING_COUNT=$(echo "$DOC_CONTENT" | grep -cE "^##\s+" || echo "0") + NEW_HEADING_COUNT=$(echo "$CLAUDE_OUTPUT" | grep -cE "^##\s+" || echo "0") + + # Allow Âą3 heading difference (for additions, not wholesale changes) + HEADING_DIFF=$((NEW_HEADING_COUNT - ORIGINAL_HEADING_COUNT)) + if [ $HEADING_DIFF -lt -3 ]; then + echo " ❌ VALIDATION FAILED: Too many sections removed ($HEADING_DIFF headings)" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Content removal detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # All validations passed + echo " ✅ Validation passed - updating $DOC_FILE" >> $GITHUB_STEP_SUMMARY + echo " 📊 Length: $INPUT_LENGTH → $OUTPUT_LENGTH chars ($(( (OUTPUT_LENGTH * 100) / INPUT_LENGTH ))%)" >> $GITHUB_STEP_SUMMARY + echo " 📊 Sections: $ORIGINAL_HEADING_COUNT → $NEW_HEADING_COUNT headings" >> $GITHUB_STEP_SUMMARY + + # Write the updated documentation + echo "$CLAUDE_OUTPUT" > "$DOC_FILE" + ANY_UPDATES=true + return 0 } # === PHASE 2: UPDATE ALL DETECTED DOCUMENTATION FILES === diff --git a/.github/actions/docsync_ai_comment/action.yml b/.github/actions/docsync_ai_comment/action.yml index 1fb2620..8fdb66a 100644 --- a/.github/actions/docsync_ai_comment/action.yml +++ b/.github/actions/docsync_ai_comment/action.yml @@ -187,7 +187,7 @@ runs: ref: ${{ steps.verify-pr.outputs.pr_branch }} fetch-depth: 0 - - name: 🤖 Run Claude 3.7 Sonnet Documentation Analysis + - name: 🤖 Run Claude Sonnet 4.5 Documentation Analysis id: claude-analysis if: steps.verify-pr.outputs.is_docsync_pr == 'true' shell: bash @@ -199,8 +199,8 @@ runs: run: | set -e - echo "## 🤖 Claude 3.7 Sonnet Analysis" >> $GITHUB_STEP_SUMMARY - echo "Model: claude-3-7-sonnet-latest" >> $GITHUB_STEP_SUMMARY + echo "## 🤖 Claude Sonnet 4.5 Analysis" >> $GITHUB_STEP_SUMMARY + echo "Model: claude-sonnet-4-5-latest" >> $GITHUB_STEP_SUMMARY # Create secure temporary directory TEMP_DIR=$(mktemp -d -t docsync-comment-XXXXXXXXXX) @@ -289,6 +289,17 @@ runs: # Read current documentation DOC_CONTENT=$(cat "$DOC_FILE") + # Extract project title/name for validation (from first heading) + PROJECT_TITLE=$(echo "$DOC_CONTENT" | head -n 30 | grep -E "^#\s+" | head -n 1 | sed 's/^#*\s*//' | tr -d '\000-\037' | head -c 200) + if [ -z "$PROJECT_TITLE" ]; then + PROJECT_TITLE="Unknown Project" + fi + + # Extract key terms from existing documentation for validation + KEY_TERMS=$(echo "$DOC_CONTENT" | head -n 100 | grep -oE '\b[A-Z][A-Za-z0-9]{3,}\b' | sort -u | head -n 20 | tr '\n' ',' | sed 's/,$//') + + echo " 📌 Project: $PROJECT_TITLE" >> $GITHUB_STEP_SUMMARY + # Validate content size CONTENT_SIZE=${#DOC_CONTENT} MAX_CONTENT_SIZE=524288 # 512KB @@ -311,36 +322,74 @@ runs: cat > "$PROMPT_FILE" << 'EOF' You are a technical documentation expert. A user has provided a suggestion for improving the documentation in a DocSync PR. - # Context + ## IMPORTANT: PROJECT IDENTITY + This documentation is for: '"$PROJECT_TITLE"' Repository: '"$REPO_NAME"' + + **CRITICAL**: You MUST maintain the project identity. This is NOT a different project. Do NOT replace this documentation with content about any other project, library, or tool. + + ## Context PR #'"$PR_NUMBER"' (DocSync Documentation PR) - # Current '"$DOC_FILE"': + ## Current Documentation Content: + ``` '"$DOC_CONTENT"' + ``` - # User Suggestion: + ## User Suggestion: + ``` '"$USER_SUGGESTION_CLEAN"' + ``` - # Format Requirements: + ## Format Requirements: '"$FORMAT_INSTRUCTION"' - # Documentation Context: + ## Additional Context: '"$OTHER_FILES_NOTE"' - # Your Task - Apply the user'\''s suggestion to update the '"$DOC_FILE"'. + ## Your Task + Apply the user'\''s suggestion to update the '"$DOC_FILE"' while maintaining project identity and existing content. + + ## ABSOLUTE REQUIREMENTS - VIOLATION WILL BE REJECTED: + + 1. **PRESERVE PROJECT IDENTITY**: + - The project name "'"$PROJECT_TITLE"'" MUST remain unchanged + - Do NOT replace this with documentation for a different project + - Maintain all existing project-specific information + + 2. **ONLY ADD OR REFINE, NEVER REMOVE**: + - Apply the user'\''s suggestion by adding or refining content + - NEVER remove existing sections, features, or content unless explicitly requested + - NEVER replace existing content with unrelated information + - Keep all existing headings, sections, and structure intact - ## CRITICAL RULES - MUST FOLLOW: - 1. **Direct output only**: Output ONLY the updated documentation content. NO acknowledgments like "I will apply", "I understand", "Here'\''s the updated", or any explanatory text. - 2. **Maintain format**: Keep the exact same documentation style and structure as the current file. - 3. **Follow user intent**: Carefully interpret and apply the user'\''s suggestion. If the suggestion is vague, make reasonable improvements. - 4. **Update relevant sections**: Only modify sections relevant to the suggestion. Keep everything else exactly as is. - 5. **Complete file output**: Provide the COMPLETE updated file content, not just the changed sections. - 6. **No meta-commentary**: No phrases like "I'\''ve updated", "Changes made", or section summaries. - 7. **Quality standards**: Ensure updates are accurate, clear, and improve documentation quality. + 3. **FOLLOW USER INTENT**: + - Carefully interpret and apply the user'\''s suggestion + - If the suggestion is about adding content, add it to the appropriate section + - If it'\''s about improving clarity, refine the existing text + - If it'\''s vague, make conservative, relevant improvements - Start your response immediately with the complete updated '"$DOC_FILE"' content. Nothing else. - 'EOF' + 4. **VERIFY PROJECT MATCH**: + - Ensure any additions are relevant to "'"$PROJECT_TITLE"'" in "'"$REPO_NAME"'" + - Do NOT add information about unrelated projects or technologies + - Maintain consistency with existing content + + 5. **OUTPUT FORMAT**: + - Output ONLY the complete updated documentation content + - NO preambles, acknowledgments, or explanatory text + - NO phrases like "Here'\''s the updated", "I'\''ve applied", etc. + - Start directly with the documentation content + + 6. **QUALITY STANDARDS**: + - Ensure updates are accurate and clear + - Maintain consistent formatting and style + - Improve documentation quality based on the suggestion + + ## Response: + Output the COMPLETE updated documentation with the user'\''s suggestion applied. + + Begin your response now: + EOF chmod 600 "$PROMPT_FILE" FULL_PROMPT=$(cat "$PROMPT_FILE") @@ -351,7 +400,7 @@ runs: # Create secure request body file REQUEST_FILE="$TEMP_DIR/request_$(openssl rand -hex 8).json" jq -n \ - --arg model "claude-3-7-sonnet-latest" \ + --arg model "claude-sonnet-4-5-latest" \ --argjson max_tokens 8192 \ --arg content "$FULL_PROMPT" \ '{ @@ -409,7 +458,66 @@ runs: # Sanitize output (remove null bytes, validate UTF-8) CLAUDE_OUTPUT=$(echo "$CLAUDE_OUTPUT" | tr -d '\000' | iconv -c -t UTF-8//IGNORE) - echo " ✅ Updating $DOC_FILE" >> $GITHUB_STEP_SUMMARY + # ===== HALLUCINATION PREVENTION VALIDATION ===== + echo " 🔍 Validating output for hallucination..." >> $GITHUB_STEP_SUMMARY + + # Validation 1: Check project title is preserved + if [ "$PROJECT_TITLE" != "Unknown Project" ]; then + PROJECT_NAME_CORE=$(echo "$PROJECT_TITLE" | cut -d: -f1 | cut -d- -f1 | xargs) + if ! echo "$CLAUDE_OUTPUT" | head -n 50 | grep -qi "$PROJECT_NAME_CORE"; then + echo " ❌ VALIDATION FAILED: Project title '$PROJECT_TITLE' not found in output" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Possible hallucination detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + fi + + # Validation 2: Check output length isn't drastically different (hallucination indicator) + INPUT_LENGTH=${#DOC_CONTENT} + OUTPUT_LENGTH=${#CLAUDE_OUTPUT} + + # Allow output to be 50%-200% of input length (prevent complete rewrites) + MIN_LENGTH=$((INPUT_LENGTH * 50 / 100)) + MAX_LENGTH=$((INPUT_LENGTH * 200 / 100)) + + if [ $OUTPUT_LENGTH -lt $MIN_LENGTH ]; then + echo " ❌ VALIDATION FAILED: Output too short ($OUTPUT_LENGTH vs $INPUT_LENGTH chars)" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Possible content removal detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + if [ $OUTPUT_LENGTH -gt $MAX_LENGTH ]; then + echo " ❌ VALIDATION FAILED: Output too long ($OUTPUT_LENGTH vs $INPUT_LENGTH chars)" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Possible hallucination detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Validation 3: Check that first heading is preserved + ORIGINAL_HEADING=$(echo "$DOC_CONTENT" | grep -E "^#\s+" | head -n 1 | sed 's/^#*\s*//') + NEW_HEADING=$(echo "$CLAUDE_OUTPUT" | grep -E "^#\s+" | head -n 1 | sed 's/^#*\s*//') + + if [ -n "$ORIGINAL_HEADING" ] && [ "$ORIGINAL_HEADING" != "$NEW_HEADING" ]; then + echo " âš ī¸ WARNING: First heading changed from '$ORIGINAL_HEADING' to '$NEW_HEADING'" >> $GITHUB_STEP_SUMMARY + echo " ❌ VALIDATION FAILED: Project heading must not change - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # Validation 4: Check key structural elements are preserved + ORIGINAL_HEADING_COUNT=$(echo "$DOC_CONTENT" | grep -cE "^##\s+" || echo "0") + NEW_HEADING_COUNT=$(echo "$CLAUDE_OUTPUT" | grep -cE "^##\s+" || echo "0") + + # Allow Âą3 heading difference (for additions, not wholesale changes) + HEADING_DIFF=$((NEW_HEADING_COUNT - ORIGINAL_HEADING_COUNT)) + if [ $HEADING_DIFF -lt -3 ]; then + echo " ❌ VALIDATION FAILED: Too many sections removed ($HEADING_DIFF headings)" >> $GITHUB_STEP_SUMMARY + echo " âš ī¸ Content removal detected - rejecting update" >> $GITHUB_STEP_SUMMARY + return 1 + fi + + # All validations passed + echo " ✅ Validation passed - updating $DOC_FILE" >> $GITHUB_STEP_SUMMARY + echo " 📊 Length: $INPUT_LENGTH → $OUTPUT_LENGTH chars ($(( (OUTPUT_LENGTH * 100) / INPUT_LENGTH ))%)" >> $GITHUB_STEP_SUMMARY + echo " 📊 Sections: $ORIGINAL_HEADING_COUNT → $NEW_HEADING_COUNT headings" >> $GITHUB_STEP_SUMMARY + # Write the updated documentation echo "$CLAUDE_OUTPUT" > "$DOC_FILE" ANY_UPDATES=true From 45226e4d4be74db2376736747384719f8ec5433b Mon Sep 17 00:00:00 2001 From: akash-deriv Date: Tue, 10 Feb 2026 10:13:52 +0400 Subject: [PATCH 4/4] workflow update final --- .github/actions/docsync_ai/action.yml | 27 +++++++++++++++++++++++---- .github/workflows/docsync-ai.yml | 7 +++++-- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.github/actions/docsync_ai/action.yml b/.github/actions/docsync_ai/action.yml index d08291e..f3a85aa 100644 --- a/.github/actions/docsync_ai/action.yml +++ b/.github/actions/docsync_ai/action.yml @@ -112,6 +112,14 @@ runs: # Sanitize and validate diff content PR_DIFF=$(echo "$PR_DIFF" | tr -d '\000' | iconv -c -t UTF-8//IGNORE) + # Filter out .github/workflows diffs (don't include workflow changes in analysis) + PR_DIFF=$(echo "$PR_DIFF" | awk ' + BEGIN { skip=0 } + /^diff --git.*\.github\/workflows\// { skip=1; next } + /^diff --git/ { skip=0 } + !skip { print } + ') + # Save PR diff to secure temp file PR_DIFF_FILE="$TEMP_DIR/pr_diff.txt" echo "$PR_DIFF" > "$PR_DIFF_FILE" @@ -123,20 +131,31 @@ runs: "/repos/$REPO_NAME/pulls/$PR_NUMBER/files" \ --jq '.[].filename' 2>&1 || echo "") + # Filter out .github/workflows files (don't document workflow changes) + CHANGED_FILES_FILTERED=$(echo "$CHANGED_FILES" | grep -v '^\.github/workflows/' || echo "") + + # Check if all changes were in workflows (if so, skip documentation) + if [ -n "$CHANGED_FILES" ] && [ -z "$CHANGED_FILES_FILTERED" ]; then + echo "â­ī¸ All changes are in .github/workflows - skipping documentation update" >> $GITHUB_STEP_SUMMARY + echo "skip_analysis=true" >> $GITHUB_OUTPUT + exit 0 + fi + # Sanitize changed files list (truncate for summary) - CHANGED_FILES_SUMMARY=$(echo "$CHANGED_FILES" | head -n 50) + CHANGED_FILES_SUMMARY=$(echo "$CHANGED_FILES_FILTERED" | head -n 50) - echo "### Changed Files:" >> $GITHUB_STEP_SUMMARY + echo "### Changed Files (excluding workflows):" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "$CHANGED_FILES_SUMMARY" >> $GITHUB_STEP_SUMMARY echo '```' >> $GITHUB_STEP_SUMMARY echo "changed_files<> $GITHUB_OUTPUT - echo "$CHANGED_FILES" >> $GITHUB_OUTPUT + echo "$CHANGED_FILES_FILTERED" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: 🤖 Run Claude Sonnet 4.5 Documentation Analysis id: claude-analysis + if: steps.pr-details.outputs.skip_analysis != 'true' shell: bash env: ANTHROPIC_API_KEY: ${{ inputs.anthropic_api_key }} @@ -742,7 +761,7 @@ runs: echo "$PR_URL" >> $GITHUB_STEP_SUMMARY - name: ✅ No Updates Needed - if: steps.claude-analysis.outputs.skip_analysis == 'true' || steps.check-changes.outputs.has_changes == 'false' + if: steps.pr-details.outputs.skip_analysis == 'true' || steps.claude-analysis.outputs.skip_analysis == 'true' || steps.check-changes.outputs.has_changes == 'false' shell: bash run: | echo "pr_created=false" >> $GITHUB_OUTPUT diff --git a/.github/workflows/docsync-ai.yml b/.github/workflows/docsync-ai.yml index c0e4866..a21088a 100644 --- a/.github/workflows/docsync-ai.yml +++ b/.github/workflows/docsync-ai.yml @@ -58,8 +58,11 @@ jobs: name: 📚 Sync Documentation runs-on: ubuntu-latest - # Only run when a PR is merged to the base branch - if: github.event.pull_request.merged == true + # Only run when a PR is merged to the base branch (but not DocSync AI's own PRs) + if: | + github.event.pull_request.merged == true && + !contains(github.event.pull_request.title, 'DocSync AI') && + !contains(github.event.pull_request.labels.*.name, 'automated') permissions: contents: write