Skip to content

Commit 0d5d389

Browse files
jdclaude
andauthored
feat(stack): embed JSON payload in stack comments (#1219)
Append a `` line to every stack comment body, carrying schema_version, stack_id, and per-pull metadata (number, change_id, head_sha, base_branch, dest_branch, is_current). Thread stack_id (the local dest_branch) from stack_push down through create_or_update_comments and _update_comment_for_pull. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b751597 commit 0d5d389

2 files changed

Lines changed: 206 additions & 24 deletions

File tree

mergify_cli/stack/push.py

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,13 @@ async def stack_push(
512512
]
513513

514514
with console.status("Updating comments..."):
515-
await create_or_update_comments(client, user, repo, changes_to_comment)
515+
await create_or_update_comments(
516+
client,
517+
user,
518+
repo,
519+
changes_to_comment,
520+
stack_id=dest_branch,
521+
)
516522

517523
console.log("[green]Comments updated.[/]")
518524

@@ -556,12 +562,45 @@ class StackComment:
556562
"This pull request is part of a [Mergify stack](https://docs.mergify.com/stacks/):\n"
557563
)
558564

559-
def body(self, current_pull: github_types.PullRequest) -> str:
565+
def _json_marker(
566+
self,
567+
current_pull: github_types.PullRequest,
568+
stack_id: str,
569+
) -> str:
570+
current_number = int(current_pull["number"])
571+
payload = {
572+
"schema_version": 1,
573+
"stack_id": stack_id,
574+
"pulls": [
575+
{
576+
"number": int(change.pull["number"]),
577+
"change_id": change.id,
578+
"head_sha": change.commit_sha,
579+
"base_branch": change.base_branch,
580+
"dest_branch": change.dest_branch,
581+
"is_current": int(change.pull["number"]) == current_number,
582+
}
583+
for change in self.local_changes
584+
if change.pull is not None
585+
],
586+
}
587+
return (
588+
"<!-- mergify-stack-data: "
589+
+ json.dumps(payload, separators=(",", ":"))
590+
+ " -->"
591+
)
592+
593+
def body(
594+
self,
595+
current_pull: github_types.PullRequest,
596+
stack_id: str,
597+
) -> str:
560598
body = self.STACK_COMMENT_HEADER
561599
body += "\n"
562600
body += "| # | Pull Request | Link | |\n"
563601
body += "|--:|---|---|---|\n"
564602

603+
current_number = int(current_pull["number"])
565604
row = 0
566605
for change in self.local_changes:
567606
if change.pull is None:
@@ -570,9 +609,10 @@ def body(self, current_pull: github_types.PullRequest) -> str:
570609
pull = change.pull
571610
title = pull["title"].replace("|", "\\|")
572611
link = f"[#{pull['number']}]({pull['html_url']})"
573-
status = "👈" if pull == current_pull else ""
612+
status = "👈" if int(pull["number"]) == current_number else ""
574613
body += f"| {row} | {title} | {link} | {status} |\n"
575614

615+
body += self._json_marker(current_pull, stack_id) + "\n"
576616
return body
577617

578618
@staticmethod
@@ -861,10 +901,11 @@ async def _update_comment_for_pull(
861901
repo: str,
862902
pull: github_types.PullRequest,
863903
stack_comment: StackComment,
904+
stack_id: str,
864905
total_pulls: int,
865906
sem: asyncio.Semaphore,
866907
) -> None:
867-
new_body = stack_comment.body(pull)
908+
new_body = stack_comment.body(pull, stack_id)
868909

869910
async with sem:
870911
r = await client.get(
@@ -893,6 +934,7 @@ async def create_or_update_comments(
893934
user: str,
894935
repo: str,
895936
local_changes: list[changes.LocalChange],
937+
stack_id: str,
896938
) -> None:
897939
stack_comment = StackComment(local_changes)
898940
pulls = [c.pull for c in local_changes if c.pull is not None]
@@ -906,6 +948,7 @@ async def create_or_update_comments(
906948
repo,
907949
pull,
908950
stack_comment,
951+
stack_id,
909952
len(pulls),
910953
sem,
911954
)

mergify_cli/tests/stack/test_push.py

Lines changed: 159 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
import datetime
1818
import json
19+
import typing
1920
from typing import TYPE_CHECKING
2021
from unittest import mock
2122

@@ -260,29 +261,35 @@ async def test_stack_create(
260261

261262
# First stack comment is created
262263
assert len(post_comment1_mock.calls) == 1
263-
expected_body = """This pull request is part of a [Mergify stack](https://docs.mergify.com/stacks/):
264-
265-
| # | Pull Request | Link | |
266-
|--:|---|---|---|
267-
| 1 | Title commit 1 | [#1](https://github.com/repo/user/pull/1) | 👈 |
268-
| 2 | Title commit 2 | [#2](https://github.com/repo/user/pull/2) | |
269-
"""
270-
assert json.loads(post_comment1_mock.calls.last.request.content) == {
271-
"body": expected_body,
272-
}
264+
request_body1 = json.loads(post_comment1_mock.calls.last.request.content)["body"]
265+
assert request_body1.startswith(
266+
"This pull request is part of a [Mergify stack](https://docs.mergify.com/stacks/):\n",
267+
)
268+
assert (
269+
"| 1 | Title commit 1 | [#1](https://github.com/repo/user/pull/1) | 👈 |"
270+
in request_body1
271+
)
272+
assert (
273+
"| 2 | Title commit 2 | [#2](https://github.com/repo/user/pull/2) | |"
274+
in request_body1
275+
)
276+
assert "<!-- mergify-stack-data: " in request_body1
273277

274278
# Second stack comment is created
275279
assert len(post_comment2_mock.calls) == 1
276-
expected_body = """This pull request is part of a [Mergify stack](https://docs.mergify.com/stacks/):
277-
278-
| # | Pull Request | Link | |
279-
|--:|---|---|---|
280-
| 1 | Title commit 1 | [#1](https://github.com/repo/user/pull/1) | |
281-
| 2 | Title commit 2 | [#2](https://github.com/repo/user/pull/2) | 👈 |
282-
"""
283-
assert json.loads(post_comment2_mock.calls.last.request.content) == {
284-
"body": expected_body,
285-
}
280+
request_body2 = json.loads(post_comment2_mock.calls.last.request.content)["body"]
281+
assert request_body2.startswith(
282+
"This pull request is part of a [Mergify stack](https://docs.mergify.com/stacks/):\n",
283+
)
284+
assert (
285+
"| 1 | Title commit 1 | [#1](https://github.com/repo/user/pull/1) | |"
286+
in request_body2
287+
)
288+
assert (
289+
"| 2 | Title commit 2 | [#2](https://github.com/repo/user/pull/2) | 👈 |"
290+
in request_body2
291+
)
292+
assert "<!-- mergify-stack-data: " in request_body2
286293

287294

288295
@pytest.mark.respx(base_url="https://api.github.com/")
@@ -2297,3 +2304,135 @@ async def test_push_branches_skips_notes_when_local_ref_absent(
22972304
"commit1_sha:refs/heads/stack/title--29617d37",
22982305
"+refs/notes/mergify/stack:refs/notes/mergify/stack",
22992306
)
2307+
2308+
2309+
def _make_local_change(
2310+
*,
2311+
change_id: str,
2312+
pull_number: int,
2313+
head_sha: str,
2314+
base_branch: str,
2315+
dest_branch: str,
2316+
title: str = "t",
2317+
) -> tuple[changes.LocalChange, github_types.PullRequest]:
2318+
pull = typing.cast(
2319+
"github_types.PullRequest",
2320+
{
2321+
"number": pull_number,
2322+
"title": title,
2323+
"html_url": f"https://github.com/owner/repo/pull/{pull_number}",
2324+
"head": {"ref": dest_branch, "sha": head_sha},
2325+
"base": {"ref": base_branch},
2326+
"draft": False,
2327+
"merged_at": None,
2328+
"merge_commit_sha": None,
2329+
"body": "",
2330+
"state": "open",
2331+
},
2332+
)
2333+
change = changes.LocalChange(
2334+
id=changes.ChangeId(change_id),
2335+
pull=pull,
2336+
commit_sha=head_sha,
2337+
title=title,
2338+
message="",
2339+
base_branch=base_branch,
2340+
dest_branch=dest_branch,
2341+
action="update",
2342+
)
2343+
return change, pull
2344+
2345+
2346+
def test_stack_comment_body_contains_json_marker() -> None:
2347+
c1, c1_pull = _make_local_change(
2348+
change_id="Iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2349+
pull_number=1,
2350+
head_sha="1111111111111111111111111111111111111111",
2351+
base_branch="main",
2352+
dest_branch="jd/feature/Iaaaaaaa",
2353+
)
2354+
c2, _ = _make_local_change(
2355+
change_id="Ibbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
2356+
pull_number=2,
2357+
head_sha="2222222222222222222222222222222222222222",
2358+
base_branch="jd/feature/Iaaaaaaa",
2359+
dest_branch="jd/feature/Ibbbbbbb",
2360+
)
2361+
comment = push.StackComment([c1, c2])
2362+
body = comment.body(c1_pull, stack_id="feature")
2363+
2364+
marker_prefix = "<!-- mergify-stack-data: "
2365+
marker_lines = [
2366+
line for line in body.splitlines() if line.startswith(marker_prefix)
2367+
]
2368+
assert len(marker_lines) == 1
2369+
assert marker_lines[0].endswith(" -->")
2370+
2371+
payload = json.loads(marker_lines[0][len(marker_prefix) : -len(" -->")])
2372+
assert payload == {
2373+
"schema_version": 1,
2374+
"stack_id": "feature",
2375+
"pulls": [
2376+
{
2377+
"number": 1,
2378+
"change_id": "Iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2379+
"head_sha": "1111111111111111111111111111111111111111",
2380+
"base_branch": "main",
2381+
"dest_branch": "jd/feature/Iaaaaaaa",
2382+
"is_current": True,
2383+
},
2384+
{
2385+
"number": 2,
2386+
"change_id": "Ibbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
2387+
"head_sha": "2222222222222222222222222222222222222222",
2388+
"base_branch": "jd/feature/Iaaaaaaa",
2389+
"dest_branch": "jd/feature/Ibbbbbbb",
2390+
"is_current": False,
2391+
},
2392+
],
2393+
}
2394+
2395+
2396+
def test_stack_comment_is_current_flips_per_pull() -> None:
2397+
c1, _ = _make_local_change(
2398+
change_id="Iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2399+
pull_number=1,
2400+
head_sha="1111111111111111111111111111111111111111",
2401+
base_branch="main",
2402+
dest_branch="jd/feature/Iaaaaaaa",
2403+
)
2404+
c2, c2_pull = _make_local_change(
2405+
change_id="Ibbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
2406+
pull_number=2,
2407+
head_sha="2222222222222222222222222222222222222222",
2408+
base_branch="jd/feature/Iaaaaaaa",
2409+
dest_branch="jd/feature/Ibbbbbbb",
2410+
)
2411+
comment = push.StackComment([c1, c2])
2412+
2413+
body2 = comment.body(c2_pull, stack_id="feature")
2414+
marker_line = next(
2415+
line
2416+
for line in body2.splitlines()
2417+
if line.startswith("<!-- mergify-stack-data: ")
2418+
)
2419+
payload = json.loads(marker_line[len("<!-- mergify-stack-data: ") : -len(" -->")])
2420+
assert [p["is_current"] for p in payload["pulls"]] == [False, True]
2421+
2422+
2423+
def test_stack_comment_body_json_marker_is_single_line_compact() -> None:
2424+
c1, c1_pull = _make_local_change(
2425+
change_id="Iaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
2426+
pull_number=1,
2427+
head_sha="1111111111111111111111111111111111111111",
2428+
base_branch="main",
2429+
dest_branch="jd/feature/Iaaaaaaa",
2430+
)
2431+
comment = push.StackComment([c1])
2432+
body = comment.body(c1_pull, stack_id="feature")
2433+
marker_prefix = "<!-- mergify-stack-data: "
2434+
marker_line = next(
2435+
line for line in body.splitlines() if line.startswith(marker_prefix)
2436+
)
2437+
assert '", ' not in marker_line
2438+
assert '": ' not in marker_line

0 commit comments

Comments
 (0)