From 259314537a6d27943296bb6d11ce208c6a3ad09c Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 27 Feb 2026 23:12:10 +0000 Subject: [PATCH] Update existing comment once user fixes issues --- .github/workflows/pr-comment.yml | 84 +++++++++++++----- lib/checks/format-comment.py | 2 + lib/checks/prepare-comment.py | 143 +++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 22 deletions(-) create mode 100644 lib/checks/prepare-comment.py diff --git a/.github/workflows/pr-comment.yml b/.github/workflows/pr-comment.yml index 44e6c33..fdc5980 100644 --- a/.github/workflows/pr-comment.yml +++ b/.github/workflows/pr-comment.yml @@ -7,6 +7,7 @@ on: permissions: actions: read + contents: read pull-requests: write jobs: @@ -16,6 +17,8 @@ jobs: if: github.event.workflow_run.event == 'pull_request' steps: + - uses: actions/checkout@v4 + - name: Download PR metadata id: download continue-on-error: true @@ -26,20 +29,13 @@ jobs: run-id: ${{ github.event.workflow_run.id }} github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Post comment + - name: Resolve PR context + id: context uses: actions/github-script@v7 with: github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} script: | const fs = require('fs'); - const marker = ''; - - // Check if there are findings to post - const commentFile = 'pr-meta/comment.md'; - if (!fs.existsSync(commentFile)) { - console.log('No findings to post — skipping.'); - return; - } // Determine the PR number let prNumber; @@ -48,8 +44,6 @@ jobs: prNumber = parseInt(fs.readFileSync(numberFile, 'utf8').trim()); } if (!prNumber) { - // workflow_run.pull_requests is empty for fork PRs, so - // fall back to searching by head SHA if needed const prs = context.payload.workflow_run.pull_requests; if (prs && prs.length > 0) { prNumber = prs[0].number; @@ -72,23 +66,69 @@ jobs: } } - // Skip if we already commented on this PR + // Fetch existing bot comment (if any) and write to file for Python + const marker = ''; const { data: comments } = await github.rest.issues.listComments({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, per_page: 100, }); - if (comments.some(c => c.body.includes(marker))) { - console.log('Bot comment already exists — skipping.'); + const existing = comments.find(c => c.body.includes(marker)); + if (existing) { + fs.mkdirSync('pr-meta', { recursive: true }); + fs.writeFileSync('pr-meta/existing-comment.md', existing.body); + fs.writeFileSync('pr-meta/existing-comment-id.txt', String(existing.id)); + } + + core.setOutput('pr_number', prNumber); + + - name: Prepare comment + if: steps.context.outputs.pr_number + env: + CHECK_RUN_ID: ${{ github.event.workflow_run.id }} + GITHUB_REPOSITORY: ${{ github.repository }} + run: python lib/checks/prepare-comment.py + + - name: Post or update comment + if: steps.context.outputs.pr_number + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.BOT_TOKEN || secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs'); + const actionFile = 'pr-meta/action.txt'; + if (!fs.existsSync(actionFile)) return; + + const action = fs.readFileSync(actionFile, 'utf8').trim(); + if (action === 'skip') { + console.log('Nothing to do — skipping.'); return; } - // Post the comment - const body = fs.readFileSync(commentFile, 'utf8').trim(); - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: prNumber, - body, - }); + const bodyFile = 'pr-meta/final-comment.md'; + if (!fs.existsSync(bodyFile)) { + console.log('No comment body found — skipping.'); + return; + } + const body = fs.readFileSync(bodyFile, 'utf8').trim(); + const prNumber = parseInt('${{ steps.context.outputs.pr_number }}'); + + if (action === 'create') { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body, + }); + console.log('Created bot comment.'); + } else if (action === 'update') { + const commentId = parseInt(fs.readFileSync('pr-meta/existing-comment-id.txt', 'utf8').trim()); + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: commentId, + body, + }); + console.log('Updated bot comment.'); + } diff --git a/lib/checks/format-comment.py b/lib/checks/format-comment.py index ca00a47..64b47d6 100644 --- a/lib/checks/format-comment.py +++ b/lib/checks/format-comment.py @@ -108,6 +108,8 @@ def main(): f.write(run_id) findings = collect_findings() + with open(os.path.join(OUTPUT_DIR, "findings-count.txt"), "w") as f: + f.write(str(len(findings))) changes_summary = load_diff_summary() write_step_summary(findings) diff --git a/lib/checks/prepare-comment.py b/lib/checks/prepare-comment.py new file mode 100644 index 0000000..56d744b --- /dev/null +++ b/lib/checks/prepare-comment.py @@ -0,0 +1,143 @@ +"""Decides whether to create, update, or skip the PR bot comment. + +Reads: + pr-meta/comment.md — new comment from format-comment.py + pr-meta/findings-count.txt — number of findings (from format-comment.py) + pr-meta/existing-comment.md — current bot comment on the PR (from workflow) + +Writes: + pr-meta/action.txt — "create", "update", or "skip" + pr-meta/final-comment.md — the body to post or update with +""" + +import os +import re + +WORK_DIR = "pr-meta" + + +def read_file(path): + """Read a file and return its stripped content, or None if missing/empty.""" + try: + with open(path) as f: + content = f.read().strip() + return content if content else None + except Exception: + return None + + +def read_findings_count(new_body): + """Return the findings count from findings-count.txt, or by counting bullets.""" + raw = read_file(os.path.join(WORK_DIR, "findings-count.txt")) + if raw is not None: + try: + return int(raw) + except ValueError: + pass + # Fallback: count bullet lines before
(diff summary has its own bullets) + body_before_details = new_body.split("
")[0] + return len(re.findall(r"^- .+$", body_before_details, re.MULTILINE)) + + +def _was_already_passing(existing_body): + """Check if the most recent state in the comment is already all-clear.""" + # If there's a previous "all passing" edit, the last state was passing + if re.search(r"^Edit(?: \d+)?: All checks are (now passing|passing now)", existing_body, re.MULTILINE): + return True + # If there are no edits at all, check the original comment body + if not re.search(r"^Edit(?: \d+)?:", existing_body, re.MULTILINE): + return "All our automated checks have passed" in existing_body + return False + + +def _previous_failing_count(existing_body): + """Extract the findings count from the most recent state in the comment.""" + # Check edit lines first (most recent state) + matches = re.findall(r"^Edit(?: \d+)?: (\d+) checks? (?:is|are) still failing", existing_body, re.MULTILINE) + if matches: + return int(matches[-1]) + # No edits — count bullets in the original comment (before
) + if not re.search(r"^Edit(?: \d+)?:", existing_body, re.MULTILINE): + body_before_details = existing_body.split("
")[0] + bullets = re.findall(r"^- .+$", body_before_details, re.MULTILINE) + return len(bullets) if bullets else None + return None + + +def build_edit_line(existing_body, findings_count, check_run_id, repo): + """Build the edit line to append, or None if nothing to do.""" + run_tag = f"" + + # Idempotency: this run was already processed + if run_tag in existing_body: + return None + + # Skip if the state hasn't changed + if findings_count == 0 and _was_already_passing(existing_body): + return None + if findings_count > 0 and _previous_failing_count(existing_body) == findings_count: + return None + + # Count previous edits to determine the next number + edits = re.findall(r"^Edit(?: \d+)?:", existing_body, re.MULTILINE) + edit_count = len(edits) + next_edit = edit_count + 1 + + run_url = f"https://github.com/{repo}/actions/runs/{check_run_id}" + + if findings_count == 0: + if edit_count == 0: + return f"Edit: All checks are now passing \U0001f389 {run_tag}" + return f"Edit {next_edit}: All checks are passing now \u2705 {run_tag}" + + verb = "check is" if findings_count == 1 else "checks are" + return ( + f"Edit {next_edit}: {findings_count} {verb} still failing, " + f"see [here]({run_url}) for details {run_tag}" + ) + + +def write_output(action, body=""): + """Write action.txt and (optionally) final-comment.md.""" + os.makedirs(WORK_DIR, exist_ok=True) + with open(os.path.join(WORK_DIR, "action.txt"), "w") as f: + f.write(action) + if body: + with open(os.path.join(WORK_DIR, "final-comment.md"), "w") as f: + f.write(body) + + +def main(): + check_run_id = os.environ.get("CHECK_RUN_ID", "") + repo = os.environ.get("GITHUB_REPOSITORY", "") + + new_body = read_file(os.path.join(WORK_DIR, "comment.md")) + if not new_body: + write_output("skip") + return + + existing_body = read_file(os.path.join(WORK_DIR, "existing-comment.md")) + + # No existing comment — create a new one + if not existing_body: + write_output("create", new_body) + return + + # Existing comment — build an edit line to append + if not check_run_id: + write_output("skip") + return + + findings_count = read_findings_count(new_body) + edit_line = build_edit_line(existing_body, findings_count, check_run_id, repo) + + if not edit_line: + write_output("skip") + return + + updated = existing_body.rstrip() + "\n\n" + edit_line + write_output("update", updated) + + +if __name__ == "__main__": + main()