suite-py 1.48.0__tar.gz → 1.49.0__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.
- {suite_py-1.48.0 → suite_py-1.49.0}/PKG-INFO +2 -2
- {suite_py-1.48.0 → suite_py-1.49.0}/pyproject.toml +2 -2
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/__version__.py +1 -1
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/cli.py +27 -6
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/context.py +7 -3
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/create_branch.py +48 -24
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/release.py +128 -16
- {suite_py-1.48.0 → suite_py-1.49.0}/LICENSE-APACHE +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/LICENSE-MIT +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/__init__.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/__init__.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/ask_review.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/bump.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/check.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/common.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/estimate_cone.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/login.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/merge_pr.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/open_pr.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/project_lock.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/set_token.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/commands/status.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/__init__.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/config.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/__init__.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/aws_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/captainhook_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/changelog_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/frequent_reviewers_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/git_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/github_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/metrics_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/okta_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/pre_commit_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/prompt_utils.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/version_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/handler/youtrack_handler.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/logger.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/metrics.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/oauth.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/requests/__init__.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/requests/auth.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/requests/session.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/symbol.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/lib/tokens.py +0 -0
- {suite_py-1.48.0 → suite_py-1.49.0}/suite_py/templates/login.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: suite-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.49.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: larrywax, EugenioLaghi, michelangelomo
|
|
6
6
|
Author-email: devops@prima.it
|
|
@@ -28,7 +28,7 @@ Requires-Dist: pytest (>=7.0.0)
|
|
|
28
28
|
Requires-Dist: python-dateutil (>=2.8.2)
|
|
29
29
|
Requires-Dist: requests (>=2.26.0)
|
|
30
30
|
Requires-Dist: requests-toolbelt (>=0.9.1)
|
|
31
|
-
Requires-Dist: rich (==14.
|
|
31
|
+
Requires-Dist: rich (==14.2.0)
|
|
32
32
|
Requires-Dist: semver (>=3.0.4,<4.0.0)
|
|
33
33
|
Requires-Dist: termcolor (>=1.1.0)
|
|
34
34
|
Requires-Dist: truststore (>=0.7,<0.11) ; python_version >= "3.10"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
authors = ["larrywax, EugenioLaghi, michelangelomo <devops@prima.it>"]
|
|
3
3
|
description = ""
|
|
4
4
|
name = "suite-py"
|
|
5
|
-
version = "1.
|
|
5
|
+
version = "1.49.0"
|
|
6
6
|
|
|
7
7
|
[tool.poetry.dependencies]
|
|
8
8
|
Click = ">=7.0"
|
|
@@ -23,7 +23,7 @@ python = ">=3.8,<4.0"
|
|
|
23
23
|
python-dateutil = ">=2.8.2"
|
|
24
24
|
requests = ">=2.26.0"
|
|
25
25
|
requests-toolbelt = ">=0.9.1"
|
|
26
|
-
rich = "==14.
|
|
26
|
+
rich = "==14.2.0"
|
|
27
27
|
semver = "^3.0.4"
|
|
28
28
|
termcolor = ">=1.1.0"
|
|
29
29
|
truststore = {version = ">=0.7,<0.11", python = ">=3.10"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.49.0"
|
|
@@ -47,8 +47,10 @@ from suite_py.lib.config import Config
|
|
|
47
47
|
from suite_py.lib.handler import git_handler as git
|
|
48
48
|
from suite_py.lib.handler import prompt_utils
|
|
49
49
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
50
|
+
from suite_py.lib.handler.git_handler import GitHandler
|
|
50
51
|
from suite_py.lib.handler.okta_handler import Okta
|
|
51
52
|
from suite_py.lib.handler.pre_commit_handler import PreCommit
|
|
53
|
+
from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
52
54
|
from suite_py.lib.tokens import Tokens
|
|
53
55
|
|
|
54
56
|
# pylint: enable=wrong-import-position
|
|
@@ -176,17 +178,21 @@ def main(ctx, project, timeout, verbose):
|
|
|
176
178
|
if timeout:
|
|
177
179
|
config.user["captainhook_timeout"] = timeout
|
|
178
180
|
|
|
179
|
-
project = os.path.basename(project)
|
|
180
181
|
tokens = Tokens()
|
|
181
182
|
okta = Okta(config, tokens)
|
|
182
183
|
captainhook = CaptainHook(config, okta, tokens)
|
|
184
|
+
project = os.path.basename(project)
|
|
185
|
+
git_handler = GitHandler(project, config)
|
|
186
|
+
youtrack_handler = YoutrackHandler(config, tokens)
|
|
183
187
|
|
|
184
188
|
ctx.obj = Context(
|
|
189
|
+
captainhook=captainhook,
|
|
190
|
+
config=config,
|
|
191
|
+
git_handler=git_handler,
|
|
192
|
+
okta=okta,
|
|
185
193
|
project=project,
|
|
186
194
|
tokens=tokens,
|
|
187
|
-
|
|
188
|
-
config=config,
|
|
189
|
-
captainhook=captainhook,
|
|
195
|
+
youtrack_handler=youtrack_handler,
|
|
190
196
|
)
|
|
191
197
|
|
|
192
198
|
ctx.obj.call(metrics.setup)
|
|
@@ -233,12 +239,27 @@ def bump(obj: Context, project: Optional[str] = None, version: Optional[str] = N
|
|
|
233
239
|
is_flag=True,
|
|
234
240
|
help="Stash uncommitted changes before creating the branch and reapply them afterward",
|
|
235
241
|
)
|
|
242
|
+
@click.option(
|
|
243
|
+
"--parent-branch",
|
|
244
|
+
type=click.STRING,
|
|
245
|
+
help="Parent branch to create the new branch from",
|
|
246
|
+
)
|
|
247
|
+
@click.option(
|
|
248
|
+
"--branch-name",
|
|
249
|
+
type=click.STRING,
|
|
250
|
+
help="Branch name template. Supports {card_id}, {type}, {summary} placeholders (ex. '{card_id}/{type}/{summary}')",
|
|
251
|
+
)
|
|
236
252
|
@click.pass_obj
|
|
237
253
|
@catch_exceptions
|
|
238
|
-
def cli_create_branch(obj, card, autostash):
|
|
254
|
+
def cli_create_branch(obj, card, autostash, parent_branch, branch_name):
|
|
239
255
|
from suite_py.commands.create_branch import CreateBranch
|
|
240
256
|
|
|
241
|
-
obj.call(CreateBranch
|
|
257
|
+
obj.call(CreateBranch).run(
|
|
258
|
+
card_id=card,
|
|
259
|
+
autostash=autostash,
|
|
260
|
+
parent_branch=parent_branch,
|
|
261
|
+
branch_name=branch_name,
|
|
262
|
+
)
|
|
242
263
|
|
|
243
264
|
|
|
244
265
|
@main.command("lock", help="Lock project on staging or prod")
|
|
@@ -3,17 +3,21 @@ from inspect import signature
|
|
|
3
3
|
|
|
4
4
|
from suite_py.lib.config import Config
|
|
5
5
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
6
|
+
from suite_py.lib.handler.git_handler import GitHandler
|
|
6
7
|
from suite_py.lib.handler.okta_handler import Okta
|
|
8
|
+
from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
7
9
|
from suite_py.lib.tokens import Tokens
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
@dataclasses.dataclass
|
|
11
13
|
class Context:
|
|
12
|
-
project: str
|
|
13
|
-
config: Config
|
|
14
14
|
captainhook: CaptainHook
|
|
15
|
-
|
|
15
|
+
config: Config
|
|
16
|
+
git_handler: GitHandler
|
|
16
17
|
okta: Okta
|
|
18
|
+
project: str
|
|
19
|
+
tokens: Tokens
|
|
20
|
+
youtrack_handler: YoutrackHandler
|
|
17
21
|
|
|
18
22
|
# Call the function to_call with kwargs, injecting fields from self as default arguments
|
|
19
23
|
def call(self, to_call, **kwargs):
|
|
@@ -11,25 +11,37 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class CreateBranch:
|
|
14
|
-
def __init__(
|
|
15
|
-
self
|
|
16
|
-
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
config,
|
|
17
|
+
git_handler: GitHandler,
|
|
18
|
+
youtrack_handler: YoutrackHandler,
|
|
19
|
+
):
|
|
17
20
|
self._config = config
|
|
18
|
-
self.
|
|
19
|
-
self.
|
|
20
|
-
self._autostash = autostash
|
|
21
|
+
self._git_handler = git_handler
|
|
22
|
+
self._youtrack = youtrack_handler
|
|
21
23
|
|
|
22
24
|
@metrics.command("create-branch")
|
|
23
|
-
def run(
|
|
24
|
-
|
|
25
|
+
def run(
|
|
26
|
+
self,
|
|
27
|
+
autostash=False,
|
|
28
|
+
branch_name=None,
|
|
29
|
+
card_id=None,
|
|
30
|
+
parent_branch=None,
|
|
31
|
+
):
|
|
32
|
+
if (
|
|
33
|
+
not self._git_handler.is_detached()
|
|
34
|
+
and self._git_handler.is_dirty()
|
|
35
|
+
and not autostash
|
|
36
|
+
):
|
|
25
37
|
# Default behaviour is to pull when not detached.
|
|
26
38
|
# Can't do that with uncommitted changes.
|
|
27
39
|
logger.error("You have some uncommitted changes, I can't continue")
|
|
28
40
|
sys.exit(-1)
|
|
29
41
|
|
|
30
42
|
try:
|
|
31
|
-
if
|
|
32
|
-
issue = self._youtrack.get_issue(
|
|
43
|
+
if card_id:
|
|
44
|
+
issue = self._youtrack.get_issue(card_id)
|
|
33
45
|
else:
|
|
34
46
|
issue = self._youtrack.get_issue(self._ask_card())
|
|
35
47
|
except Exception:
|
|
@@ -38,7 +50,7 @@ class CreateBranch:
|
|
|
38
50
|
)
|
|
39
51
|
sys.exit(-1)
|
|
40
52
|
|
|
41
|
-
self._checkout_branch(issue,
|
|
53
|
+
self._checkout_branch(issue, autostash, parent_branch, branch_name)
|
|
42
54
|
|
|
43
55
|
user = self._youtrack.get_current_user()
|
|
44
56
|
self._youtrack.assign_to(issue["id"], user["login"])
|
|
@@ -95,33 +107,45 @@ class CreateBranch:
|
|
|
95
107
|
)
|
|
96
108
|
return []
|
|
97
109
|
|
|
98
|
-
def _checkout_branch(
|
|
110
|
+
def _checkout_branch(
|
|
111
|
+
self, issue, autostash=False, parent_branch=None, branch_name_template=None
|
|
112
|
+
):
|
|
99
113
|
default_parent_branch_name = self._config.user.get(
|
|
100
|
-
"default_parent_branch", self.
|
|
114
|
+
"default_parent_branch", self._git_handler.current_branch_name()
|
|
101
115
|
)
|
|
102
116
|
|
|
103
|
-
parent_branch_name = prompt_utils.ask_questions_input(
|
|
104
|
-
"
|
|
117
|
+
parent_branch_name = parent_branch or prompt_utils.ask_questions_input(
|
|
118
|
+
"Enter parent branch: ", default_parent_branch_name
|
|
105
119
|
)
|
|
106
120
|
|
|
107
121
|
full_branch_name = ""
|
|
108
122
|
branch_name = _normalize_git_ref_segment(issue["summary"])
|
|
109
123
|
branch_type = _normalize_git_ref_segment(issue["Type"])
|
|
110
124
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
if branch_name_template:
|
|
126
|
+
# Use the template and replace placeholders
|
|
127
|
+
full_branch_name = branch_name_template.format(
|
|
128
|
+
card_id=issue["idReadable"], type=branch_type, summary=branch_name
|
|
114
129
|
)
|
|
115
130
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
131
|
+
if not is_branch_name_valid(full_branch_name):
|
|
132
|
+
logger.error(f"Invalid branch name from template: {full_branch_name}")
|
|
133
|
+
sys.exit(-1)
|
|
134
|
+
else:
|
|
135
|
+
while True:
|
|
136
|
+
branch_name = str(
|
|
137
|
+
prompt_utils.ask_questions_input("Enter branch name: ", branch_name)
|
|
138
|
+
)
|
|
119
139
|
|
|
120
|
-
|
|
140
|
+
full_branch_name = f"{issue['idReadable']}/{branch_type}/{branch_name}"
|
|
141
|
+
if is_branch_name_valid(full_branch_name):
|
|
142
|
+
break
|
|
121
143
|
|
|
122
|
-
|
|
144
|
+
logger.error(f"Invalid branch name: {full_branch_name}. Try again?")
|
|
123
145
|
|
|
124
|
-
self.
|
|
146
|
+
self._git_handler.checkout(parent_branch_name, autostash=autostash)
|
|
147
|
+
|
|
148
|
+
self._git_handler.checkout(full_branch_name, new=True, autostash=autostash)
|
|
125
149
|
|
|
126
150
|
|
|
127
151
|
# Normalize a string into a valid segment(ie. the part of the branch name between the /)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import re
|
|
3
3
|
import sys
|
|
4
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
4
5
|
|
|
5
6
|
from suite_py.commands import common
|
|
6
7
|
from suite_py.lib import logger, metrics
|
|
@@ -71,7 +72,6 @@ class Release:
|
|
|
71
72
|
sha = self._resolve_target_sha(commits)
|
|
72
73
|
self._create_release(new_version, message, sha)
|
|
73
74
|
|
|
74
|
-
# ---- helpers (extracted to reduce complexity of _create) ----
|
|
75
75
|
def _gather_commits_and_version(self, latest):
|
|
76
76
|
"""Return (commits, new_version, base_message)."""
|
|
77
77
|
if latest == "":
|
|
@@ -79,6 +79,11 @@ class Release:
|
|
|
79
79
|
|
|
80
80
|
logger.info(f"The current release is {latest}")
|
|
81
81
|
commits = self._github.get_commits_since_release(self._repo, latest)
|
|
82
|
+
if not commits:
|
|
83
|
+
logger.warning(
|
|
84
|
+
"No commits found after the latest release tag; nothing to release."
|
|
85
|
+
)
|
|
86
|
+
sys.exit(0)
|
|
82
87
|
# If user did not pass a target commit, ask interactively which commit to release.
|
|
83
88
|
# Only perform interactive selection if user explicitly requested it
|
|
84
89
|
if self._interactive:
|
|
@@ -88,7 +93,7 @@ class Release:
|
|
|
88
93
|
message = self._build_commits_message(commits)
|
|
89
94
|
logger.info(f"\nCommits list:\n{message}\n")
|
|
90
95
|
if not prompt_utils.ask_confirm("Do you want to continue?"):
|
|
91
|
-
sys.exit()
|
|
96
|
+
sys.exit()
|
|
92
97
|
new_version = self._version.select_new_version(latest, allow_prerelease=True)
|
|
93
98
|
return commits, new_version, message
|
|
94
99
|
|
|
@@ -106,7 +111,7 @@ class Release:
|
|
|
106
111
|
try:
|
|
107
112
|
resolved_commit = self._repo.get_commit(self._commit)
|
|
108
113
|
target_sha = resolved_commit.sha
|
|
109
|
-
except Exception:
|
|
114
|
+
except Exception:
|
|
110
115
|
logger.error(
|
|
111
116
|
f"The provided commit '{self._commit}' was not found in the repository."
|
|
112
117
|
)
|
|
@@ -125,6 +130,8 @@ class Release:
|
|
|
125
130
|
"The specified commit is not part of the unreleased commits (i.e., not after the latest release)."
|
|
126
131
|
)
|
|
127
132
|
sys.exit(-1)
|
|
133
|
+
# We want ONLY the selected commit and the older commits (towards the previous release),
|
|
134
|
+
# excluding any *newer* commits that appear before it in the list.
|
|
128
135
|
return commits[target_index:]
|
|
129
136
|
|
|
130
137
|
def _select_commit_if_needed(self, commits):
|
|
@@ -137,22 +144,127 @@ class Release:
|
|
|
137
144
|
"""
|
|
138
145
|
if self._commit or not commits:
|
|
139
146
|
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
"name": f"{c.sha[:8]} | {
|
|
147
|
+
|
|
148
|
+
choices = []
|
|
149
|
+
|
|
150
|
+
def build_choice(c, icon):
|
|
151
|
+
summary = c.commit.message.splitlines()[0]
|
|
152
|
+
return {
|
|
153
|
+
"name": f"{icon} {c.sha[:8]} | {summary}",
|
|
147
154
|
"value": c.sha,
|
|
148
155
|
}
|
|
149
|
-
|
|
150
|
-
|
|
156
|
+
|
|
157
|
+
with ThreadPoolExecutor(max_workers=min(8, len(commits))) as executor:
|
|
158
|
+
future_map = {
|
|
159
|
+
executor.submit(self._get_commit_status_icon, c.sha): c for c in commits
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
icon_results = {}
|
|
163
|
+
for future in as_completed(future_map):
|
|
164
|
+
c = future_map[future]
|
|
165
|
+
try:
|
|
166
|
+
icon_results[c.sha] = future.result()
|
|
167
|
+
except Exception:
|
|
168
|
+
icon_results[c.sha] = "?"
|
|
169
|
+
for c in commits:
|
|
170
|
+
choices.append(build_choice(c, icon_results.get(c.sha, "?")))
|
|
171
|
+
|
|
172
|
+
subtitle = "(only one available)" if len(choices) == 1 else "(newest first):"
|
|
151
173
|
selected = prompt_utils.ask_choices(
|
|
152
|
-
"Select commit to release
|
|
174
|
+
f"Select commit to release {subtitle}", choices
|
|
153
175
|
)
|
|
154
176
|
self._commit = selected
|
|
155
177
|
|
|
178
|
+
def _get_commit_status_icon(self, sha):
|
|
179
|
+
"""Return an icon representing the simplified CI status for a commit.
|
|
180
|
+
|
|
181
|
+
Final exposed statuses (icons):
|
|
182
|
+
success (✅), failure (❌), in_progress (🏗️), cancelled (🚫), unknown (❓)
|
|
183
|
+
|
|
184
|
+
Everything else is folded into one of those buckets:
|
|
185
|
+
pending / queued / waiting -> in_progress
|
|
186
|
+
skipped / neutral -> unknown (unless at least one success present)
|
|
187
|
+
Preference order for determining state:
|
|
188
|
+
1. Check runs API
|
|
189
|
+
2. Combined status API
|
|
190
|
+
3. Fallback unknown
|
|
191
|
+
"""
|
|
192
|
+
check_runs = self._safe_get_check_runs(sha)
|
|
193
|
+
state = self._classify_from_check_runs(check_runs) if check_runs else None
|
|
194
|
+
if state is None:
|
|
195
|
+
state = self._classify_from_combined_status(sha)
|
|
196
|
+
return self._icon_for_state(state)
|
|
197
|
+
|
|
198
|
+
def _safe_get_check_runs(self, sha):
|
|
199
|
+
try:
|
|
200
|
+
commit = self._repo.get_commit(sha)
|
|
201
|
+
return list(commit.get_check_runs())
|
|
202
|
+
except Exception:
|
|
203
|
+
return []
|
|
204
|
+
|
|
205
|
+
def _classify_from_check_runs(self, check_runs):
|
|
206
|
+
statuses = {r.status for r in check_runs if getattr(r, "status", None)}
|
|
207
|
+
conclusions = {
|
|
208
|
+
r.conclusion for r in check_runs if getattr(r, "conclusion", None)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if self._any_in_progress(statuses):
|
|
212
|
+
return "in_progress"
|
|
213
|
+
if self._any_failure(conclusions):
|
|
214
|
+
return "failure"
|
|
215
|
+
if "cancelled" in conclusions:
|
|
216
|
+
return "cancelled"
|
|
217
|
+
if self._is_success(conclusions):
|
|
218
|
+
return "success"
|
|
219
|
+
return "unknown"
|
|
220
|
+
|
|
221
|
+
@staticmethod
|
|
222
|
+
def _any_in_progress(statuses):
|
|
223
|
+
return any(
|
|
224
|
+
s in {"in_progress", "queued", "waiting", "pending"} for s in statuses
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def _any_failure(conclusions):
|
|
229
|
+
return any(
|
|
230
|
+
c in {"failure", "timed_out", "action_required", "stale"}
|
|
231
|
+
for c in conclusions
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
@staticmethod
|
|
235
|
+
def _is_success(conclusions):
|
|
236
|
+
return bool(conclusions) and (
|
|
237
|
+
all(c == "success" for c in conclusions)
|
|
238
|
+
or ("success" in conclusions and not Release._any_failure(conclusions))
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def _classify_from_combined_status(self, sha):
|
|
242
|
+
try:
|
|
243
|
+
status = self._github.get_build_status(self._project, sha)
|
|
244
|
+
combined = (status.state or "unknown").lower()
|
|
245
|
+
except Exception:
|
|
246
|
+
combined = "unknown"
|
|
247
|
+
if combined in {"pending", "queued", "waiting", "in_progress"}:
|
|
248
|
+
return "in_progress"
|
|
249
|
+
if combined == "success":
|
|
250
|
+
return "success"
|
|
251
|
+
if combined == "failure":
|
|
252
|
+
return "failure"
|
|
253
|
+
if combined == "cancelled":
|
|
254
|
+
return "cancelled"
|
|
255
|
+
return "unknown"
|
|
256
|
+
|
|
257
|
+
@staticmethod
|
|
258
|
+
def _icon_for_state(state):
|
|
259
|
+
icon_mapping = {
|
|
260
|
+
"success": "✅",
|
|
261
|
+
"failure": "❌",
|
|
262
|
+
"in_progress": "🏗️",
|
|
263
|
+
"cancelled": "🚫",
|
|
264
|
+
"unknown": "❓",
|
|
265
|
+
}
|
|
266
|
+
return icon_mapping.get(state, "❓")
|
|
267
|
+
|
|
156
268
|
@staticmethod
|
|
157
269
|
def _build_commits_message(commits):
|
|
158
270
|
return "\n".join(
|
|
@@ -179,7 +291,7 @@ class Release:
|
|
|
179
291
|
return commits[0].commit.sha if commits else ""
|
|
180
292
|
try:
|
|
181
293
|
sha = self._repo.get_commit(self._commit).sha
|
|
182
|
-
except Exception:
|
|
294
|
+
except Exception:
|
|
183
295
|
logger.error(
|
|
184
296
|
f"Unable to resolve commit '{self._commit}' during release creation."
|
|
185
297
|
)
|
|
@@ -189,13 +301,13 @@ class Release:
|
|
|
189
301
|
|
|
190
302
|
def _validate_target_commit_not_tagged(self, sha):
|
|
191
303
|
try:
|
|
192
|
-
for t in self._repo.get_tags():
|
|
304
|
+
for t in self._repo.get_tags():
|
|
193
305
|
if t.commit.sha == sha:
|
|
194
306
|
logger.error(
|
|
195
307
|
f"The commit {sha[:7]} is already tagged with {t.name}. Aborting."
|
|
196
308
|
)
|
|
197
309
|
sys.exit(-1)
|
|
198
|
-
except Exception:
|
|
310
|
+
except Exception:
|
|
199
311
|
logger.warning("Could not verify if commit already tagged; continuing.")
|
|
200
312
|
|
|
201
313
|
def _create_release(self, new_version, message, commit):
|
|
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
|
|
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
|
|
File without changes
|