Skip to content

Commit c9e5da2

Browse files
authored
Pick what needed tags need too, and put Left behind in the release post (#58)
1 parent bbbabfb commit c9e5da2

3 files changed

Lines changed: 90 additions & 11 deletions

File tree

‎docs/page.md‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,8 @@ Those issues show their key and summary too, and the services they miss sit unde
113113
A service that needs a tag has a **Pick** button for it, which picks the tag without opening the service. `no tag yet`
114114
links to create the next tag, and `not merged`, `merged into <branch>`, and `in prod or not merged` link to the merge
115115
requests or commits. When any line needs a tag, a button next to the title picks every needed tag at once, the newest
116-
one per service. A newer pick can ship more tasks, and the list then shows what those leave behind.
116+
one per service. A needed tag can ship more tasks that need tags of their own, so the button also picks those, and its
117+
count includes them.
117118

118119
Changes already in production and closed merge requests are not listed. Other relations, such as `blocks`,
119120
`relates to`, or a parent, do not hold a release back; they show only when you hover over the issue's key.
@@ -129,4 +130,6 @@ allow the copy button only over HTTPS.
129130
- **Compare**: a GitLab compare link per service from production to the picked tag, as a Markdown list. A service
130131
never deployed to production gets the tag's commit history instead.
131132
- **Release post**: one Markdown text with a line per picked tag, holding its compare link and pipeline, followed by
132-
the Jira tasks with their summaries.
133+
the Jira tasks with their summaries. When the picks leave something behind, a **Left behind** section follows with
134+
the same groups as the list above: each task, the services it misses with the reason and links to the merge
135+
requests, and the issues it depends on.

‎release_scope/_static/index.html‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -583,6 +583,7 @@ <h2 x-text="t('release')"></h2>
583583
postServices: "**Services**",
584584
postTasks: "**Jira tasks ({count})**",
585585
postNone: "- none",
586+
postGaps: "**Left behind ({count})**",
586587
postPipeline: "pipeline #{id}",
587588
legend: "Legend: ✅ success · ❌ failed · 🔄 running · ⏭ canceled or skipped · ⚠️ no pipeline",
588589
chooseIssues: "Issues",
@@ -693,6 +694,7 @@ <h2 x-text="t('release')"></h2>
693694
postServices: "**Сервисы**",
694695
postTasks: "**Задачи Jira ({count})**",
695696
postNone: "- нет",
697+
postGaps: "**Не войдёт в релиз ({count})**",
696698
postPipeline: "пайплайн #{id}",
697699
legend: "Обозначения: ✅ прошёл · ❌ упал · 🔄 выполняется · ⏭ отменён или пропущен · ⚠️ нет пайплайна",
698700
chooseIssues: "Задачи",
@@ -965,16 +967,22 @@ <h2 x-text="t('release')"></h2>
965967
return jira && jira.error ? [jira.error] : [];
966968
},
967969
get picked() {
970+
return this.pickedFor(this.picks);
971+
},
972+
pickedFor(picks) {
968973
return this.pickable
969974
.map((service) => ({
970975
service,
971-
candidate: this.pickOf(service),
976+
candidate: service.candidates.find((item) => item.tag.name === picks[service.project]),
972977
}))
973978
.filter((item) => item.candidate);
974979
},
975980
get tasks() {
981+
return this.tasksOf(this.picked);
982+
},
983+
tasksOf(picked) {
976984
const tasks = new Map();
977-
for (const { service, candidate } of this.picked) {
985+
for (const { service, candidate } of picked) {
978986
const shipped = service.rows.slice(this.tagRow(service, candidate.tag.name)).filter((row) => row.in_scope);
979987
for (const key of candidate.jira_keys) {
980988
if (!tasks.has(key.key)) tasks.set(key.key, { key: key.key, url: key.url, issue: this.issue(key.key), sources: [] });
@@ -988,10 +996,13 @@ <h2 x-text="t('release')"></h2>
988996
return [...tasks.values()];
989997
},
990998
get gaps() {
991-
const starts = new Map(this.picked.map(({ service, candidate }) => [service.project, this.tagRow(service, candidate.tag.name)]));
999+
return this.gapsOf(this.picked);
1000+
},
1001+
gapsOf(picked) {
1002+
const starts = new Map(picked.map(({ service, candidate }) => [service.project, this.tagRow(service, candidate.tag.name)]));
9921003
const lines = (key) =>
9931004
(this.reach[key] || []).map((item) => ({ ...item, key, reason: this.gapReason(item, starts) })).filter((item) => item.reason);
994-
return this.tasks
1005+
return this.tasksOf(picked)
9951006
.map((task) => ({
9961007
...task,
9971008
lines: lines(task.key),
@@ -1015,14 +1026,24 @@ <h2 x-text="t('release')"></h2>
10151026
.map((link) => ({ url: link.url, label: link.iid != null ? "!" + link.iid : link.sha.slice(0, 8) }));
10161027
},
10171028
get neededPicks() {
1029+
const picks = { ...this.picks };
10181030
const needed = new Map();
1019-
for (const task of this.gaps) {
1020-
for (const line of [...task.lines, ...task.deps.flatMap((dep) => dep.lines)]) {
1021-
const known = needed.get(line.project);
1022-
if (line.state === "tag" && (!known || line.tagIndex < known.tagIndex)) needed.set(line.project, line);
1031+
for (;;) {
1032+
const round = new Map();
1033+
for (const task of this.gapsOf(this.pickedFor(picks))) {
1034+
for (const line of [...task.lines, ...task.deps.flatMap((dep) => dep.lines)]) {
1035+
const known = round.get(line.project);
1036+
if (line.state === "tag" && picks[line.project] !== line.tag && (!known || line.tagIndex < known.tagIndex)) {
1037+
round.set(line.project, line);
1038+
}
1039+
}
1040+
}
1041+
if (!round.size) return [...needed.values()];
1042+
for (const line of round.values()) {
1043+
picks[line.project] = line.tag;
1044+
needed.set(line.project, line);
10231045
}
10241046
}
1025-
return [...needed.values()];
10261047
},
10271048
pickGap(line) {
10281049
this.pick(this.serviceOf(line.project), line.tag);
@@ -1081,14 +1102,28 @@ <h2 x-text="t('release')"></h2>
10811102
return `- ${item.service.project} ${item.candidate.tag.name}: ${compare} · ${build}`;
10821103
});
10831104
const tasks = this.tasks.map((task) => `- [${task.key}](${task.url})${task.issue ? " " + task.issue.summary : ""}`);
1105+
const gaps = this.gaps;
10841106
return [
10851107
this.t("postServices"),
10861108
...services,
10871109
"",
10881110
this.t("postTasks", { count: tasks.length }),
10891111
...(tasks.length ? tasks : [this.t("postNone")]),
1112+
...(gaps.length ? ["", this.t("postGaps", { count: gaps.length }), ...gaps.flatMap((task) => this.postGap(task))] : []),
10901113
].join("\n");
10911114
},
1115+
postGap(task) {
1116+
const line = (item, indent) =>
1117+
`${indent}- ${item.project}: ${[item.reason, ...this.gapChanges(item).map((change) => `[${change.label}](${change.url})`)].join(" ")}`;
1118+
return [
1119+
`- [${task.key}](${task.url})${task.issue ? " " + task.issue.summary : ""}`,
1120+
...task.lines.map((item) => line(item, " ")),
1121+
...task.deps.flatMap((dep) => [
1122+
` - ${this.relationLabel(dep.relation)} [${dep.key}](${dep.issue.url}) ${dep.issue.summary}${dep.lines.length ? "" : ": " + dep.issue.status}`,
1123+
...dep.lines.map((item) => line(item, " ")),
1124+
]),
1125+
];
1126+
},
10921127
get compareMarkdown() {
10931128
return this.picked
10941129
.map((item) => `- ${item.service.project}: [${this.compareLabel(item)}](${item.candidate.compare_url})`)

‎tests/test_browser.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,47 @@ def test_demo_picking_every_needed_tag_at_once(page: Page, demo_site: str, error
417417
assert errors == []
418418

419419

420+
def test_demo_picking_every_needed_tag_also_picks_what_those_tags_need(
421+
page: Page, demo_site: str, errors: list[str]
422+
) -> None:
423+
page.goto(demo_site)
424+
_pick_by_hand(page, "shop/web", "1.9.0")
425+
426+
page.get_by_role("button", name="Pick 2 needed tags").click()
427+
428+
expect(_demo_service(page, "shop/api").locator(".chip > span")).to_have_text("2.5.0")
429+
expect(_demo_service(page, "shop/worker").locator(".chip > span")).to_have_text("0.15.0")
430+
expect(page.locator(".gaps > p button")).to_be_hidden()
431+
assert errors == []
432+
433+
434+
def test_demo_release_post_lists_what_the_picks_leave_behind(page: Page, demo_site: str, errors: list[str]) -> None:
435+
page.goto(demo_site)
436+
_pick_by_hand(page, "shop/api", "2.5.0")
437+
_pick_by_hand(page, "shop/web", "1.9.0")
438+
439+
page.get_by_role("button", name="Release post").click()
440+
441+
post: typing.Final = page.locator("section", has=page.get_by_role("button", name="Copy")).locator("textarea").last
442+
expect(post).to_have_value(
443+
re.compile(
444+
re.escape(
445+
"\n\n**Left behind (3)**\n"
446+
"- [SHOP-38](https://jira.example.com/browse/SHOP-38) Export orders as CSV\n"
447+
" - partners/mobile: not in this report\n"
448+
" - is blocked by [SHOP-44](https://jira.example.com/browse/SHOP-44) Only managers may export orders\n"
449+
" - shop/admin: not merged [!15](https://gitlab.example.com/shop/admin/-/merge_requests/15)\n"
450+
" - legacy/admin: not in this report\n"
451+
"- [SHOP-35]"
452+
)
453+
)
454+
)
455+
expect(post).to_have_value(
456+
re.compile(r" - is blocked by \[SHOP-45\]\(\S+\) Pick the dark palette with design: To Do\n")
457+
)
458+
assert errors == []
459+
460+
420461
def test_demo_tasks_name_the_changes_that_bring_them(page: Page, demo_site: str, errors: list[str]) -> None:
421462
page.goto(demo_site)
422463

0 commit comments

Comments
 (0)