airbyte-internal-ops 0.1.10__py3-none-any.whl → 0.2.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.
@@ -0,0 +1,90 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """Fetch connector test secrets using the PyAirbyte secrets API.
3
+
4
+ This module uses the PyAirbyte GoogleGSMSecretManager to retrieve
5
+ integration test secrets from Google Secret Manager for connectors.
6
+
7
+ Usage:
8
+ from airbyte_ops_mcp.live_tests.cdk_secrets import get_first_config_from_secrets
9
+
10
+ # Fetch the first config for a connector
11
+ config = get_first_config_from_secrets("source-github")
12
+ if config:
13
+ # Use the config dict
14
+ ...
15
+
16
+ Note: Requires GCP credentials with access to the integration testing project.
17
+ The credentials can be provided via:
18
+ - GOOGLE_APPLICATION_CREDENTIALS environment variable
19
+ - GCP_GSM_CREDENTIALS environment variable (JSON string)
20
+ - Application Default Credentials
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import logging
26
+ import os
27
+
28
+ from airbyte.secrets import GoogleGSMSecretManager
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+ # Default GCP project for integration test secrets
33
+ DEFAULT_GSM_PROJECT = "dataline-integration-testing"
34
+
35
+
36
+ def get_first_config_from_secrets(
37
+ connector_name: str,
38
+ project: str = DEFAULT_GSM_PROJECT,
39
+ ) -> dict | None:
40
+ """Fetch the first integration test config for a connector from GSM.
41
+
42
+ This function uses the PyAirbyte GoogleGSMSecretManager to fetch secrets
43
+ labeled with the connector name and returns the first one as a parsed dict.
44
+
45
+ Args:
46
+ connector_name: The connector name (e.g., 'source-github').
47
+ project: The GCP project ID containing the secrets.
48
+
49
+ Returns:
50
+ The parsed config dict, or None if no secrets are found or fetching fails.
51
+ """
52
+ # Get credentials from environment
53
+ credentials_json: str | None = None
54
+ credentials_path: str | None = None
55
+
56
+ if "GCP_GSM_CREDENTIALS" in os.environ:
57
+ credentials_json = os.environ["GCP_GSM_CREDENTIALS"]
58
+ elif "GOOGLE_APPLICATION_CREDENTIALS" in os.environ:
59
+ credentials_path = os.environ["GOOGLE_APPLICATION_CREDENTIALS"]
60
+
61
+ # If no explicit credentials, GoogleGSMSecretManager will try ADC
62
+ try:
63
+ gsm = GoogleGSMSecretManager(
64
+ project=project,
65
+ credentials_json=credentials_json,
66
+ credentials_path=credentials_path,
67
+ )
68
+ except Exception as e:
69
+ logger.warning(f"Failed to initialize GSM client: {e}")
70
+ return None
71
+
72
+ logger.info(f"Fetching integration test config for {connector_name} from GSM")
73
+
74
+ try:
75
+ # fetch_connector_secret returns the first secret matching the connector label
76
+ secret_handle = gsm.fetch_connector_secret(connector_name)
77
+ # parse_json() calls get_value() internally and parses the result
78
+ config = secret_handle.parse_json()
79
+ logger.info(f"Successfully fetched config for {connector_name}")
80
+ return dict(config) if config else None
81
+
82
+ except StopIteration:
83
+ logger.warning(f"No secrets found for connector {connector_name}")
84
+ return None
85
+ except Exception as e:
86
+ # Log the exception type but not the message (may contain sensitive info)
87
+ logger.warning(
88
+ f"Failed to fetch config for {connector_name}: {type(e).__name__}"
89
+ )
90
+ return None
@@ -142,6 +142,35 @@ def _format_delta(delta: int) -> str:
142
142
  return "0"
143
143
 
144
144
 
145
+ def _get_github_run_url() -> str | None:
146
+ """Get the URL to the current GitHub Actions workflow run.
147
+
148
+ Returns:
149
+ The workflow run URL, or None if not running in GitHub Actions.
150
+ """
151
+ server_url = os.getenv("GITHUB_SERVER_URL")
152
+ repository = os.getenv("GITHUB_REPOSITORY")
153
+ run_id = os.getenv("GITHUB_RUN_ID")
154
+
155
+ if not all([server_url, repository, run_id]):
156
+ return None
157
+
158
+ return f"{server_url}/{repository}/actions/runs/{run_id}"
159
+
160
+
161
+ def _get_github_artifacts_url() -> str | None:
162
+ """Get the URL to the artifacts section of the current workflow run.
163
+
164
+ Returns:
165
+ The artifacts section URL, or None if not running in GitHub Actions.
166
+ """
167
+ run_url = _get_github_run_url()
168
+ if not run_url:
169
+ return None
170
+
171
+ return f"{run_url}#artifacts"
172
+
173
+
145
174
  def generate_regression_report(
146
175
  target_image: str,
147
176
  control_image: str,
@@ -190,6 +219,9 @@ def generate_regression_report(
190
219
  target_image.rsplit(":", 1)[0] if ":" in target_image else target_image
191
220
  )
192
221
 
222
+ run_url = _get_github_run_url()
223
+ artifacts_url = _get_github_artifacts_url()
224
+
193
225
  lines: list[str] = [
194
226
  "# Regression Test Report",
195
227
  "",
@@ -200,12 +232,23 @@ def generate_regression_report(
200
232
  f"- **Control Version:** `{control_version}`",
201
233
  f"- **Target Version:** `{target_version}`",
202
234
  f"- **Command:** `{command.upper()}`",
203
- f"- **Artifacts:** `{artifact_name}`",
204
- "",
205
- "## Summary",
206
- "",
207
235
  ]
208
236
 
237
+ if run_url:
238
+ lines.append(f"- **Workflow Run:** [View Execution]({run_url})")
239
+ if artifacts_url:
240
+ lines.append(f"- **Artifacts:** [Download `{artifact_name}`]({artifacts_url})")
241
+ else:
242
+ lines.append(f"- **Artifacts:** `{artifact_name}`")
243
+
244
+ lines.extend(
245
+ [
246
+ "",
247
+ "## Summary",
248
+ "",
249
+ ]
250
+ )
251
+
209
252
  if regression_detected:
210
253
  if target_result["success"] and not control_result["success"]:
211
254
  lines.append("**Result:** Target succeeded, control failed (improvement)")
@@ -341,9 +384,16 @@ def get_report_summary(report_path: Path) -> str:
341
384
  f"regression-test-artifacts-{run_id}" if run_id else "regression-test-artifacts"
342
385
  )
343
386
 
387
+ artifacts_url = _get_github_artifacts_url()
388
+ artifact_link = (
389
+ f"[`{artifact_name}`]({artifacts_url})"
390
+ if artifacts_url
391
+ else f"`{artifact_name}`"
392
+ )
393
+
344
394
  return f"""## Regression Test Report
345
395
 
346
- Full report available in the **Regression Test Report** check or in artifact `{artifact_name}`.
396
+ Full report available in the **Regression Test Report** check or in artifact {artifact_link}.
347
397
 
348
398
  See the Checks tab for the complete report with message counts and execution details.
349
399
  """
@@ -168,6 +168,9 @@ class ConnectorRunner:
168
168
 
169
169
  with tempfile.TemporaryDirectory() as temp_dir:
170
170
  temp_path = Path(temp_dir)
171
+ # Make temp directory world-readable so non-root container users can access it
172
+ # Many connector images run as non-root users (e.g., 'airbyte' user)
173
+ temp_path.chmod(0o755)
171
174
  self._prepare_data_directory(temp_path)
172
175
 
173
176
  docker_cmd = self._build_docker_command(temp_path)
@@ -0,0 +1,198 @@
1
+ # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
+ """HTTP header extraction for Airbyte Cloud credentials.
3
+
4
+ This module provides internal helper functions for extracting Airbyte Cloud
5
+ authentication credentials from HTTP headers when running as an MCP HTTP server.
6
+ This enables per-request credential passing from upstream services like coral-agents.
7
+
8
+ The resolution order for credentials is:
9
+ 1. HTTP headers (when running as MCP HTTP server)
10
+ 2. Environment variables (fallback)
11
+
12
+ Note: This module is prefixed with "_" to indicate it is internal helper logic
13
+ for the MCP module and should not be imported directly by external code.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from airbyte.cloud.auth import (
19
+ resolve_cloud_api_url,
20
+ resolve_cloud_client_id,
21
+ resolve_cloud_client_secret,
22
+ resolve_cloud_workspace_id,
23
+ )
24
+ from airbyte.secrets.base import SecretString
25
+ from fastmcp.server.dependencies import get_http_headers
26
+
27
+ from airbyte_ops_mcp.constants import (
28
+ HEADER_AIRBYTE_CLOUD_API_URL,
29
+ HEADER_AIRBYTE_CLOUD_CLIENT_ID,
30
+ HEADER_AIRBYTE_CLOUD_CLIENT_SECRET,
31
+ HEADER_AIRBYTE_CLOUD_WORKSPACE_ID,
32
+ )
33
+
34
+
35
+ def _get_header_value(headers: dict[str, str], header_name: str) -> str | None:
36
+ """Get a header value from a headers dict, case-insensitively.
37
+
38
+ Args:
39
+ headers: Dictionary of HTTP headers.
40
+ header_name: The header name to look for (case-insensitive).
41
+
42
+ Returns:
43
+ The header value if found, None otherwise.
44
+ """
45
+ header_name_lower = header_name.lower()
46
+ for key, value in headers.items():
47
+ if key.lower() == header_name_lower:
48
+ return value
49
+ return None
50
+
51
+
52
+ def get_client_id_from_headers() -> SecretString | None:
53
+ """Extract client ID from HTTP headers.
54
+
55
+ Returns:
56
+ The client ID as a SecretString, or None if not found or not in HTTP context.
57
+ """
58
+ headers = get_http_headers()
59
+ if not headers:
60
+ return None
61
+
62
+ value = _get_header_value(headers, HEADER_AIRBYTE_CLOUD_CLIENT_ID)
63
+ if value:
64
+ return SecretString(value)
65
+ return None
66
+
67
+
68
+ def get_client_secret_from_headers() -> SecretString | None:
69
+ """Extract client secret from HTTP headers.
70
+
71
+ Returns:
72
+ The client secret as a SecretString, or None if not found or not in HTTP context.
73
+ """
74
+ headers = get_http_headers()
75
+ if not headers:
76
+ return None
77
+
78
+ value = _get_header_value(headers, HEADER_AIRBYTE_CLOUD_CLIENT_SECRET)
79
+ if value:
80
+ return SecretString(value)
81
+ return None
82
+
83
+
84
+ def get_workspace_id_from_headers() -> str | None:
85
+ """Extract workspace ID from HTTP headers.
86
+
87
+ Returns:
88
+ The workspace ID, or None if not found or not in HTTP context.
89
+ """
90
+ headers = get_http_headers()
91
+ if not headers:
92
+ return None
93
+
94
+ return _get_header_value(headers, HEADER_AIRBYTE_CLOUD_WORKSPACE_ID)
95
+
96
+
97
+ def get_api_url_from_headers() -> str | None:
98
+ """Extract API URL from HTTP headers.
99
+
100
+ Returns:
101
+ The API URL, or None if not found or not in HTTP context.
102
+ """
103
+ headers = get_http_headers()
104
+ if not headers:
105
+ return None
106
+
107
+ return _get_header_value(headers, HEADER_AIRBYTE_CLOUD_API_URL)
108
+
109
+
110
+ def resolve_client_id() -> SecretString:
111
+ """Resolve client ID from HTTP headers or environment variables.
112
+
113
+ Resolution order:
114
+ 1. HTTP header X-Airbyte-Cloud-Client-Id
115
+ 2. Environment variable AIRBYTE_CLOUD_CLIENT_ID (via PyAirbyte)
116
+
117
+ Returns:
118
+ The resolved client ID as a SecretString.
119
+
120
+ Raises:
121
+ PyAirbyteSecretNotFoundError: If no client ID can be resolved.
122
+ """
123
+ header_value = get_client_id_from_headers()
124
+ if header_value:
125
+ return header_value
126
+
127
+ return resolve_cloud_client_id()
128
+
129
+
130
+ def resolve_client_secret() -> SecretString:
131
+ """Resolve client secret from HTTP headers or environment variables.
132
+
133
+ Resolution order:
134
+ 1. HTTP header X-Airbyte-Cloud-Client-Secret
135
+ 2. Environment variable AIRBYTE_CLOUD_CLIENT_SECRET (via PyAirbyte)
136
+
137
+ Returns:
138
+ The resolved client secret as a SecretString.
139
+
140
+ Raises:
141
+ PyAirbyteSecretNotFoundError: If no client secret can be resolved.
142
+ """
143
+ header_value = get_client_secret_from_headers()
144
+ if header_value:
145
+ return header_value
146
+
147
+ return resolve_cloud_client_secret()
148
+
149
+
150
+ def resolve_workspace_id(workspace_id: str | None = None) -> str:
151
+ """Resolve workspace ID from multiple sources.
152
+
153
+ Resolution order:
154
+ 1. Explicit workspace_id parameter (if provided)
155
+ 2. HTTP header X-Airbyte-Cloud-Workspace-Id
156
+ 3. Environment variable AIRBYTE_CLOUD_WORKSPACE_ID (via PyAirbyte)
157
+
158
+ Args:
159
+ workspace_id: Optional explicit workspace ID.
160
+
161
+ Returns:
162
+ The resolved workspace ID.
163
+
164
+ Raises:
165
+ PyAirbyteSecretNotFoundError: If no workspace ID can be resolved.
166
+ """
167
+ if workspace_id is not None:
168
+ return workspace_id
169
+
170
+ header_value = get_workspace_id_from_headers()
171
+ if header_value:
172
+ return header_value
173
+
174
+ return resolve_cloud_workspace_id()
175
+
176
+
177
+ def resolve_api_url(api_url: str | None = None) -> str:
178
+ """Resolve API URL from multiple sources.
179
+
180
+ Resolution order:
181
+ 1. Explicit api_url parameter (if provided)
182
+ 2. HTTP header X-Airbyte-Cloud-Api-Url
183
+ 3. Environment variable / default (via PyAirbyte)
184
+
185
+ Args:
186
+ api_url: Optional explicit API URL.
187
+
188
+ Returns:
189
+ The resolved API URL.
190
+ """
191
+ if api_url is not None:
192
+ return api_url
193
+
194
+ header_value = get_api_url_from_headers()
195
+ if header_value:
196
+ return header_value
197
+
198
+ return resolve_cloud_api_url()
@@ -15,10 +15,6 @@ from typing import Annotated, Literal
15
15
 
16
16
  from airbyte import constants
17
17
  from airbyte.cloud import CloudWorkspace
18
- from airbyte.cloud.auth import (
19
- resolve_cloud_client_id,
20
- resolve_cloud_client_secret,
21
- )
22
18
  from airbyte.exceptions import PyAirbyteInputError
23
19
  from fastmcp import FastMCP
24
20
  from pydantic import Field
@@ -26,19 +22,26 @@ from pydantic import Field
26
22
  from airbyte_ops_mcp.cloud_admin import api_client
27
23
  from airbyte_ops_mcp.cloud_admin.auth import (
28
24
  CloudAuthError,
29
- get_admin_user_email,
30
- require_internal_admin,
25
+ require_internal_admin_flag_only,
31
26
  )
32
27
  from airbyte_ops_mcp.cloud_admin.models import (
33
28
  ConnectorVersionInfo,
34
29
  VersionOverrideOperationResult,
35
30
  )
31
+ from airbyte_ops_mcp.mcp._http_headers import (
32
+ resolve_client_id,
33
+ resolve_client_secret,
34
+ )
36
35
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
37
36
 
38
37
 
39
38
  def _get_workspace(workspace_id: str) -> CloudWorkspace:
40
39
  """Get a CloudWorkspace instance for the specified workspace.
41
40
 
41
+ Credentials are resolved in priority order:
42
+ 1. HTTP headers (X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret)
43
+ 2. Environment variables (AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET)
44
+
42
45
  Args:
43
46
  workspace_id: The Airbyte Cloud workspace ID (required).
44
47
 
@@ -46,19 +49,21 @@ def _get_workspace(workspace_id: str) -> CloudWorkspace:
46
49
  CloudWorkspace instance configured for the specified workspace.
47
50
 
48
51
  Raises:
49
- CloudAuthError: If required environment variables are not set.
52
+ CloudAuthError: If credentials cannot be resolved from headers or env vars.
50
53
  """
51
54
  try:
52
55
  return CloudWorkspace(
53
56
  workspace_id=workspace_id,
54
- client_id=resolve_cloud_client_id(),
55
- client_secret=resolve_cloud_client_secret(),
57
+ client_id=resolve_client_id(),
58
+ client_secret=resolve_client_secret(),
56
59
  api_root=constants.CLOUD_API_ROOT, # Used for workspace operations
57
60
  )
58
61
  except Exception as e:
59
62
  raise CloudAuthError(
60
- f"Failed to initialize CloudWorkspace. Ensure AIRBYTE_CLIENT_ID "
61
- f"and AIRBYTE_CLIENT_SECRET are set. Error: {e}"
63
+ f"Failed to initialize CloudWorkspace. Ensure credentials are provided "
64
+ f"via HTTP headers (X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret) "
65
+ f"or environment variables (AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET). "
66
+ f"Error: {e}"
62
67
  ) from e
63
68
 
64
69
 
@@ -85,8 +90,9 @@ def get_cloud_connector_version(
85
90
  Returns version details including the current version string and whether
86
91
  an override (pin) is applied.
87
92
 
88
- The `AIRBYTE_CLIENT_ID`, `AIRBYTE_CLIENT_SECRET`, and `AIRBYTE_API_ROOT`
89
- environment variables will be used to authenticate with the Airbyte Cloud API.
93
+ Authentication credentials are resolved in priority order:
94
+ 1. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
95
+ 2. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
90
96
  """
91
97
  try:
92
98
  workspace = _get_workspace(workspace_id)
@@ -163,11 +169,47 @@ def set_cloud_connector_version_override(
163
169
  default=None,
164
170
  ),
165
171
  ],
172
+ admin_user_email: Annotated[
173
+ str | None,
174
+ Field(
175
+ description="Email of the admin user authorizing this operation. "
176
+ "Must be an @airbyte.io email address. Required for authorization.",
177
+ default=None,
178
+ ),
179
+ ],
180
+ issue_url: Annotated[
181
+ str | None,
182
+ Field(
183
+ description="URL to the GitHub issue providing context for this operation. "
184
+ "Must be a valid GitHub URL (https://github.com/...). Required for authorization.",
185
+ default=None,
186
+ ),
187
+ ],
188
+ approval_comment_url: Annotated[
189
+ str | None,
190
+ Field(
191
+ description="URL to a GitHub comment where the admin has explicitly "
192
+ "requested or authorized this deployment. Must be a valid GitHub comment URL. "
193
+ "Required for authorization.",
194
+ default=None,
195
+ ),
196
+ ],
197
+ ai_agent_session_url: Annotated[
198
+ str | None,
199
+ Field(
200
+ description="URL to the AI agent session driving this operation, if applicable. "
201
+ "Provides additional auditability for AI-driven operations.",
202
+ default=None,
203
+ ),
204
+ ],
166
205
  ) -> VersionOverrideOperationResult:
167
206
  """Set or clear a version override for a deployed connector.
168
207
 
169
- **Admin-only operation** - Requires AIRBYTE_INTERNAL_ADMIN_FLAG=airbyte.io
170
- and AIRBYTE_INTERNAL_ADMIN_USER environment variables.
208
+ **Admin-only operation** - Requires:
209
+ - AIRBYTE_INTERNAL_ADMIN_FLAG=airbyte.io environment variable
210
+ - admin_user_email parameter (must be @airbyte.io email)
211
+ - issue_url parameter (GitHub issue URL for context)
212
+ - approval_comment_url parameter (GitHub comment URL with approval)
171
213
 
172
214
  You must specify EXACTLY ONE of `version` OR `unset=True`, but not both.
173
215
  When setting a version, `override_reason` is required.
@@ -177,13 +219,13 @@ def set_cloud_connector_version_override(
177
219
  - Production versions: Require strong justification mentioning customer/support/investigation
178
220
  - Release candidates (-rc): Any admin can pin/unpin RC versions
179
221
 
180
- The `AIRBYTE_CLIENT_ID`, `AIRBYTE_CLIENT_SECRET`, and `AIRBYTE_API_ROOT`
181
- environment variables will be used to authenticate with the Airbyte Cloud API.
222
+ Authentication credentials are resolved in priority order:
223
+ 1. HTTP headers: X-Airbyte-Cloud-Client-Id, X-Airbyte-Cloud-Client-Secret
224
+ 2. Environment variables: AIRBYTE_CLOUD_CLIENT_ID, AIRBYTE_CLOUD_CLIENT_SECRET
182
225
  """
183
- # Validate admin access
226
+ # Validate admin access (check env var flag)
184
227
  try:
185
- require_internal_admin()
186
- user_email = get_admin_user_email()
228
+ require_internal_admin_flag_only()
187
229
  except CloudAuthError as e:
188
230
  return VersionOverrideOperationResult(
189
231
  success=False,
@@ -192,6 +234,60 @@ def set_cloud_connector_version_override(
192
234
  connector_type=actor_type,
193
235
  )
194
236
 
237
+ # Validate new authorization parameters
238
+ validation_errors: list[str] = []
239
+
240
+ if not admin_user_email:
241
+ validation_errors.append("admin_user_email is required for authorization")
242
+ elif "@airbyte.io" not in admin_user_email:
243
+ validation_errors.append(
244
+ f"admin_user_email must be an @airbyte.io email address, got: {admin_user_email}"
245
+ )
246
+
247
+ if not issue_url:
248
+ validation_errors.append(
249
+ "issue_url is required for authorization (GitHub issue URL)"
250
+ )
251
+ elif not issue_url.startswith("https://github.com/"):
252
+ validation_errors.append(
253
+ f"issue_url must be a valid GitHub URL (https://github.com/...), got: {issue_url}"
254
+ )
255
+
256
+ if not approval_comment_url:
257
+ validation_errors.append(
258
+ "approval_comment_url is required for authorization (GitHub comment URL)"
259
+ )
260
+ elif not approval_comment_url.startswith("https://github.com/"):
261
+ validation_errors.append(
262
+ f"approval_comment_url must be a valid GitHub URL, got: {approval_comment_url}"
263
+ )
264
+ elif (
265
+ "#issuecomment-" not in approval_comment_url
266
+ and "#discussion_r" not in approval_comment_url
267
+ ):
268
+ validation_errors.append(
269
+ "approval_comment_url must be a GitHub comment URL "
270
+ "(containing #issuecomment- or #discussion_r)"
271
+ )
272
+
273
+ if validation_errors:
274
+ return VersionOverrideOperationResult(
275
+ success=False,
276
+ message="Authorization validation failed: " + "; ".join(validation_errors),
277
+ connector_id=actor_id,
278
+ connector_type=actor_type,
279
+ )
280
+
281
+ # Build enhanced override reason with audit fields (only for 'set' operations)
282
+ enhanced_override_reason = override_reason
283
+ if not unset and override_reason:
284
+ audit_parts = [override_reason]
285
+ audit_parts.append(f"Issue: {issue_url}")
286
+ audit_parts.append(f"Approval: {approval_comment_url}")
287
+ if ai_agent_session_url:
288
+ audit_parts.append(f"AI Session: {ai_agent_session_url}")
289
+ enhanced_override_reason = " | ".join(audit_parts)
290
+
195
291
  # Get workspace and current version info
196
292
  try:
197
293
  workspace = _get_workspace(workspace_id)
@@ -233,9 +329,9 @@ def set_cloud_connector_version_override(
233
329
  workspace_id=workspace_id,
234
330
  version=version,
235
331
  unset=unset,
236
- override_reason=override_reason,
332
+ override_reason=enhanced_override_reason,
237
333
  override_reason_reference_url=override_reason_reference_url,
238
- user_email=user_email,
334
+ user_email=admin_user_email,
239
335
  )
240
336
 
241
337
  # Get updated version info after the operation
@@ -7,7 +7,6 @@ Docker image availability, and other related operations.
7
7
 
8
8
  from __future__ import annotations
9
9
 
10
- import os
11
10
  import re
12
11
  from typing import Annotated
13
12
 
@@ -15,9 +14,9 @@ import requests
15
14
  from fastmcp import FastMCP
16
15
  from pydantic import BaseModel, Field
17
16
 
17
+ from airbyte_ops_mcp.github_actions import GITHUB_API_BASE, resolve_github_token
18
18
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
19
19
 
20
- GITHUB_API_BASE = "https://api.github.com"
21
20
  DOCKERHUB_API_BASE = "https://hub.docker.com/v2"
22
21
 
23
22
 
@@ -37,24 +36,6 @@ class WorkflowRunStatus(BaseModel):
37
36
  jobs_url: str
38
37
 
39
38
 
40
- def _get_github_token() -> str:
41
- """Get GitHub token from environment.
42
-
43
- Returns:
44
- GitHub token string.
45
-
46
- Raises:
47
- ValueError: If GITHUB_TOKEN environment variable is not set.
48
- """
49
- token = os.getenv("GITHUB_TOKEN")
50
- if not token:
51
- raise ValueError(
52
- "GITHUB_TOKEN environment variable is required. "
53
- "Please set it to a GitHub personal access token."
54
- )
55
- return token
56
-
57
-
58
39
  def _parse_workflow_url(url: str) -> tuple[str, str, int]:
59
40
  """Parse a GitHub Actions workflow run URL into components.
60
41
 
@@ -162,7 +143,7 @@ def check_workflow_status(
162
143
  )
163
144
 
164
145
  # Guard: Check for required token
165
- token = _get_github_token()
146
+ token = resolve_github_token()
166
147
 
167
148
  # Get workflow run details
168
149
  run_data = _get_workflow_run(owner, repo, run_id, token)