From ecb94f53b883b4b1a624af481920c6d4f1534852 Mon Sep 17 00:00:00 2001 From: james-haytko_nwx Date: Fri, 27 Feb 2026 15:28:05 -0600 Subject: [PATCH 1/2] Deterministic preexisting issues comment using last comment ID Record the last PR comment ID before Claude runs. After Claude runs, any comment with a higher ID was posted by the action in this run. Replace that comment with the normalized content + hardcoded footer. This eliminates all content-based guesswork about which comment to patch. Generated with AI Co-Authored-By: Claude Code --- .../workflows/claude-documentation-fixer.yml | 64 +++++++++++-------- 1 file changed, 36 insertions(+), 28 deletions(-) diff --git a/.github/workflows/claude-documentation-fixer.yml b/.github/workflows/claude-documentation-fixer.yml index 97974e17bb..460ca8e2cc 100644 --- a/.github/workflows/claude-documentation-fixer.yml +++ b/.github/workflows/claude-documentation-fixer.yml @@ -74,6 +74,16 @@ jobs: 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: Record last comment ID + id: pre-claude + if: steps.pr-info.outputs.is_fork == 'false' + 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: Apply fixes if: steps.pr-info.outputs.is_fork == 'false' uses: anthropics/claude-code-action@v1 @@ -92,6 +102,7 @@ jobs: 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 @@ -100,6 +111,7 @@ jobs: 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" @@ -108,8 +120,6 @@ jobs: "Note: Automated fixes are only available for branches in this repository, not forks." ) - FOOTER_MARKER = "Automated fixes are only available" - def normalize_body(body): """Keep only the expected structured content from the preexisting issues output.""" idx = body.find('## Preexisting issues') @@ -129,38 +139,36 @@ jobs: clean_lines.pop() return '\n'.join(clean_lines) - def patch_comment(comment_id, new_body): - payload = {'body': new_body} + # Only run for @claude detect preexisting issues + summary_path = '/tmp/preexisting-issues.md' + if not os.path.exists(summary_path): + exit(0) + + with open(summary_path) as f: + clean_body = normalize_body(f.read()) + FOOTER + + # Find all comments posted during this workflow run (ID > last recorded ID) + 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_comments = [c for c in comments if c['id'] > last_comment_id] + + if new_comments: + # Replace the most recent new comment (the action's auto-posted output) + target_id = new_comments[-1]['id'] + payload = {'body': clean_body} subprocess.run( - ['gh', 'api', f'repos/{repo}/issues/comments/{comment_id}', + ['gh', 'api', f'repos/{repo}/issues/comments/{target_id}', '-X', 'PATCH', '--input', '-'], input=json.dumps(payload), capture_output=True, text=True, check=True, ) - - summary_path = '/tmp/preexisting-issues.md' - if os.path.exists(summary_path): - # Claude wrote to the file as instructed — post a new comment with footer - with open(summary_path) as f: - body = normalize_body(f.read()) + else: + # No new comment found — post one directly subprocess.run( - ['gh', 'pr', 'comment', pr_number, '--repo', repo, '--body', body + FOOTER], + ['gh', 'pr', 'comment', pr_number, '--repo', repo, '--body', clean_body], check=True, ) - else: - # Claude posted directly — find that comment, strip any intro and notes, - # and add this exact footer: - # To apply the suggested fixes to preexisting issues, comment `@claude` on this PR followed by your instructions - # (`@claude fix all issues` or `@claude fix only the first issue`). - # Note: Automated fixes are only available for branches in this repository, not forks. - result = subprocess.run( - ['gh', 'api', f'repos/{repo}/issues/{pr_number}/comments'], - capture_output=True, text=True, check=True, - ) - comments = json.loads(result.stdout) - for comment in reversed(comments): - body = comment['body'] - if '## Preexisting issues' in body and FOOTER_MARKER not in body: - patch_comment(comment['id'], normalize_body(body) + FOOTER) - break PYTHON_EOF From e87bb7eed086bd37a4b1257accdd646e8dcd3bd9 Mon Sep 17 00:00:00 2001 From: james-haytko_nwx Date: Fri, 27 Feb 2026 15:30:23 -0600 Subject: [PATCH 2/2] Filter new comments by both ID and bot login Combining ID > last_comment_id with user.login == 'github-actions[bot]' ensures we only target the action's own comment, not any unrelated comment a user might post during the workflow run. Generated with AI Co-Authored-By: Claude Code --- .github/workflows/claude-documentation-fixer.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/claude-documentation-fixer.yml b/.github/workflows/claude-documentation-fixer.yml index 460ca8e2cc..06bfdbb6fa 100644 --- a/.github/workflows/claude-documentation-fixer.yml +++ b/.github/workflows/claude-documentation-fixer.yml @@ -153,7 +153,9 @@ jobs: capture_output=True, text=True, check=True, ) comments = json.loads(result.stdout) - new_comments = [c for c in comments if c['id'] > last_comment_id] + new_comments = [c for c in comments + if c['id'] > last_comment_id + and c['user']['login'] == 'github-actions[bot]'] if new_comments: # Replace the most recent new comment (the action's auto-posted output)