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
6 changes: 3 additions & 3 deletions docs/page.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,9 +112,9 @@ 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 needed tag can ship more tasks that need tags of their own, so the button also picks those, and its
count includes them.
requests or commits. A service `not in this report` links to its project in GitLab. When any line needs a tag, a
button next to the title picks every needed tag at once, the newest 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 Down
34 changes: 22 additions & 12 deletions release_scope/_static/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@
.gap-dep-head, .gap-line { padding-left: 16px; }
.gap-line.nested { padding-left: 32px; }
.gap-line button { padding: 0 8px; font-size: 12px; }
.gap-summary { flex: 1 1 0; min-width: 8em; }
.gap-dep-head > .status { margin-left: auto; }
.row-key.via-jira > a { text-decoration: underline dashed; text-underline-offset: 3px; }
.link-icon { width: 13px; height: 13px; margin-right: 2px; vertical-align: -2px; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.issue-row .summary { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
Expand Down Expand Up @@ -334,7 +336,7 @@ <h2 x-text="t('services')"></h2>
</template>
<template x-for="entry in unlisted" :key="entry.project">
<div class="service unlisted">
<b :title="entry.project" x-text="shortName(entry.project)"></b>
<b :title="entry.project"><a :href="entry.url" x-text="shortName(entry.project)"></a></b>
<span class="cell cell-outside" x-text="t('notInReport')"></span>
<span class="muted" x-text="t('linkedTo')"></span>
<template x-for="key in entry.keys" :key="key"><span class="key" x-text="key"></span></template>
Expand Down Expand Up @@ -394,7 +396,7 @@ <h2 x-text="t('release')"></h2>
<div class="gap-head">
<span class="dot" :class="'dot-' + (task.issue ? task.issue.status_category : 'new')" :title="task.issue && task.issue.status"></span>
<a :href="task.url" x-text="task.key"></a>
<span x-text="task.issue ? task.issue.summary : ''"></span>
<span class="gap-summary" x-text="task.issue ? task.issue.summary : ''"></span>
</div>
<template x-for="(item, index) in gapRows(task)" :key="index">
<div>
Expand All @@ -403,15 +405,20 @@ <h2 x-text="t('release')"></h2>
<span class="muted" x-text="relationLabel(item.dep.relation)"></span>
<span class="dot" :class="'dot-' + (item.dep.issue.status_category || 'new')" :title="item.dep.issue.status"></span>
<a :href="item.dep.issue.url" x-text="item.dep.key"></a>
<span class="muted" x-text="item.dep.issue.summary"></span>
<span class="muted gap-summary" x-text="item.dep.issue.summary"></span>
<template x-if="!item.dep.lines.length">
<span :class="statusClass(item.dep.issue)" x-text="item.dep.issue.status"></span>
</template>
</div>
</template>
<template x-if="item.line">
<div class="gap-line" :class="{ nested: item.nested }">
<span :title="item.line.project" x-text="shortName(item.line.project)"></span>
<template x-if="item.line.url">
<a :href="item.line.url" :title="item.line.project" x-text="shortName(item.line.project)"></a>
</template>
<template x-if="!item.line.url">
<span :title="item.line.project" x-text="shortName(item.line.project)"></span>
</template>
<span class="cell" :class="'cell-' + (item.line.state === 'tag' ? 'untagged' : item.line.state)" x-text="item.line.reason"></span>
<template x-if="item.line.state === 'tag'">
<button type="button" @click="pickGap(item.line)" x-text="t('gapPick', { tag: item.line.tag })"></button>
Expand Down Expand Up @@ -864,8 +871,8 @@ <h2 x-text="t('release')"></h2>
}
}
for (const [key, issue] of Object.entries(issues)) {
const outside = new Set(issue.links.map((link) => link.project).filter((project) => !projects.has(project)));
for (const project of outside) reach[key].push({ project, state: "outside" });
const outside = new Map(issue.links.filter((link) => !projects.has(link.project)).map((link) => [link.project, link.project_url]));
for (const [project, url] of outside) reach[key].push({ project, state: "outside", url });
}
return reach;
};
Expand Down Expand Up @@ -899,7 +906,7 @@ <h2 x-text="t('release')"></h2>
const report = await response.json();
for (const service of report.services) {
const tag = service.release && service.release.tag ? service.release.tag.name : "";
this.picks[service.project] = service.candidates.some((item) => item.tag.name === tag) ? tag : "";
this.picks[service.project] = this.candidateOf(service, tag) ? tag : "";
}
this.reach = reachOf(report);
this.names = namesOf(report);
Expand Down Expand Up @@ -973,7 +980,7 @@ <h2 x-text="t('release')"></h2>
return this.pickable
.map((service) => ({
service,
candidate: service.candidates.find((item) => item.tag.name === picks[service.project]),
candidate: this.candidateOf(service, picks[service.project]),
}))
.filter((item) => item.candidate);
},
Expand Down Expand Up @@ -1114,7 +1121,7 @@ <h2 x-text="t('release')"></h2>
},
postGap(task) {
const line = (item, indent) =>
`${indent}- ${item.project}: ${[item.reason, ...this.gapChanges(item).map((change) => `[${change.label}](${change.url})`)].join(" ")}`;
`${indent}- ${item.url ? `[${item.project}](${item.url})` : 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, " ")),
Expand Down Expand Up @@ -1153,7 +1160,7 @@ <h2 x-text="t('release')"></h2>
for (const key of this.chosen) {
for (const item of this.reach[key]) {
if (!plan.has(item.project)) {
plan.set(item.project, { project: item.project, state: item.state, keys: [], best: null, untagged: [], elsewhere: [] });
plan.set(item.project, { project: item.project, state: item.state, url: item.url, keys: [], best: null, untagged: [], elsewhere: [] });
}
const entry = plan.get(item.project);
entry.keys.push(key);
Expand Down Expand Up @@ -1259,7 +1266,7 @@ <h2 x-text="t('release')"></h2>
if (wanted.size) {
for (const service of this.pickable) {
const tag = wanted.get(service.project) || "";
const known = !tag || service.candidates.some((item) => item.tag.name === tag);
const known = !tag || this.candidateOf(service, tag);
this.picks[service.project] = known ? tag : "";
if (known) wanted.delete(service.project);
}
Expand All @@ -1278,7 +1285,10 @@ <h2 x-text="t('release')"></h2>
history.replaceState(null, "", hash ? `#${hash}` : location.pathname + location.search);
},
pickOf(service) {
return service.candidates.find((item) => item.tag.name === this.picks[service.project]);
return this.candidateOf(service, this.picks[service.project]);
},
candidateOf(service, tag) {
return service.candidates.find((item) => item.tag.name === tag);
},
pickHint(service) {
return service.candidates.length ? this.t("pickTag") : "";
Expand Down
8 changes: 6 additions & 2 deletions tests/test_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,7 @@ def test_demo_names_a_service_by_its_repo_and_keeps_the_group_only_when_two_shar
outside: typing.Final = page.locator(".service.unlisted b")
expect(outside).to_have_text(["mobile"])
expect(outside).to_have_attribute("title", "partners/mobile")
expect(outside.get_by_role("link")).to_have_attribute("href", "https://gitlab.example.com/partners/mobile")
expect(page.locator(".issue-row", has=page.locator("code", has_text="SHOP-44")).locator(".cell")).to_have_text(
["shop/admin not merged", "legacy/admin not in this report"]
)
Expand Down Expand Up @@ -374,6 +375,9 @@ def test_demo_a_hand_pick_lists_what_it_leaves_behind_under_each_task(
]
)
expect(export.locator(".gap-dep-head")).to_have_text(["is blocked by SHOP-44 Only managers may export orders"])
expect(export.get_by_role("link", name="mobile")).to_have_attribute(
"href", "https://gitlab.example.com/partners/mobile"
)
expect(export.get_by_role("link", name="!15")).to_have_attribute(
"href", "https://gitlab.example.com/shop/admin/-/merge_requests/15"
)
Expand Down Expand Up @@ -444,10 +448,10 @@ def test_demo_release_post_lists_what_the_picks_leave_behind(page: Page, demo_si
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"
" - [partners/mobile](https://gitlab.example.com/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"
" - [legacy/admin](https://gitlab.example.com/legacy/admin): not in this report\n"
"- [SHOP-35]"
)
)
Expand Down
Loading