recce-cloud-nightly 1.26.0.20251119__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.

Potentially problematic release.


This version of recce-cloud-nightly might be problematic. Click here for more details.

@@ -0,0 +1,82 @@
1
+ """
2
+ Base CI provider interface.
3
+ """
4
+
5
+ import subprocess
6
+ from abc import ABC, abstractmethod
7
+ from dataclasses import dataclass
8
+ from typing import Optional
9
+
10
+
11
+ @dataclass
12
+ class CIInfo:
13
+ """Information extracted from CI environment."""
14
+
15
+ platform: Optional[str] = None # "github-actions", "gitlab-ci", etc.
16
+ cr_number: Optional[int] = None # Change request number (PR/MR)
17
+ cr_url: Optional[str] = None # Change request URL (for session linking)
18
+ session_type: Optional[str] = None # "cr", "prod", "dev"
19
+ commit_sha: Optional[str] = None # Full commit SHA
20
+ base_branch: Optional[str] = None # Target/base branch
21
+ source_branch: Optional[str] = None # Source/head branch
22
+ repository: Optional[str] = None # Repository path (owner/repo or group/project)
23
+ access_token: Optional[str] = None # CI-provided access token (GITHUB_TOKEN, CI_JOB_TOKEN)
24
+
25
+
26
+ class BaseCIProvider(ABC):
27
+ """Abstract base class for CI provider detection and info extraction."""
28
+
29
+ @abstractmethod
30
+ def can_handle(self) -> bool:
31
+ """
32
+ Check if this provider can handle the current environment.
33
+
34
+ Returns:
35
+ True if the provider's CI platform is detected
36
+ """
37
+ pass
38
+
39
+ @abstractmethod
40
+ def extract_ci_info(self) -> CIInfo:
41
+ """
42
+ Extract CI information from environment variables.
43
+
44
+ Returns:
45
+ CIInfo object with extracted information
46
+ """
47
+ pass
48
+
49
+ @staticmethod
50
+ def run_git_command(command: list[str]) -> Optional[str]:
51
+ """
52
+ Run a git command and return output.
53
+
54
+ Args:
55
+ command: Git command as list (e.g., ['git', 'rev-parse', 'HEAD'])
56
+
57
+ Returns:
58
+ Command output stripped of whitespace, or None if command fails
59
+ """
60
+ try:
61
+ result = subprocess.run(command, capture_output=True, text=True, check=True, timeout=5)
62
+ return result.stdout.strip()
63
+ except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
64
+ return None
65
+
66
+ @staticmethod
67
+ def determine_session_type(cr_number: Optional[int], source_branch: Optional[str]) -> str:
68
+ """
69
+ Determine session type based on context.
70
+
71
+ Args:
72
+ cr_number: Change request number (PR/MR)
73
+ source_branch: Source branch name
74
+
75
+ Returns:
76
+ Session type: "cr", "prod", or "dev"
77
+ """
78
+ if cr_number is not None:
79
+ return "cr"
80
+ if source_branch in ["main", "master"]:
81
+ return "prod"
82
+ return "dev"
@@ -0,0 +1,147 @@
1
+ """
2
+ CI provider detection and orchestration.
3
+ """
4
+
5
+ import logging
6
+ import os
7
+ from typing import Optional
8
+
9
+ from recce_cloud.ci_providers.base import BaseCIProvider, CIInfo
10
+ from recce_cloud.ci_providers.github_actions import GitHubActionsProvider
11
+ from recce_cloud.ci_providers.gitlab_ci import GitLabCIProvider
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class CIDetector:
17
+ """
18
+ Detects CI platform and extracts information.
19
+
20
+ Supports:
21
+ - GitHub Actions
22
+ - GitLab CI/CD
23
+ - Generic fallback (git commands)
24
+ """
25
+
26
+ # Order matters: check in priority order
27
+ PROVIDERS = [
28
+ GitHubActionsProvider,
29
+ GitLabCIProvider,
30
+ ]
31
+
32
+ @classmethod
33
+ def detect(cls) -> CIInfo:
34
+ """
35
+ Detect CI platform and extract information.
36
+
37
+ Returns:
38
+ CIInfo object with detected information
39
+ """
40
+ # Try each provider in order
41
+ for provider_class in cls.PROVIDERS:
42
+ provider = provider_class()
43
+ if provider.can_handle():
44
+ logger.info(f"CI Platform: {provider_class.__name__.replace('Provider', '')}")
45
+ ci_info = provider.extract_ci_info()
46
+ cls._log_detected_values(ci_info)
47
+ return ci_info
48
+
49
+ # No CI platform detected, use generic fallback
50
+ logger.info("No CI platform detected, using git fallback")
51
+ ci_info = cls._fallback_detection()
52
+ cls._log_detected_values(ci_info)
53
+ return ci_info
54
+
55
+ @classmethod
56
+ def apply_overrides(
57
+ cls,
58
+ ci_info: CIInfo,
59
+ cr: Optional[int] = None,
60
+ session_type: Optional[str] = None,
61
+ ) -> CIInfo:
62
+ """
63
+ Apply manual overrides to detected CI information.
64
+
65
+ Args:
66
+ ci_info: Detected CI information
67
+ cr: Manual change request number override
68
+ session_type: Manual session type override
69
+
70
+ Returns:
71
+ CIInfo with overrides applied
72
+ """
73
+ # Log overrides
74
+ if cr is not None and cr != ci_info.cr_number:
75
+ logger.info(f"Using manual override: --cr {cr} (detected: {ci_info.cr_number})")
76
+ ci_info.cr_number = cr
77
+ # Rebuild CR URL if we have repository info
78
+ if ci_info.repository:
79
+ if ci_info.platform == "github-actions":
80
+ ci_info.cr_url = f"https://github.com/{ci_info.repository}/pull/{cr}"
81
+ elif ci_info.platform == "gitlab-ci":
82
+ server_url = os.getenv("CI_SERVER_URL", "https://gitlab.com")
83
+ ci_info.cr_url = f"{server_url}/{ci_info.repository}/-/merge_requests/{cr}"
84
+
85
+ if session_type is not None and session_type != ci_info.session_type:
86
+ logger.info(f"Using manual override: --type {session_type} (detected: {ci_info.session_type})")
87
+ ci_info.session_type = session_type
88
+
89
+ # Re-determine session type if CR was overridden
90
+ if cr is not None:
91
+ if session_type is None: # Only if not manually overridden
92
+ ci_info.session_type = BaseCIProvider.determine_session_type(ci_info.cr_number, ci_info.source_branch)
93
+
94
+ return ci_info
95
+
96
+ @classmethod
97
+ def _fallback_detection(cls) -> CIInfo:
98
+ """
99
+ Fallback detection using git commands when no CI platform is detected.
100
+
101
+ Returns:
102
+ CIInfo with basic git information
103
+ """
104
+ commit_sha = BaseCIProvider.run_git_command(["git", "rev-parse", "HEAD"])
105
+ source_branch = BaseCIProvider.run_git_command(["git", "branch", "--show-current"])
106
+
107
+ session_type = BaseCIProvider.determine_session_type(None, source_branch)
108
+
109
+ return CIInfo(
110
+ platform=None,
111
+ cr_number=None,
112
+ session_type=session_type,
113
+ commit_sha=commit_sha,
114
+ base_branch="main", # Default
115
+ source_branch=source_branch,
116
+ repository=None,
117
+ )
118
+
119
+ @classmethod
120
+ def _log_detected_values(cls, ci_info: CIInfo) -> None:
121
+ """
122
+ Log detected values for transparency.
123
+
124
+ Args:
125
+ ci_info: Detected CI information
126
+ """
127
+ if ci_info.cr_number is not None:
128
+ if ci_info.platform == "github-actions":
129
+ logger.info(f"Detected PR number: {ci_info.cr_number}")
130
+ elif ci_info.platform == "gitlab-ci":
131
+ logger.info(f"Detected MR number: {ci_info.cr_number}")
132
+ else:
133
+ logger.info(f"Detected CR number: {ci_info.cr_number}")
134
+ else:
135
+ logger.info("No CR number detected")
136
+
137
+ if ci_info.commit_sha:
138
+ logger.info(f"Detected commit SHA: {ci_info.commit_sha[:8]}...")
139
+ else:
140
+ logger.warning("Could not detect commit SHA")
141
+
142
+ logger.info(f"Detected base branch: {ci_info.base_branch}")
143
+ logger.info(f"Detected source branch: {ci_info.source_branch}")
144
+ logger.info(f"Session type: {ci_info.session_type}")
145
+
146
+ if ci_info.repository:
147
+ logger.info(f"Repository: {ci_info.repository}")
@@ -0,0 +1,136 @@
1
+ """
2
+ GitHub Actions CI provider.
3
+ """
4
+
5
+ import json
6
+ import os
7
+ from typing import Optional
8
+
9
+ from recce_cloud.ci_providers.base import BaseCIProvider, CIInfo
10
+
11
+
12
+ class GitHubActionsProvider(BaseCIProvider):
13
+ """GitHub Actions CI provider implementation."""
14
+
15
+ def can_handle(self) -> bool:
16
+ """
17
+ Check if running in GitHub Actions.
18
+
19
+ Returns:
20
+ True if GITHUB_ACTIONS environment variable is 'true'
21
+ """
22
+ return os.getenv("GITHUB_ACTIONS") == "true"
23
+
24
+ def extract_ci_info(self) -> CIInfo:
25
+ """
26
+ Extract CI information from GitHub Actions environment.
27
+
28
+ Environment variables used:
29
+ - GITHUB_EVENT_PATH: Path to event payload JSON (for PR number)
30
+ - GITHUB_SHA: Commit SHA
31
+ - GITHUB_BASE_REF: Base/target branch (PR only)
32
+ - GITHUB_HEAD_REF: Source/head branch (PR only)
33
+ - GITHUB_REF_NAME: Branch name (fallback)
34
+ - GITHUB_REPOSITORY: Repository (owner/repo)
35
+ - GITHUB_TOKEN: Default access token (automatically provided by GitHub Actions)
36
+
37
+ Returns:
38
+ CIInfo object with extracted information
39
+ """
40
+ cr_number = self._extract_pr_number()
41
+ commit_sha = self._extract_commit_sha()
42
+ base_branch = self._extract_base_branch()
43
+ source_branch = self._extract_source_branch()
44
+ repository = os.getenv("GITHUB_REPOSITORY")
45
+ access_token = os.getenv("GITHUB_TOKEN")
46
+
47
+ # Build CR URL (PR URL) if we have the necessary information
48
+ cr_url = None
49
+ if cr_number is not None and repository:
50
+ cr_url = f"https://github.com/{repository}/pull/{cr_number}"
51
+
52
+ session_type = self.determine_session_type(cr_number, source_branch)
53
+
54
+ return CIInfo(
55
+ platform="github-actions",
56
+ cr_number=cr_number,
57
+ cr_url=cr_url,
58
+ session_type=session_type,
59
+ commit_sha=commit_sha,
60
+ base_branch=base_branch,
61
+ source_branch=source_branch,
62
+ repository=repository,
63
+ access_token=access_token,
64
+ )
65
+
66
+ def _extract_pr_number(self) -> Optional[int]:
67
+ """
68
+ Extract PR number from GitHub event payload.
69
+
70
+ Returns:
71
+ PR number if detected, None otherwise
72
+ """
73
+ event_path = os.getenv("GITHUB_EVENT_PATH")
74
+ if not event_path or not os.path.exists(event_path):
75
+ return None
76
+
77
+ try:
78
+ with open(event_path, "r") as f:
79
+ event_data = json.load(f)
80
+ pr_data = event_data.get("pull_request", {})
81
+ pr_number = pr_data.get("number")
82
+ if pr_number is not None:
83
+ return int(pr_number)
84
+ except (json.JSONDecodeError, ValueError, OSError):
85
+ pass
86
+
87
+ return None
88
+
89
+ def _extract_commit_sha(self) -> Optional[str]:
90
+ """
91
+ Extract commit SHA from GitHub Actions environment.
92
+
93
+ Returns:
94
+ Commit SHA if detected, falls back to git command
95
+ """
96
+ commit_sha = os.getenv("GITHUB_SHA")
97
+ if commit_sha:
98
+ return commit_sha
99
+
100
+ # Fallback to git command
101
+ return self.run_git_command(["git", "rev-parse", "HEAD"])
102
+
103
+ def _extract_base_branch(self) -> str:
104
+ """
105
+ Extract base/target branch from GitHub Actions environment.
106
+
107
+ Returns:
108
+ Base branch name, defaults to 'main' if not detected
109
+ """
110
+ # GITHUB_BASE_REF is only set for pull_request events
111
+ base_branch = os.getenv("GITHUB_BASE_REF")
112
+ if base_branch:
113
+ return base_branch
114
+
115
+ # Default to main
116
+ return "main"
117
+
118
+ def _extract_source_branch(self) -> Optional[str]:
119
+ """
120
+ Extract source/head branch from GitHub Actions environment.
121
+
122
+ Returns:
123
+ Source branch name if detected
124
+ """
125
+ # GITHUB_HEAD_REF is only set for pull_request events
126
+ source_branch = os.getenv("GITHUB_HEAD_REF")
127
+ if source_branch:
128
+ return source_branch
129
+
130
+ # Fallback to GITHUB_REF_NAME
131
+ source_branch = os.getenv("GITHUB_REF_NAME")
132
+ if source_branch:
133
+ return source_branch
134
+
135
+ # Fallback to git command
136
+ return self.run_git_command(["git", "branch", "--show-current"])
@@ -0,0 +1,130 @@
1
+ """
2
+ GitLab CI/CD provider.
3
+ """
4
+
5
+ import os
6
+ from typing import Optional
7
+
8
+ from recce_cloud.ci_providers.base import BaseCIProvider, CIInfo
9
+
10
+
11
+ class GitLabCIProvider(BaseCIProvider):
12
+ """GitLab CI/CD provider implementation."""
13
+
14
+ def can_handle(self) -> bool:
15
+ """
16
+ Check if running in GitLab CI.
17
+
18
+ Returns:
19
+ True if GITLAB_CI environment variable is 'true'
20
+ """
21
+ return os.getenv("GITLAB_CI") == "true"
22
+
23
+ def extract_ci_info(self) -> CIInfo:
24
+ """
25
+ Extract CI information from GitLab CI environment.
26
+
27
+ Environment variables used:
28
+ - CI_MERGE_REQUEST_IID: Merge request number
29
+ - CI_COMMIT_SHA: Commit SHA
30
+ - CI_MERGE_REQUEST_TARGET_BRANCH_NAME: Target branch (MR only)
31
+ - CI_MERGE_REQUEST_SOURCE_BRANCH_NAME: Source branch (MR only)
32
+ - CI_COMMIT_REF_NAME: Branch name (fallback)
33
+ - CI_PROJECT_PATH: Repository path (group/project)
34
+ - CI_SERVER_URL: GitLab instance URL (defaults to https://gitlab.com)
35
+ - CI_JOB_TOKEN: Default access token (automatically provided by GitLab CI)
36
+
37
+ Returns:
38
+ CIInfo object with extracted information
39
+ """
40
+ cr_number = self._extract_mr_number()
41
+ commit_sha = self._extract_commit_sha()
42
+ base_branch = self._extract_base_branch()
43
+ source_branch = self._extract_source_branch()
44
+ repository = os.getenv("CI_PROJECT_PATH")
45
+ access_token = os.getenv("CI_JOB_TOKEN")
46
+
47
+ # Build CR URL (MR URL) if we have the necessary information
48
+ cr_url = None
49
+ if cr_number is not None and repository:
50
+ server_url = os.getenv("CI_SERVER_URL", "https://gitlab.com")
51
+ cr_url = f"{server_url}/{repository}/-/merge_requests/{cr_number}"
52
+
53
+ session_type = self.determine_session_type(cr_number, source_branch)
54
+
55
+ return CIInfo(
56
+ platform="gitlab-ci",
57
+ cr_number=cr_number,
58
+ cr_url=cr_url,
59
+ session_type=session_type,
60
+ commit_sha=commit_sha,
61
+ base_branch=base_branch,
62
+ source_branch=source_branch,
63
+ repository=repository,
64
+ access_token=access_token,
65
+ )
66
+
67
+ def _extract_mr_number(self) -> Optional[int]:
68
+ """
69
+ Extract MR number from GitLab CI environment.
70
+
71
+ Returns:
72
+ MR number if detected, None otherwise
73
+ """
74
+ mr_iid = os.getenv("CI_MERGE_REQUEST_IID")
75
+ if mr_iid:
76
+ try:
77
+ return int(mr_iid)
78
+ except ValueError:
79
+ pass
80
+
81
+ return None
82
+
83
+ def _extract_commit_sha(self) -> Optional[str]:
84
+ """
85
+ Extract commit SHA from GitLab CI environment.
86
+
87
+ Returns:
88
+ Commit SHA if detected, falls back to git command
89
+ """
90
+ commit_sha = os.getenv("CI_COMMIT_SHA")
91
+ if commit_sha:
92
+ return commit_sha
93
+
94
+ # Fallback to git command
95
+ return self.run_git_command(["git", "rev-parse", "HEAD"])
96
+
97
+ def _extract_base_branch(self) -> str:
98
+ """
99
+ Extract base/target branch from GitLab CI environment.
100
+
101
+ Returns:
102
+ Base branch name, defaults to 'main' if not detected
103
+ """
104
+ # CI_MERGE_REQUEST_TARGET_BRANCH_NAME is only set for merge request pipelines
105
+ base_branch = os.getenv("CI_MERGE_REQUEST_TARGET_BRANCH_NAME")
106
+ if base_branch:
107
+ return base_branch
108
+
109
+ # Default to main
110
+ return "main"
111
+
112
+ def _extract_source_branch(self) -> Optional[str]:
113
+ """
114
+ Extract source branch from GitLab CI environment.
115
+
116
+ Returns:
117
+ Source branch name if detected
118
+ """
119
+ # CI_MERGE_REQUEST_SOURCE_BRANCH_NAME is only set for merge request pipelines
120
+ source_branch = os.getenv("CI_MERGE_REQUEST_SOURCE_BRANCH_NAME")
121
+ if source_branch:
122
+ return source_branch
123
+
124
+ # Fallback to CI_COMMIT_REF_NAME
125
+ source_branch = os.getenv("CI_COMMIT_REF_NAME")
126
+ if source_branch:
127
+ return source_branch
128
+
129
+ # Fallback to git command
130
+ return self.run_git_command(["git", "branch", "--show-current"])