Skip to content

Commit c86fc2e

Browse files
authored
Merge pull request #418 from igerber/fix-rerun-on-push
Create new comment on synchronize/reopened (not update canonical)
2 parents fe8e717 + 702e951 commit c86fc2e

2 files changed

Lines changed: 98 additions & 4 deletions

File tree

.github/workflows/ai_pr_review.yml

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,12 @@ jobs:
113113
PR_BODY: ${{ steps.pr.outputs.body }}
114114
BASE_SHA: ${{ steps.pr.outputs.base_sha }}
115115
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
116-
IS_RERUN: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' }}
116+
# IS_RERUN must agree with the post-comment step's `isRerun` JS
117+
# expression below: any non-`opened` pull_request event is also a
118+
# rerun. Otherwise synchronize/reopened pushes get a new comment
119+
# but the prompt omits <previous-ai-review-output>, and the model
120+
# cannot focus on whether prior findings were addressed.
121+
IS_RERUN: ${{ github.event_name == 'issue_comment' || github.event_name == 'pull_request_review_comment' || (github.event_name == 'pull_request' && github.event.action != 'opened') }}
117122
PREV_REVIEW: ${{ steps.prev_review.outputs.body }}
118123
PREV_REVIEW_FOUND: ${{ steps.prev_review.outputs.found }}
119124
run: |
@@ -213,7 +218,7 @@ jobs:
213218
model: gpt-5.4
214219
effort: xhigh
215220

216-
- name: Post PR comment (new on /ai-review, update on opened)
221+
- name: Post PR comment (new on every event except initial open)
217222
uses: actions/github-script@v9
218223
env:
219224
CODEX_FINAL_MESSAGE: ${{ steps.run_codex.outputs.final-message }}
@@ -227,10 +232,19 @@ jobs:
227232
const { owner, repo } = context.repo;
228233
const issue_number = Number(process.env.PR_NUMBER);
229234
230-
// If this run was triggered by /ai-review (issue_comment or review_comment), create a NEW comment.
235+
// Treat anything other than the PR's initial `opened` event as a
236+
// rerun for comment-posting purposes:
237+
// - issue_comment / pull_request_review_comment: explicit /ai-review trigger
238+
// - pull_request.synchronize: push to PR branch
239+
// - pull_request.reopened: PR was reopened
240+
// Only `pull_request.opened` updates the canonical comment in-place
241+
// (creating it if absent). All other events create a new comment so
242+
// that prior review history is preserved.
231243
const isRerun =
232244
context.eventName === "issue_comment" ||
233-
context.eventName === "pull_request_review_comment";
245+
context.eventName === "pull_request_review_comment" ||
246+
(context.eventName === "pull_request" &&
247+
context.payload.action !== "opened");
234248
235249
// Marker for the "canonical" auto comment
236250
const marker = "<!-- ai-pr-review:codex:auto -->";

tests/test_openai_review.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1755,6 +1755,86 @@ def test_workflow_sanitizes_pr_body_closing_tag(self):
17551755
assert "&lt;/previous-ai-review-output&gt;" in text
17561756

17571757

1758+
class TestWorkflowCommentPosting:
1759+
"""The workflow has TWO rerun-detection gates that must agree:
1760+
1. YAML `IS_RERUN` env in the prompt-build step — controls whether
1761+
the prompt includes the <previous-ai-review-output> block and
1762+
re-review framing.
1763+
2. JS `isRerun` in the post-comment step — controls whether the
1764+
comment is created fresh or updates the canonical comment.
1765+
1766+
If they disagree, you get nonsense states like "new comment posted but
1767+
prompt didn't see prior review" (synchronize bug pre-fix) or "canonical
1768+
comment updated but prompt was framed as rerun" (the inverse).
1769+
1770+
The contract: `pull_request.opened` is non-rerun; everything else
1771+
(`pull_request.synchronize`, `pull_request.reopened`, `issue_comment`,
1772+
`pull_request_review_comment`) is a rerun."""
1773+
1774+
@pytest.fixture
1775+
def workflow_text(self):
1776+
assert _SCRIPT_PATH is not None
1777+
repo_root = _SCRIPT_PATH.parent.parent.parent
1778+
wf = repo_root / ".github" / "workflows" / "ai_pr_review.yml"
1779+
if not wf.exists():
1780+
pytest.skip("workflow not found")
1781+
return wf.read_text()
1782+
1783+
# --- Post-comment JS gate ---
1784+
1785+
def test_js_isrerun_includes_non_opened_pull_request(self, workflow_text):
1786+
"""JS gate: non-opened pull_request events create a new comment."""
1787+
assert 'context.payload.action !== "opened"' in workflow_text, (
1788+
"post-comment isRerun must treat pull_request events other than "
1789+
"'opened' as reruns; otherwise synchronize/reopened overwrite the "
1790+
"canonical review comment and lose prior content."
1791+
)
1792+
1793+
def test_js_isrerun_still_includes_comment_events(self, workflow_text):
1794+
assert 'context.eventName === "issue_comment"' in workflow_text
1795+
assert 'context.eventName === "pull_request_review_comment"' in workflow_text
1796+
1797+
# --- YAML IS_RERUN gate ---
1798+
1799+
def test_yaml_isrerun_includes_non_opened_pull_request(self, workflow_text):
1800+
"""YAML gate: non-opened pull_request events make the prompt include
1801+
the previous-review block. Must agree with the JS gate above."""
1802+
assert (
1803+
"github.event_name == 'pull_request' && github.event.action != 'opened'"
1804+
in workflow_text
1805+
), (
1806+
"prompt-build IS_RERUN must include non-opened pull_request events "
1807+
"alongside comment triggers; otherwise synchronize/reopened pushes "
1808+
"create a new comment but the prompt omits <previous-ai-review-output>."
1809+
)
1810+
1811+
def test_yaml_isrerun_still_includes_comment_events(self, workflow_text):
1812+
assert "github.event_name == 'issue_comment'" in workflow_text
1813+
assert "github.event_name == 'pull_request_review_comment'" in workflow_text
1814+
1815+
# --- Parity contract: both gates must enumerate the same trigger set ---
1816+
1817+
def test_both_gates_enumerate_same_triggers(self, workflow_text):
1818+
"""Whatever the gates use to express the rerun set, both must mention
1819+
each of the four rerun-trigger names so they cannot silently disagree.
1820+
1821+
This is a string-presence check (not a true semantic equality), but it
1822+
catches the realistic regression: someone editing one gate and
1823+
forgetting the other."""
1824+
rerun_signals = [
1825+
"issue_comment",
1826+
"pull_request_review_comment",
1827+
# The synchronize/reopened branch is expressed via action != opened
1828+
# in both gates, so we anchor on the action-comparison strings:
1829+
"github.event.action != 'opened'", # YAML
1830+
'context.payload.action !== "opened"', # JS
1831+
]
1832+
for signal in rerun_signals:
1833+
assert signal in workflow_text, (
1834+
f"Expected rerun-set signal {signal!r} not found in workflow YAML"
1835+
)
1836+
1837+
17581838
class TestExtractResponseText:
17591839
def test_prefers_output_text_field(self, review_mod):
17601840
result = {"output_text": "Direct text.", "output": []}

0 commit comments

Comments
 (0)