release-scope 0.5.2__tar.gz → 0.5.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {release_scope-0.5.2 → release_scope-0.5.3}/PKG-INFO +5 -1
- {release_scope-0.5.2 → release_scope-0.5.3}/README.md +4 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/pyproject.toml +1 -1
- {release_scope-0.5.2 → release_scope-0.5.3}/pyproject.toml.orig +1 -1
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_static/index.html +263 -60
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/__init__.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/__main__.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_cache.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_candidates.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_errors.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_files.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_gitlab.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_jira.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_jira_keys.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_links.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_messages.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_report.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_rows.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_settings.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_static/alpine.LICENSE.txt +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_static/alpine.min.js +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/_use_case.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/ioc.py +0 -0
- {release_scope-0.5.2 → release_scope-0.5.3}/release_scope/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: release-scope
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
4
4
|
Summary: Collect what sits between production and the default branch across GitLab services into a GitLab Pages site for picking release tags
|
|
5
5
|
Keywords: release,gitlab,jira,deployments,merge-requests,ci,cli,python
|
|
6
6
|
Author: Artur Shiriev
|
|
@@ -141,6 +141,10 @@ Besides `report.json`, `collect` writes `index.html` and its script into the out
|
|
|
141
141
|
installed package and change only with it, so the page always matches the report schema. The page loads
|
|
142
142
|
`report.json` from next to itself; it needs a web server, not a `file://` URL.
|
|
143
143
|
|
|
144
|
+
Switches at the top pick the language, English or Russian, and the theme: as in the system, light, or dark. The page
|
|
145
|
+
starts in the browser's language and remembers both choices in the browser. The copied lists and release post follow
|
|
146
|
+
the language; warnings and errors from `collect`, and texts from GitLab and Jira, stay as written.
|
|
147
|
+
|
|
144
148
|
**Services** lists every service with a production deployment or with rows, and every service that failed to collect,
|
|
145
149
|
as one line: what production runs, the picked tag, how many merge requests or commits and Jira tasks it ships, failed jobs
|
|
146
150
|
with the ones allowed to fail counted apart, and a mark when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`.
|
|
@@ -108,6 +108,10 @@ Besides `report.json`, `collect` writes `index.html` and its script into the out
|
|
|
108
108
|
installed package and change only with it, so the page always matches the report schema. The page loads
|
|
109
109
|
`report.json` from next to itself; it needs a web server, not a `file://` URL.
|
|
110
110
|
|
|
111
|
+
Switches at the top pick the language, English or Russian, and the theme: as in the system, light, or dark. The page
|
|
112
|
+
starts in the browser's language and remembers both choices in the browser. The copied lists and release post follow
|
|
113
|
+
the language; warnings and errors from `collect`, and texts from GitLab and Jira, stay as written.
|
|
114
|
+
|
|
111
115
|
**Services** lists every service with a production deployment or with rows, and every service that failed to collect,
|
|
112
116
|
as one line: what production runs, the picked tag, how many merge requests or commits and Jira tasks it ships, failed jobs
|
|
113
117
|
with the ones allowed to fail counted apart, and a mark when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`.
|
|
@@ -7,18 +7,29 @@
|
|
|
7
7
|
<base target="_blank">
|
|
8
8
|
<script>
|
|
9
9
|
if (!/(\/|\.html)$/.test(location.pathname)) location.replace(location.pathname + "/" + location.search + location.hash);
|
|
10
|
+
try {
|
|
11
|
+
const theme = localStorage.getItem("release-scope-theme");
|
|
12
|
+
if (theme === "light" || theme === "dark") document.documentElement.dataset.theme = theme;
|
|
13
|
+
} catch {}
|
|
10
14
|
</script>
|
|
11
15
|
<style>
|
|
12
16
|
:root {
|
|
13
17
|
--bg: #ffffff; --fg: #1f2328; --muted: #656d76; --border: #d0d7de; --panel: #f6f8fa;
|
|
14
18
|
--accent: #0969da; --bad: #cf222e; --warn: #9a6700; --good: #1a7f37;
|
|
19
|
+
color-scheme: light;
|
|
15
20
|
}
|
|
16
21
|
@media (prefers-color-scheme: dark) {
|
|
17
|
-
:root {
|
|
22
|
+
:root:not([data-theme="light"]) {
|
|
18
23
|
--bg: #0d1117; --fg: #e6edf3; --muted: #8d96a0; --border: #30363d; --panel: #161b22;
|
|
19
24
|
--accent: #4493f8; --bad: #f85149; --warn: #d29922; --good: #3fb950;
|
|
25
|
+
color-scheme: dark;
|
|
20
26
|
}
|
|
21
27
|
}
|
|
28
|
+
:root[data-theme="dark"] {
|
|
29
|
+
--bg: #0d1117; --fg: #e6edf3; --muted: #8d96a0; --border: #30363d; --panel: #161b22;
|
|
30
|
+
--accent: #4493f8; --bad: #f85149; --warn: #d29922; --good: #3fb950;
|
|
31
|
+
color-scheme: dark;
|
|
32
|
+
}
|
|
22
33
|
[x-cloak] { display: none !important; }
|
|
23
34
|
* { box-sizing: border-box; }
|
|
24
35
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 system-ui, sans-serif; }
|
|
@@ -37,8 +48,10 @@
|
|
|
37
48
|
summary { cursor: pointer; }
|
|
38
49
|
textarea { width: 100%; min-height: 72px; font: 12px ui-monospace, monospace; background: var(--panel);
|
|
39
50
|
color: var(--fg); border: 1px solid var(--border); border-radius: 6px; padding: 6px; resize: vertical; }
|
|
40
|
-
button { font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 6px;
|
|
51
|
+
button, select { font: inherit; padding: 2px 10px; border: 1px solid var(--border); border-radius: 6px;
|
|
41
52
|
background: var(--panel); color: var(--fg); cursor: pointer; }
|
|
53
|
+
.toolbar { display: flex; justify-content: flex-end; flex-wrap: wrap; gap: 8px; }
|
|
54
|
+
.toolbar label { display: flex; align-items: center; gap: 6px; color: var(--muted); }
|
|
42
55
|
.muted { color: var(--muted); }
|
|
43
56
|
.bad { color: var(--bad); }
|
|
44
57
|
.warn { color: var(--warn); }
|
|
@@ -68,21 +81,36 @@
|
|
|
68
81
|
</head>
|
|
69
82
|
<body>
|
|
70
83
|
<main x-data="releaseScope" x-cloak>
|
|
71
|
-
<
|
|
84
|
+
<div class="toolbar">
|
|
85
|
+
<label><span x-text="t('language')"></span>
|
|
86
|
+
<select :value="lang" @change="setLang($event.target.value)">
|
|
87
|
+
<option value="en">English</option>
|
|
88
|
+
<option value="ru">Русский</option>
|
|
89
|
+
</select>
|
|
90
|
+
</label>
|
|
91
|
+
<label><span x-text="t('theme')"></span>
|
|
92
|
+
<select :value="theme" @change="setTheme($event.target.value)">
|
|
93
|
+
<option value="auto" x-text="t('themeAuto')"></option>
|
|
94
|
+
<option value="light" x-text="t('themeLight')"></option>
|
|
95
|
+
<option value="dark" x-text="t('themeDark')"></option>
|
|
96
|
+
</select>
|
|
97
|
+
</label>
|
|
98
|
+
</div>
|
|
99
|
+
<p x-show="failure" class="bad" x-text="t('loadFailed', { error: failure })"></p>
|
|
72
100
|
<template x-if="report">
|
|
73
101
|
<div>
|
|
74
|
-
<h1 x-text="report.jira_scope.length ? '
|
|
102
|
+
<h1 x-text="report.jira_scope.length ? t('titleIssues', { keys: report.jira_scope.join(', ') }) : t('title')"></h1>
|
|
75
103
|
<p class="muted">
|
|
76
|
-
|
|
77
|
-
<code x-text="report.production_environment"></code>
|
|
78
|
-
|
|
104
|
+
<span x-text="t('rangeStart', { at: collectedAt })"></span>
|
|
105
|
+
<code x-text="report.production_environment"></code>
|
|
106
|
+
<span x-text="report.jira_scope.length ? t('rangeEndIssues') : t('rangeEnd')"></span>
|
|
79
107
|
</p>
|
|
80
108
|
<ul x-show="report.jira_scope.length">
|
|
81
109
|
<template x-for="key in report.jira_scope" :key="key">
|
|
82
110
|
<li>
|
|
83
111
|
<a :href="issue(key) && issue(key).url" x-text="key"></a>
|
|
84
112
|
<span x-show="issue(key)" :class="statusClass(issue(key))" x-text="issue(key) && issue(key).status"></span>
|
|
85
|
-
<span x-text="issue(key) ? issue(key).summary : '
|
|
113
|
+
<span x-text="issue(key) ? issue(key).summary : t('notInJira')"></span>
|
|
86
114
|
</li>
|
|
87
115
|
</template>
|
|
88
116
|
</ul>
|
|
@@ -90,14 +118,14 @@
|
|
|
90
118
|
<p class="bad" x-text="message"></p>
|
|
91
119
|
</template>
|
|
92
120
|
|
|
93
|
-
<h2
|
|
94
|
-
<p x-show="pickable.length" class="muted"
|
|
95
|
-
<p x-show="ignored" class="warn" x-text="ignored"></p>
|
|
121
|
+
<h2 x-text="t('services')"></h2>
|
|
122
|
+
<p x-show="pickable.length" class="muted" x-text="t('servicesIntro')"></p>
|
|
123
|
+
<p x-show="ignored.length" class="warn" x-text="t('ignored', { picks: ignored.join(', ') })"></p>
|
|
96
124
|
<template x-for="service in listed" :key="service.project">
|
|
97
125
|
<details class="service" name="services">
|
|
98
126
|
<summary>
|
|
99
127
|
<span class="name" x-text="service.project"></span>
|
|
100
|
-
<span x-show="service.error" class="bad"
|
|
128
|
+
<span x-show="service.error" class="bad" x-text="t('failedToCollect')"></span>
|
|
101
129
|
<span x-show="!service.error">
|
|
102
130
|
<span class="muted" x-text="productionRef(service) + ' →'"></span>
|
|
103
131
|
<span x-show="pickOf(service)" class="picked-tag" x-text="picks[service.project]"></span>
|
|
@@ -105,7 +133,7 @@
|
|
|
105
133
|
</span>
|
|
106
134
|
<span class="muted" x-text="lineDetails(service)"></span>
|
|
107
135
|
<span x-text="failureCounts(service)"></span>
|
|
108
|
-
<span x-show="service.truncated" class="warn" title="
|
|
136
|
+
<span x-show="service.truncated" class="warn" :title="t('rangeCutTitle')" x-text="t('rangeCut')"></span>
|
|
109
137
|
</summary>
|
|
110
138
|
<div class="body">
|
|
111
139
|
<p>
|
|
@@ -119,7 +147,7 @@
|
|
|
119
147
|
<p class="warn" x-text="warning"></p>
|
|
120
148
|
</template>
|
|
121
149
|
<p x-show="service.release && service.release.pending_merge_requests.length">
|
|
122
|
-
|
|
150
|
+
<span x-text="t('notMerged')"></span>
|
|
123
151
|
<template x-for="item in service.release ? service.release.pending_merge_requests : []" :key="item.iid">
|
|
124
152
|
<a :href="item.url" x-text="'!' + item.iid + ' ' + item.title + ' '"></a>
|
|
125
153
|
</template>
|
|
@@ -127,7 +155,10 @@
|
|
|
127
155
|
<div class="scroll" x-show="service.rows.length">
|
|
128
156
|
<table>
|
|
129
157
|
<thead>
|
|
130
|
-
<tr
|
|
158
|
+
<tr>
|
|
159
|
+
<th x-text="t('columnTag')"></th><th x-text="t('columnChange')"></th><th>Jira</th>
|
|
160
|
+
<th x-text="t('columnDeployed')"></th><th x-text="t('columnFailed')"></th>
|
|
161
|
+
</tr>
|
|
131
162
|
</thead>
|
|
132
163
|
<tbody>
|
|
133
164
|
<template x-for="(row, index) in service.rows" :key="index">
|
|
@@ -137,14 +168,14 @@
|
|
|
137
168
|
<div class="tag">
|
|
138
169
|
<button type="button" :class="{ picked: picks[service.project] === tag.name }"
|
|
139
170
|
@click="pick(service, tag.name, $el)"
|
|
140
|
-
x-text="picks[service.project] === tag.name ? 'picked' : 'pick'"></button>
|
|
171
|
+
x-text="picks[service.project] === tag.name ? t('picked') : t('pick')"></button>
|
|
141
172
|
<a :href="tag.pipeline ? tag.pipeline.url : tag.url" x-text="tag.name"></a>
|
|
142
173
|
<span x-text="pipelineIcon(tag.pipeline)"></span>
|
|
143
174
|
</div>
|
|
144
175
|
</template>
|
|
145
176
|
</td>
|
|
146
177
|
<td>
|
|
147
|
-
<span x-show="row.linked" title="
|
|
178
|
+
<span x-show="row.linked" :title="t('linked')">🎯</span>
|
|
148
179
|
<template x-for="item in row.merge_requests" :key="item.iid">
|
|
149
180
|
<div>
|
|
150
181
|
<a :href="item.url" x-text="'!' + item.iid"></a>
|
|
@@ -157,7 +188,7 @@
|
|
|
157
188
|
<span x-text="item.title + (item.author ? ' · ' + item.author : '')"></span>
|
|
158
189
|
</div>
|
|
159
190
|
</template>
|
|
160
|
-
<span x-show="!row.in_scope" class="muted"
|
|
191
|
+
<span x-show="!row.in_scope" class="muted" x-text="t('outOfScope')"></span>
|
|
161
192
|
</td>
|
|
162
193
|
<td>
|
|
163
194
|
<template x-for="key in row.jira_keys" :key="key.key">
|
|
@@ -174,7 +205,7 @@
|
|
|
174
205
|
<div>
|
|
175
206
|
<span x-text="line.label"></span>
|
|
176
207
|
<a :href="line.url" x-text="line.name"></a>
|
|
177
|
-
<span x-show="line.allowed" class="muted"
|
|
208
|
+
<span x-show="line.allowed" class="muted" x-text="t('allowedToFail')"></span>
|
|
178
209
|
</div>
|
|
179
210
|
</template>
|
|
180
211
|
</td>
|
|
@@ -184,24 +215,24 @@
|
|
|
184
215
|
</table>
|
|
185
216
|
</div>
|
|
186
217
|
<p x-show="pickOf(service)">
|
|
187
|
-
<button type="button" @click="pick(service, '', $el)"
|
|
218
|
+
<button type="button" @click="pick(service, '', $el)" x-text="t('clearPick')"></button>
|
|
188
219
|
</p>
|
|
189
220
|
</div>
|
|
190
221
|
</details>
|
|
191
222
|
</template>
|
|
192
223
|
|
|
193
|
-
<h2
|
|
194
|
-
<p x-show="!pickable.length" class="muted"
|
|
195
|
-
<p x-show="pickable.length && !picked.length" class="muted"
|
|
196
|
-
<p x-show="
|
|
224
|
+
<h2 x-text="t('release')"></h2>
|
|
225
|
+
<p x-show="!pickable.length" class="muted" x-text="t('noCandidates')"></p>
|
|
226
|
+
<p x-show="pickable.length && !picked.length" class="muted" x-text="t('pickToFill')"></p>
|
|
227
|
+
<p x-show="copyFailed" class="warn" x-text="t('copyFailed')"></p>
|
|
197
228
|
|
|
198
229
|
<div class="outputs" x-show="picked.length">
|
|
199
230
|
<section>
|
|
200
231
|
<div class="out-head">
|
|
201
|
-
<h3 x-text="'
|
|
232
|
+
<h3 x-text="t('tasks', { count: tasks.length })"></h3>
|
|
202
233
|
<span>
|
|
203
|
-
<button type="button" @click="copy(taskKeys)"
|
|
204
|
-
<button type="button" @click="copy(taskJql)"
|
|
234
|
+
<button type="button" @click="copy(taskKeys)" x-text="t('copyKeys')"></button>
|
|
235
|
+
<button type="button" @click="copy(taskJql)" x-text="t('copyJql')"></button>
|
|
205
236
|
</span>
|
|
206
237
|
</div>
|
|
207
238
|
<ul>
|
|
@@ -210,7 +241,7 @@
|
|
|
210
241
|
<a :href="task.url" x-text="task.key"></a>
|
|
211
242
|
<span x-show="task.issue" :class="statusClass(task.issue)" x-text="task.issue && task.issue.status"></span>
|
|
212
243
|
<span x-text="task.issue ? task.issue.summary : ''"></span>
|
|
213
|
-
<span x-show="isMissing(task.key)" class="warn"
|
|
244
|
+
<span x-show="isMissing(task.key)" class="warn" x-text="t('notInJira')"></span>
|
|
214
245
|
</li>
|
|
215
246
|
</template>
|
|
216
247
|
</ul>
|
|
@@ -219,8 +250,8 @@
|
|
|
219
250
|
</section>
|
|
220
251
|
<section>
|
|
221
252
|
<div class="out-head">
|
|
222
|
-
<h3
|
|
223
|
-
<button type="button" @click="copy(pipelinesMarkdown)"
|
|
253
|
+
<h3 x-text="t('tagPipelines')"></h3>
|
|
254
|
+
<button type="button" @click="copy(pipelinesMarkdown)" x-text="t('copy')"></button>
|
|
224
255
|
</div>
|
|
225
256
|
<ul>
|
|
226
257
|
<template x-for="item in picked" :key="item.service.project">
|
|
@@ -230,7 +261,7 @@
|
|
|
230
261
|
<a :href="item.candidate.tag.pipeline.url"
|
|
231
262
|
x-text="pipelineIcon(item.candidate.tag.pipeline) + ' #' + item.candidate.tag.pipeline.id"></a>
|
|
232
263
|
</template>
|
|
233
|
-
<span x-show="!item.candidate.tag.pipeline" class="warn"
|
|
264
|
+
<span x-show="!item.candidate.tag.pipeline" class="warn" x-text="t('noPipeline')"></span>
|
|
234
265
|
</li>
|
|
235
266
|
</template>
|
|
236
267
|
</ul>
|
|
@@ -238,8 +269,8 @@
|
|
|
238
269
|
</section>
|
|
239
270
|
<section>
|
|
240
271
|
<div class="out-head">
|
|
241
|
-
<h3
|
|
242
|
-
<button type="button" @click="copy(compareMarkdown)"
|
|
272
|
+
<h3 x-text="t('compare')"></h3>
|
|
273
|
+
<button type="button" @click="copy(compareMarkdown)" x-text="t('copy')"></button>
|
|
243
274
|
</div>
|
|
244
275
|
<ul>
|
|
245
276
|
<template x-for="item in picked" :key="item.service.project">
|
|
@@ -253,14 +284,14 @@
|
|
|
253
284
|
</section>
|
|
254
285
|
<section class="wide">
|
|
255
286
|
<div class="out-head">
|
|
256
|
-
<h3
|
|
257
|
-
<button type="button" @click="copy(releasePost)"
|
|
287
|
+
<h3 x-text="t('releasePost')"></h3>
|
|
288
|
+
<button type="button" @click="copy(releasePost)" x-text="t('copy')"></button>
|
|
258
289
|
</div>
|
|
259
290
|
<textarea readonly rows="12" x-text="releasePost"></textarea>
|
|
260
291
|
</section>
|
|
261
292
|
</div>
|
|
262
293
|
|
|
263
|
-
<p class="muted"
|
|
294
|
+
<p class="muted" x-text="t('legend')"></p>
|
|
264
295
|
</div>
|
|
265
296
|
</template>
|
|
266
297
|
</main>
|
|
@@ -270,15 +301,160 @@
|
|
|
270
301
|
running: "🔄", scheduled: "🔄", canceled: "⏭", skipped: "⏭", manual: "⏭",
|
|
271
302
|
};
|
|
272
303
|
|
|
304
|
+
const STRINGS = {
|
|
305
|
+
en: {
|
|
306
|
+
language: "Language",
|
|
307
|
+
theme: "Theme",
|
|
308
|
+
themeAuto: "Auto",
|
|
309
|
+
themeLight: "Light",
|
|
310
|
+
themeDark: "Dark",
|
|
311
|
+
loadFailed: "Cannot load the report: {error}.",
|
|
312
|
+
title: "Release scope",
|
|
313
|
+
titleIssues: "Release scope: {keys}",
|
|
314
|
+
rangeStart: "Collected {at} UTC. Changes run from the commit on",
|
|
315
|
+
rangeEnd: "to the head of the default branch.",
|
|
316
|
+
rangeEndIssues:
|
|
317
|
+
"to the head of the default branch; rows above the latest merge request or commit linked to the issues are out of scope.",
|
|
318
|
+
notInJira: "not found in Jira",
|
|
319
|
+
services: "Services",
|
|
320
|
+
servicesIntro:
|
|
321
|
+
"Open a service and pick the tag to release: it ships every merge request and commit from its row down to production. The address keeps the picks, so share it to share them.",
|
|
322
|
+
ignored: "Ignored picks from the address that are not in this report: {picks}.",
|
|
323
|
+
failedToCollect: "failed to collect",
|
|
324
|
+
notInProduction: "not in production",
|
|
325
|
+
pickTag: "pick a tag",
|
|
326
|
+
noTagYet: "no tag yet",
|
|
327
|
+
upToDate: "up to date",
|
|
328
|
+
rows: { one: "{count} merge request or commit", other: "{count} merge requests or commits" },
|
|
329
|
+
taskCount: { one: "{count} task", other: "{count} tasks" },
|
|
330
|
+
shipsOf: "{shipped} of {rows}",
|
|
331
|
+
failedCount: "❌ {count} failed",
|
|
332
|
+
allowedCount: "{count} failed, allowed to fail",
|
|
333
|
+
rangeCut: "range cut short",
|
|
334
|
+
rangeCutTitle: "Older merge requests and commits were not collected; open for details",
|
|
335
|
+
notMerged: "Not merged:",
|
|
336
|
+
columnTag: "Tag",
|
|
337
|
+
columnChange: "Change",
|
|
338
|
+
columnDeployed: "Deployed to",
|
|
339
|
+
columnFailed: "Failed jobs",
|
|
340
|
+
pick: "pick",
|
|
341
|
+
picked: "picked",
|
|
342
|
+
linked: "Linked to the issues",
|
|
343
|
+
outOfScope: "out of scope",
|
|
344
|
+
tagJobs: "tag {name}",
|
|
345
|
+
allowedToFail: "(allowed to fail)",
|
|
346
|
+
clearPick: "Clear pick",
|
|
347
|
+
release: "Release",
|
|
348
|
+
noCandidates: "No service has a tag between production and the head of the default branch.",
|
|
349
|
+
pickToFill: "Pick a tag in a service above to fill the lists.",
|
|
350
|
+
copyFailed: "Copying needs HTTPS; select the text in the box instead.",
|
|
351
|
+
tasks: "Jira tasks ({count})",
|
|
352
|
+
copyKeys: "Copy keys",
|
|
353
|
+
copyJql: "Copy JQL",
|
|
354
|
+
copy: "Copy",
|
|
355
|
+
tagPipelines: "Tag pipelines",
|
|
356
|
+
noPipeline: "no pipeline",
|
|
357
|
+
compare: "Compare",
|
|
358
|
+
commitsTo: "commits to {tag}",
|
|
359
|
+
releasePost: "Release post",
|
|
360
|
+
postServices: "**Services**",
|
|
361
|
+
postTasks: "**Jira tasks ({count})**",
|
|
362
|
+
postNone: "- none",
|
|
363
|
+
postPipeline: "pipeline #{id}",
|
|
364
|
+
legend: "Legend: ✅ success · ❌ failed · 🔄 running · ⏭ canceled or skipped · ⚠️ no pipeline",
|
|
365
|
+
},
|
|
366
|
+
ru: {
|
|
367
|
+
language: "Язык",
|
|
368
|
+
theme: "Тема",
|
|
369
|
+
themeAuto: "Как в системе",
|
|
370
|
+
themeLight: "Светлая",
|
|
371
|
+
themeDark: "Тёмная",
|
|
372
|
+
loadFailed: "Не удалось загрузить отчёт: {error}.",
|
|
373
|
+
title: "Состав релиза",
|
|
374
|
+
titleIssues: "Состав релиза: {keys}",
|
|
375
|
+
rangeStart: "Собрано {at} UTC. Изменения считаются от коммита на",
|
|
376
|
+
rangeEnd: "до головы основной ветки.",
|
|
377
|
+
rangeEndIssues:
|
|
378
|
+
"до головы основной ветки; строки выше последнего MR или коммита, связанного с задачами, вне скоупа.",
|
|
379
|
+
notInJira: "нет в Jira",
|
|
380
|
+
services: "Сервисы",
|
|
381
|
+
servicesIntro:
|
|
382
|
+
"Откройте сервис и выберите тег для релиза: в него войдут все MR и коммиты от его строки до прода. Выбор хранится в адресе страницы, так что им можно поделиться ссылкой.",
|
|
383
|
+
ignored: "Из адреса пропущены теги, которых нет в этом отчёте: {picks}.",
|
|
384
|
+
failedToCollect: "не удалось собрать",
|
|
385
|
+
notInProduction: "нет на проде",
|
|
386
|
+
pickTag: "выберите тег",
|
|
387
|
+
noTagYet: "тега ещё нет",
|
|
388
|
+
upToDate: "всё на проде",
|
|
389
|
+
rows: { one: "{count} MR или коммит", few: "{count} MR или коммита", many: "{count} MR или коммитов" },
|
|
390
|
+
taskCount: { one: "{count} задача", few: "{count} задачи", many: "{count} задач" },
|
|
391
|
+
shipsOf: "{shipped} из {rows}",
|
|
392
|
+
failedCount: "❌ упало: {count}",
|
|
393
|
+
allowedCount: "упало, но падать разрешено: {count}",
|
|
394
|
+
rangeCut: "диапазон обрезан",
|
|
395
|
+
rangeCutTitle: "Старые MR и коммиты не собраны; подробности внутри",
|
|
396
|
+
notMerged: "Не влиты:",
|
|
397
|
+
columnTag: "Тег",
|
|
398
|
+
columnChange: "Изменение",
|
|
399
|
+
columnDeployed: "Задеплоено на",
|
|
400
|
+
columnFailed: "Упавшие джобы",
|
|
401
|
+
pick: "выбрать",
|
|
402
|
+
picked: "выбран",
|
|
403
|
+
linked: "Связано с задачами",
|
|
404
|
+
outOfScope: "вне скоупа",
|
|
405
|
+
tagJobs: "тег {name}",
|
|
406
|
+
allowedToFail: "(падать разрешено)",
|
|
407
|
+
clearPick: "Сбросить выбор",
|
|
408
|
+
release: "Релиз",
|
|
409
|
+
noCandidates: "Ни у одного сервиса нет тега между продом и головой основной ветки.",
|
|
410
|
+
pickToFill: "Выберите тег в сервисе выше, чтобы заполнить списки.",
|
|
411
|
+
copyFailed: "Кнопка копирования работает только по HTTPS; выделите текст в поле.",
|
|
412
|
+
tasks: "Задачи Jira ({count})",
|
|
413
|
+
copyKeys: "Копировать ключи",
|
|
414
|
+
copyJql: "Копировать JQL",
|
|
415
|
+
copy: "Копировать",
|
|
416
|
+
tagPipelines: "Пайплайны тегов",
|
|
417
|
+
noPipeline: "нет пайплайна",
|
|
418
|
+
compare: "Сравнение",
|
|
419
|
+
commitsTo: "коммиты до {tag}",
|
|
420
|
+
releasePost: "Пост о релизе",
|
|
421
|
+
postServices: "**Сервисы**",
|
|
422
|
+
postTasks: "**Задачи Jira ({count})**",
|
|
423
|
+
postNone: "- нет",
|
|
424
|
+
postPipeline: "пайплайн #{id}",
|
|
425
|
+
legend: "Обозначения: ✅ успешно · ❌ упал · 🔄 выполняется · ⏭ отменён или пропущен · ⚠️ нет пайплайна",
|
|
426
|
+
},
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const PLURALS = Object.fromEntries(Object.keys(STRINGS).map((lang) => [lang, new Intl.PluralRules(lang)]));
|
|
430
|
+
|
|
431
|
+
const stored = (key) => {
|
|
432
|
+
try {
|
|
433
|
+
return localStorage.getItem(key);
|
|
434
|
+
} catch {
|
|
435
|
+
return null;
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const store = (key, value) => {
|
|
439
|
+
try {
|
|
440
|
+
localStorage.setItem(key, value);
|
|
441
|
+
} catch {}
|
|
442
|
+
};
|
|
443
|
+
|
|
273
444
|
document.addEventListener("alpine:init", () => {
|
|
274
445
|
Alpine.data("releaseScope", () => ({
|
|
275
446
|
report: null,
|
|
276
447
|
failure: "",
|
|
277
|
-
|
|
278
|
-
ignored:
|
|
448
|
+
copyFailed: false,
|
|
449
|
+
ignored: [],
|
|
279
450
|
picks: {},
|
|
451
|
+
lang: "en",
|
|
452
|
+
theme: "auto",
|
|
280
453
|
|
|
281
454
|
async init() {
|
|
455
|
+
const lang = stored("release-scope-lang");
|
|
456
|
+
this.setLang(lang in STRINGS ? lang : navigator.language.toLowerCase().startsWith("ru") ? "ru" : "en");
|
|
457
|
+
this.theme = document.documentElement.dataset.theme || "auto";
|
|
282
458
|
try {
|
|
283
459
|
const response = await fetch("report.json", { cache: "no-store" });
|
|
284
460
|
if (!response.ok) throw new Error(`report.json returned ${response.status}`);
|
|
@@ -291,10 +467,31 @@
|
|
|
291
467
|
this.readHash();
|
|
292
468
|
window.addEventListener("hashchange", () => this.readHash());
|
|
293
469
|
} catch (error) {
|
|
294
|
-
this.failure =
|
|
470
|
+
this.failure = error.message;
|
|
295
471
|
}
|
|
296
472
|
},
|
|
297
473
|
|
|
474
|
+
t(key, params = {}) {
|
|
475
|
+
let text = STRINGS[this.lang][key] ?? STRINGS.en[key];
|
|
476
|
+
if (typeof text === "object") {
|
|
477
|
+
const form = PLURALS[this.lang].select(params.count);
|
|
478
|
+
text = text[form] ?? text.many ?? text.other;
|
|
479
|
+
}
|
|
480
|
+
return text.replace(/\{(\w+)\}/g, (match, name) => (name in params ? params[name] : match));
|
|
481
|
+
},
|
|
482
|
+
setLang(lang) {
|
|
483
|
+
this.lang = lang;
|
|
484
|
+
document.documentElement.lang = lang;
|
|
485
|
+
document.title = STRINGS[lang].title;
|
|
486
|
+
store("release-scope-lang", lang);
|
|
487
|
+
},
|
|
488
|
+
setTheme(theme) {
|
|
489
|
+
this.theme = theme;
|
|
490
|
+
if (theme === "auto") delete document.documentElement.dataset.theme;
|
|
491
|
+
else document.documentElement.dataset.theme = theme;
|
|
492
|
+
store("release-scope-theme", theme);
|
|
493
|
+
},
|
|
494
|
+
|
|
298
495
|
get collectedAt() {
|
|
299
496
|
return this.report.collected_at.slice(0, 16).replace("T", " ");
|
|
300
497
|
},
|
|
@@ -336,19 +533,27 @@
|
|
|
336
533
|
.map(({ service, candidate }) => {
|
|
337
534
|
const pipeline = candidate.tag.pipeline;
|
|
338
535
|
const label = `${service.project} ${candidate.tag.name}`;
|
|
339
|
-
return pipeline ? `- [${label}](${pipeline.url}) ${pipeline.status}` : `- ${label}:
|
|
536
|
+
return pipeline ? `- [${label}](${pipeline.url}) ${pipeline.status}` : `- ${label}: ${this.t("noPipeline")}`;
|
|
340
537
|
})
|
|
341
538
|
.join("\n");
|
|
342
539
|
},
|
|
343
540
|
get releasePost() {
|
|
344
541
|
const services = this.picked.map((item) => {
|
|
345
542
|
const pipeline = item.candidate.tag.pipeline;
|
|
346
|
-
const build = pipeline
|
|
543
|
+
const build = pipeline
|
|
544
|
+
? `[${this.t("postPipeline", { id: pipeline.id })}](${pipeline.url}) ${pipeline.status}`
|
|
545
|
+
: this.t("noPipeline");
|
|
347
546
|
const compare = `[${this.compareLabel(item)}](${item.candidate.compare_url})`;
|
|
348
547
|
return `- ${item.service.project} ${item.candidate.tag.name}: ${compare} · ${build}`;
|
|
349
548
|
});
|
|
350
549
|
const tasks = this.tasks.map((task) => `- [${task.key}](${task.url})${task.issue ? " " + task.issue.summary : ""}`);
|
|
351
|
-
return [
|
|
550
|
+
return [
|
|
551
|
+
this.t("postServices"),
|
|
552
|
+
...services,
|
|
553
|
+
"",
|
|
554
|
+
this.t("postTasks", { count: tasks.length }),
|
|
555
|
+
...(tasks.length ? tasks : [this.t("postNone")]),
|
|
556
|
+
].join("\n");
|
|
352
557
|
},
|
|
353
558
|
get compareMarkdown() {
|
|
354
559
|
return this.picked
|
|
@@ -381,8 +586,7 @@
|
|
|
381
586
|
if (known) wanted.delete(service.project);
|
|
382
587
|
}
|
|
383
588
|
}
|
|
384
|
-
|
|
385
|
-
this.ignored = unknown.length ? `Ignored picks from the address that are not in this report: ${unknown.join(", ")}.` : "";
|
|
589
|
+
this.ignored = [...wanted].map(([project, tag]) => (tag ? `${project} ${tag}` : project));
|
|
386
590
|
this.writeHash();
|
|
387
591
|
},
|
|
388
592
|
writeHash() {
|
|
@@ -394,20 +598,19 @@
|
|
|
394
598
|
return service.candidates.find((item) => item.tag.name === this.picks[service.project]);
|
|
395
599
|
},
|
|
396
600
|
pickHint(service) {
|
|
397
|
-
if (service.candidates.length) return "
|
|
398
|
-
return service.rows.length ? "
|
|
601
|
+
if (service.candidates.length) return this.t("pickTag");
|
|
602
|
+
return service.rows.length ? this.t("noTagYet") : this.t("upToDate");
|
|
399
603
|
},
|
|
400
604
|
lineDetails(service) {
|
|
401
605
|
const candidate = this.pickOf(service);
|
|
606
|
+
const rows = this.t("rows", { count: service.rows.length });
|
|
402
607
|
if (candidate) {
|
|
403
|
-
|
|
608
|
+
const shipped = this.t("shipsOf", { shipped: candidate.rows, rows });
|
|
609
|
+
return `${shipped} · ${this.t("taskCount", { count: candidate.jira_keys.length })}`;
|
|
404
610
|
}
|
|
405
611
|
if (!service.rows.length) return "";
|
|
406
612
|
const keys = new Set(service.rows.filter((row) => row.in_scope).flatMap((row) => row.jira_keys.map((key) => key.key)));
|
|
407
|
-
return `${
|
|
408
|
-
},
|
|
409
|
-
rowsLabel(count) {
|
|
410
|
-
return count === 1 ? "1 merge request or commit" : `${count} merge requests or commits`;
|
|
613
|
+
return `${rows} · ${this.t("taskCount", { count: keys.size })}`;
|
|
411
614
|
},
|
|
412
615
|
ships(service, index) {
|
|
413
616
|
const tag = this.picks[service.project];
|
|
@@ -432,25 +635,25 @@
|
|
|
432
635
|
},
|
|
433
636
|
productionRef(service) {
|
|
434
637
|
const production = this.production(service);
|
|
435
|
-
if (!production) return "
|
|
638
|
+
if (!production) return this.t("notInProduction");
|
|
436
639
|
return production.tag ? production.ref : production.sha.slice(0, 8);
|
|
437
640
|
},
|
|
438
641
|
compareLabel({ service, candidate }) {
|
|
439
|
-
if (!this.production(service)) return
|
|
642
|
+
if (!this.production(service)) return this.t("commitsTo", { tag: candidate.tag.name });
|
|
440
643
|
return `${this.productionRef(service)}...${candidate.tag.name}`;
|
|
441
644
|
},
|
|
442
645
|
pipelineIcon(pipeline) {
|
|
443
646
|
return pipeline ? ICONS[pipeline.status] || pipeline.status : "⚠️";
|
|
444
647
|
},
|
|
445
|
-
plural(count, noun) {
|
|
446
|
-
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
447
|
-
},
|
|
448
648
|
failureCounts(service) {
|
|
449
649
|
const pipelines = service.rows.flatMap((row) => [row.main_pipeline, ...row.tags.map((tag) => tag.pipeline)]);
|
|
450
650
|
const jobs = pipelines.filter(Boolean).flatMap((pipeline) => pipeline.failed_jobs);
|
|
451
651
|
const allowed = jobs.filter((job) => job.allow_failure).length;
|
|
452
652
|
const blocking = jobs.length - allowed;
|
|
453
|
-
return [
|
|
653
|
+
return [
|
|
654
|
+
blocking ? this.t("failedCount", { count: blocking }) : "",
|
|
655
|
+
allowed ? this.t("allowedCount", { count: allowed }) : "",
|
|
656
|
+
].filter(Boolean).join(" · ");
|
|
454
657
|
},
|
|
455
658
|
failedJobs(row) {
|
|
456
659
|
const lines = [];
|
|
@@ -460,15 +663,15 @@
|
|
|
460
663
|
}
|
|
461
664
|
};
|
|
462
665
|
if (row.main_pipeline) add(`main ${this.pipelineIcon(row.main_pipeline)}`, row.main_pipeline);
|
|
463
|
-
for (const tag of row.tags) if (tag.pipeline) add(
|
|
666
|
+
for (const tag of row.tags) if (tag.pipeline) add(this.t("tagJobs", { name: tag.name }), tag.pipeline);
|
|
464
667
|
return lines;
|
|
465
668
|
},
|
|
466
669
|
async copy(text) {
|
|
467
670
|
try {
|
|
468
671
|
await navigator.clipboard.writeText(text);
|
|
469
|
-
this.
|
|
672
|
+
this.copyFailed = false;
|
|
470
673
|
} catch {
|
|
471
|
-
this.
|
|
674
|
+
this.copyFailed = true;
|
|
472
675
|
}
|
|
473
676
|
},
|
|
474
677
|
}));
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|