release-scope 0.5.0__tar.gz → 0.5.2__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.0 → release_scope-0.5.2}/PKG-INFO +27 -17
- {release_scope-0.5.0 → release_scope-0.5.2}/README.md +25 -15
- {release_scope-0.5.0 → release_scope-0.5.2}/pyproject.toml +2 -2
- {release_scope-0.5.0 → release_scope-0.5.2}/pyproject.toml.orig +2 -2
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_candidates.py +7 -3
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_static/index.html +77 -14
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_use_case.py +13 -5
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/__init__.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/__main__.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_cache.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_errors.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_files.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_gitlab.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_jira.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_jira_keys.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_links.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_messages.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_report.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_rows.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_settings.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_static/alpine.LICENSE.txt +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/_static/alpine.min.js +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/ioc.py +0 -0
- {release_scope-0.5.0 → release_scope-0.5.2}/release_scope/py.typed +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: release-scope
|
|
3
|
-
Version: 0.5.
|
|
4
|
-
Summary: Collect what sits between production and the default branch across GitLab services
|
|
3
|
+
Version: 0.5.2
|
|
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
|
|
7
7
|
Author-email: Artur Shiriev <me@shiriev.ru>
|
|
@@ -54,9 +54,9 @@ Description-Content-Type: text/markdown
|
|
|
54
54
|
Jira issues, failed jobs.
|
|
55
55
|
|
|
56
56
|
For every service it reads the latest successful production deployment, walks the default branch down to that
|
|
57
|
-
commit,
|
|
58
|
-
|
|
59
|
-
and tag pipelines. With a Jira token, it also reads the summary and status of every key in one batched search,
|
|
57
|
+
commit, or down to its first commit when the service was never deployed to production, and writes a static site with
|
|
58
|
+
one JSON report: a row per merge request or direct commit, newest first, with the tags that point into it, the
|
|
59
|
+
environments running it, the Jira keys its MR mentions, and the failed jobs of its main-branch and tag pipelines. With a Jira token, it also reads the summary and status of every key in one batched search,
|
|
60
60
|
and the GitLab merge requests and commits linked to each issue. GitLab's Jira integration adds those links to the
|
|
61
61
|
issue's Web links whenever a commit or MR mentions it; a link counts only if it starts with
|
|
62
62
|
`RELEASE_SCOPE_GITLAB__ENDPOINT`. The projects they point to are the issue's related services.
|
|
@@ -120,7 +120,8 @@ The report is versioned by `schema_version`; the models live in
|
|
|
120
120
|
Top-level `jira` is `null` without a Jira token; otherwise it holds `issues` by key (summary, status, status category,
|
|
121
121
|
issue type, linked GitLab changes), the `missing` keys Jira did not return, and an `error` if a Jira request failed.
|
|
122
122
|
Each service lists its `candidates`: the tags a release could ship, newest first, each with its pipeline, the number of
|
|
123
|
-
rows it ships, the in-scope Jira keys of those rows, and the compare link from production
|
|
123
|
+
rows it ships, the in-scope Jira keys of those rows, and the compare link from production, or the tag's commit history
|
|
124
|
+
when the service has no production deployment. One row, trimmed:
|
|
124
125
|
|
|
125
126
|
```json
|
|
126
127
|
{
|
|
@@ -140,22 +141,31 @@ Besides `report.json`, `collect` writes `index.html` and its script into the out
|
|
|
140
141
|
installed package and change only with it, so the page always matches the report schema. The page loads
|
|
141
142
|
`report.json` from next to itself; it needs a web server, not a `file://` URL.
|
|
142
143
|
|
|
143
|
-
**Services** lists every service with a production deployment, and every service that failed to collect,
|
|
144
|
-
line: what production runs, the picked tag, how many merge requests or commits and Jira tasks it ships, failed jobs
|
|
145
|
-
and a mark when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`.
|
|
146
|
-
warnings, merge requests that are not merged yet, and its rows with
|
|
147
|
-
commits, Jira keys, environments, and failed jobs; rows
|
|
144
|
+
**Services** lists every service with a production deployment or with rows, and every service that failed to collect,
|
|
145
|
+
as one line: what production runs, the picked tag, how many merge requests or commits and Jira tasks it ships, failed jobs
|
|
146
|
+
with the ones allowed to fail counted apart, and a mark when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`.
|
|
147
|
+
Opening a line shows the service's environments, warnings, merge requests that are not merged yet, and its rows with
|
|
148
|
+
tags and their pipelines, merge requests or commits, Jira keys with their status, environments, and failed jobs; rows
|
|
149
|
+
out of scope are dimmed. Links open in a new tab. Each tag has a **pick** button:
|
|
148
150
|
picking it highlights the rows it ships and closes the line again. A `--jira` report starts with each service's
|
|
149
|
-
release tag picked. Services
|
|
151
|
+
release tag picked. Services with neither a production deployment nor rows are left out of the page.
|
|
150
152
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
+
The address keeps the picks after `#`, as `team/api=2.4.0&team/web=5.12.0`, so sharing or reloading the page keeps
|
|
154
|
+
them. An address with picks replaces the release tags of a `--jira` report; picks that are not in the report, such as
|
|
155
|
+
a tag that a later run no longer lists, are skipped with a warning. Clearing every pick empties the address, so a
|
|
156
|
+
reload starts from the release tags again.
|
|
157
|
+
|
|
158
|
+
**Release** at the bottom turns the picked tags into three lists and a release post, each with a copy button and a
|
|
159
|
+
text box to copy from by hand, since browsers allow the copy button only over HTTPS:
|
|
153
160
|
|
|
154
161
|
- **Jira tasks**: the keys of every in-scope row from each picked tag down to production, without duplicates, with
|
|
155
|
-
summary and status,
|
|
156
|
-
each in its own box.
|
|
162
|
+
summary and a status badge: grey to do, blue in progress, green done. Copy them one per line or as a JQL
|
|
163
|
+
`key in (...)` clause, each in its own box.
|
|
157
164
|
- **Tag pipelines**: the pipeline of each picked tag, as a Markdown list.
|
|
158
|
-
- **Compare**: a GitLab compare link per service from production to the picked tag, as a Markdown list.
|
|
165
|
+
- **Compare**: a GitLab compare link per service from production to the picked tag, as a Markdown list. A service
|
|
166
|
+
never deployed to production gets the commit history of the tag instead.
|
|
167
|
+
- **Release post**: one Markdown text with a line per picked tag, holding its compare link and pipeline, followed by
|
|
168
|
+
the Jira tasks with their summaries.
|
|
159
169
|
|
|
160
170
|
## GitLab Pages
|
|
161
171
|
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
Jira issues, failed jobs.
|
|
22
22
|
|
|
23
23
|
For every service it reads the latest successful production deployment, walks the default branch down to that
|
|
24
|
-
commit,
|
|
25
|
-
|
|
26
|
-
and tag pipelines. With a Jira token, it also reads the summary and status of every key in one batched search,
|
|
24
|
+
commit, or down to its first commit when the service was never deployed to production, and writes a static site with
|
|
25
|
+
one JSON report: a row per merge request or direct commit, newest first, with the tags that point into it, the
|
|
26
|
+
environments running it, the Jira keys its MR mentions, and the failed jobs of its main-branch and tag pipelines. With a Jira token, it also reads the summary and status of every key in one batched search,
|
|
27
27
|
and the GitLab merge requests and commits linked to each issue. GitLab's Jira integration adds those links to the
|
|
28
28
|
issue's Web links whenever a commit or MR mentions it; a link counts only if it starts with
|
|
29
29
|
`RELEASE_SCOPE_GITLAB__ENDPOINT`. The projects they point to are the issue's related services.
|
|
@@ -87,7 +87,8 @@ The report is versioned by `schema_version`; the models live in
|
|
|
87
87
|
Top-level `jira` is `null` without a Jira token; otherwise it holds `issues` by key (summary, status, status category,
|
|
88
88
|
issue type, linked GitLab changes), the `missing` keys Jira did not return, and an `error` if a Jira request failed.
|
|
89
89
|
Each service lists its `candidates`: the tags a release could ship, newest first, each with its pipeline, the number of
|
|
90
|
-
rows it ships, the in-scope Jira keys of those rows, and the compare link from production
|
|
90
|
+
rows it ships, the in-scope Jira keys of those rows, and the compare link from production, or the tag's commit history
|
|
91
|
+
when the service has no production deployment. One row, trimmed:
|
|
91
92
|
|
|
92
93
|
```json
|
|
93
94
|
{
|
|
@@ -107,22 +108,31 @@ Besides `report.json`, `collect` writes `index.html` and its script into the out
|
|
|
107
108
|
installed package and change only with it, so the page always matches the report schema. The page loads
|
|
108
109
|
`report.json` from next to itself; it needs a web server, not a `file://` URL.
|
|
109
110
|
|
|
110
|
-
**Services** lists every service with a production deployment, and every service that failed to collect,
|
|
111
|
-
line: what production runs, the picked tag, how many merge requests or commits and Jira tasks it ships, failed jobs
|
|
112
|
-
and a mark when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`.
|
|
113
|
-
warnings, merge requests that are not merged yet, and its rows with
|
|
114
|
-
commits, Jira keys, environments, and failed jobs; rows
|
|
111
|
+
**Services** lists every service with a production deployment or with rows, and every service that failed to collect,
|
|
112
|
+
as one line: what production runs, the picked tag, how many merge requests or commits and Jira tasks it ships, failed jobs
|
|
113
|
+
with the ones allowed to fail counted apart, and a mark when the range was cut at `RELEASE_SCOPE_MAX_COMMITS`.
|
|
114
|
+
Opening a line shows the service's environments, warnings, merge requests that are not merged yet, and its rows with
|
|
115
|
+
tags and their pipelines, merge requests or commits, Jira keys with their status, environments, and failed jobs; rows
|
|
116
|
+
out of scope are dimmed. Links open in a new tab. Each tag has a **pick** button:
|
|
115
117
|
picking it highlights the rows it ships and closes the line again. A `--jira` report starts with each service's
|
|
116
|
-
release tag picked. Services
|
|
118
|
+
release tag picked. Services with neither a production deployment nor rows are left out of the page.
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
The address keeps the picks after `#`, as `team/api=2.4.0&team/web=5.12.0`, so sharing or reloading the page keeps
|
|
121
|
+
them. An address with picks replaces the release tags of a `--jira` report; picks that are not in the report, such as
|
|
122
|
+
a tag that a later run no longer lists, are skipped with a warning. Clearing every pick empties the address, so a
|
|
123
|
+
reload starts from the release tags again.
|
|
124
|
+
|
|
125
|
+
**Release** at the bottom turns the picked tags into three lists and a release post, each with a copy button and a
|
|
126
|
+
text box to copy from by hand, since browsers allow the copy button only over HTTPS:
|
|
120
127
|
|
|
121
128
|
- **Jira tasks**: the keys of every in-scope row from each picked tag down to production, without duplicates, with
|
|
122
|
-
summary and status,
|
|
123
|
-
each in its own box.
|
|
129
|
+
summary and a status badge: grey to do, blue in progress, green done. Copy them one per line or as a JQL
|
|
130
|
+
`key in (...)` clause, each in its own box.
|
|
124
131
|
- **Tag pipelines**: the pipeline of each picked tag, as a Markdown list.
|
|
125
|
-
- **Compare**: a GitLab compare link per service from production to the picked tag, as a Markdown list.
|
|
132
|
+
- **Compare**: a GitLab compare link per service from production to the picked tag, as a Markdown list. A service
|
|
133
|
+
never deployed to production gets the commit history of the tag instead.
|
|
134
|
+
- **Release post**: one Markdown text with a line per picked tag, holding its compare link and pipeline, followed by
|
|
135
|
+
the Jira tasks with their summaries.
|
|
126
136
|
|
|
127
137
|
## GitLab Pages
|
|
128
138
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "release-scope"
|
|
3
|
-
description = "Collect what sits between production and the default branch across GitLab services
|
|
3
|
+
description = "Collect what sits between production and the default branch across GitLab services into a GitLab Pages site for picking release tags"
|
|
4
4
|
requires-python = ">=3.11,<4"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
readme = "README.md"
|
|
@@ -25,7 +25,7 @@ classifiers = [
|
|
|
25
25
|
"Typing :: Typed",
|
|
26
26
|
"Topic :: Software Development :: Build Tools",
|
|
27
27
|
]
|
|
28
|
-
version = "0.5.
|
|
28
|
+
version = "0.5.2"
|
|
29
29
|
dependencies = [
|
|
30
30
|
"typer>=0.13",
|
|
31
31
|
"pydantic>=2; python_version < '3.12'",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "release-scope"
|
|
3
|
-
description = "Collect what sits between production and the default branch across GitLab services
|
|
3
|
+
description = "Collect what sits between production and the default branch across GitLab services into a GitLab Pages site for picking release tags"
|
|
4
4
|
authors = [{ name = "Artur Shiriev", email = "me@shiriev.ru" }]
|
|
5
5
|
requires-python = ">=3.11,<4"
|
|
6
6
|
license = "MIT"
|
|
@@ -17,7 +17,7 @@ classifiers = [
|
|
|
17
17
|
"Typing :: Typed",
|
|
18
18
|
"Topic :: Software Development :: Build Tools",
|
|
19
19
|
]
|
|
20
|
-
version = "0.5.
|
|
20
|
+
version = "0.5.2"
|
|
21
21
|
dependencies = [
|
|
22
22
|
"typer>=0.13",
|
|
23
23
|
# The first release whose pydantic-core ships a wheel for each interpreter.
|
|
@@ -3,8 +3,10 @@ from urllib.parse import quote
|
|
|
3
3
|
from release_scope._report import Candidate, EnvironmentState, Service
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def build_candidates(service: Service, production: EnvironmentState) -> list[Candidate]:
|
|
7
|
-
base: str
|
|
6
|
+
def build_candidates(service: Service, production: EnvironmentState | None) -> list[Candidate]:
|
|
7
|
+
base: str | None = None
|
|
8
|
+
if production is not None:
|
|
9
|
+
base = quote(production.ref if production.tag else production.sha)
|
|
8
10
|
candidates: list[Candidate] = []
|
|
9
11
|
for index, row in enumerate(service.rows):
|
|
10
12
|
shipped = service.rows[index:]
|
|
@@ -12,7 +14,9 @@ def build_candidates(service: Service, production: EnvironmentState) -> list[Can
|
|
|
12
14
|
candidates.extend(
|
|
13
15
|
Candidate(
|
|
14
16
|
tag=tag,
|
|
15
|
-
compare_url=f"{service.project_url}/-/compare/{base}...{quote(tag.name)}"
|
|
17
|
+
compare_url=f"{service.project_url}/-/compare/{base}...{quote(tag.name)}"
|
|
18
|
+
if base
|
|
19
|
+
else f"{service.project_url}/-/commits/{quote(tag.name)}",
|
|
16
20
|
rows=len(shipped),
|
|
17
21
|
jira_keys=list(keys.values()),
|
|
18
22
|
)
|
|
@@ -4,18 +4,19 @@
|
|
|
4
4
|
<meta charset="utf-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
6
|
<title>Release scope</title>
|
|
7
|
+
<base target="_blank">
|
|
7
8
|
<script>
|
|
8
9
|
if (!/(\/|\.html)$/.test(location.pathname)) location.replace(location.pathname + "/" + location.search + location.hash);
|
|
9
10
|
</script>
|
|
10
11
|
<style>
|
|
11
12
|
:root {
|
|
12
13
|
--bg: #ffffff; --fg: #1f2328; --muted: #656d76; --border: #d0d7de; --panel: #f6f8fa;
|
|
13
|
-
--accent: #0969da; --bad: #cf222e; --warn: #9a6700;
|
|
14
|
+
--accent: #0969da; --bad: #cf222e; --warn: #9a6700; --good: #1a7f37;
|
|
14
15
|
}
|
|
15
16
|
@media (prefers-color-scheme: dark) {
|
|
16
17
|
:root {
|
|
17
18
|
--bg: #0d1117; --fg: #e6edf3; --muted: #8d96a0; --border: #30363d; --panel: #161b22;
|
|
18
|
-
--accent: #4493f8; --bad: #f85149; --warn: #d29922;
|
|
19
|
+
--accent: #4493f8; --bad: #f85149; --warn: #d29922; --good: #3fb950;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
22
|
[x-cloak] { display: none !important; }
|
|
@@ -43,6 +44,7 @@
|
|
|
43
44
|
.warn { color: var(--warn); }
|
|
44
45
|
.scroll { overflow-x: auto; }
|
|
45
46
|
.outputs { display: grid; grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); gap: 12px; margin-top: 16px; }
|
|
47
|
+
.outputs .wide { grid-column: 1 / -1; }
|
|
46
48
|
.out-head { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
47
49
|
tr.out-of-scope td { opacity: 0.55; }
|
|
48
50
|
tr.shipped td { background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
|
@@ -57,6 +59,10 @@
|
|
|
57
59
|
.service .body { padding: 8px 12px; }
|
|
58
60
|
.service .body > p:first-child { margin-top: 0; }
|
|
59
61
|
.picked-tag { font-weight: 600; }
|
|
62
|
+
.status { display: inline-block; padding: 0 8px; border: 1px solid currentColor; border-radius: 10px; font-size: 12px;
|
|
63
|
+
font-weight: 600; white-space: nowrap; color: var(--muted); }
|
|
64
|
+
.status-indeterminate { color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); }
|
|
65
|
+
.status-done { color: var(--good); background: color-mix(in srgb, var(--good) 12%, transparent); }
|
|
60
66
|
button.picked { border-color: var(--accent); color: var(--accent); }
|
|
61
67
|
</style>
|
|
62
68
|
</head>
|
|
@@ -75,7 +81,8 @@
|
|
|
75
81
|
<template x-for="key in report.jira_scope" :key="key">
|
|
76
82
|
<li>
|
|
77
83
|
<a :href="issue(key) && issue(key).url" x-text="key"></a>
|
|
78
|
-
<span x-
|
|
84
|
+
<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 : 'not found in Jira'"></span>
|
|
79
86
|
</li>
|
|
80
87
|
</template>
|
|
81
88
|
</ul>
|
|
@@ -84,8 +91,9 @@
|
|
|
84
91
|
</template>
|
|
85
92
|
|
|
86
93
|
<h2>Services</h2>
|
|
87
|
-
<p x-show="pickable.length" class="muted">Open a service and pick the tag to release: it ships every merge request and commit from its row down to production.</p>
|
|
88
|
-
<
|
|
94
|
+
<p x-show="pickable.length" class="muted">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.</p>
|
|
95
|
+
<p x-show="ignored" class="warn" x-text="ignored"></p>
|
|
96
|
+
<template x-for="service in listed" :key="service.project">
|
|
89
97
|
<details class="service" name="services">
|
|
90
98
|
<summary>
|
|
91
99
|
<span class="name" x-text="service.project"></span>
|
|
@@ -155,7 +163,8 @@
|
|
|
155
163
|
<template x-for="key in row.jira_keys" :key="key.key">
|
|
156
164
|
<div>
|
|
157
165
|
<a :href="key.url" x-text="key.key"></a>
|
|
158
|
-
<span x-
|
|
166
|
+
<span x-show="issue(key.key)" :class="statusClass(issue(key.key))" x-text="issue(key.key) && issue(key.key).status"></span>
|
|
167
|
+
<span x-text="issue(key.key) ? issue(key.key).summary : ''"></span>
|
|
159
168
|
</div>
|
|
160
169
|
</template>
|
|
161
170
|
</td>
|
|
@@ -165,7 +174,7 @@
|
|
|
165
174
|
<div>
|
|
166
175
|
<span x-text="line.label"></span>
|
|
167
176
|
<a :href="line.url" x-text="line.name"></a>
|
|
168
|
-
<span x-show="line.allowed" class="muted">(allowed)</span>
|
|
177
|
+
<span x-show="line.allowed" class="muted">(allowed to fail)</span>
|
|
169
178
|
</div>
|
|
170
179
|
</template>
|
|
171
180
|
</td>
|
|
@@ -199,8 +208,8 @@
|
|
|
199
208
|
<template x-for="task in tasks" :key="task.key">
|
|
200
209
|
<li>
|
|
201
210
|
<a :href="task.url" x-text="task.key"></a>
|
|
202
|
-
<span x-
|
|
203
|
-
<span x-
|
|
211
|
+
<span x-show="task.issue" :class="statusClass(task.issue)" x-text="task.issue && task.issue.status"></span>
|
|
212
|
+
<span x-text="task.issue ? task.issue.summary : ''"></span>
|
|
204
213
|
<span x-show="isMissing(task.key)" class="warn">not found in Jira</span>
|
|
205
214
|
</li>
|
|
206
215
|
</template>
|
|
@@ -242,6 +251,13 @@
|
|
|
242
251
|
</ul>
|
|
243
252
|
<textarea readonly x-text="compareMarkdown"></textarea>
|
|
244
253
|
</section>
|
|
254
|
+
<section class="wide">
|
|
255
|
+
<div class="out-head">
|
|
256
|
+
<h3>Release post</h3>
|
|
257
|
+
<button type="button" @click="copy(releasePost)">Copy</button>
|
|
258
|
+
</div>
|
|
259
|
+
<textarea readonly rows="12" x-text="releasePost"></textarea>
|
|
260
|
+
</section>
|
|
245
261
|
</div>
|
|
246
262
|
|
|
247
263
|
<p class="muted">Legend: ✅ success · ❌ failed · 🔄 running · ⏭ canceled or skipped · ⚠️ no pipeline</p>
|
|
@@ -259,6 +275,7 @@
|
|
|
259
275
|
report: null,
|
|
260
276
|
failure: "",
|
|
261
277
|
notice: "",
|
|
278
|
+
ignored: "",
|
|
262
279
|
picks: {},
|
|
263
280
|
|
|
264
281
|
async init() {
|
|
@@ -271,6 +288,8 @@
|
|
|
271
288
|
this.picks[service.project] = service.candidates.some((item) => item.tag.name === tag) ? tag : "";
|
|
272
289
|
}
|
|
273
290
|
this.report = report;
|
|
291
|
+
this.readHash();
|
|
292
|
+
window.addEventListener("hashchange", () => this.readHash());
|
|
274
293
|
} catch (error) {
|
|
275
294
|
this.failure = `Cannot load the report: ${error.message}.`;
|
|
276
295
|
}
|
|
@@ -279,11 +298,11 @@
|
|
|
279
298
|
get collectedAt() {
|
|
280
299
|
return this.report.collected_at.slice(0, 16).replace("T", " ");
|
|
281
300
|
},
|
|
282
|
-
get
|
|
283
|
-
return this.report.services.filter((service) => service.error || this.production(service));
|
|
301
|
+
get listed() {
|
|
302
|
+
return this.report.services.filter((service) => service.error || this.production(service) || service.rows.length);
|
|
284
303
|
},
|
|
285
304
|
get pickable() {
|
|
286
|
-
return this.
|
|
305
|
+
return this.listed.filter((service) => service.candidates.length);
|
|
287
306
|
},
|
|
288
307
|
get problems() {
|
|
289
308
|
const jira = this.report.jira;
|
|
@@ -321,6 +340,16 @@
|
|
|
321
340
|
})
|
|
322
341
|
.join("\n");
|
|
323
342
|
},
|
|
343
|
+
get releasePost() {
|
|
344
|
+
const services = this.picked.map((item) => {
|
|
345
|
+
const pipeline = item.candidate.tag.pipeline;
|
|
346
|
+
const build = pipeline ? `[pipeline #${pipeline.id}](${pipeline.url}) ${pipeline.status}` : "no pipeline";
|
|
347
|
+
const compare = `[${this.compareLabel(item)}](${item.candidate.compare_url})`;
|
|
348
|
+
return `- ${item.service.project} ${item.candidate.tag.name}: ${compare} · ${build}`;
|
|
349
|
+
});
|
|
350
|
+
const tasks = this.tasks.map((task) => `- [${task.key}](${task.url})${task.issue ? " " + task.issue.summary : ""}`);
|
|
351
|
+
return ["**Services**", ...services, "", `**Jira tasks (${tasks.length})**`, ...(tasks.length ? tasks : ["- none"])].join("\n");
|
|
352
|
+
},
|
|
324
353
|
get compareMarkdown() {
|
|
325
354
|
return this.picked
|
|
326
355
|
.map((item) => `- ${item.service.project}: [${this.compareLabel(item)}](${item.candidate.compare_url})`)
|
|
@@ -330,6 +359,36 @@
|
|
|
330
359
|
pick(service, tag, element) {
|
|
331
360
|
this.picks[service.project] = tag;
|
|
332
361
|
element.closest("details").open = false;
|
|
362
|
+
this.writeHash();
|
|
363
|
+
},
|
|
364
|
+
readHash() {
|
|
365
|
+
const decode = (value) => {
|
|
366
|
+
try {
|
|
367
|
+
return decodeURIComponent(value);
|
|
368
|
+
} catch {
|
|
369
|
+
return value;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const wanted = new Map(
|
|
373
|
+
location.hash.slice(1).split("&").filter(Boolean)
|
|
374
|
+
.map((part) => part.split("=", 2).map(decode)),
|
|
375
|
+
);
|
|
376
|
+
if (wanted.size) {
|
|
377
|
+
for (const service of this.pickable) {
|
|
378
|
+
const tag = wanted.get(service.project) || "";
|
|
379
|
+
const known = !tag || service.candidates.some((item) => item.tag.name === tag);
|
|
380
|
+
this.picks[service.project] = known ? tag : "";
|
|
381
|
+
if (known) wanted.delete(service.project);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
const unknown = [...wanted].map(([project, tag]) => (tag ? `${project} ${tag}` : project));
|
|
385
|
+
this.ignored = unknown.length ? `Ignored picks from the address that are not in this report: ${unknown.join(", ")}.` : "";
|
|
386
|
+
this.writeHash();
|
|
387
|
+
},
|
|
388
|
+
writeHash() {
|
|
389
|
+
const encode = (value) => encodeURIComponent(value).replaceAll("%2F", "/");
|
|
390
|
+
const hash = this.picked.map(({ service, candidate }) => `${encode(service.project)}=${encode(candidate.tag.name)}`).join("&");
|
|
391
|
+
history.replaceState(null, "", hash ? `#${hash}` : location.pathname + location.search);
|
|
333
392
|
},
|
|
334
393
|
pickOf(service) {
|
|
335
394
|
return service.candidates.find((item) => item.tag.name === this.picks[service.project]);
|
|
@@ -359,6 +418,9 @@
|
|
|
359
418
|
issue(key) {
|
|
360
419
|
return this.report.jira ? this.report.jira.issues[key] : undefined;
|
|
361
420
|
},
|
|
421
|
+
statusClass(issue) {
|
|
422
|
+
return issue ? `status status-${issue.status_category || "new"}` : "";
|
|
423
|
+
},
|
|
362
424
|
isMissing(key) {
|
|
363
425
|
return Boolean(this.report.jira && this.report.jira.missing.includes(key));
|
|
364
426
|
},
|
|
@@ -370,10 +432,11 @@
|
|
|
370
432
|
},
|
|
371
433
|
productionRef(service) {
|
|
372
434
|
const production = this.production(service);
|
|
373
|
-
if (!production) return "";
|
|
435
|
+
if (!production) return "not in production";
|
|
374
436
|
return production.tag ? production.ref : production.sha.slice(0, 8);
|
|
375
437
|
},
|
|
376
438
|
compareLabel({ service, candidate }) {
|
|
439
|
+
if (!this.production(service)) return `commits to ${candidate.tag.name}`;
|
|
377
440
|
return `${this.productionRef(service)}...${candidate.tag.name}`;
|
|
378
441
|
},
|
|
379
442
|
pipelineIcon(pipeline) {
|
|
@@ -387,7 +450,7 @@
|
|
|
387
450
|
const jobs = pipelines.filter(Boolean).flatMap((pipeline) => pipeline.failed_jobs);
|
|
388
451
|
const allowed = jobs.filter((job) => job.allow_failure).length;
|
|
389
452
|
const blocking = jobs.length - allowed;
|
|
390
|
-
return [blocking ? `❌ ${blocking}` : "", allowed ?
|
|
453
|
+
return [blocking ? `❌ ${blocking} failed` : "", allowed ? `${allowed} failed, allowed to fail` : ""].filter(Boolean).join(" · ");
|
|
391
454
|
},
|
|
392
455
|
failedJobs(row) {
|
|
393
456
|
const lines = [];
|
|
@@ -251,10 +251,14 @@ class CollectUseCase:
|
|
|
251
251
|
service.warnings.append("Project has no default branch.")
|
|
252
252
|
return service
|
|
253
253
|
if production is None:
|
|
254
|
-
service.warnings.append(
|
|
255
|
-
|
|
254
|
+
service.warnings.append(
|
|
255
|
+
f"No successful deployment to '{self.settings.production_environment}'; "
|
|
256
|
+
f"rows run from the first commit of {project.default_branch}."
|
|
257
|
+
)
|
|
256
258
|
|
|
257
|
-
walk: typing.Final = self._walk(
|
|
259
|
+
walk: typing.Final = self._walk(
|
|
260
|
+
project, project.default_branch, production.sha if production else None, service, cache
|
|
261
|
+
)
|
|
258
262
|
drafts, linked, in_scope = walk.drafts, [False] * len(walk.drafts), [True] * len(walk.drafts)
|
|
259
263
|
if links is not None:
|
|
260
264
|
drafts, linked, in_scope = self._scope_rows(project, project.default_branch, service, walk, links, cache)
|
|
@@ -272,9 +276,13 @@ class CollectUseCase:
|
|
|
272
276
|
service.candidates.extend(build_candidates(service, production))
|
|
273
277
|
return service
|
|
274
278
|
|
|
275
|
-
def _walk(
|
|
279
|
+
def _walk(
|
|
280
|
+
self, project: Project, default_branch: str, baseline: str | None, service: Service, cache: Cache
|
|
281
|
+
) -> _Walk:
|
|
276
282
|
commits, truncated = self.api.list_first_parent_commits(
|
|
277
|
-
project.id,
|
|
283
|
+
project.id,
|
|
284
|
+
f"{baseline}..{default_branch}" if baseline else default_branch,
|
|
285
|
+
max_items=self.settings.max_commits,
|
|
278
286
|
)
|
|
279
287
|
walk: typing.Final = _Walk(truncated=truncated)
|
|
280
288
|
service.truncated = truncated
|
|
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
|