doc rewrite 3 #34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Documentation Fixer | |
| on: | |
| issue_comment: | |
| types: [created] | |
| jobs: | |
| claude-response: | |
| runs-on: ubuntu-latest | |
| if: | | |
| github.event.issue.pull_request && | |
| contains(github.event.comment.body, '@claude') && | |
| github.event.comment.user.login != 'github-actions[bot]' | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| issues: write | |
| id-token: write | |
| actions: read | |
| steps: | |
| - name: Get PR info | |
| id: pr-info | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUMBER="${{ github.event.issue.number }}" | |
| PR_DATA=$(gh pr view $PR_NUMBER --repo ${{ github.repository }} --json headRefName,isCrossRepository) | |
| echo "number=$PR_NUMBER" >> "$GITHUB_OUTPUT" | |
| echo "branch=$(echo "$PR_DATA" | jq -r '.headRefName')" >> "$GITHUB_OUTPUT" | |
| echo "is_fork=$(echo "$PR_DATA" | jq -r '.isCrossRepository')" >> "$GITHUB_OUTPUT" | |
| - name: Post fork notice | |
| if: steps.pr-info.outputs.is_fork == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| gh pr comment ${{ steps.pr-info.outputs.number }} --repo ${{ github.repository }} \ | |
| --body "This PR is from a fork. Automated fixes cannot be pushed directly. Apply the suggested changes from the inline comments manually." | |
| - name: Checkout repository | |
| if: steps.pr-info.outputs.is_fork == 'false' | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ steps.pr-info.outputs.branch }} | |
| fetch-depth: 0 | |
| - name: Checkout system prompt repository | |
| if: steps.pr-info.outputs.is_fork == 'false' | |
| uses: actions/checkout@v4 | |
| with: | |
| repository: netwrix-eng/internal-agents | |
| token: ${{ secrets.PRIVATE_AGENTS_REPO }} | |
| path: system-prompt-repo | |
| ref: builds | |
| sparse-checkout: | | |
| engineering/technical_writing/system-prompt.md | |
| sparse-checkout-cone-mode: false | |
| - name: Read system prompt | |
| id: read-prompt | |
| if: steps.pr-info.outputs.is_fork == 'false' | |
| run: | | |
| { | |
| echo "prompt<<EOF" | |
| cat system-prompt-repo/engineering/technical_writing/system-prompt.md | |
| echo "" # Forces a newline to prevent EOF delimiter errors | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| - name: Detect command type | |
| id: cmd-type | |
| if: steps.pr-info.outputs.is_fork == 'false' | |
| run: | | |
| COMMENT="${{ github.event.comment.body }}" | |
| if echo "$COMMENT" | grep -qi 'preexisting'; then | |
| echo "is_preexisting=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "is_preexisting=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| - name: Install Vale | |
| if: steps.pr-info.outputs.is_fork == 'false' && steps.cmd-type.outputs.is_preexisting == 'false' | |
| run: | | |
| VERSION=$(curl -s "https://api.github.com/repos/errata-ai/vale/releases/latest" | jq -r '.tag_name') | |
| curl -sfL "https://github.com/errata-ai/vale/releases/download/${VERSION}/vale_${VERSION#v}_Linux_64-bit.tar.gz" \ | |
| | sudo tar -xz -C /usr/local/bin vale | |
| - name: Apply fixes | |
| if: steps.pr-info.outputs.is_fork == 'false' && steps.cmd-type.outputs.is_preexisting == 'false' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| show_full_output: true | |
| claude_args: | | |
| --model claude-sonnet-4-5-20250929 | |
| --allowedTools "Read,Write,Edit,Bash(vale:*),Bash(gh pr view:*),Bash(gh pr diff:*),Bash(git config:*),Bash(git add:*),Bash(git commit:*),Bash(git push:*),Bash(git status:*),Bash(git diff:*)" | |
| --append-system-prompt "${{ steps.read-prompt.outputs.prompt }}" | |
| - name: Record last comment ID | |
| id: pre-claude | |
| if: steps.pr-info.outputs.is_fork == 'false' && steps.cmd-type.outputs.is_preexisting == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| LAST_ID=$(gh api repos/${{ github.repository }}/issues/${{ steps.pr-info.outputs.number }}/comments \ | |
| --jq 'if length > 0 then .[-1].id else 0 end' 2>/dev/null || echo "0") | |
| echo "last_comment_id=$LAST_ID" >> "$GITHUB_OUTPUT" | |
| - name: Detect preexisting issues | |
| if: steps.pr-info.outputs.is_fork == 'false' && steps.cmd-type.outputs.is_preexisting == 'true' | |
| uses: anthropics/claude-code-action@v1 | |
| with: | |
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | |
| github_token: ${{ secrets.GITHUB_TOKEN }} | |
| show_full_output: false | |
| prompt: | | |
| Detect preexisting issues in PR ${{ steps.pr-info.outputs.number }}. | |
| Follow these steps in order: | |
| 1. Use `gh pr diff ${{ steps.pr-info.outputs.number }}` to get the diff. In the diff output, lines starting with `+` are lines added or changed by this PR. Lines starting with `-` were removed. Lines starting with a space are unchanged context lines. | |
| 2. Use `gh pr view ${{ steps.pr-info.outputs.number }}` to get the list of changed files, then read the full content of each changed markdown file. | |
| 3. Run all three review passes on the full content of each file. Report ONLY issues on lines that do NOT start with `+` in the diff — that is, lines that were not added or changed by this PR. This includes unchanged context lines (space prefix in the diff) and lines not shown in the diff at all. | |
| 4. You MUST write your results ONLY to `/tmp/preexisting-issues.md` — always, even if there are no issues. Do not post a comment. Do not write anything else. Use this exact format: | |
| ## Preexisting issues | |
| ### path/to/file.md | |
| - Line N: issue. Suggested change: '...' | |
| Or if there are no issues: | |
| ## Preexisting issues | |
| None. | |
| claude_args: | | |
| --model claude-sonnet-4-5-20250929 | |
| --allowedTools "Read,Write,Edit,Bash(gh pr view:*),Bash(gh pr diff:*)" | |
| --append-system-prompt "${{ steps.read-prompt.outputs.prompt }}" | |
| - name: Post preexisting issues comment | |
| if: steps.pr-info.outputs.is_fork == 'false' && steps.cmd-type.outputs.is_preexisting == 'true' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUMBER: ${{ steps.pr-info.outputs.number }} | |
| REPO: ${{ github.repository }} | |
| LAST_COMMENT_ID: ${{ steps.pre-claude.outputs.last_comment_id }} | |
| run: | | |
| python3 << 'PYTHON_EOF' | |
| import os | |
| import json | |
| import re | |
| import subprocess | |
| pr_number = os.environ['PR_NUMBER'] | |
| repo = os.environ['REPO'] | |
| last_comment_id = int(os.environ.get('LAST_COMMENT_ID', '0')) | |
| FOOTER = ( | |
| "\n\n---\n\n" | |
| "To apply the suggested fixes to preexisting issues, comment `@claude` on this PR followed by your instructions\n" | |
| "(`@claude fix all issues` or `@claude fix only the first issue`).\n" | |
| "Note: Automated fixes are only available for branches in this repository, not forks." | |
| ) | |
| def normalize_issues(body): | |
| """Normalize issue lines to single bullet points, same as the reviewer.""" | |
| src = body.split('\n') | |
| result = [] | |
| i = 0 | |
| while i < len(src): | |
| line = src[i] | |
| # Convert heading format: ### Line N: ... → - Line N: ... | |
| m = re.match(r'^#{1,6}\s+(Line \d+:.+)$', line) | |
| if m: | |
| result.append(f'- {m.group(1)}') | |
| i += 1 | |
| continue | |
| # Convert bold format: **Line N: title** + sub-bullets → - Line N: single line | |
| m = re.match(r'^\*\*(Line \d+:.*?)\*\*\s*$', line) | |
| if m: | |
| title = m.group(1).rstrip('.') | |
| i += 1 | |
| parts = [] | |
| while i < len(src) and re.match(r'^\s*[-*]\s+', src[i]): | |
| sub = re.sub(r'^\s*[-*]\s+', '', src[i]) | |
| sub = re.sub(r'^(Issue|Fix|Description|Suggested change):\s*', '', sub, flags=re.IGNORECASE) | |
| if sub.strip(): | |
| parts.append(sub.strip().rstrip('.')) | |
| i += 1 | |
| combined = f'- {title}. {". ".join(parts)}.' if parts else f'- {title}.' | |
| result.append(combined) | |
| continue | |
| result.append(line) | |
| i += 1 | |
| return '\n'.join(result) | |
| def normalize_body(body): | |
| """Extract the ## Preexisting issues section without fluff, normalized to match reviewer format.""" | |
| idx = body.find('## Preexisting issues') | |
| if idx == -1: | |
| return '## Preexisting issues\nNone.' | |
| body = body[idx:] | |
| # Strip any footer Claude may have appended after a --- divider | |
| footer_idx = body.find('\n---') | |
| if footer_idx != -1: | |
| body = body[:footer_idx] | |
| # Strip any prose intro between the header and first subheading/content | |
| lines = body.split('\n') | |
| result = [] | |
| past_intro = False | |
| for line in lines: | |
| s = line.strip() | |
| if s == '## Preexisting issues': | |
| result.append(line) | |
| elif not past_intro: | |
| if s.startswith('### ') or s == 'None.' or s == '': | |
| if s: | |
| past_intro = True | |
| result.append(line) | |
| # else: prose intro line — skip it | |
| else: | |
| result.append(line) | |
| while result and not result[-1].strip(): | |
| result.pop() | |
| return normalize_issues('\n'.join(result)) | |
| summary_path = '/tmp/preexisting-issues.md' | |
| if os.path.exists(summary_path): | |
| with open(summary_path) as f: | |
| clean_body = normalize_body(f.read()) + FOOTER | |
| else: | |
| clean_body = '## Preexisting issues\nNone.' + FOOTER | |
| # Find the action's auto-posted comment (ID > last recorded, posted by a bot) | |
| result = subprocess.run( | |
| ['gh', 'api', f'repos/{repo}/issues/{pr_number}/comments'], | |
| capture_output=True, text=True, check=True, | |
| ) | |
| comments = json.loads(result.stdout) | |
| new_bot_comments = [c for c in comments | |
| if c['id'] > last_comment_id | |
| and c['user']['login'].endswith('[bot]')] | |
| if new_bot_comments: | |
| # Replace the action's auto-comment in-place with our formatted output | |
| target_id = new_bot_comments[-1]['id'] | |
| subprocess.run( | |
| ['gh', 'api', f'repos/{repo}/issues/comments/{target_id}', | |
| '-X', 'PATCH', '--input', '-'], | |
| input=json.dumps({'body': clean_body}), | |
| capture_output=True, text=True, check=True, | |
| ) | |
| else: | |
| subprocess.run( | |
| ['gh', 'pr', 'comment', pr_number, '--repo', repo, '--body', clean_body], | |
| check=True, | |
| ) | |
| PYTHON_EOF |