ai-cr 3.2.1__py3-none-any.whl → 3.3.0__py3-none-any.whl
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.
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/LICENSE +21 -21
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/METADATA +1 -1
- ai_cr-3.3.0.dist-info/RECORD +41 -0
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/WHEEL +1 -1
- gito/__main__.py +4 -4
- gito/bootstrap.py +90 -90
- gito/cli.py +255 -244
- gito/cli_base.py +104 -94
- gito/commands/__init__.py +1 -1
- gito/commands/deploy.py +138 -138
- gito/commands/fix.py +160 -160
- gito/commands/gh_post_review_comment.py +111 -111
- gito/commands/gh_react_to_comment.py +217 -217
- gito/commands/linear_comment.py +53 -53
- gito/commands/repl.py +30 -30
- gito/commands/version.py +8 -8
- gito/config.toml +450 -448
- gito/constants.py +15 -14
- gito/context.py +19 -19
- gito/core.py +520 -508
- gito/env.py +8 -7
- gito/gh_api.py +116 -116
- gito/issue_trackers.py +50 -50
- gito/pipeline.py +83 -83
- gito/pipeline_steps/jira.py +62 -62
- gito/pipeline_steps/linear.py +85 -85
- gito/project_config.py +85 -85
- gito/report_struct.py +136 -136
- gito/tpl/answer.j2 +25 -25
- gito/tpl/github_workflows/components/env-vars.j2 +11 -11
- gito/tpl/github_workflows/components/installs.j2 +23 -23
- gito/tpl/github_workflows/gito-code-review.yml.j2 +32 -32
- gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +70 -70
- gito/tpl/partial/aux_files.j2 +8 -8
- gito/tpl/questions/changes_summary.j2 +55 -55
- gito/tpl/questions/release_notes.j2 +26 -26
- gito/tpl/questions/test_cases.j2 +37 -37
- gito/utils.py +267 -242
- ai_cr-3.2.1.dist-info/RECORD +0 -41
- {ai_cr-3.2.1.dist-info → ai_cr-3.3.0.dist-info}/entry_points.txt +0 -0
gito/env.py
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
from importlib.metadata import version
|
2
|
-
|
3
|
-
|
4
|
-
class Env:
|
5
|
-
logging_level: int = 1
|
6
|
-
verbosity: int = 1
|
7
|
-
gito_version: str = version("gito.bot")
|
1
|
+
from importlib.metadata import version
|
2
|
+
|
3
|
+
|
4
|
+
class Env:
|
5
|
+
logging_level: int = 1
|
6
|
+
verbosity: int = 1
|
7
|
+
gito_version: str = version("gito.bot")
|
8
|
+
working_folder = "."
|
gito/gh_api.py
CHANGED
@@ -1,116 +1,116 @@
|
|
1
|
-
import os
|
2
|
-
import logging
|
3
|
-
|
4
|
-
import requests
|
5
|
-
import git
|
6
|
-
from fastcore.basics import AttrDict # objects returned by ghapi
|
7
|
-
from ghapi.core import GhApi
|
8
|
-
|
9
|
-
from .project_config import ProjectConfig
|
10
|
-
from .utils import extract_gh_owner_repo
|
11
|
-
|
12
|
-
|
13
|
-
def gh_api(
|
14
|
-
repo: git.Repo = None, # used to resolve owner/repo
|
15
|
-
config: ProjectConfig | None = None, # used to resolve owner/repo
|
16
|
-
token: str | None = None
|
17
|
-
) -> GhApi:
|
18
|
-
if repo:
|
19
|
-
# resolve owner/repo from repo.remotes.origin.url
|
20
|
-
owner, repo_name = extract_gh_owner_repo(repo)
|
21
|
-
else:
|
22
|
-
if not config:
|
23
|
-
config = ProjectConfig.load()
|
24
|
-
# resolve owner/repo from github env vars (github actions)
|
25
|
-
gh_env = config.prompt_vars.get("github_env", {})
|
26
|
-
gh_repo = gh_env.get("github_repo")
|
27
|
-
if not gh_repo:
|
28
|
-
raise ValueError("GitHub repository not specified and not found in project config.")
|
29
|
-
parts = gh_repo.split('/')
|
30
|
-
if len(parts) != 2:
|
31
|
-
raise ValueError(f"Invalid GitHub repository format: {gh_repo}. Expected 'owner/repo'.")
|
32
|
-
owner, repo_name = parts
|
33
|
-
|
34
|
-
token = resolve_gh_token(token)
|
35
|
-
api = GhApi(owner, repo_name, token=token)
|
36
|
-
return api
|
37
|
-
|
38
|
-
|
39
|
-
def resolve_gh_token(token_or_none: str | None = None) -> str | None:
|
40
|
-
return token_or_none or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
|
41
|
-
|
42
|
-
|
43
|
-
def post_gh_comment(
|
44
|
-
gh_repository: str, # e.g. "owner/repo"
|
45
|
-
pr_or_issue_number: int,
|
46
|
-
gh_token: str,
|
47
|
-
text: str,
|
48
|
-
) -> bool:
|
49
|
-
"""
|
50
|
-
Post a comment to a GitHub pull request or issue.
|
51
|
-
Arguments:
|
52
|
-
gh_repository (str): The GitHub repository in the format "owner/repo".
|
53
|
-
pr_or_issue_number (int): The pull request or issue number.
|
54
|
-
gh_token (str): GitHub personal access token with permissions to post comments.
|
55
|
-
text (str): The comment text to post.
|
56
|
-
Returns:
|
57
|
-
True if the comment was posted successfully, False otherwise.
|
58
|
-
"""
|
59
|
-
api_url = f"https://api.github.com/repos/{gh_repository}/issues/{pr_or_issue_number}/comments"
|
60
|
-
headers = {
|
61
|
-
"Authorization": f"token {gh_token}",
|
62
|
-
"Accept": "application/vnd.github+json",
|
63
|
-
}
|
64
|
-
data = {"body": text}
|
65
|
-
|
66
|
-
resp = requests.post(api_url, headers=headers, json=data)
|
67
|
-
if 200 <= resp.status_code < 300:
|
68
|
-
logging.info(f"Posted review comment to #{pr_or_issue_number} in {gh_repository}")
|
69
|
-
return True
|
70
|
-
else:
|
71
|
-
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
72
|
-
return False
|
73
|
-
|
74
|
-
|
75
|
-
def hide_gh_comment(
|
76
|
-
comment: dict | str,
|
77
|
-
token: str = None,
|
78
|
-
reason: str = "OUTDATED"
|
79
|
-
) -> bool:
|
80
|
-
"""
|
81
|
-
Hide a GitHub comment using GraphQL API with specified reason.
|
82
|
-
Args:
|
83
|
-
comment (dict | str):
|
84
|
-
The comment to hide,
|
85
|
-
either as a object returned from ghapi or a string node ID.
|
86
|
-
note: comment.id is not the same as node_id.
|
87
|
-
token (str): GitHub personal access token with permissions to minimize comments.
|
88
|
-
reason (str): The reason for hiding the comment, e.g., "OUTDATED".
|
89
|
-
"""
|
90
|
-
comment_node_id = comment.node_id if isinstance(comment, AttrDict) else comment
|
91
|
-
token = resolve_gh_token(token)
|
92
|
-
mutation = """
|
93
|
-
mutation($commentId: ID!, $reason: ReportedContentClassifiers!) {
|
94
|
-
minimizeComment(input: {subjectId: $commentId, classifier: $reason}) {
|
95
|
-
minimizedComment { isMinimized }
|
96
|
-
}
|
97
|
-
}"""
|
98
|
-
|
99
|
-
response = requests.post(
|
100
|
-
"https://api.github.com/graphql",
|
101
|
-
headers={"Authorization": f"Bearer {token}"},
|
102
|
-
json={
|
103
|
-
"query": mutation,
|
104
|
-
"variables": {"commentId": comment_node_id, "reason": reason}
|
105
|
-
}
|
106
|
-
)
|
107
|
-
success = (
|
108
|
-
response.status_code == 200
|
109
|
-
and response.json().get("data", {}).get("minimizeComment") is not None
|
110
|
-
)
|
111
|
-
if not success:
|
112
|
-
logging.error(
|
113
|
-
f"Failed to hide comment {comment_node_id}: "
|
114
|
-
f"{response.status_code} {response.reason}\n{response.text}"
|
115
|
-
)
|
116
|
-
return success
|
1
|
+
import os
|
2
|
+
import logging
|
3
|
+
|
4
|
+
import requests
|
5
|
+
import git
|
6
|
+
from fastcore.basics import AttrDict # objects returned by ghapi
|
7
|
+
from ghapi.core import GhApi
|
8
|
+
|
9
|
+
from .project_config import ProjectConfig
|
10
|
+
from .utils import extract_gh_owner_repo
|
11
|
+
|
12
|
+
|
13
|
+
def gh_api(
|
14
|
+
repo: git.Repo = None, # used to resolve owner/repo
|
15
|
+
config: ProjectConfig | None = None, # used to resolve owner/repo
|
16
|
+
token: str | None = None
|
17
|
+
) -> GhApi:
|
18
|
+
if repo:
|
19
|
+
# resolve owner/repo from repo.remotes.origin.url
|
20
|
+
owner, repo_name = extract_gh_owner_repo(repo)
|
21
|
+
else:
|
22
|
+
if not config:
|
23
|
+
config = ProjectConfig.load()
|
24
|
+
# resolve owner/repo from github env vars (github actions)
|
25
|
+
gh_env = config.prompt_vars.get("github_env", {})
|
26
|
+
gh_repo = gh_env.get("github_repo")
|
27
|
+
if not gh_repo:
|
28
|
+
raise ValueError("GitHub repository not specified and not found in project config.")
|
29
|
+
parts = gh_repo.split('/')
|
30
|
+
if len(parts) != 2:
|
31
|
+
raise ValueError(f"Invalid GitHub repository format: {gh_repo}. Expected 'owner/repo'.")
|
32
|
+
owner, repo_name = parts
|
33
|
+
|
34
|
+
token = resolve_gh_token(token)
|
35
|
+
api = GhApi(owner, repo_name, token=token)
|
36
|
+
return api
|
37
|
+
|
38
|
+
|
39
|
+
def resolve_gh_token(token_or_none: str | None = None) -> str | None:
|
40
|
+
return token_or_none or os.getenv("GITHUB_TOKEN", None) or os.getenv("GH_TOKEN", None)
|
41
|
+
|
42
|
+
|
43
|
+
def post_gh_comment(
|
44
|
+
gh_repository: str, # e.g. "owner/repo"
|
45
|
+
pr_or_issue_number: int,
|
46
|
+
gh_token: str,
|
47
|
+
text: str,
|
48
|
+
) -> bool:
|
49
|
+
"""
|
50
|
+
Post a comment to a GitHub pull request or issue.
|
51
|
+
Arguments:
|
52
|
+
gh_repository (str): The GitHub repository in the format "owner/repo".
|
53
|
+
pr_or_issue_number (int): The pull request or issue number.
|
54
|
+
gh_token (str): GitHub personal access token with permissions to post comments.
|
55
|
+
text (str): The comment text to post.
|
56
|
+
Returns:
|
57
|
+
True if the comment was posted successfully, False otherwise.
|
58
|
+
"""
|
59
|
+
api_url = f"https://api.github.com/repos/{gh_repository}/issues/{pr_or_issue_number}/comments"
|
60
|
+
headers = {
|
61
|
+
"Authorization": f"token {gh_token}",
|
62
|
+
"Accept": "application/vnd.github+json",
|
63
|
+
}
|
64
|
+
data = {"body": text}
|
65
|
+
|
66
|
+
resp = requests.post(api_url, headers=headers, json=data)
|
67
|
+
if 200 <= resp.status_code < 300:
|
68
|
+
logging.info(f"Posted review comment to #{pr_or_issue_number} in {gh_repository}")
|
69
|
+
return True
|
70
|
+
else:
|
71
|
+
logging.error(f"Failed to post comment: {resp.status_code} {resp.reason}\n{resp.text}")
|
72
|
+
return False
|
73
|
+
|
74
|
+
|
75
|
+
def hide_gh_comment(
|
76
|
+
comment: dict | str,
|
77
|
+
token: str = None,
|
78
|
+
reason: str = "OUTDATED"
|
79
|
+
) -> bool:
|
80
|
+
"""
|
81
|
+
Hide a GitHub comment using GraphQL API with specified reason.
|
82
|
+
Args:
|
83
|
+
comment (dict | str):
|
84
|
+
The comment to hide,
|
85
|
+
either as a object returned from ghapi or a string node ID.
|
86
|
+
note: comment.id is not the same as node_id.
|
87
|
+
token (str): GitHub personal access token with permissions to minimize comments.
|
88
|
+
reason (str): The reason for hiding the comment, e.g., "OUTDATED".
|
89
|
+
"""
|
90
|
+
comment_node_id = comment.node_id if isinstance(comment, AttrDict) else comment
|
91
|
+
token = resolve_gh_token(token)
|
92
|
+
mutation = """
|
93
|
+
mutation($commentId: ID!, $reason: ReportedContentClassifiers!) {
|
94
|
+
minimizeComment(input: {subjectId: $commentId, classifier: $reason}) {
|
95
|
+
minimizedComment { isMinimized }
|
96
|
+
}
|
97
|
+
}"""
|
98
|
+
|
99
|
+
response = requests.post(
|
100
|
+
"https://api.github.com/graphql",
|
101
|
+
headers={"Authorization": f"Bearer {token}"},
|
102
|
+
json={
|
103
|
+
"query": mutation,
|
104
|
+
"variables": {"commentId": comment_node_id, "reason": reason}
|
105
|
+
}
|
106
|
+
)
|
107
|
+
success = (
|
108
|
+
response.status_code == 200
|
109
|
+
and response.json().get("data", {}).get("minimizeComment") is not None
|
110
|
+
)
|
111
|
+
if not success:
|
112
|
+
logging.error(
|
113
|
+
f"Failed to hide comment {comment_node_id}: "
|
114
|
+
f"{response.status_code} {response.reason}\n{response.text}"
|
115
|
+
)
|
116
|
+
return success
|
gito/issue_trackers.py
CHANGED
@@ -1,50 +1,50 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
import re
|
4
|
-
from dataclasses import dataclass, field
|
5
|
-
|
6
|
-
import git
|
7
|
-
from gito.utils import is_running_in_github_action
|
8
|
-
|
9
|
-
|
10
|
-
@dataclass
|
11
|
-
class IssueTrackerIssue:
|
12
|
-
title: str = field(default="")
|
13
|
-
description: str = field(default="")
|
14
|
-
url: str = field(default="")
|
15
|
-
|
16
|
-
|
17
|
-
def extract_issue_key(branch_name: str, min_len=2, max_len=10) -> str | None:
|
18
|
-
boundary = r'\b|_|-|/|\\'
|
19
|
-
pattern = fr"(?:{boundary})([A-Z][A-Z0-9]{{{min_len - 1},{max_len - 1}}}-\d+)(?:{boundary})"
|
20
|
-
match = re.search(pattern, branch_name)
|
21
|
-
return match.group(1) if match else None
|
22
|
-
|
23
|
-
|
24
|
-
def get_branch(repo: git.Repo):
|
25
|
-
if is_running_in_github_action():
|
26
|
-
branch_name = os.getenv('GITHUB_HEAD_REF')
|
27
|
-
if branch_name:
|
28
|
-
return branch_name
|
29
|
-
|
30
|
-
github_ref = os.getenv('GITHUB_REF', '')
|
31
|
-
if github_ref.startswith('refs/heads/'):
|
32
|
-
return github_ref.replace('refs/heads/', '')
|
33
|
-
try:
|
34
|
-
branch_name = repo.active_branch.name
|
35
|
-
return branch_name
|
36
|
-
except Exception as e: # @todo: specify more precise exception
|
37
|
-
logging.error("Could not determine the active branch name: %s", e)
|
38
|
-
return None
|
39
|
-
|
40
|
-
|
41
|
-
def resolve_issue_key(repo: git.Repo):
|
42
|
-
branch_name = get_branch(repo)
|
43
|
-
if not branch_name:
|
44
|
-
logging.error("No active branch found in the repository, cannot determine issue key.")
|
45
|
-
return None
|
46
|
-
|
47
|
-
if not (issue_key := extract_issue_key(branch_name)):
|
48
|
-
logging.error(f"No issue key found in branch name: {branch_name}")
|
49
|
-
return None
|
50
|
-
return issue_key
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
import re
|
4
|
+
from dataclasses import dataclass, field
|
5
|
+
|
6
|
+
import git
|
7
|
+
from gito.utils import is_running_in_github_action
|
8
|
+
|
9
|
+
|
10
|
+
@dataclass
|
11
|
+
class IssueTrackerIssue:
|
12
|
+
title: str = field(default="")
|
13
|
+
description: str = field(default="")
|
14
|
+
url: str = field(default="")
|
15
|
+
|
16
|
+
|
17
|
+
def extract_issue_key(branch_name: str, min_len=2, max_len=10) -> str | None:
|
18
|
+
boundary = r'\b|_|-|/|\\'
|
19
|
+
pattern = fr"(?:{boundary})([A-Z][A-Z0-9]{{{min_len - 1},{max_len - 1}}}-\d+)(?:{boundary})"
|
20
|
+
match = re.search(pattern, branch_name)
|
21
|
+
return match.group(1) if match else None
|
22
|
+
|
23
|
+
|
24
|
+
def get_branch(repo: git.Repo):
|
25
|
+
if is_running_in_github_action():
|
26
|
+
branch_name = os.getenv('GITHUB_HEAD_REF')
|
27
|
+
if branch_name:
|
28
|
+
return branch_name
|
29
|
+
|
30
|
+
github_ref = os.getenv('GITHUB_REF', '')
|
31
|
+
if github_ref.startswith('refs/heads/'):
|
32
|
+
return github_ref.replace('refs/heads/', '')
|
33
|
+
try:
|
34
|
+
branch_name = repo.active_branch.name
|
35
|
+
return branch_name
|
36
|
+
except Exception as e: # @todo: specify more precise exception
|
37
|
+
logging.error("Could not determine the active branch name: %s", e)
|
38
|
+
return None
|
39
|
+
|
40
|
+
|
41
|
+
def resolve_issue_key(repo: git.Repo):
|
42
|
+
branch_name = get_branch(repo)
|
43
|
+
if not branch_name:
|
44
|
+
logging.error("No active branch found in the repository, cannot determine issue key.")
|
45
|
+
return None
|
46
|
+
|
47
|
+
if not (issue_key := extract_issue_key(branch_name)):
|
48
|
+
logging.error(f"No issue key found in branch name: {branch_name}")
|
49
|
+
return None
|
50
|
+
return issue_key
|
gito/pipeline.py
CHANGED
@@ -1,83 +1,83 @@
|
|
1
|
-
import logging
|
2
|
-
from enum import StrEnum
|
3
|
-
from dataclasses import dataclass, field
|
4
|
-
|
5
|
-
from microcore import ui
|
6
|
-
from microcore.utils import resolve_callable
|
7
|
-
|
8
|
-
from .context import Context
|
9
|
-
from .utils import is_running_in_github_action
|
10
|
-
|
11
|
-
|
12
|
-
class PipelineEnv(StrEnum):
|
13
|
-
LOCAL = "local"
|
14
|
-
GH_ACTION = "gh-action"
|
15
|
-
|
16
|
-
@staticmethod
|
17
|
-
def all():
|
18
|
-
return [PipelineEnv.LOCAL, PipelineEnv.GH_ACTION]
|
19
|
-
|
20
|
-
@staticmethod
|
21
|
-
def current():
|
22
|
-
return (
|
23
|
-
PipelineEnv.GH_ACTION
|
24
|
-
if is_running_in_github_action()
|
25
|
-
else PipelineEnv.LOCAL
|
26
|
-
)
|
27
|
-
|
28
|
-
|
29
|
-
@dataclass
|
30
|
-
class PipelineStep:
|
31
|
-
call: str
|
32
|
-
envs: list[PipelineEnv] = field(default_factory=PipelineEnv.all)
|
33
|
-
enabled: bool = field(default=True)
|
34
|
-
|
35
|
-
def get_callable(self):
|
36
|
-
"""
|
37
|
-
Resolve the callable from the string representation.
|
38
|
-
"""
|
39
|
-
return resolve_callable(self.call)
|
40
|
-
|
41
|
-
def run(self, *args, **kwargs):
|
42
|
-
return self.get_callable()(*args, **kwargs)
|
43
|
-
|
44
|
-
|
45
|
-
@dataclass
|
46
|
-
class Pipeline:
|
47
|
-
ctx: Context = field()
|
48
|
-
steps: dict[str, PipelineStep] = field(default_factory=dict)
|
49
|
-
verbose: bool = False
|
50
|
-
|
51
|
-
@property
|
52
|
-
def enabled_steps(self):
|
53
|
-
return {
|
54
|
-
k: v for k, v in self.steps.items() if v.enabled
|
55
|
-
}
|
56
|
-
|
57
|
-
def run(self, *args, **kwargs):
|
58
|
-
cur_env = PipelineEnv.current()
|
59
|
-
logging.info("Running pipeline... [env: %s]", ui.yellow(cur_env))
|
60
|
-
for step_name, step in self.enabled_steps.items():
|
61
|
-
if cur_env in step.envs:
|
62
|
-
logging.info(f"Running pipeline step: {step_name}")
|
63
|
-
try:
|
64
|
-
step_output = step.run(*args, **kwargs, **vars(self.ctx))
|
65
|
-
if isinstance(step_output, dict):
|
66
|
-
self.ctx.pipeline_out.update(step_output)
|
67
|
-
self.ctx.pipeline_out[step_name] = step_output
|
68
|
-
if self.verbose and step_output:
|
69
|
-
logging.info(
|
70
|
-
f"Pipeline step {step_name} output: {repr(step_output)}"
|
71
|
-
)
|
72
|
-
if not step_output:
|
73
|
-
logging.warning(
|
74
|
-
f'Pipeline step "{step_name}" returned {repr(step_output)}.'
|
75
|
-
)
|
76
|
-
except Exception as e:
|
77
|
-
logging.error(f'Error in pipeline step "{step_name}": {e}')
|
78
|
-
else:
|
79
|
-
logging.info(
|
80
|
-
f"Skipping pipeline step: {step_name}"
|
81
|
-
f" [env: {ui.yellow(cur_env)} not in {step.envs}]"
|
82
|
-
)
|
83
|
-
return self.ctx.pipeline_out
|
1
|
+
import logging
|
2
|
+
from enum import StrEnum
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
|
5
|
+
from microcore import ui
|
6
|
+
from microcore.utils import resolve_callable
|
7
|
+
|
8
|
+
from .context import Context
|
9
|
+
from .utils import is_running_in_github_action
|
10
|
+
|
11
|
+
|
12
|
+
class PipelineEnv(StrEnum):
|
13
|
+
LOCAL = "local"
|
14
|
+
GH_ACTION = "gh-action"
|
15
|
+
|
16
|
+
@staticmethod
|
17
|
+
def all():
|
18
|
+
return [PipelineEnv.LOCAL, PipelineEnv.GH_ACTION]
|
19
|
+
|
20
|
+
@staticmethod
|
21
|
+
def current():
|
22
|
+
return (
|
23
|
+
PipelineEnv.GH_ACTION
|
24
|
+
if is_running_in_github_action()
|
25
|
+
else PipelineEnv.LOCAL
|
26
|
+
)
|
27
|
+
|
28
|
+
|
29
|
+
@dataclass
|
30
|
+
class PipelineStep:
|
31
|
+
call: str
|
32
|
+
envs: list[PipelineEnv] = field(default_factory=PipelineEnv.all)
|
33
|
+
enabled: bool = field(default=True)
|
34
|
+
|
35
|
+
def get_callable(self):
|
36
|
+
"""
|
37
|
+
Resolve the callable from the string representation.
|
38
|
+
"""
|
39
|
+
return resolve_callable(self.call)
|
40
|
+
|
41
|
+
def run(self, *args, **kwargs):
|
42
|
+
return self.get_callable()(*args, **kwargs)
|
43
|
+
|
44
|
+
|
45
|
+
@dataclass
|
46
|
+
class Pipeline:
|
47
|
+
ctx: Context = field()
|
48
|
+
steps: dict[str, PipelineStep] = field(default_factory=dict)
|
49
|
+
verbose: bool = False
|
50
|
+
|
51
|
+
@property
|
52
|
+
def enabled_steps(self):
|
53
|
+
return {
|
54
|
+
k: v for k, v in self.steps.items() if v.enabled
|
55
|
+
}
|
56
|
+
|
57
|
+
def run(self, *args, **kwargs):
|
58
|
+
cur_env = PipelineEnv.current()
|
59
|
+
logging.info("Running pipeline... [env: %s]", ui.yellow(cur_env))
|
60
|
+
for step_name, step in self.enabled_steps.items():
|
61
|
+
if cur_env in step.envs:
|
62
|
+
logging.info(f"Running pipeline step: {step_name}")
|
63
|
+
try:
|
64
|
+
step_output = step.run(*args, **kwargs, **vars(self.ctx))
|
65
|
+
if isinstance(step_output, dict):
|
66
|
+
self.ctx.pipeline_out.update(step_output)
|
67
|
+
self.ctx.pipeline_out[step_name] = step_output
|
68
|
+
if self.verbose and step_output:
|
69
|
+
logging.info(
|
70
|
+
f"Pipeline step {step_name} output: {repr(step_output)}"
|
71
|
+
)
|
72
|
+
if not step_output:
|
73
|
+
logging.warning(
|
74
|
+
f'Pipeline step "{step_name}" returned {repr(step_output)}.'
|
75
|
+
)
|
76
|
+
except Exception as e:
|
77
|
+
logging.error(f'Error in pipeline step "{step_name}": {e}')
|
78
|
+
else:
|
79
|
+
logging.info(
|
80
|
+
f"Skipping pipeline step: {step_name}"
|
81
|
+
f" [env: {ui.yellow(cur_env)} not in {step.envs}]"
|
82
|
+
)
|
83
|
+
return self.ctx.pipeline_out
|
gito/pipeline_steps/jira.py
CHANGED
@@ -1,62 +1,62 @@
|
|
1
|
-
import logging
|
2
|
-
import os
|
3
|
-
|
4
|
-
import git
|
5
|
-
from jira import JIRA, JIRAError
|
6
|
-
|
7
|
-
from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
|
8
|
-
|
9
|
-
|
10
|
-
def fetch_issue(issue_key, jira_url, username, api_token) -> IssueTrackerIssue | None:
|
11
|
-
try:
|
12
|
-
jira = JIRA(jira_url, basic_auth=(username, api_token))
|
13
|
-
issue = jira.issue(issue_key)
|
14
|
-
return IssueTrackerIssue(
|
15
|
-
title=issue.fields.summary,
|
16
|
-
description=issue.fields.description or "",
|
17
|
-
url=f"{jira_url.rstrip('/')}/browse/{issue_key}"
|
18
|
-
)
|
19
|
-
except JIRAError as e:
|
20
|
-
logging.error(
|
21
|
-
f"Failed to fetch Jira issue {issue_key}: code {e.status_code} :: {e.text}"
|
22
|
-
)
|
23
|
-
return None
|
24
|
-
except Exception as e:
|
25
|
-
logging.error(f"Failed to fetch Jira issue {issue_key}: {e}")
|
26
|
-
return None
|
27
|
-
|
28
|
-
|
29
|
-
def fetch_associated_issue(
|
30
|
-
repo: git.Repo,
|
31
|
-
jira_url=None,
|
32
|
-
jira_username=None,
|
33
|
-
jira_api_token=None,
|
34
|
-
**kwargs
|
35
|
-
):
|
36
|
-
"""
|
37
|
-
Pipeline step to fetch a Jira issue based on the current branch name.
|
38
|
-
"""
|
39
|
-
jira_url = jira_url or os.getenv("JIRA_URL")
|
40
|
-
jira_username = (
|
41
|
-
jira_username
|
42
|
-
or os.getenv("JIRA_USERNAME")
|
43
|
-
or os.getenv("JIRA_USER")
|
44
|
-
or os.getenv("JIRA_EMAIL")
|
45
|
-
)
|
46
|
-
jira_token = (
|
47
|
-
jira_api_token
|
48
|
-
or os.getenv("JIRA_API_TOKEN")
|
49
|
-
or os.getenv("JIRA_API_KEY")
|
50
|
-
or os.getenv("JIRA_TOKEN")
|
51
|
-
)
|
52
|
-
try:
|
53
|
-
assert jira_url, "JIRA_URL is not set"
|
54
|
-
assert jira_username, "JIRA_USERNAME is not set"
|
55
|
-
assert jira_token, "JIRA_API_TOKEN is not set"
|
56
|
-
except AssertionError as e:
|
57
|
-
logging.error(f"Jira configuration error: {e}")
|
58
|
-
return None
|
59
|
-
issue_key = resolve_issue_key(repo)
|
60
|
-
return dict(
|
61
|
-
associated_issue=fetch_issue(issue_key, jira_url, jira_username, jira_token)
|
62
|
-
) if issue_key else None
|
1
|
+
import logging
|
2
|
+
import os
|
3
|
+
|
4
|
+
import git
|
5
|
+
from jira import JIRA, JIRAError
|
6
|
+
|
7
|
+
from gito.issue_trackers import IssueTrackerIssue, resolve_issue_key
|
8
|
+
|
9
|
+
|
10
|
+
def fetch_issue(issue_key, jira_url, username, api_token) -> IssueTrackerIssue | None:
|
11
|
+
try:
|
12
|
+
jira = JIRA(jira_url, basic_auth=(username, api_token))
|
13
|
+
issue = jira.issue(issue_key)
|
14
|
+
return IssueTrackerIssue(
|
15
|
+
title=issue.fields.summary,
|
16
|
+
description=issue.fields.description or "",
|
17
|
+
url=f"{jira_url.rstrip('/')}/browse/{issue_key}"
|
18
|
+
)
|
19
|
+
except JIRAError as e:
|
20
|
+
logging.error(
|
21
|
+
f"Failed to fetch Jira issue {issue_key}: code {e.status_code} :: {e.text}"
|
22
|
+
)
|
23
|
+
return None
|
24
|
+
except Exception as e:
|
25
|
+
logging.error(f"Failed to fetch Jira issue {issue_key}: {e}")
|
26
|
+
return None
|
27
|
+
|
28
|
+
|
29
|
+
def fetch_associated_issue(
|
30
|
+
repo: git.Repo,
|
31
|
+
jira_url=None,
|
32
|
+
jira_username=None,
|
33
|
+
jira_api_token=None,
|
34
|
+
**kwargs
|
35
|
+
):
|
36
|
+
"""
|
37
|
+
Pipeline step to fetch a Jira issue based on the current branch name.
|
38
|
+
"""
|
39
|
+
jira_url = jira_url or os.getenv("JIRA_URL")
|
40
|
+
jira_username = (
|
41
|
+
jira_username
|
42
|
+
or os.getenv("JIRA_USERNAME")
|
43
|
+
or os.getenv("JIRA_USER")
|
44
|
+
or os.getenv("JIRA_EMAIL")
|
45
|
+
)
|
46
|
+
jira_token = (
|
47
|
+
jira_api_token
|
48
|
+
or os.getenv("JIRA_API_TOKEN")
|
49
|
+
or os.getenv("JIRA_API_KEY")
|
50
|
+
or os.getenv("JIRA_TOKEN")
|
51
|
+
)
|
52
|
+
try:
|
53
|
+
assert jira_url, "JIRA_URL is not set"
|
54
|
+
assert jira_username, "JIRA_USERNAME is not set"
|
55
|
+
assert jira_token, "JIRA_API_TOKEN is not set"
|
56
|
+
except AssertionError as e:
|
57
|
+
logging.error(f"Jira configuration error: {e}")
|
58
|
+
return None
|
59
|
+
issue_key = resolve_issue_key(repo)
|
60
|
+
return dict(
|
61
|
+
associated_issue=fetch_issue(issue_key, jira_url, jira_username, jira_token)
|
62
|
+
) if issue_key else None
|