diff --git a/check_kernel_commits.py b/check_kernel_commits.py index 2240dc9..5a16864 100644 --- a/check_kernel_commits.py +++ b/check_kernel_commits.py @@ -1,18 +1,18 @@ #!/usr/bin/env python3 import argparse -import os import re import subprocess import sys import textwrap -from typing import Optional -from ciq_helpers import ( +from kt.ktlib.ciq_helpers import ( CIQ_find_fixes_in_mainline, + CIQ_find_matching_cve, CIQ_get_commit_body, CIQ_hash_exists_in_ref, CIQ_run_git, + CIQ_setup_vulns_repo, ) @@ -67,24 +67,6 @@ def extract_cve_from_message(msg): return None -def run_cve_search(vulns_repo, kernel_repo, query) -> tuple[bool, Optional[str]]: - """ - Run the cve_search script from the vulns repo. - Returns (success, output_message). - """ - cve_search_path = os.path.join(vulns_repo, "scripts", "cve_search") - if not os.path.exists(cve_search_path): - raise RuntimeError(f"cve_search script not found at {cve_search_path}") - - env = os.environ.copy() - env["CVEKERNELTREE"] = kernel_repo - - result = subprocess.run([cve_search_path, query], text=True, capture_output=True, check=False, env=env) - - # cve_search outputs results to stdout - return result.returncode == 0, result.stdout.strip() - - def main(): parser = argparse.ArgumentParser(description="Check upstream references and Fixes: tags in PR branch commits.") parser.add_argument("--repo", help="Path to the git repo", required=True) @@ -112,27 +94,11 @@ def main(): vulns_repo = None if args.check_cves: vulns_repo = args.vulns_dir - vulns_repo_url = "https://git.kernel.org/pub/scm/linux/security/vulns.git" - - if os.path.exists(vulns_repo): - # Repository exists, update it with git pull - try: - CIQ_run_git(vulns_repo, ["pull"]) - except RuntimeError as e: - print(f"WARNING: Failed to update vulns repo: {e}") - print("Continuing with existing repository...") - else: - # Repository doesn't exist, clone it - try: - result = subprocess.run( - ["git", "clone", vulns_repo_url, vulns_repo], text=True, capture_output=True, check=False - ) - if result.returncode != 0: - print(f"ERROR: Failed to clone vulns repo: {result.stderr}") - sys.exit(1) - except Exception as e: - print(f"ERROR: Failed to clone vulns repo: {e}") - sys.exit(1) + try: + CIQ_setup_vulns_repo(vulns_repo=vulns_repo) + except RuntimeError as e: + print(e) + sys.exit(1) # Validate that all required refs exist before continuing missing_refs = [] @@ -197,17 +163,9 @@ def main(): fix_cves = {} if args.check_cves: for fix_hash, fix_display in fixes: - try: - success, cve_output = run_cve_search(vulns_repo, args.repo, fix_hash) - if success: - # Parse the CVE from the result - match = re.search(r"(CVE-\d{4}-\d+)\s+is assigned to git id", cve_output) - if match: - bugfix_cve = match.group(1) - fix_cves[fix_hash] = bugfix_cve - except (RuntimeError, subprocess.SubprocessError) as e: - # Log a warning instead of silently ignoring errors when checking bugfix CVEs - print(f"Warning: Failed to check CVE for bugfix commit {fix_hash}: {e}", file=sys.stderr) + bugfix_cve = CIQ_find_matching_cve(vulns_repo=vulns_repo, kernel_repo=args.repo, hash_=fix_hash) + if bugfix_cve: + fix_cves[fix_hash] = bugfix_cve # Build the fixes display text with CVE info fixes_lines = [] @@ -248,48 +206,21 @@ def main(): # Check if the upstream commit has a CVE associated with it try: - success, cve_output = run_cve_search(vulns_repo, args.repo, uhash) - if success: - # Parse the output to get the CVE from the result - # Expected format: "CVE-2024-35962 is assigned to git id - # 65acf6e0501ac8880a4f73980d01b5d27648b956" - match = re.search(r"(CVE-\d{4}-\d+)\s+is assigned to git id", cve_output) - if match: - found_cve = match.group(1) - - if cve_id: - # PR commit has a CVE reference - check if it matches - if found_cve != cve_id: - any_findings = True - if args.markdown: - out_lines.append( - f"- ❌ PR commit `{pr_commit_desc}` references `{cve_id}` but \n" - f" upstream commit `{short_uhash}` is associated with `{found_cve}`\n" - ) - else: - prefix = "[CVE-MISMATCH] " - header = ( - f"{prefix}PR commit {pr_commit_desc} references {cve_id} but " - f"upstream commit {short_uhash} is associated with {found_cve}" - ) - out_lines.append( - wrap_paragraph( - header, width=80, initial_indent="", subsequent_indent=" " * len(prefix) - ) - ) - out_lines.append("") # blank line - else: - # PR commit doesn't reference a CVE, but upstream has one + found_cve = CIQ_find_matching_cve(vulns_repo=vulns_repo, kernel_repo=args.repo, hash_=uhash) + if found_cve: + if cve_id: + # PR commit has a CVE reference - check if it matches + if found_cve != cve_id: any_findings = True if args.markdown: out_lines.append( - f"- ⚠️ PR commit `{pr_commit_desc}` does not reference a CVE but \n" + f"- ❌ PR commit `{pr_commit_desc}` references `{cve_id}` but \n" f" upstream commit `{short_uhash}` is associated with `{found_cve}`\n" ) else: - prefix = "[CVE-MISSING] " + prefix = "[CVE-MISMATCH] " header = ( - f"{prefix}PR commit {pr_commit_desc} does not reference a CVE but " + f"{prefix}PR commit {pr_commit_desc} references {cve_id} but " f"upstream commit {short_uhash} is associated with {found_cve}" ) out_lines.append( @@ -298,6 +229,26 @@ def main(): ) ) out_lines.append("") # blank line + else: + # PR commit doesn't reference a CVE, but upstream has one + any_findings = True + if args.markdown: + out_lines.append( + f"- ⚠️ PR commit `{pr_commit_desc}` does not reference a CVE but \n" + f" upstream commit `{short_uhash}` is associated with `{found_cve}`\n" + ) + else: + prefix = "[CVE-MISSING] " + header = ( + f"{prefix}PR commit {pr_commit_desc} does not reference a CVE but " + f"upstream commit {short_uhash} is associated with {found_cve}" + ) + out_lines.append( + wrap_paragraph( + header, width=80, initial_indent="", subsequent_indent=" " * len(prefix) + ) + ) + out_lines.append("") # blank line else: # The upstream commit has no CVE assigned if cve_id: diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index 436e498..db670e6 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -5,20 +5,24 @@ import subprocess import sys import traceback +from datetime import datetime import git -from ciq_helpers import ( +from kt.ktlib.ciq_helpers import ( CIQ_cherry_pick_commit_standardization, CIQ_commit_exists_in_current_branch, CIQ_find_fixes_in_mainline_current_branch, + CIQ_find_matching_cve, CIQ_fixes_references, CIQ_get_full_hash, CIQ_original_commit_author_to_tag_string, CIQ_raise_or_warn, CIQ_reset_HEAD, CIQ_run_git, + CIQ_setup_vulns_repo, ) +from kt.ktlib.jira import JiraInstance MERGE_MSG = git.Repo(os.getcwd()).git_dir + "/MERGE_MSG" MERGE_MSG_BAK = f"{MERGE_MSG}.bak" @@ -28,6 +32,106 @@ class CherryPickException(Exception): pass +def extract_cve_from_tag(tag): + """ + Extract CVE ID from matches like 'cve CVE-2026-1234' or 'cve-bf CVE-2026-1234' + Return None if no match + """ + match = re.search(r"(?. If any, these will also be cherry picked with the ciq @@ -158,28 +264,62 @@ def cherry_pick_fixes(sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_che jira_ticket=jira_ticket, upstream_ref=upstream_ref, ignore_fixes_check=ignore_fixes_check, + jira_instance=jira_instance, + vulns_repo=vulns_repo, + jira_dry_run=jira_dry_run, ) -def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_check): +def full_cherry_pick( + sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_check, jira_instance, vulns_repo, jira_dry_run +): """ Cherry picks a commit from upstream-ref along with its Fixes: references. If cherry-pick or cherry_pick_fixes fail, the exception is propagated If one of the cherry picks fails, an exception is returned and the previous successful cherry picks are left as they are. """ - # Cherry pick the commit - cherry_pick(sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, ignore_fixes_check=ignore_fixes_check) - # Cherry pick the fixed-by dependencies - cherry_pick_fixes( + # Double check if cve number and jira matches the actual commit + updated_ciq_tags, updated_jira_ticket = check_cve_number( sha=sha, ciq_tags=ciq_tags, jira_ticket=jira_ticket, - upstream_ref=upstream_ref, - ignore_fixes_check=ignore_fixes_check, + jira_instance=jira_instance, + vulns_repo=vulns_repo, ) + # Cherry pick the commit + try: + cherry_pick( + sha=sha, + ciq_tags=updated_ciq_tags, + jira_ticket=updated_jira_ticket, + ignore_fixes_check=ignore_fixes_check, + ) + except (CherryPickException, RuntimeError) as e: + update_jira_failure(jira_instance=jira_instance, ticket_key=updated_jira_ticket, jira_dry_run=jira_dry_run) + raise e + + # Cherry pick the fixed-by dependencies + try: + cherry_pick_fixes( + sha=sha, + ciq_tags=updated_ciq_tags, + jira_ticket=updated_jira_ticket, + upstream_ref=upstream_ref, + ignore_fixes_check=ignore_fixes_check, + jira_instance=jira_instance, + vulns_repo=vulns_repo, + jira_dry_run=jira_dry_run, + ) + except (CherryPickException, RuntimeError) as e: + # TODO would add some extra information to jira if the cve-bf deps are not applied + raise e + + # Update jira only if its deps are applied as well + update_jira_success(jira_instance=jira_instance, ticket_key=updated_jira_ticket, jira_dry_run=jira_dry_run) + if __name__ == "__main__": print("CIQ custom cherry picker") @@ -207,9 +347,49 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_chec action="store_true", help="Continue even if the commit(s) referenced in Fixes: tags are not present in the current branch", ) - + parser.add_argument( + "--jira-url", + required=False, + help="JIRA server URL.", + ) + parser.add_argument( + "--jira-user", + required=False, + help="JIRA user email", + ) + parser.add_argument( + "--jira-key", + required=False, + help="JIRA API Key", + ) + parser.add_argument( + "--jira-dry-run", + help="Do not make any changes to JIRA, just print what would be done", + action="store_true", + default=False, + ) + parser.add_argument( + "--vulns-dir", default="../vulns", help="Path to the kernel vulnerabilities repo (default: ../vulns)" + ) args = parser.parse_args() + jira_url = args.jira_url or os.environ.get("JIRA_URL") + jira_user = args.jira_user or os.environ.get("JIRA_API_USER") + jira_key = args.jira_key or os.environ.get("JIRA_API_TOKEN") + + if not all([jira_url, jira_user, jira_key]): + print("[NOTE]: JIRA credentials not provided. Set via --jira-* args or environment variables.") + jira_instance = None + else: + jira_instance = JiraInstance(server_url=jira_url, api_user=jira_user, api_key=jira_key, project_key="VULN") + + if args.ciq_tag is not None: + try: + CIQ_setup_vulns_repo(vulns_repo=args.vulns_dir) + except RuntimeError as e: + print(e) + sys.exit(1) + # Expand the provided SHA1 to the full SHA1 in case it's either abbreviated or an expression git_sha_res = subprocess.run(["git", "show", "--pretty=%H", "-s", args.sha], stdout=subprocess.PIPE) if git_sha_res.returncode != 0: @@ -231,6 +411,9 @@ def full_cherry_pick(sha, ciq_tags, jira_ticket, upstream_ref, ignore_fixes_chec jira_ticket=args.ticket, upstream_ref=args.upstream_ref, ignore_fixes_check=args.ignore_fixes_check, + jira_instance=jira_instance, + vulns_repo=args.vulns_dir, + jira_dry_run=args.jira_dry_run, ) except CherryPickException as e: logging.error(e) diff --git a/kt/KT.md b/kt/KT.md index 498e13d..9a66386 100644 --- a/kt/KT.md +++ b/kt/KT.md @@ -103,11 +103,9 @@ environment. ## Commands -Make sure kt is reachable from anywhere by adding it's location to PATH. -Example -``` -export PATH=$HOME/ciq/kernel-src-tree-tools/bin:$PATH -``` +Kt is now installed when kernel-src-tree-tools is installed, no need +to do anything extra. + If you are unsure how to use kt, just run it with --help. Example: ``` diff --git a/kt/commands/kernel_info/command.py b/kt/commands/kernel_info/command.py new file mode 100644 index 0000000..7432747 --- /dev/null +++ b/kt/commands/kernel_info/command.py @@ -0,0 +1,28 @@ +import click + +from kt.commands.kernel_info.impl import main +from kt.ktlib.shell_completion import ShellCompletion + +epilog = """ +Show information about a specific kernel. +It is basically a wrapper that shows what's in kt/data/kernels.yaml, except +for the nested values, like dist_git_root and src_tree_root. + +Examples: + +\b +$ kt kernel-info lts-9.4 +{ + "name": "lts-9.4", + "src_tree_branch": "ciqlts9_4", + "dist_git_branch": "lts94-9", + "mock_config": "rocky-lts94", + "automated": true +} +""" + + +@click.command(epilog=epilog) +@click.argument("kernel", required=True, type=str, shell_complete=ShellCompletion.show_kernels) +def kernel_info(kernel): + main(name=kernel) diff --git a/kt/commands/kernel_info/impl.py b/kt/commands/kernel_info/impl.py new file mode 100644 index 0000000..ef46c33 --- /dev/null +++ b/kt/commands/kernel_info/impl.py @@ -0,0 +1,17 @@ +import json +from dataclasses import asdict + +from kt.ktlib.config import Config +from kt.ktlib.kernels import KernelsInfo + + +def main(name: str): + config = Config.load() + kernels = KernelsInfo.from_yaml(config=config).kernels + if name not in kernels: + raise ValueError(f"Invalid param: {name} does not exist") + + kernel_info = kernels[name] + data = asdict(kernel_info) + filtered = {k: v for k, v in data.items() if not isinstance(v, dict)} + print(json.dumps(filtered, indent=2)) diff --git a/bin/kt b/kt/kt.py similarity index 90% rename from bin/kt rename to kt/kt.py index f7e492f..e85b109 100755 --- a/bin/kt +++ b/kt/kt.py @@ -7,6 +7,7 @@ from kt.commands.checkout.command import checkout from kt.commands.content_release.command import content_release from kt.commands.git_push.command import git_push +from kt.commands.kernel_info.command import kernel_info from kt.commands.list_kernels.command import list_kernels from kt.commands.setup.command import setup from kt.commands.vm.command import vm @@ -32,6 +33,7 @@ def main(): cli.add_command(git_push) cli.add_command(vm) cli.add_command(content_release) + cli.add_command(kernel_info) cli() diff --git a/ciq_helpers.py b/kt/ktlib/ciq_helpers.py similarity index 83% rename from ciq_helpers.py rename to kt/ktlib/ciq_helpers.py index 7cf3029..16e7376 100644 --- a/ciq_helpers.py +++ b/kt/ktlib/ciq_helpers.py @@ -7,6 +7,8 @@ import os import re import subprocess +import sys +from typing import Optional import git @@ -476,3 +478,89 @@ def read_spec_el_version(spec_lines): Raises ValueError if not found. """ return _read_spec_define(spec_lines, "el_version", r"\d+") + + +def run_cve_search(vulns_repo, kernel_repo, query) -> tuple[bool, Optional[str]]: + """ + Run the cve_search script from the vulns repo. + Returns (success, output_message). + """ + + cve_search_path = os.path.join(vulns_repo, "scripts", "cve_search") + if not os.path.exists(cve_search_path): + raise RuntimeError(f"cve_search script not found at {cve_search_path}") + + env = os.environ.copy() + env["CVEKERNELTREE"] = kernel_repo + + result = subprocess.run([cve_search_path, query], text=True, capture_output=True, check=False, env=env) + + # cve_search outputs results to stdout + return result.returncode == 0, result.stdout.strip() + + +def CIQ_check_if_published_cve(vulns_repo, cve_id): + if not cve_id: + return False + + cve_id_year = cve_id.split("-")[1] + published_path = f"{vulns_repo}/cve/published/{cve_id_year}/{cve_id}.sha1" + if not os.path.isfile(published_path): + print(f"[NOTE]: {cve_id} is not published, it has been rejected") + return False + + return True + + +def CIQ_find_matching_cve(vulns_repo, kernel_repo, hash_) -> str | None: + """ + Returns the CVE (i.e CVE-2023-526) if there is a corresponding CVE to that commit hash + and the CVE is published, not rejected. + Otherwise it returns None + """ + + cve_id = None + try: + success, cve_output = run_cve_search(vulns_repo, kernel_repo, hash_) + if success: + # Parse the CVE from the result + match = re.search(r"(CVE-\d{4}-\d+)\s+is assigned to git id", cve_output) + if match: + cve_id = match.group(1) + except (RuntimeError, subprocess.SubprocessError) as e: + # Log a warning instead of silently ignoring errors when checking bugfix CVEs + print(f"Warning: Failed to check CVE for bugfix commit {hash_}: {e}", file=sys.stderr) + + if CIQ_check_if_published_cve(vulns_repo=vulns_repo, cve_id=cve_id): + return cve_id + + return None + + +def CIQ_setup_vulns_repo(vulns_repo): + """ + Setups the vuln repo, either by doing a pull update or cloning it from scratch + if the repo does not exist. + Raises RuntimeError exception for failures during a clone from scratch. + If git pull fails, it is not considered an errros because we can still + use the current version of the repo, even if it's older. + """ + + vulns_repo_url = "https://git.kernel.org/pub/scm/linux/security/vulns.git" + if os.path.exists(vulns_repo): + # Repository exists, update it with git pull + try: + CIQ_run_git(vulns_repo, ["pull"]) + except RuntimeError as e: + print(f"WARNING: Failed to update vulns repo: {e}") + print("Continuing with existing repository...") + else: + # Repository doesn't exist, clone it + try: + result = subprocess.run( + ["git", "clone", vulns_repo_url, vulns_repo], text=True, capture_output=True, check=False + ) + if result.returncode != 0: + raise RuntimeError(f"ERROR: Failed to clone vulns repo: {result.stderr}") + except Exception as e: + raise RuntimeError(f"ERROR: Failed to clone vulns repo: {e}") diff --git a/kt/ktlib/jira.py b/kt/ktlib/jira.py new file mode 100644 index 0000000..fd4403a --- /dev/null +++ b/kt/ktlib/jira.py @@ -0,0 +1,224 @@ +from datetime import datetime + +import requests +from jira import JIRA, Issue, JIRAError +from requests.auth import HTTPBasicAuth + + +class JiraException(Exception): + pass + + +class JiraInstance: + _jira: JIRA + _project_key: str + _api_user: str + _api_key: str + + def __init__(self, server_url: str, api_user: str, api_key: str, project_key: str): + try: + self._jira = JIRA(server=server_url, basic_auth=(api_user, api_key)) + except JIRAError as e: + raise JiraException(e) + + self._server_url = server_url + self._project_key = project_key + self._api_user = api_user + self._api_key = api_key + + def create_ticket( + self, + summary: str, + description: str, + issue_type: str, + priority: str, + work_category: str, + product_line: str, + linux_space: str, + ) -> Issue: + issue_dict = { + "project": {"key": self._project_key}, + "summary": summary, + "description": description, + "issuetype": {"name": issue_type}, + "priority": {"name": priority}, + "customfield_10350": {"value": work_category}, + "customfield_10316": [{"value": product_line}], + "customfield_10398": {"value": linux_space}, + } + + try: + issue = self._jira.create_issue(fields=issue_dict) + except JIRAError as e: + raise JiraException(e) + + return issue + + def get_issue(self, issue_key: str) -> Issue: + try: + return self._jira.issue(issue_key) + except JIRAError as e: + raise JiraException(e) + + def update_labels(self, issue_key: str, labels: list[str]): + try: + issue = self.get_issue(issue_key=issue_key) + issue.update(fields={"labels": labels}) + except JIRAError as e: + raise JiraException(e) + + def add_comment(self, issue_key: str, comment: str): + try: + self._jira.add_comment(issue_key, comment) + except JIRAError as e: + raise JiraException(e) + + def add_worklog(self, issue_key: str, time_spent: str, comment: str, started: datetime): + try: + self._jira.add_worklog(issue=issue_key, timeSpent=time_spent, comment=comment, started=started) + except JIRAError as e: + raise JiraException(e) + + def transition_issue(self, issue_key: str, transition_name: str): + try: + issue = self.get_issue(issue_key=issue_key) + transition_id = self._get_transition_id(issue=issue, transition_name=transition_name) + self._jira.transition_issue(issue_key, transition_id) + except JIRAError as e: + raise JiraException(e) + + def _get_transition_id(self, issue: Issue, transition_name: str) -> str: + try: + transitions = self._jira.transitions(issue) + for t in transitions: + if t["name"] == transition_name: + return t["id"] + except JIRAError as e: + raise JiraException(e) + raise JiraException(f"Transition '{transition_name}' not found") + + def search_issues(self, jql: str, max_results: int = 1, next_page_token: str = None) -> tuple[list, str]: + try: + result = self._jira.enhanced_search_issues( + jql_str=jql, maxResults=max_results, nextPageToken=next_page_token + ) + + if hasattr(result, "issues"): + issues = result.issues + elif isinstance(result, dict) and "issues" in result: + issues = result["issues"] + else: + issues = result + + next_page_token = None + if hasattr(result, "nextPageToken"): + next_page_token = result.nextPageToken + elif isinstance(result, dict) and "nextPageToken" in result: + next_page_token = result["nextPageToken"] + + return (issues, next_page_token) + except JIRAError as e: + raise JiraException(e) + + def _get_user_id(self, email: str) -> str: + try: + users = self._jira.search_users(query=email) + if users: + return users[0].accountId + except JIRAError as e: + raise JiraException(e) + + raise JiraException(f"Could not find user id for {email}") + + def get_user_id(self, email: str) -> str: + try: + return self._get_user_id(email=email) + except JiraException: + print("Fallback get_user_id") + return self._get_user_id_fallback(email=email) + + def _get_user_id_fallback(self, email: str) -> str: + """Fallback: resolve account ID via direct REST API call.""" + auth = HTTPBasicAuth(email, self._api_key) + headers = {"Accept": "application/json"} + response = requests.get( + f"{self._server_url}/rest/api/3/user/assignable/search", + params={"project": self._project_key, "query": email}, + headers=headers, + auth=auth, + ) + if response.status_code != 200: + raise JiraException(f"Error {response.status_code}: {response.text}") + + users = response.json() + if not users: + raise JiraException("No assignable users found for that query.") + + return users[0].get("accountId") + + def _assign_issue_fallback(self, issue_key: str, account_id: str): + """Fallback: assign via direct REST API call.""" + url = f"{self._server_url}/rest/api/3/issue/{issue_key}/assignee" + response = self._jira._session.put(url, json={"accountId": account_id}) + if not response.ok: + raise JiraException( + f"Failed to assign issue {issue_key} to account {account_id}: {response.status_code} {response.text}" + ) + + def assign_ticket(self, issue_key: str, assignee_email: str = None): + """Try JIRA lib first, fall back to direct REST API.""" + + if assignee_email is None: + # If there is no assignee given as param, use the default one + assignee_email = self._api_user + + try: + assignee_user_id = self.get_user_id(email=assignee_email) + except JiraException: + raise JiraException(f"Could not resolve account ID for {assignee_email}") + + try: + self._jira.assign_issue(issue=issue_key, assignee=assignee_user_id) + except JIRAError as e: + print(f"assign_ticket failed ({e}), trying fallback...") + self._assign_issue_fallback(issue_key=issue_key, account_id=assignee_user_id) + + def unassign_ticket(self, issue_key: str): + try: + self._jira.assign_issue(issue=issue_key, assignee=None) + except JIRAError as e: + raise JiraException(e) + + def get_matching_tickets(self, summary: str) -> list[Issue]: + escaped_summary = summary.replace('"', '\\"') + matching_issues = [] + try: + issues = self._jira.enhanced_search_issues( + jql_str=f'project={str(self._project_key)} AND summary~"\\"{escaped_summary}\\""', + maxResults=50, + nextPageToken=None, + ) + except JIRAError as e: + raise JiraException(e) + + if len(issues) > 0: + for issue in issues: + if issue.fields.summary == summary: + matching_issues.append(issue) + + return matching_issues + + def get_matching_tickets_not_done(self, summary: str) -> list[Issue]: + matching_issues_not_done = [] + matching_issues = self.get_matching_tickets(summary=summary) + + for issue in matching_issues: + if issue.fields.statusCategory.name != "Done": + matching_issues_not_done.append(issue) + + return matching_issues_not_done + + def ticket_in_progress_exists(self, summary: str) -> bool: + matching_issues = self.get_matching_tickets_not_done(summary=summary) + + return len(matching_issues) > 0 diff --git a/pyproject.toml b/pyproject.toml index adb7a24..1bf0455 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,3 @@ -# This is not an installable project at the moment. # This is used to control dependencies and liter configuration [project] name = "kernel-src-tree-tools" @@ -12,6 +11,8 @@ dependencies = [ "python3-wget", "oyaml", "pexpect", + "jira", + "requests", ] [project.optional-dependencies] @@ -38,3 +39,6 @@ include = ["kt*"] [tool.setuptools.package-data] "kt" = ["data/*.yaml"] + +[project.scripts] +kt = "kt.kt:main" diff --git a/rolling-release-update.py b/rolling-release-update.py index 4492d80..645dae0 100644 --- a/rolling-release-update.py +++ b/rolling-release-update.py @@ -6,7 +6,7 @@ import git -from ciq_helpers import get_backport_commit_data +from kt.ktlib.ciq_helpers import get_backport_commit_data FIPS_PROTECTED_DIRECTORIES = [ b"arch/x86/crypto/", diff --git a/update_lt_spec.py b/update_lt_spec.py index 695f982..304e050 100755 --- a/update_lt_spec.py +++ b/update_lt_spec.py @@ -18,7 +18,7 @@ print("ERROR: GitPython is not installed. Install it with: pip install GitPython") sys.exit(1) -from ciq_helpers import ( +from kt.ktlib.ciq_helpers import ( get_git_user, last_git_tag, parse_ciq_tag_release,