Skip to content

Commit 235a271

Browse files
authored
Mark fully deployed services, show picks as chips, and move Release into a folding side column (#27)
* Mark fully deployed services and move Release into a tabbed side column * Mark services with a picked tag and clear the pick from the line * Show the picked tag as a chip and fold the Release column while a service is open
1 parent e0705a6 commit 235a271

2 files changed

Lines changed: 73 additions & 30 deletions

File tree

‎README.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -129,23 +129,27 @@ as one line: what production runs, the picked tag, how many merge requests or co
129129
failed jobs of the picked tag, or of the newest tag before a pick, with the ones allowed to fail counted apart, a mark
130130
when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`, and how many rows have no tag yet, with a `+` when the cut
131131
range holds no tag, so more untagged rows may lie below it. The failed jobs are those of the tag's pipeline and of
132-
the main pipeline of its commit; failures in other rows show only when the line is opened.
132+
the main pipeline of its commit; failures in other rows show only when the line is opened. A service whose
133+
production runs the head of the default branch, with no rows left to ship, is marked green as fully deployed.
133134
Opening a line shows the service's environments with when each was deployed, warnings, a link to create the next tag
134135
on the head when rows have no tag yet, merge requests that are not merged yet, and its rows with tags and their
135136
pipelines, merge requests or commits with when they were merged or committed, Jira keys with their status,
136137
environments with the time each first ran the row, and failed jobs; rows out of scope are dimmed.
137138
Links, including the GitLab settings pages that warnings and errors point to, open in a new
138-
tab. Each tag has a **pick** button: picking it highlights the rows it ships and closes the line again. A `--jira`
139-
report starts with each service's release tag picked. Services with neither a production deployment nor rows are left
139+
tab. Each tag has a **pick** button: picking it highlights the rows it ships and closes the line again, which then
140+
shows the picked tag as a blue chip. The chip's `×` clears the pick without opening the line. A `--jira` report starts
141+
with each service's release tag picked. Services with neither a production deployment nor rows are left
140142
out of the page.
141143

142144
The address keeps the picks after `#`, as `team/api=2.4.0&team/web=5.12.0`, so sharing or reloading the page keeps
143145
them. An address with picks replaces the release tags of a `--jira` report; picks that are not in the report, such as
144146
a tag that a later run no longer lists, are skipped with a warning. Clearing every pick empties the address, so a
145147
reload starts from the release tags again.
146148

147-
**Release** at the bottom turns the picked tags into three lists and a release post, each with a copy button and a
148-
text box to copy from by hand, since browsers allow the copy button only over HTTPS:
149+
**Release**, in a column next to the services that stays in view while they scroll, or below them on a narrow screen,
150+
turns the picked tags into three lists and a release post. While a service is open, the column folds into a strip
151+
with the number of picked tags and tasks, so the rows get the full width; clicking the strip closes the service. Each has a tab of its own, with a copy button and a text box
152+
to copy from by hand, since browsers allow the copy button only over HTTPS:
149153

150154
- **Jira tasks**: the keys of every in-scope row from each picked tag down to production, without duplicates, with
151155
summary and a status badge: grey to do, blue in progress, green done. Copy them one per line or as a JQL

‎release_scope/_static/index.html‎

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,9 @@
3333
[x-cloak] { display: none !important; }
3434
* { box-sizing: border-box; }
3535
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 system-ui, sans-serif; }
36-
main { max-width: 1200px; margin: 0 auto; padding: 16px; }
36+
main { max-width: 1680px; margin: 0 auto; padding: 16px; }
3737
h1 { font-size: 24px; margin: 0 0 4px; }
3838
h2 { font-size: 18px; margin: 32px 0 8px; border-bottom: 1px solid var(--border); padding-bottom: 4px; }
39-
h3 { font-size: 15px; margin: 0 0 8px; }
4039
a { color: var(--accent); text-decoration: none; }
4140
a:hover { text-decoration: underline; }
4241
code { font: 12px ui-monospace, monospace; }
@@ -57,22 +56,40 @@
5756
.warn { color: var(--warn); }
5857
.message { white-space: pre-line; }
5958
.scroll { overflow-x: auto; }
60-
.outputs { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; margin-top: 16px; }
61-
.outputs .wide { grid-column: 1 / -1; }
62-
.out-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; }
59+
.layout { display: grid; grid-template-columns: minmax(0, 1fr) 420px; gap: 24px; align-items: start; }
60+
.layout > aside { position: sticky; top: 0; max-height: 100vh; overflow-y: auto; padding: 0 4px 16px; }
61+
.strip { display: none; writing-mode: vertical-rl; width: 40px; min-height: 240px; margin-top: 32px; padding: 12px 4px;
62+
border-color: var(--accent); color: var(--accent); font-weight: 600; }
63+
.layout.folded { grid-template-columns: minmax(0, 1fr) 44px; gap: 12px; }
64+
.layout.folded .strip { display: block; }
65+
.layout.folded .aside-body { display: none; }
66+
@media (max-width: 1000px) {
67+
.layout { display: block; }
68+
.layout > aside { position: static; max-height: none; }
69+
.layout.folded .strip { display: none; }
70+
.layout.folded .aside-body { display: block; }
71+
}
72+
.tabs { display: flex; flex-wrap: wrap; gap: 4px; margin: 8px 0; }
73+
.outputs textarea { min-height: 160px; }
74+
.out-head { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
6375
tr.out-of-scope td { opacity: 0.55; }
6476
tr.shipped td { background: color-mix(in srgb, var(--accent) 12%, transparent); }
6577
.tag { white-space: nowrap; }
6678
.service { border: 1px solid var(--border); border-radius: 6px; margin: 6px 0; }
6779
.service > summary { display: flex; flex-wrap: wrap; gap: 4px 12px; align-items: baseline; padding: 8px 12px; cursor: pointer;
6880
list-style: none; }
6981
.service[open] > summary { border-bottom: 1px solid var(--border); }
82+
.service.deployed { border-left: 4px solid var(--good); background: color-mix(in srgb, var(--good) 6%, transparent); }
83+
.service.deployed .name { color: var(--good); }
84+
.chip { display: inline-flex; align-items: center; gap: 4px; padding: 0 4px 0 10px; border-radius: 999px;
85+
background: var(--accent); color: var(--bg); font-weight: 600; }
86+
.chip button { border: 0; border-radius: 999px; padding: 0 6px; background: transparent; color: inherit; }
87+
.chip button:hover { background: color-mix(in srgb, var(--bg) 25%, transparent); }
7088
.service > summary::before { content: "▸"; color: var(--muted); }
7189
.service[open] > summary::before { content: "▾"; }
7290
.service .name { min-width: 200px; font-weight: 600; }
7391
.service .body { padding: 8px 12px; }
7492
.service .body > p:first-child { margin-top: 0; }
75-
.picked-tag { font-weight: 600; }
7693
.status { display: inline-block; padding: 0 8px; border: 1px solid currentColor; border-radius: 10px; font-size: 12px;
7794
font-weight: 600; white-space: nowrap; color: var(--muted); }
7895
.status-indeterminate { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
@@ -119,18 +136,23 @@ <h1 x-text="report.jira_scope.length ? t('titleIssues', { keys: report.jira_scop
119136
<p class="bad message"><template x-for="(part, index) in messageParts(item)" :key="index"><span><template x-if="part.url"><a :href="part.url" x-text="part.text"></a></template><template x-if="!part.url"><span x-text="part.text"></span></template></span></template></p>
120137
</template>
121138

139+
<div class="layout" :class="{ folded: openCount > 0 }">
140+
<section>
122141
<h2 x-text="t('services')"></h2>
123142
<p x-show="pickable.length" class="muted" x-text="t('servicesIntro')"></p>
124143
<p x-show="ignored.length" class="warn" x-text="t('ignored', { picks: ignored.join(', ') })"></p>
125144
<template x-for="service in listed" :key="service.project">
126-
<details class="service" name="services">
145+
<details class="service" :class="{ deployed: deployed(service) }" name="services"
146+
@toggle="openCount = document.querySelectorAll('details.service[open]').length">
127147
<summary>
128148
<span class="name" x-text="service.project"></span>
129149
<span x-show="service.error" class="bad" x-text="t('failedToCollect')"></span>
130150
<span x-show="!service.error">
131151
<span class="muted" x-text="productionRef(service) + ' →'"></span>
132-
<span x-show="pickOf(service)" class="picked-tag" x-text="picks[service.project]"></span>
152+
<span x-show="pickOf(service)" class="chip"><span x-text="picks[service.project]"></span><button type="button"
153+
:title="t('clearPick')" :aria-label="t('clearPick')" @click.prevent="pick(service, '', $el)">×</button></span>
133154
<span x-show="!pickOf(service)" class="muted" x-text="pickHint(service)"></span>
155+
<span x-show="deployed(service)" class="status status-done" x-text="t('upToDate')"></span>
134156
<span x-show="service.untagged" class="warn" x-text="untaggedText(service, 'untagged', 'untaggedAtLeast')"></span>
135157
</span>
136158
<span class="muted" x-text="lineDetails(service)"></span>
@@ -238,20 +260,28 @@ <h2 x-text="t('services')"></h2>
238260
</div>
239261
</details>
240262
</template>
263+
</section>
241264

265+
<aside>
266+
<button type="button" class="strip" @click="closeServices()"
267+
x-text="[t('release'), t('tagCount', { count: picked.length }), t('taskCount', { count: tasks.length })].join(' · ')"></button>
268+
<div class="aside-body">
242269
<h2 x-text="t('release')"></h2>
243270
<p x-show="!pickable.length" class="muted" x-text="t('noCandidates')"></p>
244271
<p x-show="pickable.length && !picked.length" class="muted" x-text="t('pickToFill')"></p>
245272
<p x-show="copyFailed" class="warn" x-text="t('copyFailed')"></p>
246273

247274
<div class="outputs" x-show="picked.length">
248-
<section>
275+
<div class="tabs">
276+
<button type="button" :class="{ picked: tab === 'tasks' }" @click="tab = 'tasks'" x-text="t('tasks', { count: tasks.length })"></button>
277+
<button type="button" :class="{ picked: tab === 'pipelines' }" @click="tab = 'pipelines'" x-text="t('tagPipelines')"></button>
278+
<button type="button" :class="{ picked: tab === 'compare' }" @click="tab = 'compare'" x-text="t('compare')"></button>
279+
<button type="button" :class="{ picked: tab === 'post' }" @click="tab = 'post'" x-text="t('releasePost')"></button>
280+
</div>
281+
<section x-show="tab === 'tasks'">
249282
<div class="out-head">
250-
<h3 x-text="t('tasks', { count: tasks.length })"></h3>
251-
<span>
252-
<button type="button" @click="copy(taskKeys)" x-text="t('copyKeys')"></button>
253-
<button type="button" @click="copy(taskJql)" x-text="t('copyJql')"></button>
254-
</span>
283+
<button type="button" @click="copy(taskKeys)" x-text="t('copyKeys')"></button>
284+
<button type="button" @click="copy(taskJql)" x-text="t('copyJql')"></button>
255285
</div>
256286
<ul>
257287
<template x-for="task in tasks" :key="task.key">
@@ -266,9 +296,8 @@ <h3 x-text="t('tasks', { count: tasks.length })"></h3>
266296
<textarea readonly x-text="taskKeys"></textarea>
267297
<textarea readonly x-text="taskJql"></textarea>
268298
</section>
269-
<section>
299+
<section x-show="tab === 'pipelines'">
270300
<div class="out-head">
271-
<h3 x-text="t('tagPipelines')"></h3>
272301
<button type="button" @click="copy(pipelinesMarkdown)" x-text="t('copy')"></button>
273302
</div>
274303
<ul>
@@ -285,9 +314,8 @@ <h3 x-text="t('tagPipelines')"></h3>
285314
</ul>
286315
<textarea readonly x-text="pipelinesMarkdown"></textarea>
287316
</section>
288-
<section>
317+
<section x-show="tab === 'compare'">
289318
<div class="out-head">
290-
<h3 x-text="t('compare')"></h3>
291319
<button type="button" @click="copy(compareMarkdown)" x-text="t('copy')"></button>
292320
</div>
293321
<ul>
@@ -300,14 +328,16 @@ <h3 x-text="t('compare')"></h3>
300328
</ul>
301329
<textarea readonly x-text="compareMarkdown"></textarea>
302330
</section>
303-
<section class="wide">
331+
<section x-show="tab === 'post'">
304332
<div class="out-head">
305-
<h3 x-text="t('releasePost')"></h3>
306333
<button type="button" @click="copy(releasePost)" x-text="t('copy')"></button>
307334
</div>
308-
<textarea readonly rows="12" x-text="releasePost"></textarea>
335+
<textarea readonly rows="16" x-text="releasePost"></textarea>
309336
</section>
310337
</div>
338+
</div>
339+
</aside>
340+
</div>
311341

312342
<p class="muted" x-text="t('legend')"></p>
313343
</div>
@@ -353,10 +383,11 @@ <h3 x-text="t('releasePost')"></h3>
353383
},
354384
createTag: "Create tag {name}",
355385
createAnyTag: "Create a tag",
356-
upToDate: "up to date",
386+
upToDate: "✓ fully deployed",
357387
rows: { one: "{count} merge request or commit", other: "{count} merge requests or commits" },
358388
rowsOf: { one: "{count} merge request or commit", other: "{count} merge requests or commits" },
359389
taskCount: { one: "{count} task", other: "{count} tasks" },
390+
tagCount: { one: "{count} tag", other: "{count} tags" },
360391
shipsOf: "{shipped} of {rows}",
361392
failedCount: "❌ {count} failed",
362393
allowedCount: "{count} failed, allowed to fail",
@@ -429,10 +460,11 @@ <h3 x-text="t('releasePost')"></h3>
429460
},
430461
createTag: "Создать тег {name}",
431462
createAnyTag: "Создать тег",
432-
upToDate: "всё на проде",
463+
upToDate: "✓ всё на проде",
433464
rows: { one: "{count} MR или коммит", few: "{count} MR или коммита", many: "{count} MR или коммитов" },
434465
rowsOf: { one: "{count} MR или коммита", few: "{count} MR или коммитов", many: "{count} MR или коммитов" },
435466
taskCount: { one: "{count} задача", few: "{count} задачи", many: "{count} задач" },
467+
tagCount: { one: "{count} тег", few: "{count} тега", many: "{count} тегов" },
436468
shipsOf: "{shipped} из {rows}",
437469
failedCount: "❌ упало: {count}",
438470
allowedCount: "упало, но падать разрешено: {count}",
@@ -539,6 +571,8 @@ <h3 x-text="t('releasePost')"></h3>
539571
copyFailed: false,
540572
ignored: [],
541573
picks: {},
574+
tab: "tasks",
575+
openCount: 0,
542576
lang: "en",
543577
theme: "auto",
544578

@@ -672,6 +706,9 @@ <h3 x-text="t('releasePost')"></h3>
672706
element.closest("details").open = false;
673707
this.writeHash();
674708
},
709+
closeServices() {
710+
for (const item of document.querySelectorAll("details.service[open]")) item.open = false;
711+
},
675712
readHash() {
676713
const decode = (value) => {
677714
try {
@@ -704,8 +741,10 @@ <h3 x-text="t('releasePost')"></h3>
704741
return service.candidates.find((item) => item.tag.name === this.picks[service.project]);
705742
},
706743
pickHint(service) {
707-
if (service.candidates.length) return this.t("pickTag");
708-
return service.rows.length ? "" : this.t("upToDate");
744+
return service.candidates.length ? this.t("pickTag") : "";
745+
},
746+
deployed(service) {
747+
return Boolean(!service.error && this.production(service) && !service.rows.length);
709748
},
710749
lineDetails(service) {
711750
const candidate = this.pickOf(service);

0 commit comments

Comments
 (0)