suite-py 1.49.1__tar.gz → 1.52.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.49.1 → suite_py-1.52.0}/PKG-INFO +4 -5
- {suite_py-1.49.1 → suite_py-1.52.0}/pyproject.toml +5 -5
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/__version__.py +1 -1
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/cli.py +12 -8
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/check.py +12 -1
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/context.py +4 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/create_branch.py +12 -12
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/merge_pr.py +6 -1
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/open_pr.py +5 -1
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/project_lock.py +6 -1
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/release.py +59 -3
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/config.py +4 -0
- suite_py-1.52.0/suite_py/lib/handler/backstage_handler.py +82 -0
- suite_py-1.52.0/suite_py/lib/handler/github_handler.py +263 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/okta_handler.py +1 -1
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/youtrack_handler.py +45 -9
- suite_py-1.49.1/suite_py/lib/handler/github_handler.py +0 -92
- {suite_py-1.49.1 → suite_py-1.52.0}/LICENSE-APACHE +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/LICENSE-MIT +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/__init__.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/__init__.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/ask_review.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/bump.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/common.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/estimate_cone.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/login.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/set_token.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/commands/status.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/__init__.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/__init__.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/aws_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/captainhook_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/changelog_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/frequent_reviewers_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/git_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/metrics_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/pre_commit_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/prompt_utils.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/handler/version_handler.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/logger.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/metrics.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/oauth.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/requests/__init__.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/requests/auth.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/requests/session.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/symbol.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/lib/tokens.py +0 -0
- {suite_py-1.49.1 → suite_py-1.52.0}/suite_py/templates/login.html +0 -0
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: suite-py
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.52.0
|
|
4
4
|
Summary:
|
|
5
5
|
Author: larrywax, EugenioLaghi, michelangelomo
|
|
6
6
|
Author-email: devops@prima.it
|
|
7
|
-
Requires-Python: >=3.
|
|
7
|
+
Requires-Python: >=3.9,<4.0
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
10
9
|
Classifier: Programming Language :: Python :: 3.9
|
|
11
10
|
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
@@ -16,14 +15,14 @@ Requires-Dist: InquirerPy (>=0.2.0)
|
|
|
16
15
|
Requires-Dist: PyGithub (>=1.57)
|
|
17
16
|
Requires-Dist: PyYaml (>=5.4)
|
|
18
17
|
Requires-Dist: autoupgrade-prima (>=0.6)
|
|
19
|
-
Requires-Dist: boto3 (>=1.
|
|
18
|
+
Requires-Dist: boto3 (>=1.40)
|
|
20
19
|
Requires-Dist: cement (>=3.0)
|
|
21
20
|
Requires-Dist: colorama (>=0.4.3)
|
|
22
21
|
Requires-Dist: halo (>=0.0.28)
|
|
23
22
|
Requires-Dist: inquirer (>=3.1.4,<4.0.0)
|
|
24
23
|
Requires-Dist: itsdangerous (==2.2.0)
|
|
25
24
|
Requires-Dist: keyring (>=23.9.1,<26.0.0)
|
|
26
|
-
Requires-Dist: pylint (
|
|
25
|
+
Requires-Dist: pylint (<5.0.0)
|
|
27
26
|
Requires-Dist: pytest (>=7.0.0)
|
|
28
27
|
Requires-Dist: python-dateutil (>=2.8.2)
|
|
29
28
|
Requires-Dist: requests (>=2.26.0)
|
|
@@ -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.52.0"
|
|
6
6
|
|
|
7
7
|
[tool.poetry.dependencies]
|
|
8
8
|
Click = ">=7.0"
|
|
@@ -10,16 +10,16 @@ InquirerPy = ">=0.2.0"
|
|
|
10
10
|
PyGithub = ">=1.57"
|
|
11
11
|
PyYaml = ">=5.4"
|
|
12
12
|
autoupgrade-prima = ">=0.6"
|
|
13
|
-
boto3 = ">=1.
|
|
13
|
+
boto3 = ">=1.40"
|
|
14
14
|
cement = ">=3.0"
|
|
15
15
|
colorama = ">=0.4.3"
|
|
16
16
|
halo = ">=0.0.28"
|
|
17
17
|
inquirer = "^3.1.4"
|
|
18
18
|
itsdangerous = "==2.2.0"
|
|
19
19
|
keyring = ">=23.9.1,<26.0.0"
|
|
20
|
-
pylint = "
|
|
20
|
+
pylint = "<5.0.0"
|
|
21
21
|
pytest = ">=7.0.0"
|
|
22
|
-
python = ">=3.
|
|
22
|
+
python = ">=3.9,<4.0"
|
|
23
23
|
python-dateutil = ">=2.8.2"
|
|
24
24
|
requests = ">=2.26.0"
|
|
25
25
|
requests-toolbelt = ">=0.9.1"
|
|
@@ -29,7 +29,7 @@ termcolor = ">=1.1.0"
|
|
|
29
29
|
truststore = {version = ">=0.7,<0.11", python = ">=3.10"}
|
|
30
30
|
|
|
31
31
|
[tool.poetry.dev-dependencies]
|
|
32
|
-
black = ">=22.6,<
|
|
32
|
+
black = ">=22.6,<26.0"
|
|
33
33
|
|
|
34
34
|
[tool.poetry.scripts]
|
|
35
35
|
suite-py = "suite_py.cli:main"
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
|
-
__version__ = "1.
|
|
2
|
+
__version__ = "1.52.0"
|
|
@@ -46,6 +46,7 @@ from suite_py.lib import logger, metrics
|
|
|
46
46
|
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
|
+
from suite_py.lib.handler.backstage_handler import Backstage
|
|
49
50
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
50
51
|
from suite_py.lib.handler.git_handler import GitHandler
|
|
51
52
|
from suite_py.lib.handler.okta_handler import Okta
|
|
@@ -132,7 +133,7 @@ def catch_exceptions(func):
|
|
|
132
133
|
@click.option(
|
|
133
134
|
"--timeout",
|
|
134
135
|
type=click.INT,
|
|
135
|
-
help="Timeout in seconds for Captainhook operations",
|
|
136
|
+
help="Timeout in seconds for Captainhook/Backstage operations",
|
|
136
137
|
)
|
|
137
138
|
@click.option("-v", "--verbose", count=True)
|
|
138
139
|
@click.pass_context
|
|
@@ -177,16 +178,19 @@ def main(ctx, project, timeout, verbose):
|
|
|
177
178
|
ctx.exit()
|
|
178
179
|
if timeout:
|
|
179
180
|
config.user["captainhook_timeout"] = timeout
|
|
181
|
+
config.user["backstage_timeout"] = timeout
|
|
180
182
|
|
|
181
183
|
tokens = Tokens()
|
|
182
184
|
okta = Okta(config, tokens)
|
|
183
185
|
captainhook = CaptainHook(config, okta, tokens)
|
|
186
|
+
backstage = Backstage(config, okta, tokens)
|
|
184
187
|
project = os.path.basename(project)
|
|
185
188
|
git_handler = GitHandler(project, config)
|
|
186
189
|
youtrack_handler = YoutrackHandler(config, tokens)
|
|
187
190
|
|
|
188
191
|
ctx.obj = Context(
|
|
189
192
|
captainhook=captainhook,
|
|
193
|
+
backstage=backstage,
|
|
190
194
|
config=config,
|
|
191
195
|
git_handler=git_handler,
|
|
192
196
|
okta=okta,
|
|
@@ -233,21 +237,21 @@ def bump(obj: Context, project: Optional[str] = None, version: Optional[str] = N
|
|
|
233
237
|
@main.command(
|
|
234
238
|
"create-branch", help="Create local branch and set the YouTrack card in progress"
|
|
235
239
|
)
|
|
236
|
-
@click.option("--card", type=click.STRING, help="YouTrack card number (ex. PRIMA-123)")
|
|
237
240
|
@click.option(
|
|
238
241
|
"--autostash",
|
|
239
242
|
is_flag=True,
|
|
240
243
|
help="Stash uncommitted changes before creating the branch and reapply them afterward",
|
|
241
244
|
)
|
|
242
245
|
@click.option(
|
|
243
|
-
"--
|
|
246
|
+
"--branch-name",
|
|
244
247
|
type=click.STRING,
|
|
245
|
-
help="
|
|
248
|
+
help="Branch name template. Supports {card_id}, {type}, {summary} placeholders (ex. '{card_id}/{type}/{summary}')",
|
|
246
249
|
)
|
|
250
|
+
@click.option("--card", type=click.STRING, help="YouTrack card ID (ex. PRIMA-4423)")
|
|
247
251
|
@click.option(
|
|
248
|
-
"--branch
|
|
252
|
+
"--parent-branch",
|
|
249
253
|
type=click.STRING,
|
|
250
|
-
help="
|
|
254
|
+
help="Parent branch to create the new branch from",
|
|
251
255
|
)
|
|
252
256
|
@click.pass_obj
|
|
253
257
|
@catch_exceptions
|
|
@@ -255,10 +259,10 @@ def cli_create_branch(obj, card, autostash, parent_branch, branch_name):
|
|
|
255
259
|
from suite_py.commands.create_branch import CreateBranch
|
|
256
260
|
|
|
257
261
|
obj.call(CreateBranch).run(
|
|
258
|
-
card_id=card,
|
|
259
262
|
autostash=autostash,
|
|
260
|
-
parent_branch=parent_branch,
|
|
261
263
|
branch_name=branch_name,
|
|
264
|
+
card_id=card,
|
|
265
|
+
parent_branch=parent_branch,
|
|
262
266
|
)
|
|
263
267
|
|
|
264
268
|
|
|
@@ -8,18 +8,22 @@ from suite_py.lib.symbol import CHECKMARK, CROSSMARK
|
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
class Check:
|
|
11
|
-
|
|
11
|
+
# delete this after `captainhook` has been removed
|
|
12
|
+
# pylint: disable=too-many-instance-attributes
|
|
13
|
+
def __init__(self, captainhook, backstage, config, tokens):
|
|
12
14
|
self.config = config
|
|
13
15
|
self._invalid_or_missing_tokens = []
|
|
14
16
|
self._tokens = tokens
|
|
15
17
|
self._youtrack = YoutrackHandler(config, tokens)
|
|
16
18
|
self._github = GithubHandler(tokens)
|
|
17
19
|
self._captainhook = captainhook
|
|
20
|
+
self._backstage = backstage
|
|
18
21
|
|
|
19
22
|
self._checks = [
|
|
20
23
|
("Github", self._check_github),
|
|
21
24
|
("Youtrack", self._check_youtrack),
|
|
22
25
|
("CaptainHook", self._check_captainhook),
|
|
26
|
+
("Backstage", self._check_backstage),
|
|
23
27
|
("AWS", self._check_aws),
|
|
24
28
|
]
|
|
25
29
|
|
|
@@ -69,6 +73,13 @@ class Check:
|
|
|
69
73
|
except Exception:
|
|
70
74
|
return "unreachable"
|
|
71
75
|
|
|
76
|
+
def _check_backstage(self):
|
|
77
|
+
try:
|
|
78
|
+
self._backstage.check()
|
|
79
|
+
return "ok"
|
|
80
|
+
except Exception:
|
|
81
|
+
return "unreachable"
|
|
82
|
+
|
|
72
83
|
def _check_aws(self):
|
|
73
84
|
try:
|
|
74
85
|
if self.config.aws != "":
|
|
@@ -2,6 +2,7 @@ import dataclasses
|
|
|
2
2
|
from inspect import signature
|
|
3
3
|
|
|
4
4
|
from suite_py.lib.config import Config
|
|
5
|
+
from suite_py.lib.handler.backstage_handler import Backstage
|
|
5
6
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
6
7
|
from suite_py.lib.handler.git_handler import GitHandler
|
|
7
8
|
from suite_py.lib.handler.okta_handler import Okta
|
|
@@ -9,9 +10,12 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
|
|
|
9
10
|
from suite_py.lib.tokens import Tokens
|
|
10
11
|
|
|
11
12
|
|
|
13
|
+
# delete this after `captainhook` has been removed
|
|
14
|
+
# pylint: disable=too-many-instance-attributes
|
|
12
15
|
@dataclasses.dataclass
|
|
13
16
|
class Context:
|
|
14
17
|
captainhook: CaptainHook
|
|
18
|
+
backstage: Backstage
|
|
15
19
|
config: Config
|
|
16
20
|
git_handler: GitHandler
|
|
17
21
|
okta: Okta
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
import re
|
|
3
3
|
import sys
|
|
4
|
-
|
|
5
4
|
import requests
|
|
6
5
|
|
|
7
6
|
from suite_py.lib import logger, metrics
|
|
@@ -19,7 +18,7 @@ class CreateBranch:
|
|
|
19
18
|
):
|
|
20
19
|
self._config = config
|
|
21
20
|
self._git_handler = git_handler
|
|
22
|
-
self.
|
|
21
|
+
self._youtrack_handler = youtrack_handler
|
|
23
22
|
|
|
24
23
|
@metrics.command("create-branch")
|
|
25
24
|
def run(
|
|
@@ -41,22 +40,22 @@ class CreateBranch:
|
|
|
41
40
|
|
|
42
41
|
try:
|
|
43
42
|
if card_id:
|
|
44
|
-
issue = self.
|
|
43
|
+
issue = self._youtrack_handler.get_issue(card_id)
|
|
45
44
|
else:
|
|
46
|
-
issue = self.
|
|
47
|
-
except Exception:
|
|
45
|
+
issue = self._youtrack_handler.get_issue(self._ask_card_id())
|
|
46
|
+
except Exception as e:
|
|
48
47
|
logger.error(
|
|
49
|
-
"There was a problem retrieving the issue from YouTrack.
|
|
48
|
+
f"There was a problem retrieving the issue `{card_id}` from YouTrack. {e}"
|
|
50
49
|
)
|
|
51
50
|
sys.exit(-1)
|
|
52
51
|
|
|
53
52
|
self._checkout_branch(issue, autostash, parent_branch, branch_name)
|
|
54
53
|
|
|
55
|
-
user = self.
|
|
56
|
-
self.
|
|
54
|
+
user = self._youtrack_handler.get_current_user()
|
|
55
|
+
self._youtrack_handler.assign_to(issue["id"], user["login"])
|
|
57
56
|
|
|
58
57
|
try:
|
|
59
|
-
self.
|
|
58
|
+
self._youtrack_handler.update_state(
|
|
60
59
|
issue["id"], self._config.youtrack["picked_state"]
|
|
61
60
|
)
|
|
62
61
|
except requests.exceptions.HTTPError:
|
|
@@ -87,7 +86,7 @@ class CreateBranch:
|
|
|
87
86
|
"Insert the YouTrack issue number:", self._config.user["default_slug"]
|
|
88
87
|
)
|
|
89
88
|
|
|
90
|
-
def
|
|
89
|
+
def _ask_card_id(self):
|
|
91
90
|
suggestions = self._get_card_suggestions()
|
|
92
91
|
user_choice = (
|
|
93
92
|
self._select_card(suggestions)
|
|
@@ -98,8 +97,9 @@ class CreateBranch:
|
|
|
98
97
|
|
|
99
98
|
def _get_card_suggestions(self):
|
|
100
99
|
try:
|
|
101
|
-
return self.
|
|
102
|
-
self._config.user["card_suggest_query"],
|
|
100
|
+
return self._youtrack_handler.search_issues(
|
|
101
|
+
self._config.user["card_suggest_query"],
|
|
102
|
+
self._config.user["card_suggestions_limit"],
|
|
103
103
|
)
|
|
104
104
|
except Exception:
|
|
105
105
|
logger.warning(
|
|
@@ -6,6 +6,7 @@ from halo import Halo
|
|
|
6
6
|
from suite_py.lib import logger, metrics
|
|
7
7
|
from suite_py.lib.handler import git_handler as git
|
|
8
8
|
from suite_py.lib.handler import prompt_utils
|
|
9
|
+
from suite_py.lib.handler.backstage_handler import Backstage
|
|
9
10
|
from suite_py.lib.handler.captainhook_handler import CaptainHook
|
|
10
11
|
from suite_py.lib.handler.git_handler import GitHandler
|
|
11
12
|
from suite_py.lib.handler.github_handler import GithubHandler
|
|
@@ -14,11 +15,14 @@ from suite_py.lib.symbol import CHECKMARK, CROSSMARK
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class MergePR:
|
|
17
|
-
def __init__(
|
|
18
|
+
def __init__(
|
|
19
|
+
self, project, captainhook: CaptainHook, backstage: Backstage, config, tokens
|
|
20
|
+
):
|
|
18
21
|
self._project = project
|
|
19
22
|
self._config = config
|
|
20
23
|
self._youtrack = YoutrackHandler(config, tokens)
|
|
21
24
|
self._captainhook = captainhook
|
|
25
|
+
self._backstage = backstage
|
|
22
26
|
self._git = GitHandler(project, config)
|
|
23
27
|
self._github = GithubHandler(tokens)
|
|
24
28
|
|
|
@@ -63,6 +67,7 @@ class MergePR:
|
|
|
63
67
|
default=False,
|
|
64
68
|
):
|
|
65
69
|
self._captainhook.lock_project(self._project, "staging")
|
|
70
|
+
self._backstage.lock_project(self._project, "staging")
|
|
66
71
|
|
|
67
72
|
if youtrack_id:
|
|
68
73
|
logger.info("Updating card status on YouTrack...")
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
|
2
2
|
import sys
|
|
3
|
+
from string import Template
|
|
3
4
|
|
|
4
5
|
from github import GithubException
|
|
5
6
|
|
|
@@ -77,7 +78,10 @@ class OpenPR:
|
|
|
77
78
|
|
|
78
79
|
default_title = self._youtrack.get_issue(youtrack_id)["summary"]
|
|
79
80
|
title_without_prefix = _ask_for_title(default_title)
|
|
80
|
-
|
|
81
|
+
|
|
82
|
+
title = Template(self._config.user["pr_title_template"]).substitute(
|
|
83
|
+
card_id=youtrack_id, title=title_without_prefix
|
|
84
|
+
)
|
|
81
85
|
else:
|
|
82
86
|
logger.warning(
|
|
83
87
|
f"Creating pull request on the {self._project} project for the {self._branch_name} branch NOT linked to YouTrack card"
|
|
@@ -7,11 +7,12 @@ from suite_py.lib import logger, metrics
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class ProjectLock:
|
|
10
|
-
def __init__(self, project, env, action, captainhook):
|
|
10
|
+
def __init__(self, project, env, action, captainhook, backstage):
|
|
11
11
|
self._project = project
|
|
12
12
|
self._env = _parse_env(env)
|
|
13
13
|
self._action = action
|
|
14
14
|
self._captainhook = captainhook
|
|
15
|
+
self._backstage = backstage
|
|
15
16
|
|
|
16
17
|
@metrics.command("manage-project-lock")
|
|
17
18
|
def run(self):
|
|
@@ -25,6 +26,8 @@ class ProjectLock:
|
|
|
25
26
|
"Captainhook request timed out. Try with suite-py --timeout=60 lock-project lock"
|
|
26
27
|
)
|
|
27
28
|
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
self._backstage.lock_project(self._project, self._env)
|
|
28
31
|
elif self._action == "unlock":
|
|
29
32
|
try:
|
|
30
33
|
req = self._captainhook.unlock_project(self._project, self._env)
|
|
@@ -37,6 +40,8 @@ class ProjectLock:
|
|
|
37
40
|
"Captainhook request timed out. Try with suite-py --timeout=60 lock-project lock"
|
|
38
41
|
)
|
|
39
42
|
sys.exit(1)
|
|
43
|
+
|
|
44
|
+
self._backstage.unlock_project(self._project, self._env)
|
|
40
45
|
else:
|
|
41
46
|
logger.warning("I'm confused. Make the correct choice.")
|
|
42
47
|
sys.exit(-1)
|
|
@@ -42,6 +42,8 @@ class Release:
|
|
|
42
42
|
self._repo = self._github.get_repo(project)
|
|
43
43
|
self._git = GitHandler(project, config)
|
|
44
44
|
self._version = VersionHandler(self._repo, self._git, self._github)
|
|
45
|
+
# Cache for workflow events {check_run_database_id: event_type}
|
|
46
|
+
self.workflow_events = {}
|
|
45
47
|
|
|
46
48
|
@metrics.command("release")
|
|
47
49
|
def run(self):
|
|
@@ -145,6 +147,12 @@ class Release:
|
|
|
145
147
|
if self._commit or not commits:
|
|
146
148
|
return
|
|
147
149
|
|
|
150
|
+
# Batch fetch workflow events for all commits upfront for efficiency
|
|
151
|
+
shas = [c.sha for c in commits]
|
|
152
|
+
self.workflow_events = self._github.get_workflow_events_for_commits(
|
|
153
|
+
self._repo, shas
|
|
154
|
+
)
|
|
155
|
+
|
|
148
156
|
choices = []
|
|
149
157
|
|
|
150
158
|
def build_choice(c, icon):
|
|
@@ -196,9 +204,20 @@ class Release:
|
|
|
196
204
|
return []
|
|
197
205
|
|
|
198
206
|
def _classify_from_check_runs(self, check_runs):
|
|
199
|
-
|
|
207
|
+
"""Classify commit status from check runs, filtering by event type.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
check_runs: List of check run objects from GitHub API
|
|
211
|
+
|
|
212
|
+
Returns:
|
|
213
|
+
str: One of "in_progress", "failure", "cancelled", "success", "unknown"
|
|
214
|
+
"""
|
|
215
|
+
# Filter check runs by event type if workflow events are available
|
|
216
|
+
filtered_runs = self._filter_check_runs_by_event(check_runs)
|
|
217
|
+
|
|
218
|
+
statuses = {r.status for r in filtered_runs if getattr(r, "status", None)}
|
|
200
219
|
conclusions = {
|
|
201
|
-
r.conclusion for r in
|
|
220
|
+
r.conclusion for r in filtered_runs if getattr(r, "conclusion", None)
|
|
202
221
|
}
|
|
203
222
|
|
|
204
223
|
if self._any_in_progress(statuses):
|
|
@@ -211,6 +230,39 @@ class Release:
|
|
|
211
230
|
return "success"
|
|
212
231
|
return "unknown"
|
|
213
232
|
|
|
233
|
+
def _filter_check_runs_by_event(self, check_runs):
|
|
234
|
+
"""Filter check runs to include only push events and unknown events.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
check_runs: List of check run objects from GitHub API
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
list: Filtered list of check runs
|
|
241
|
+
"""
|
|
242
|
+
# If no workflow events cached, return all check runs (fallback)
|
|
243
|
+
if not self.workflow_events:
|
|
244
|
+
return check_runs
|
|
245
|
+
|
|
246
|
+
filtered = []
|
|
247
|
+
|
|
248
|
+
for check_run in check_runs:
|
|
249
|
+
# Get the database ID for this check run
|
|
250
|
+
database_id = getattr(check_run, "id", None)
|
|
251
|
+
if not database_id:
|
|
252
|
+
continue
|
|
253
|
+
|
|
254
|
+
# Look up the event type for this check run
|
|
255
|
+
event_type = self.workflow_events.get(database_id, "unknown")
|
|
256
|
+
|
|
257
|
+
# Include check runs with "push" or "unknown" event types
|
|
258
|
+
# "unknown" covers manual checks and status checks without workflows
|
|
259
|
+
if event_type in ("push", "unknown"):
|
|
260
|
+
filtered.append(check_run)
|
|
261
|
+
|
|
262
|
+
# If filtering resulted in no check runs, fall back to all check runs
|
|
263
|
+
# This handles edge cases where GraphQL data might be incomplete
|
|
264
|
+
return filtered if filtered else check_runs
|
|
265
|
+
|
|
214
266
|
@staticmethod
|
|
215
267
|
def _any_in_progress(statuses):
|
|
216
268
|
return any(
|
|
@@ -228,7 +280,11 @@ class Release:
|
|
|
228
280
|
def _is_success(conclusions):
|
|
229
281
|
return bool(conclusions) and (
|
|
230
282
|
all(c == "success" for c in conclusions)
|
|
231
|
-
or (
|
|
283
|
+
or (
|
|
284
|
+
"success" in conclusions
|
|
285
|
+
and not Release._any_failure(conclusions)
|
|
286
|
+
and "cancelled" not in conclusions
|
|
287
|
+
)
|
|
232
288
|
)
|
|
233
289
|
|
|
234
290
|
@staticmethod
|
|
@@ -36,11 +36,15 @@ class Config:
|
|
|
36
36
|
conf["user"].setdefault("default_slug", "PRIMA-XXX")
|
|
37
37
|
default_search = f"in:{conf['user']['default_slug'].split('-')[0]} #{{To Do}}"
|
|
38
38
|
conf["user"].setdefault("card_suggest_query", default_search)
|
|
39
|
+
conf["user"].setdefault("card_suggestions_limit", 5)
|
|
39
40
|
# This is in seconds
|
|
40
41
|
conf["user"].setdefault("captainhook_timeout", 30)
|
|
41
42
|
conf["user"].setdefault("captainhook_url", "https://captainhook.prima.it")
|
|
43
|
+
conf["user"].setdefault("backstage_timeout", 30)
|
|
44
|
+
conf["user"].setdefault("backstage_url", "https://backstage.helloprima.com")
|
|
42
45
|
conf["user"].setdefault("use_commits_in_pr_body", False)
|
|
43
46
|
conf["user"].setdefault("frequent_reviewers_max_number", 5)
|
|
47
|
+
conf["user"].setdefault("pr_title_template", "[$card_id]: $title")
|
|
44
48
|
|
|
45
49
|
conf["youtrack"].setdefault("add_reviewers_tags", True)
|
|
46
50
|
conf["youtrack"].setdefault("default_issue_type", "Task")
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
from suite_py.__version__ import __version__
|
|
5
|
+
from suite_py.lib.handler.github_handler import GithubHandler
|
|
6
|
+
from suite_py.lib.handler.okta_handler import Okta
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BackstageResponseError(Exception): ...
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Backstage:
|
|
13
|
+
_okta: Okta
|
|
14
|
+
_github: GithubHandler
|
|
15
|
+
|
|
16
|
+
def __init__(self, config, okta: Okta, tokens=None):
|
|
17
|
+
backstage_url = config.user["backstage_url"]
|
|
18
|
+
self._baseurl = f"{backstage_url}/api/entity-locking"
|
|
19
|
+
self._timeout = config.user["backstage_timeout"]
|
|
20
|
+
self._okta = okta
|
|
21
|
+
|
|
22
|
+
if tokens is not None:
|
|
23
|
+
self._github = GithubHandler(tokens)
|
|
24
|
+
|
|
25
|
+
def lock_project(self, project, env):
|
|
26
|
+
return self.send_request(
|
|
27
|
+
"post",
|
|
28
|
+
"/projects/manage-lock",
|
|
29
|
+
json={
|
|
30
|
+
"project": project,
|
|
31
|
+
"status": "locked",
|
|
32
|
+
"user": self._get_user(),
|
|
33
|
+
"environment": env,
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def unlock_project(self, project, env):
|
|
38
|
+
return self.send_request(
|
|
39
|
+
"post",
|
|
40
|
+
"/projects/manage-lock",
|
|
41
|
+
json={
|
|
42
|
+
"project": project,
|
|
43
|
+
"status": "unlocked",
|
|
44
|
+
"user": self._get_user(),
|
|
45
|
+
"environment": env,
|
|
46
|
+
},
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
def status(self, project, env):
|
|
50
|
+
return self.send_request(
|
|
51
|
+
"get", f"/projects/check?project={project}&environment={env}"
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def check(self):
|
|
55
|
+
response = self.send_request("get", "/ping", timeout=(2, self._timeout))
|
|
56
|
+
assert response.text == "pong"
|
|
57
|
+
|
|
58
|
+
def send_request(self, method, endpoint, data=None, json=None, timeout=None):
|
|
59
|
+
response = requests.request(
|
|
60
|
+
method,
|
|
61
|
+
f"{self._baseurl}{endpoint}",
|
|
62
|
+
headers=self._headers(),
|
|
63
|
+
data=data,
|
|
64
|
+
json=json,
|
|
65
|
+
timeout=(timeout or self._timeout),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
if response.status_code == 200:
|
|
69
|
+
return response
|
|
70
|
+
|
|
71
|
+
raise BackstageResponseError(
|
|
72
|
+
f"Got unexpected status code from Backstage: {response.status_code}"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def _get_user(self):
|
|
76
|
+
return self._github.get_user().login
|
|
77
|
+
|
|
78
|
+
def _headers(self):
|
|
79
|
+
return {
|
|
80
|
+
"User-Agent": f"suite-py/{__version__}",
|
|
81
|
+
"Authorization": f"Bearer {self._okta.get_id_token()}",
|
|
82
|
+
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
from github import Auth, Github
|
|
3
|
+
from github.GitRelease import GitRelease # pylint: disable=unused-import
|
|
4
|
+
from github.Repository import Repository # pylint: disable=unused-import
|
|
5
|
+
|
|
6
|
+
from suite_py.lib import logger
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class GithubHandler:
|
|
10
|
+
# pylint: disable=too-many-public-methods
|
|
11
|
+
_organization = "primait"
|
|
12
|
+
|
|
13
|
+
def __init__(self, tokens):
|
|
14
|
+
auth = Auth.Token(tokens.github)
|
|
15
|
+
self._client = Github(auth=auth)
|
|
16
|
+
|
|
17
|
+
def get_repo(self, repo_name):
|
|
18
|
+
return self._client.get_repo(f"{self._organization}/{repo_name}")
|
|
19
|
+
|
|
20
|
+
def get_organization(self):
|
|
21
|
+
return self._client.get_organization(self._organization)
|
|
22
|
+
|
|
23
|
+
def get_user(self):
|
|
24
|
+
return self._client.get_user()
|
|
25
|
+
|
|
26
|
+
# pylint: disable=too-many-positional-arguments
|
|
27
|
+
def create_pr(self, repo, branch, title, body="", base="master", is_draft=False):
|
|
28
|
+
return self.get_repo(repo).create_pull(
|
|
29
|
+
title=title, head=branch, base=base, body=body, draft=is_draft
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def get_pr(self, repo, pr_number):
|
|
33
|
+
return self.get_repo(repo).get_pull(pr_number)
|
|
34
|
+
|
|
35
|
+
def get_branch_from_pr(self, repo, pr_number):
|
|
36
|
+
repo = self.get_repo(repo)
|
|
37
|
+
return repo.get_branch(repo.get_pull(pr_number).head.ref)
|
|
38
|
+
|
|
39
|
+
def get_tags(self, repo):
|
|
40
|
+
return self.get_repo(repo).get_tags()
|
|
41
|
+
|
|
42
|
+
def get_team_members(self, team_name=""):
|
|
43
|
+
return self.get_organization().get_team_by_slug(team_name).get_members()
|
|
44
|
+
|
|
45
|
+
def get_all_users(self):
|
|
46
|
+
return self.get_organization().get_members()
|
|
47
|
+
|
|
48
|
+
def get_list_pr(self, repo):
|
|
49
|
+
pulls = self.get_repo(repo).get_pulls(
|
|
50
|
+
state="open", sort="created", base="master"
|
|
51
|
+
)
|
|
52
|
+
return pulls
|
|
53
|
+
|
|
54
|
+
def get_pr_from_branch(self, repo, branch):
|
|
55
|
+
return self.get_repo(repo).get_pulls(head=f"primait:{branch}")
|
|
56
|
+
|
|
57
|
+
def get_link_from_pr(self, repo, pr_number):
|
|
58
|
+
return f"https://github.com/primait/{repo}/pull/{pr_number}"
|
|
59
|
+
|
|
60
|
+
def get_commits_since_release(self, repo, tag):
|
|
61
|
+
release_commit = repo.get_commit(tag)
|
|
62
|
+
commits = []
|
|
63
|
+
for c in repo.get_commits():
|
|
64
|
+
if c == release_commit:
|
|
65
|
+
break
|
|
66
|
+
commits.append(c)
|
|
67
|
+
return commits
|
|
68
|
+
|
|
69
|
+
def get_latest_release_if_exists(self, repo):
|
|
70
|
+
try:
|
|
71
|
+
return repo.get_latest_release()
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def user_is_admin(self, repo):
|
|
76
|
+
return self.get_repo(repo).permissions.admin
|
|
77
|
+
|
|
78
|
+
def get_release_if_exists(self, repo, release):
|
|
79
|
+
try:
|
|
80
|
+
return repo.get_release(release)
|
|
81
|
+
except Exception:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
def get_build_status(self, repo, ref):
|
|
85
|
+
return self.get_repo(repo).get_commit(ref).get_combined_status()
|
|
86
|
+
|
|
87
|
+
def get_releases(self, repo):
|
|
88
|
+
return self.get_repo(repo).get_releases()
|
|
89
|
+
|
|
90
|
+
def get_branches(self, repo):
|
|
91
|
+
return self.get_repo(repo).get_branches()
|
|
92
|
+
|
|
93
|
+
def get_raw_content(self, repo, ref, file):
|
|
94
|
+
repo = self.get_repo(repo)
|
|
95
|
+
content = repo.get_contents(file, ref)
|
|
96
|
+
return content.decoded_content
|
|
97
|
+
|
|
98
|
+
def _execute_graphql(self, query, variables):
|
|
99
|
+
"""
|
|
100
|
+
Execute a GraphQL query against GitHub's GraphQL API.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
query: The GraphQL query string
|
|
104
|
+
variables: Dictionary of variables for the query (can be None)
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
dict: The parsed JSON response, or None if an error occurs
|
|
108
|
+
"""
|
|
109
|
+
try:
|
|
110
|
+
requester = self._client.requester
|
|
111
|
+
response = requester.graphql_query(query, variables)
|
|
112
|
+
return response
|
|
113
|
+
except Exception as error:
|
|
114
|
+
logger.error(
|
|
115
|
+
"GraphQL query failed: %s. Variables: %s",
|
|
116
|
+
str(error),
|
|
117
|
+
variables,
|
|
118
|
+
)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
def get_workflow_events_for_commits(self, repo, shas):
|
|
122
|
+
"""Fetch workflow events for a list of commit SHAs using GraphQL.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
repo: Repository object from PyGithub
|
|
126
|
+
shas: List of commit SHA strings
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
dict: Mapping of {check_run_database_id: event_type}
|
|
130
|
+
Returns empty dict if GraphQL query fails or response contains errors.
|
|
131
|
+
This allows callers to continue with unfiltered check runs as fallback.
|
|
132
|
+
|
|
133
|
+
Raises:
|
|
134
|
+
ValueError: If response parsing fails (raised by _parse_workflow_events_response)
|
|
135
|
+
"""
|
|
136
|
+
if not shas:
|
|
137
|
+
return {}
|
|
138
|
+
|
|
139
|
+
# Build GraphQL query using variables for commit SHAs (not string interpolation)
|
|
140
|
+
# The GitHub GraphQL API only accepts single oid per object() call
|
|
141
|
+
commit_fragments = []
|
|
142
|
+
variables = {
|
|
143
|
+
"owner": repo.owner.login,
|
|
144
|
+
"repo": repo.name,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
# Add each SHA as a variable and build corresponding query fragment
|
|
148
|
+
for idx, sha in enumerate(shas):
|
|
149
|
+
sha_var = f"sha{idx}"
|
|
150
|
+
variables[sha_var] = sha
|
|
151
|
+
|
|
152
|
+
commit_fragments.append(
|
|
153
|
+
f"""
|
|
154
|
+
commit{idx}: object(oid: ${sha_var}) {{
|
|
155
|
+
... on Commit {{
|
|
156
|
+
oid
|
|
157
|
+
checkSuites(first: 100) {{
|
|
158
|
+
nodes {{
|
|
159
|
+
workflowRun {{
|
|
160
|
+
event
|
|
161
|
+
}}
|
|
162
|
+
checkRuns(first: 100) {{
|
|
163
|
+
nodes {{
|
|
164
|
+
databaseId
|
|
165
|
+
name
|
|
166
|
+
}}
|
|
167
|
+
}}
|
|
168
|
+
}}
|
|
169
|
+
}}
|
|
170
|
+
}}
|
|
171
|
+
}}
|
|
172
|
+
"""
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Build query signature with all SHA variables
|
|
176
|
+
sha_params = ", ".join([f"$sha{i}: GitObjectID!" for i in range(len(shas))])
|
|
177
|
+
commit_fragments_str = "\n".join(commit_fragments)
|
|
178
|
+
query = f"""
|
|
179
|
+
query($owner: String!, $repo: String!, {sha_params}) {{
|
|
180
|
+
repository(owner: $owner, name: $repo) {{
|
|
181
|
+
{commit_fragments_str}
|
|
182
|
+
}}
|
|
183
|
+
}}
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
response = self._execute_graphql(query, variables)
|
|
187
|
+
|
|
188
|
+
if not response or "errors" in response or "data" not in response:
|
|
189
|
+
if response and "errors" in response:
|
|
190
|
+
logger.error(
|
|
191
|
+
"GraphQL errors for workflow events (repo=%s/%s, commits=%d): %s",
|
|
192
|
+
repo.owner.login,
|
|
193
|
+
repo.name,
|
|
194
|
+
len(shas),
|
|
195
|
+
response["errors"],
|
|
196
|
+
)
|
|
197
|
+
elif not response:
|
|
198
|
+
logger.warning(
|
|
199
|
+
"No GraphQL response for workflow events (repo=%s/%s, commits=%d)",
|
|
200
|
+
repo.owner.login,
|
|
201
|
+
repo.name,
|
|
202
|
+
len(shas),
|
|
203
|
+
)
|
|
204
|
+
return {}
|
|
205
|
+
|
|
206
|
+
return self._parse_workflow_events_response(response, shas)
|
|
207
|
+
|
|
208
|
+
def _parse_workflow_events_response(self, response, shas):
|
|
209
|
+
"""Parse GraphQL response to extract workflow events for commits.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
response: GraphQL response dict from GitHub API
|
|
213
|
+
shas: List of commit SHA strings (used for indexing aliased queries)
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
dict: Mapping of {check_run_database_id: event_type}
|
|
217
|
+
|
|
218
|
+
Raises:
|
|
219
|
+
ValueError: If response structure is invalid or unexpected
|
|
220
|
+
"""
|
|
221
|
+
result = {}
|
|
222
|
+
try:
|
|
223
|
+
repository = response.get("data", {}).get("repository", {})
|
|
224
|
+
|
|
225
|
+
# Iterate through the aliased commit responses
|
|
226
|
+
for idx in range(len(shas)):
|
|
227
|
+
commit_alias = f"commit{idx}"
|
|
228
|
+
commit_data = repository.get(commit_alias)
|
|
229
|
+
|
|
230
|
+
if not commit_data:
|
|
231
|
+
continue
|
|
232
|
+
|
|
233
|
+
check_suites = commit_data.get("checkSuites", {}).get("nodes", [])
|
|
234
|
+
for suite in check_suites:
|
|
235
|
+
if not suite:
|
|
236
|
+
continue
|
|
237
|
+
|
|
238
|
+
# Handle case where workflowRun is None (manual checks, status checks)
|
|
239
|
+
workflow_run = suite.get("workflowRun")
|
|
240
|
+
event_type = (
|
|
241
|
+
workflow_run.get("event") if workflow_run else "unknown"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
check_runs = suite.get("checkRuns", {}).get("nodes", [])
|
|
245
|
+
for check_run in check_runs:
|
|
246
|
+
if not check_run:
|
|
247
|
+
continue
|
|
248
|
+
|
|
249
|
+
database_id = check_run.get("databaseId")
|
|
250
|
+
if database_id:
|
|
251
|
+
result[database_id] = event_type
|
|
252
|
+
|
|
253
|
+
except (AttributeError, KeyError, TypeError) as error:
|
|
254
|
+
# Log and raise if there's any issue parsing the response
|
|
255
|
+
logger.error(
|
|
256
|
+
"Failed to parse workflow events response: %s. Response structure may be unexpected.",
|
|
257
|
+
str(error),
|
|
258
|
+
)
|
|
259
|
+
raise ValueError(
|
|
260
|
+
f"Failed to parse GraphQL workflow events response: {error}"
|
|
261
|
+
) from error
|
|
262
|
+
|
|
263
|
+
return result
|
|
@@ -19,6 +19,7 @@ WORKABLE_CARD_TYPES = [
|
|
|
19
19
|
"Rework",
|
|
20
20
|
"Analysis",
|
|
21
21
|
"Sec Issue",
|
|
22
|
+
"Task",
|
|
22
23
|
"UXUI",
|
|
23
24
|
]
|
|
24
25
|
|
|
@@ -53,10 +54,12 @@ class YoutrackHandler:
|
|
|
53
54
|
"customFields(name,value(name))",
|
|
54
55
|
]
|
|
55
56
|
params = {"fields": ",".join(_fields)}
|
|
57
|
+
|
|
56
58
|
issue = self._client.get(f"issues/{issue_id}", params=params).json()
|
|
57
|
-
issue["Type"] = self._get_issue_type_name(issue)
|
|
58
59
|
logger.debug(f"YouTrack Issue json {issue}")
|
|
59
60
|
|
|
61
|
+
issue["Type"] = self._get_issue_type_name(issue)
|
|
62
|
+
|
|
60
63
|
return issue
|
|
61
64
|
|
|
62
65
|
def search_issues(self, search, amount=5):
|
|
@@ -210,17 +213,50 @@ class YoutrackHandler:
|
|
|
210
213
|
return re.sub(REGEX, f"[\\1]({self._issue_url}\\1)", text)
|
|
211
214
|
|
|
212
215
|
def _get_issue_type_name(self, issue):
|
|
213
|
-
|
|
216
|
+
try:
|
|
217
|
+
type_field = [x for x in issue["customFields"] if x["name"] == "Type"][0]
|
|
218
|
+
|
|
219
|
+
if isinstance(type_field, list):
|
|
220
|
+
logger.debug(
|
|
221
|
+
"Type field is a list. Defaulting to first value!",
|
|
222
|
+
extra={"card_id": issue["idReadable"], "type_field": type_field},
|
|
223
|
+
)
|
|
224
|
+
type_field = type_field[0]
|
|
225
|
+
|
|
226
|
+
if type_field["value"]["name"] in WORKABLE_CARD_TYPES:
|
|
227
|
+
return type_field["value"]["name"]
|
|
228
|
+
|
|
229
|
+
logger.debug(
|
|
230
|
+
"Type field %s is not workable",
|
|
231
|
+
type_field["value"]["name"],
|
|
232
|
+
extra={"card_id": issue["idReadable"]},
|
|
233
|
+
)
|
|
214
234
|
|
|
215
|
-
|
|
216
|
-
|
|
235
|
+
if len(issue["parent"]["issues"]) == 0:
|
|
236
|
+
return self._default_issue_type
|
|
217
237
|
|
|
218
|
-
|
|
219
|
-
|
|
238
|
+
parent = issue["parent"]["issues"][0]
|
|
239
|
+
# recursively get parent issue's type
|
|
240
|
+
return self.get_issue(parent["id"])["Type"]
|
|
220
241
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
242
|
+
except Exception:
|
|
243
|
+
logger.warning(
|
|
244
|
+
"""
|
|
245
|
+
Failed to get the issue type from youtrack card: %s. Defaulting to %s.
|
|
246
|
+
This is likely caused by either a misconfiguration of your YouTrack board or a suite-py bug. Ask the DevEx team for help.
|
|
247
|
+
""",
|
|
248
|
+
issue["idReadable"],
|
|
249
|
+
self._default_issue_type,
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
# Log exception in debug mode
|
|
253
|
+
logger.debug(
|
|
254
|
+
"Failed to get issue type from youtrack card %s",
|
|
255
|
+
issue["idReadable"],
|
|
256
|
+
exc_info=True,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return self._default_issue_type
|
|
224
260
|
|
|
225
261
|
def get_issue_descendants(self, issue_id):
|
|
226
262
|
_fields = [
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
|
-
from github import Github
|
|
3
|
-
from github.GitRelease import GitRelease # pylint: disable=unused-import
|
|
4
|
-
from github.Repository import Repository # pylint: disable=unused-import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class GithubHandler:
|
|
8
|
-
_organization = "primait"
|
|
9
|
-
|
|
10
|
-
def __init__(self, tokens):
|
|
11
|
-
self._client = Github(tokens.github)
|
|
12
|
-
|
|
13
|
-
def get_repo(self, repo_name):
|
|
14
|
-
return self._client.get_repo(f"{self._organization}/{repo_name}")
|
|
15
|
-
|
|
16
|
-
def get_organization(self):
|
|
17
|
-
return self._client.get_organization(self._organization)
|
|
18
|
-
|
|
19
|
-
def get_user(self):
|
|
20
|
-
return self._client.get_user()
|
|
21
|
-
|
|
22
|
-
# pylint: disable=too-many-positional-arguments
|
|
23
|
-
def create_pr(self, repo, branch, title, body="", base="master", is_draft=False):
|
|
24
|
-
return self.get_repo(repo).create_pull(
|
|
25
|
-
title=title, head=branch, base=base, body=body, draft=is_draft
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
def get_pr(self, repo, pr_number):
|
|
29
|
-
return self.get_repo(repo).get_pull(pr_number)
|
|
30
|
-
|
|
31
|
-
def get_branch_from_pr(self, repo, pr_number):
|
|
32
|
-
repo = self.get_repo(repo)
|
|
33
|
-
return repo.get_branch(repo.get_pull(pr_number).head.ref)
|
|
34
|
-
|
|
35
|
-
def get_tags(self, repo):
|
|
36
|
-
return self.get_repo(repo).get_tags()
|
|
37
|
-
|
|
38
|
-
def get_team_members(self, team_name=""):
|
|
39
|
-
return self.get_organization().get_team_by_slug(team_name).get_members()
|
|
40
|
-
|
|
41
|
-
def get_all_users(self):
|
|
42
|
-
return self.get_organization().get_members()
|
|
43
|
-
|
|
44
|
-
def get_list_pr(self, repo):
|
|
45
|
-
pulls = self.get_repo(repo).get_pulls(
|
|
46
|
-
state="open", sort="created", base="master"
|
|
47
|
-
)
|
|
48
|
-
return pulls
|
|
49
|
-
|
|
50
|
-
def get_pr_from_branch(self, repo, branch):
|
|
51
|
-
return self.get_repo(repo).get_pulls(head=f"primait:{branch}")
|
|
52
|
-
|
|
53
|
-
def get_link_from_pr(self, repo, pr_number):
|
|
54
|
-
return f"https://github.com/primait/{repo}/pull/{pr_number}"
|
|
55
|
-
|
|
56
|
-
def get_commits_since_release(self, repo, tag):
|
|
57
|
-
release_commit = repo.get_commit(tag)
|
|
58
|
-
commits = []
|
|
59
|
-
for c in repo.get_commits():
|
|
60
|
-
if c == release_commit:
|
|
61
|
-
break
|
|
62
|
-
commits.append(c)
|
|
63
|
-
return commits
|
|
64
|
-
|
|
65
|
-
def get_latest_release_if_exists(self, repo):
|
|
66
|
-
try:
|
|
67
|
-
return repo.get_latest_release()
|
|
68
|
-
except Exception:
|
|
69
|
-
return None
|
|
70
|
-
|
|
71
|
-
def user_is_admin(self, repo):
|
|
72
|
-
return self.get_repo(repo).permissions.admin
|
|
73
|
-
|
|
74
|
-
def get_release_if_exists(self, repo, release):
|
|
75
|
-
try:
|
|
76
|
-
return repo.get_release(release)
|
|
77
|
-
except Exception:
|
|
78
|
-
return None
|
|
79
|
-
|
|
80
|
-
def get_build_status(self, repo, ref):
|
|
81
|
-
return self.get_repo(repo).get_commit(ref).get_combined_status()
|
|
82
|
-
|
|
83
|
-
def get_releases(self, repo):
|
|
84
|
-
return self.get_repo(repo).get_releases()
|
|
85
|
-
|
|
86
|
-
def get_branches(self, repo):
|
|
87
|
-
return self.get_repo(repo).get_branches()
|
|
88
|
-
|
|
89
|
-
def get_raw_content(self, repo, ref, file):
|
|
90
|
-
repo = self.get_repo(repo)
|
|
91
|
-
content = repo.get_contents(file, ref)
|
|
92
|
-
return content.decoded_content
|
|
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
|