ai-cr 3.2.2__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.
Files changed (40) hide show
  1. {ai_cr-3.2.2.dist-info → ai_cr-3.3.0.dist-info}/LICENSE +21 -21
  2. {ai_cr-3.2.2.dist-info → ai_cr-3.3.0.dist-info}/METADATA +1 -1
  3. ai_cr-3.3.0.dist-info/RECORD +41 -0
  4. {ai_cr-3.2.2.dist-info → ai_cr-3.3.0.dist-info}/WHEEL +1 -1
  5. gito/__main__.py +4 -4
  6. gito/bootstrap.py +90 -90
  7. gito/cli.py +255 -244
  8. gito/cli_base.py +104 -94
  9. gito/commands/__init__.py +1 -1
  10. gito/commands/deploy.py +138 -138
  11. gito/commands/fix.py +160 -160
  12. gito/commands/gh_post_review_comment.py +111 -111
  13. gito/commands/gh_react_to_comment.py +217 -217
  14. gito/commands/linear_comment.py +53 -53
  15. gito/commands/repl.py +30 -30
  16. gito/commands/version.py +8 -8
  17. gito/config.toml +450 -448
  18. gito/constants.py +15 -14
  19. gito/context.py +19 -19
  20. gito/core.py +520 -508
  21. gito/env.py +8 -7
  22. gito/gh_api.py +116 -116
  23. gito/issue_trackers.py +50 -50
  24. gito/pipeline.py +83 -83
  25. gito/pipeline_steps/jira.py +62 -62
  26. gito/pipeline_steps/linear.py +85 -85
  27. gito/project_config.py +85 -85
  28. gito/report_struct.py +136 -136
  29. gito/tpl/answer.j2 +25 -25
  30. gito/tpl/github_workflows/components/env-vars.j2 +11 -11
  31. gito/tpl/github_workflows/components/installs.j2 +23 -23
  32. gito/tpl/github_workflows/gito-code-review.yml.j2 +32 -32
  33. gito/tpl/github_workflows/gito-react-to-comments.yml.j2 +70 -70
  34. gito/tpl/partial/aux_files.j2 +8 -8
  35. gito/tpl/questions/changes_summary.j2 +55 -55
  36. gito/tpl/questions/release_notes.j2 +26 -26
  37. gito/tpl/questions/test_cases.j2 +37 -37
  38. gito/utils.py +267 -267
  39. ai_cr-3.2.2.dist-info/RECORD +0 -41
  40. {ai_cr-3.2.2.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
@@ -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