From 4db110b0cf34571b79900da4a1bb591e7f17221a Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Tue, 14 Apr 2026 15:06:26 +0200 Subject: [PATCH 01/10] ciq_helpers.py: Add wrapper for run_cve_search to avoid duplication 1. Moved run_cve_search from check_kernel_commits.py to ciq_helpers.py 2. Created a wrapper that parses the output of run_cve_search and return the cve number. 3. Used the wrapper instead of doing the same thing twice in check_kernel_commits.py Bonus: This also reduces the level of identation in check_kernel_commits. Signed-off-by: Roxana Nicolescu --- check_kernel_commits.py | 97 ++++++++++++++--------------------------- ciq_helpers.py | 42 ++++++++++++++++++ 2 files changed, 74 insertions(+), 65 deletions(-) diff --git a/check_kernel_commits.py b/check_kernel_commits.py index 2240dc9..0ef5931 100644 --- a/check_kernel_commits.py +++ b/check_kernel_commits.py @@ -6,10 +6,10 @@ import subprocess import sys import textwrap -from typing import Optional from ciq_helpers import ( CIQ_find_fixes_in_mainline, + CIQ_find_matching_cve, CIQ_get_commit_body, CIQ_hash_exists_in_ref, CIQ_run_git, @@ -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) @@ -197,17 +179,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 +222,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 +245,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_helpers.py b/ciq_helpers.py index 7cf3029..80d1e1d 100644 --- a/ciq_helpers.py +++ b/ciq_helpers.py @@ -7,6 +7,8 @@ import os import re import subprocess +import sys +from typing import Optional import git @@ -476,3 +478,43 @@ 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_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. + Otherwise it returns None + """ + + cve = 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 = 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) + + return cve From a12bbd33b2c6df39201ad909327dbe640257357f Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Fri, 17 Apr 2026 15:21:59 +0200 Subject: [PATCH 02/10] kt: Move ciq_helpers.py to ktlib Useful because kt is exposed as package and these helpers can be used in multiple places, not only this repo. Signed-off-by: Roxana Nicolescu --- check_kernel_commits.py | 2 +- ciq-cherry-pick.py | 2 +- ciq_helpers.py => kt/ktlib/ciq_helpers.py | 0 rolling-release-update.py | 2 +- update_lt_spec.py | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename ciq_helpers.py => kt/ktlib/ciq_helpers.py (100%) diff --git a/check_kernel_commits.py b/check_kernel_commits.py index 0ef5931..ccc6b63 100644 --- a/check_kernel_commits.py +++ b/check_kernel_commits.py @@ -7,7 +7,7 @@ import sys import textwrap -from ciq_helpers import ( +from kt.ktlib.ciq_helpers import ( CIQ_find_fixes_in_mainline, CIQ_find_matching_cve, CIQ_get_commit_body, diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index 436e498..df7bb3c 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -8,7 +8,7 @@ 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, diff --git a/ciq_helpers.py b/kt/ktlib/ciq_helpers.py similarity index 100% rename from ciq_helpers.py rename to kt/ktlib/ciq_helpers.py 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, From 656037c2928d0ea0579d211da436531200ee6612 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Fri, 17 Apr 2026 15:29:42 +0200 Subject: [PATCH 03/10] kt/ktlib: Introduce jira helpers Signed-off-by: Roxana Nicolescu --- kt/ktlib/jira.py | 213 +++++++++++++++++++++++++++++++++++++++++++++++ pyproject.toml | 2 + 2 files changed, 215 insertions(+) create mode 100644 kt/ktlib/jira.py diff --git a/kt/ktlib/jira.py b/kt/ktlib/jira.py new file mode 100644 index 0000000..8d75c38 --- /dev/null +++ b/kt/ktlib/jira.py @@ -0,0 +1,213 @@ +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): + """Try JIRA lib first, fall back to direct REST API.""" + 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 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..5107363 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,8 @@ dependencies = [ "python3-wget", "oyaml", "pexpect", + "jira", + "requests", ] [project.optional-dependencies] From 65ca0797480e63b4ca77dc248e234e3f5c2b67aa Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 20 Apr 2026 13:23:18 +0200 Subject: [PATCH 04/10] kt/ktlib/ciq_helpers.py: Add helper that sets up the vuln repo Signed-off-by: Roxana Nicolescu --- check_kernel_commits.py | 28 ++++++---------------------- kt/ktlib/ciq_helpers.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 22 deletions(-) diff --git a/check_kernel_commits.py b/check_kernel_commits.py index ccc6b63..5a16864 100644 --- a/check_kernel_commits.py +++ b/check_kernel_commits.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import argparse -import os import re import subprocess import sys @@ -13,6 +12,7 @@ CIQ_get_commit_body, CIQ_hash_exists_in_ref, CIQ_run_git, + CIQ_setup_vulns_repo, ) @@ -94,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 = [] diff --git a/kt/ktlib/ciq_helpers.py b/kt/ktlib/ciq_helpers.py index 80d1e1d..feaace3 100644 --- a/kt/ktlib/ciq_helpers.py +++ b/kt/ktlib/ciq_helpers.py @@ -518,3 +518,32 @@ def CIQ_find_matching_cve(vulns_repo, kernel_repo, hash_) -> str | None: print(f"Warning: Failed to check CVE for bugfix commit {hash_}: {e}", file=sys.stderr) return cve + + +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}") From 553547d9f35e9ef6daa2f5fdb68de2aca00fe9f1 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 20 Apr 2026 17:13:14 +0200 Subject: [PATCH 05/10] ciq-cherry-pick.py: Check if cve-bf commits are actual cves If so, use the proper tag and jira ticket (if it exists). If no jira ticket is found, the original one will be used. In case the cve-bf dependency is a cve and has a corresponding jira ticket, it would be left unassigned. Then the pr_jira_check.py would complain. In order to avoid this, and the "incomplete" logic of updating jira tickets only for cve-bf commits, ciq-cherry-pick.py now updates the jira tickets by default (for the initial CVE and for the dependencies that are CVEs), unless --jira-dry-run is being used. This way, we don't end up with a weird state where just one ticket is updated and the others are not. Extra arguments were needed: - jira credentials because we now do jira queries - vuln repo path to check if a CVE matches a commit - jira-dry-run as described above Signed-off-by: Roxana Nicolescu --- ciq-cherry-pick.py | 201 +++++++++++++++++++++++++++++++++++++++++++-- kt/ktlib/jira.py | 7 +- 2 files changed, 198 insertions(+), 10 deletions(-) diff --git a/ciq-cherry-pick.py b/ciq-cherry-pick.py index df7bb3c..db670e6 100644 --- a/ciq-cherry-pick.py +++ b/ciq-cherry-pick.py @@ -5,6 +5,7 @@ import subprocess import sys import traceback +from datetime import datetime import git @@ -12,13 +13,16 @@ 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/ktlib/jira.py b/kt/ktlib/jira.py index 8d75c38..af0c370 100644 --- a/kt/ktlib/jira.py +++ b/kt/ktlib/jira.py @@ -165,8 +165,13 @@ def _assign_issue_fallback(self, issue_key: str, account_id: str): 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): + 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: From 61130ebfc55d228a005ed05fc3aaec83bfac767a Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Mon, 20 Apr 2026 17:27:50 +0200 Subject: [PATCH 06/10] kt/ktlib/ciq_helpers: find matching cve only if the cve is published CIQ_find_matching_cve returned the matching CVE even if it's rejected because the cve_search script from the vuln repo does not check if the CVE is published or rejected. Signed-off-by: Roxana Nicolescu --- kt/ktlib/ciq_helpers.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/kt/ktlib/ciq_helpers.py b/kt/ktlib/ciq_helpers.py index feaace3..16e7376 100644 --- a/kt/ktlib/ciq_helpers.py +++ b/kt/ktlib/ciq_helpers.py @@ -499,25 +499,42 @@ def run_cve_search(vulns_repo, kernel_repo, query) -> tuple[bool, Optional[str]] 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. + 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 = 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 = match.group(1) + 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) - return cve + 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): From cdb0f4fe57c6b7cc2fb06e954e1ca3b153d485e1 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Wed, 22 Apr 2026 13:25:14 +0200 Subject: [PATCH 07/10] kt: Add kernel-info command It prints a json that matches the kernel information from kt/data/kernels.yml Signed-off-by: Roxana Nicolescu --- bin/kt | 2 ++ kt/commands/kernel_info/command.py | 28 ++++++++++++++++++++++++++++ kt/commands/kernel_info/impl.py | 17 +++++++++++++++++ 3 files changed, 47 insertions(+) create mode 100644 kt/commands/kernel_info/command.py create mode 100644 kt/commands/kernel_info/impl.py diff --git a/bin/kt b/bin/kt index f7e492f..e85b109 100755 --- a/bin/kt +++ b/bin/kt @@ -7,6 +7,7 @@ import click 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/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)) From 362a68e9a0544157ebcd09f40ed2df1ad517e623 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Thu, 23 Apr 2026 16:36:18 +0200 Subject: [PATCH 08/10] kt/ktlib/jira.py: Add helper to unassign tickets Signed-off-by: Roxana Nicolescu --- kt/ktlib/jira.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kt/ktlib/jira.py b/kt/ktlib/jira.py index af0c370..fd4403a 100644 --- a/kt/ktlib/jira.py +++ b/kt/ktlib/jira.py @@ -183,6 +183,12 @@ def assign_ticket(self, issue_key: str, assignee_email: str = None): 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 = [] From ff9921e0af5f4882b68bc249ce6f0a7a98f127da Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Fri, 24 Apr 2026 14:50:48 +0200 Subject: [PATCH 09/10] pyproject.toml: Automatically kt as CLI tool when the project is installed Had to move kt script to kt/kt.py Signed-off-by: Roxana Nicolescu --- kt/KT.md | 8 +++----- bin/kt => kt/kt.py | 0 pyproject.toml | 3 +++ 3 files changed, 6 insertions(+), 5 deletions(-) rename bin/kt => kt/kt.py (100%) 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/bin/kt b/kt/kt.py similarity index 100% rename from bin/kt rename to kt/kt.py diff --git a/pyproject.toml b/pyproject.toml index 5107363..2f5f1ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,3 +40,6 @@ include = ["kt*"] [tool.setuptools.package-data] "kt" = ["data/*.yaml"] + +[project.scripts] +kt = "kt.kt:main" From 079de11ec8bf8704c4cbf1c8a0186a9a11e1ba18 Mon Sep 17 00:00:00 2001 From: Roxana Nicolescu Date: Fri, 24 Apr 2026 14:52:34 +0200 Subject: [PATCH 10/10] pyproject.toml: Remove obsolete comment The project is installable. Signed-off-by: Roxana Nicolescu --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2f5f1ce..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"