diff --git a/demo/scenario.py b/demo/scenario.py index bfe1869..d09c2a5 100644 --- a/demo/scenario.py +++ b/demo/scenario.py @@ -62,6 +62,9 @@ class DemoIssue: issue_type: str = "Story" links: tuple[str, ...] = () blocked_by: tuple[str, ...] = () + blocks: tuple[str, ...] = () + parent: str | None = None + subtasks: tuple[str, ...] = () SERVICES: typing.Final = ( @@ -199,6 +202,7 @@ class DemoIssue: category="done", issue_type="Bug", links=(f"{ENDPOINT}/shop/worker/-/merge_requests/31",), + blocks=("SHOP-40",), ), DemoIssue( key="SHOP-36", @@ -207,6 +211,7 @@ class DemoIssue: category="indeterminate", links=(f"{ENDPOINT}/shop/api/-/merge_requests/40",), blocked_by=("SHOP-45",), + subtasks=("SHOP-46",), ), DemoIssue( key="SHOP-38", @@ -242,6 +247,7 @@ class DemoIssue: links=(f"{ENDPOINT}/shop/admin/-/merge_requests/15",), ), DemoIssue(key="SHOP-45", summary="Pick the dark palette with design", status="To Do", category="new"), + DemoIssue(key="SHOP-46", summary="Dark mode in emails", status="To Do", category="new", parent="SHOP-36"), ) @@ -392,6 +398,9 @@ def _mock_service(router: respx.Router, service: DemoService, now: dt.datetime) router.get(f"{api}/pipelines", params={"source": "push"}).respond(json=push) +_BLOCKS: typing.Final = {"name": "Blocks", "inward": "is blocked by", "outward": "blocks"} + + def _issue(issue: DemoIssue) -> dict[str, typing.Any]: return { "key": issue.key, @@ -400,12 +409,11 @@ def _issue(issue: DemoIssue) -> dict[str, typing.Any]: "status": {"name": issue.status, "statusCategory": {"key": issue.category}}, "issuetype": {"name": issue.issue_type}, "issuelinks": [ - { - "type": {"name": "Blocks", "inward": "is blocked by", "outward": "blocks"}, - "inwardIssue": {"key": key}, - } - for key in issue.blocked_by + *({"type": _BLOCKS, "inwardIssue": {"key": key}} for key in issue.blocked_by), + *({"type": _BLOCKS, "outwardIssue": {"key": key}} for key in issue.blocks), ], + "parent": {"key": issue.parent} if issue.parent else None, + "subtasks": [{"key": key} for key in issue.subtasks], }, } diff --git a/docs/page.md b/docs/page.md index d64b8a0..ac35061 100644 --- a/docs/page.md +++ b/docs/page.md @@ -100,14 +100,15 @@ screen. While a service is open, the column folds into a strip with the number o get the full width. Clicking the strip closes the service. Above the tabs, **Left behind by these picks** lists what the picked tags leave out for every Jira task they ship, -and for every issue related to one: +and for every issue a task depends on: one that blocks it through Jira's `Blocks` link type, and its subtasks. -- a service where the task or related issue has changes that no pick covers, with the tag it needs or why there is - none: `no tag yet`, `not merged`, `merged into `, `in prod or not merged`, `not in this report`, or +- a service where the task or an issue it depends on has changes that no pick covers, with the tag it needs or why + there is none: `no tag yet`, `not merged`, `merged into `, `in prod or not merged`, `not in this report`, or `failed to collect`; -- a related issue with no changes at all that is not done in Jira, with its status. +- an issue the task depends on with no changes at all that is not done in Jira, with its status. -Changes already in production and closed merge requests are not listed. +Changes already in production and closed merge requests are not listed. Other relations, such as `blocks`, +`relates to`, or a parent, do not hold a release back; they show only when you hover over the issue's key. It turns the picked tags into four tabs. Each has a copy button and a text box to copy from by hand, since browsers allow the copy button only over HTTPS. diff --git a/release_scope/_static/index.html b/release_scope/_static/index.html index 44c1f4c..6447798 100644 --- a/release_scope/_static/index.html +++ b/release_scope/_static/index.html @@ -675,10 +675,10 @@

viaJira: "через Jira", viaJiraTitle: "{change} не упоминает задачу. {key} ссылается на это изменение в Jira.", taskFrom: "из {items}", - gapsTitle: "Не войдёт с этими тегами:", - gapNeeds: "нужен {tag}", - relationParent: "родитель", - relationSubtask: "подзадача", + gapsTitle: "Не войдёт в релиз с выбранными тегами:", + gapNeeds: "нужен тег {tag}", + relationParent: "входит в", + relationSubtask: "включает подзадачу", }, }; @@ -744,6 +744,9 @@

const byKey = (a, b) => a.localeCompare(b, undefined, { numeric: true }); + const dependsOn = (relation) => + relation.kind === "subtask" || (relation.kind === "link" && relation.type === "Blocks" && relation.direction === "inward"); + const OUT_OF_RANGE = new Set(["production", "older", "open", "branch", "closed", "elsewhere"]); const outOfRange = (service, links) => { @@ -937,7 +940,7 @@

}; for (const task of this.tasks) { check(task.key, null, task.key); - for (const relation of task.issue ? task.issue.relations : []) check(task.key, relation, relation.key); + for (const relation of task.issue ? task.issue.relations.filter(dependsOn) : []) check(task.key, relation, relation.key); } return [...gaps]; }, diff --git a/tests/test_browser.py b/tests/test_browser.py index e76d0a3..504637f 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -117,7 +117,7 @@ def test_demo_lists_its_services_and_picks_tags_for_an_issue(page: Page, demo_si names = page.locator("details.service .name") expect(names).to_have_text(["admin", "api", "web", "worker"]) expect(names.first).to_have_attribute("title", "shop/admin") - expect(page.locator(".issue-row code")).to_have_count(8) + expect(page.locator(".issue-row code")).to_have_count(9) _issue(page, "SHOP-38").check() expect(page).to_have_url(re.compile(r"#issues=SHOP-38&shop/api=2\.5\.0&shop/web=1\.9\.0$")) assert errors == [] @@ -355,6 +355,7 @@ def test_demo_a_hand_pick_lists_the_changes_it_leaves_behind(page: Page, demo_si "SHOP-38 is blocked by SHOP-44 → admin: not merged", "SHOP-35 → worker: needs 0.15.0", "SHOP-36 is blocked by SHOP-45: To Do", + "SHOP-36 has subtask SHOP-46: To Do", ] ) assert errors == [] @@ -378,6 +379,25 @@ def test_demo_related_issues_have_their_own_group_and_relations_show_on_hover( ) -> None: page.goto(demo_site) - expect(page.locator(".issue-group")).to_have_text(["Done · 3", "Not done · 3", "Related · 2"]) + expect(page.locator(".issue-group")).to_have_text(["Done · 3", "Not done · 3", "Related · 3"]) expect(page.locator(".issue-row code", has_text="SHOP-38")).to_have_attribute("title", "is blocked by SHOP-44") assert errors == [] + + +def test_demo_left_behind_list_reads_in_russian(page: Page, demo_site: str, errors: list[str]) -> None: + page.goto(demo_site) + for project, tag in (("shop/api", "2.5.0"), ("shop/web", "1.9.0")): + service = _demo_service(page, project) + service.locator("summary").click() + service.locator(".tag", has_text=tag).get_by_role("button").click() + + page.get_by_role("combobox").first.select_option("ru") + + expect(page.locator(".gaps p")).to_have_text("Не войдёт в релиз с выбранными тегами:") # noqa: RUF001 + expect(page.locator(".gaps li")).to_contain_text( + [ + "SHOP-35 → worker: нужен тег 0.15.0", + "SHOP-36 включает подзадачу SHOP-46: To Do", + ] + ) + assert errors == []