Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ Those issues show their key and summary too, and the services they miss sit unde
A service that needs a tag has a **Pick** button for it, which picks the tag without opening the service. `no tag yet`
links to create the next tag, and `not merged`, `merged into <branch>`, and `in prod or not merged` link to the merge
requests or commits. When any line needs a tag, a button next to the title picks every needed tag at once, the newest
one per service. A newer pick can ship more tasks, and the list then shows what those leave behind.
one per service. A needed tag can ship more tasks that need tags of their own, so the button also picks those, and its
count includes them.

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.
Expand All @@ -129,4 +130,6 @@ allow the copy button only over HTTPS.
- **Compare**: a GitLab compare link per service from production to the picked tag, as a Markdown list. A service
never deployed to production gets the tag's commit history instead.
- **Release post**: one Markdown text with a line per picked tag, holding its compare link and pipeline, followed by
the Jira tasks with their summaries.
the Jira tasks with their summaries. When the picks leave something behind, a **Left behind** section follows with
the same groups as the list above: each task, the services it misses with the reason and links to the merge
requests, and the issues it depends on.
53 changes: 44 additions & 9 deletions release_scope/_static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ <h2 x-text="t('release')"></h2>
postServices: "**Services**",
postTasks: "**Jira tasks ({count})**",
postNone: "- none",
postGaps: "**Left behind ({count})**",
postPipeline: "pipeline #{id}",
legend: "Legend: ✅ success · ❌ failed · 🔄 running · ⏭ canceled or skipped · ⚠️ no pipeline",
chooseIssues: "Issues",
Expand Down Expand Up @@ -693,6 +694,7 @@ <h2 x-text="t('release')"></h2>
postServices: "**Сервисы**",
postTasks: "**Задачи Jira ({count})**",
postNone: "- нет",
postGaps: "**Не войдёт в релиз ({count})**",
postPipeline: "пайплайн #{id}",
legend: "Обозначения: ✅ прошёл · ❌ упал · 🔄 выполняется · ⏭ отменён или пропущен · ⚠️ нет пайплайна",
chooseIssues: "Задачи",
Expand Down Expand Up @@ -965,16 +967,22 @@ <h2 x-text="t('release')"></h2>
return jira && jira.error ? [jira.error] : [];
},
get picked() {
return this.pickedFor(this.picks);
},
pickedFor(picks) {
return this.pickable
.map((service) => ({
service,
candidate: this.pickOf(service),
candidate: service.candidates.find((item) => item.tag.name === picks[service.project]),
}))
.filter((item) => item.candidate);
},
get tasks() {
return this.tasksOf(this.picked);
},
tasksOf(picked) {
const tasks = new Map();
for (const { service, candidate } of this.picked) {
for (const { service, candidate } of picked) {
const shipped = service.rows.slice(this.tagRow(service, candidate.tag.name)).filter((row) => row.in_scope);
for (const key of candidate.jira_keys) {
if (!tasks.has(key.key)) tasks.set(key.key, { key: key.key, url: key.url, issue: this.issue(key.key), sources: [] });
Expand All @@ -988,10 +996,13 @@ <h2 x-text="t('release')"></h2>
return [...tasks.values()];
},
get gaps() {
const starts = new Map(this.picked.map(({ service, candidate }) => [service.project, this.tagRow(service, candidate.tag.name)]));
return this.gapsOf(this.picked);
},
gapsOf(picked) {
const starts = new Map(picked.map(({ service, candidate }) => [service.project, this.tagRow(service, candidate.tag.name)]));
const lines = (key) =>
(this.reach[key] || []).map((item) => ({ ...item, key, reason: this.gapReason(item, starts) })).filter((item) => item.reason);
return this.tasks
return this.tasksOf(picked)
.map((task) => ({
...task,
lines: lines(task.key),
Expand All @@ -1015,14 +1026,24 @@ <h2 x-text="t('release')"></h2>
.map((link) => ({ url: link.url, label: link.iid != null ? "!" + link.iid : link.sha.slice(0, 8) }));
},
get neededPicks() {
const picks = { ...this.picks };
const needed = new Map();
for (const task of this.gaps) {
for (const line of [...task.lines, ...task.deps.flatMap((dep) => dep.lines)]) {
const known = needed.get(line.project);
if (line.state === "tag" && (!known || line.tagIndex < known.tagIndex)) needed.set(line.project, line);
for (;;) {
const round = new Map();
for (const task of this.gapsOf(this.pickedFor(picks))) {
for (const line of [...task.lines, ...task.deps.flatMap((dep) => dep.lines)]) {
const known = round.get(line.project);
if (line.state === "tag" && picks[line.project] !== line.tag && (!known || line.tagIndex < known.tagIndex)) {
round.set(line.project, line);
}
}
}
if (!round.size) return [...needed.values()];
for (const line of round.values()) {
picks[line.project] = line.tag;
needed.set(line.project, line);
}
}
return [...needed.values()];
},
pickGap(line) {
this.pick(this.serviceOf(line.project), line.tag);
Expand Down Expand Up @@ -1081,14 +1102,28 @@ <h2 x-text="t('release')"></h2>
return `- ${item.service.project} ${item.candidate.tag.name}: ${compare} · ${build}`;
});
const tasks = this.tasks.map((task) => `- [${task.key}](${task.url})${task.issue ? " " + task.issue.summary : ""}`);
const gaps = this.gaps;
return [
this.t("postServices"),
...services,
"",
this.t("postTasks", { count: tasks.length }),
...(tasks.length ? tasks : [this.t("postNone")]),
...(gaps.length ? ["", this.t("postGaps", { count: gaps.length }), ...gaps.flatMap((task) => this.postGap(task))] : []),
].join("\n");
},
postGap(task) {
const line = (item, indent) =>
`${indent}- ${item.project}: ${[item.reason, ...this.gapChanges(item).map((change) => `[${change.label}](${change.url})`)].join(" ")}`;
return [
`- [${task.key}](${task.url})${task.issue ? " " + task.issue.summary : ""}`,
...task.lines.map((item) => line(item, " ")),
...task.deps.flatMap((dep) => [
` - ${this.relationLabel(dep.relation)} [${dep.key}](${dep.issue.url}) ${dep.issue.summary}${dep.lines.length ? "" : ": " + dep.issue.status}`,
...dep.lines.map((item) => line(item, " ")),
]),
];
},
get compareMarkdown() {
return this.picked
.map((item) => `- ${item.service.project}: [${this.compareLabel(item)}](${item.candidate.compare_url})`)
Expand Down
41 changes: 41 additions & 0 deletions tests/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,47 @@ def test_demo_picking_every_needed_tag_at_once(page: Page, demo_site: str, error
assert errors == []


def test_demo_picking_every_needed_tag_also_picks_what_those_tags_need(
page: Page, demo_site: str, errors: list[str]
) -> None:
page.goto(demo_site)
_pick_by_hand(page, "shop/web", "1.9.0")

page.get_by_role("button", name="Pick 2 needed tags").click()

expect(_demo_service(page, "shop/api").locator(".chip > span")).to_have_text("2.5.0")
expect(_demo_service(page, "shop/worker").locator(".chip > span")).to_have_text("0.15.0")
expect(page.locator(".gaps > p button")).to_be_hidden()
assert errors == []


def test_demo_release_post_lists_what_the_picks_leave_behind(page: Page, demo_site: str, errors: list[str]) -> None:
page.goto(demo_site)
_pick_by_hand(page, "shop/api", "2.5.0")
_pick_by_hand(page, "shop/web", "1.9.0")

page.get_by_role("button", name="Release post").click()

post: typing.Final = page.locator("section", has=page.get_by_role("button", name="Copy")).locator("textarea").last
expect(post).to_have_value(
re.compile(
re.escape(
"\n\n**Left behind (3)**\n"
"- [SHOP-38](https://jira.example.com/browse/SHOP-38) Export orders as CSV\n"
" - partners/mobile: not in this report\n"
" - is blocked by [SHOP-44](https://jira.example.com/browse/SHOP-44) Only managers may export orders\n"
" - shop/admin: not merged [!15](https://gitlab.example.com/shop/admin/-/merge_requests/15)\n"
" - legacy/admin: not in this report\n"
"- [SHOP-35]"
)
)
)
expect(post).to_have_value(
re.compile(r" - is blocked by \[SHOP-45\]\(\S+\) Pick the dark palette with design: To Do\n")
)
assert errors == []


def test_demo_tasks_name_the_changes_that_bring_them(page: Page, demo_site: str, errors: list[str]) -> None:
page.goto(demo_site)

Expand Down
Loading