airbyte-internal-ops 0.1.9__py3-none-any.whl → 0.1.11__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)
@@ -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)
@@ -9,7 +9,6 @@ in GitHub Actions and results can be polled via workflow status.
9
9
  from __future__ import annotations
10
10
 
11
11
  import logging
12
- import os
13
12
  import uuid
14
13
  from datetime import datetime
15
14
  from enum import Enum
@@ -21,6 +20,11 @@ from airbyte.cloud.auth import resolve_cloud_client_id, resolve_cloud_client_sec
21
20
  from fastmcp import FastMCP
22
21
  from pydantic import BaseModel, Field
23
22
 
23
+ from airbyte_ops_mcp.github_actions import (
24
+ GITHUB_API_BASE,
25
+ resolve_github_token,
26
+ trigger_workflow_dispatch,
27
+ )
24
28
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
25
29
 
26
30
  logger = logging.getLogger(__name__)
@@ -29,7 +33,6 @@ logger = logging.getLogger(__name__)
29
33
  # GitHub Workflow Configuration
30
34
  # =============================================================================
31
35
 
32
- GITHUB_API_BASE = "https://api.github.com"
33
36
  LIVE_TEST_REPO_OWNER = "airbytehq"
34
37
  LIVE_TEST_REPO_NAME = "airbyte-ops-mcp"
35
38
  LIVE_TEST_DEFAULT_BRANCH = "main"
@@ -37,76 +40,6 @@ LIVE_TEST_WORKFLOW_FILE = "connector-live-test.yml"
37
40
  REGRESSION_TEST_WORKFLOW_FILE = "connector-regression-test.yml"
38
41
 
39
42
 
40
- # =============================================================================
41
- # GitHub API Helper Functions
42
- # =============================================================================
43
-
44
-
45
- def _get_github_token() -> str:
46
- """Get GitHub token from environment.
47
-
48
- Checks for tokens in order of specificity:
49
- 1. GITHUB_CI_WORKFLOW_TRIGGER_PAT (general workflow triggering)
50
- 2. GITHUB_TOKEN (fallback)
51
-
52
- Returns:
53
- GitHub token string.
54
-
55
- Raises:
56
- ValueError: If no GitHub token environment variable is set.
57
- """
58
- token = os.getenv("GITHUB_CI_WORKFLOW_TRIGGER_PAT") or os.getenv("GITHUB_TOKEN")
59
- if not token:
60
- raise ValueError(
61
- "No GitHub token found. Set GITHUB_CI_WORKFLOW_TRIGGER_PAT or GITHUB_TOKEN "
62
- "environment variable with 'actions:write' permission."
63
- )
64
- return token
65
-
66
-
67
- def _trigger_workflow_dispatch(
68
- owner: str,
69
- repo: str,
70
- workflow_file: str,
71
- ref: str,
72
- inputs: dict[str, Any],
73
- token: str,
74
- ) -> str:
75
- """Trigger a GitHub Actions workflow via workflow_dispatch.
76
-
77
- Args:
78
- owner: Repository owner (e.g., "airbytehq")
79
- repo: Repository name (e.g., "airbyte-ops-mcp")
80
- workflow_file: Workflow file name (e.g., "connector-live-test.yml")
81
- ref: Git ref to run the workflow on (branch name)
82
- inputs: Workflow inputs dictionary
83
- token: GitHub API token
84
-
85
- Returns:
86
- URL to view workflow runs.
87
-
88
- Raises:
89
- requests.HTTPError: If API request fails.
90
- """
91
- url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/workflows/{workflow_file}/dispatches"
92
- headers = {
93
- "Authorization": f"Bearer {token}",
94
- "Accept": "application/vnd.github+json",
95
- "X-GitHub-Api-Version": "2022-11-28",
96
- }
97
- payload = {
98
- "ref": ref,
99
- "inputs": inputs,
100
- }
101
-
102
- response = requests.post(url, headers=headers, json=payload, timeout=30)
103
- response.raise_for_status()
104
-
105
- # workflow_dispatch returns 204 No Content on success
106
- # Return URL to view workflow runs
107
- return f"https://github.com/{owner}/{repo}/actions/workflows/{workflow_file}"
108
-
109
-
110
43
  def _get_workflow_run_status(
111
44
  owner: str,
112
45
  repo: str,
@@ -293,12 +226,22 @@ class LiveConnectionTestResult(BaseModel):
293
226
  class RunLiveConnectionTestsResponse(BaseModel):
294
227
  """Response from starting a live connection test via GitHub Actions workflow."""
295
228
 
296
- run_id: str = Field(description="Unique identifier for the test run")
229
+ run_id: str = Field(
230
+ description="Unique identifier for the test run (internal tracking ID)"
231
+ )
297
232
  status: TestRunStatus = Field(description="Initial status of the test run")
298
233
  message: str = Field(description="Human-readable status message")
299
234
  workflow_url: str | None = Field(
300
235
  default=None,
301
- description="URL to view the GitHub Actions workflow runs",
236
+ description="URL to view the GitHub Actions workflow file",
237
+ )
238
+ github_run_id: int | None = Field(
239
+ default=None,
240
+ description="GitHub Actions workflow run ID (use with check_workflow_status)",
241
+ )
242
+ github_run_url: str | None = Field(
243
+ default=None,
244
+ description="Direct URL to the GitHub Actions workflow run",
302
245
  )
303
246
 
304
247
 
@@ -348,9 +291,16 @@ def run_live_connection_tests(
348
291
  ] = None,
349
292
  connector_name: Annotated[
350
293
  str | None,
351
- "Connector name to build target image from source for regression tests "
352
- "(e.g., 'source-pokeapi'). If provided, builds the target image locally. "
353
- "Only used when skip_regression_tests=False.",
294
+ "Connector name to build the connector image from source "
295
+ "(e.g., 'source-pokeapi'). If provided, builds the image locally with tag 'dev'. "
296
+ "For live tests, this builds the test image. For regression tests, this builds "
297
+ "the target image while control is auto-detected from the connection.",
298
+ ] = None,
299
+ airbyte_ref: Annotated[
300
+ str | None,
301
+ "Git ref or PR number to checkout from the airbyte monorepo "
302
+ "(e.g., 'master', '70847', 'refs/pull/70847/head'). "
303
+ "Only used when connector_name is provided. Defaults to 'master' if not specified.",
354
304
  ] = None,
355
305
  ) -> RunLiveConnectionTestsResponse:
356
306
  """Start a live connection test run via GitHub Actions workflow.
@@ -377,7 +327,7 @@ def run_live_connection_tests(
377
327
 
378
328
  # Get GitHub token
379
329
  try:
380
- token = _get_github_token()
330
+ token = resolve_github_token()
381
331
  except ValueError as e:
382
332
  return RunLiveConnectionTestsResponse(
383
333
  run_id=run_id,
@@ -422,9 +372,11 @@ def run_live_connection_tests(
422
372
  }
423
373
  if connector_image:
424
374
  workflow_inputs["connector_image"] = connector_image
375
+ if connector_name:
376
+ workflow_inputs["connector_name"] = connector_name
425
377
 
426
378
  try:
427
- workflow_url = _trigger_workflow_dispatch(
379
+ dispatch_result = trigger_workflow_dispatch(
428
380
  owner=LIVE_TEST_REPO_OWNER,
429
381
  repo=LIVE_TEST_REPO_NAME,
430
382
  workflow_file=LIVE_TEST_WORKFLOW_FILE,
@@ -441,12 +393,15 @@ def run_live_connection_tests(
441
393
  workflow_url=None,
442
394
  )
443
395
 
396
+ view_url = dispatch_result.run_url or dispatch_result.workflow_url
444
397
  return RunLiveConnectionTestsResponse(
445
398
  run_id=run_id,
446
399
  status=TestRunStatus.QUEUED,
447
400
  message=f"Live-test workflow triggered for connection {connection_id}. "
448
- f"View progress at: {workflow_url}",
449
- workflow_url=workflow_url,
401
+ f"View progress at: {view_url}",
402
+ workflow_url=dispatch_result.workflow_url,
403
+ github_run_id=dispatch_result.run_id,
404
+ github_run_url=dispatch_result.run_url,
450
405
  )
451
406
 
452
407
  # Regression test workflow (skip_regression_tests=False)
@@ -472,9 +427,11 @@ def run_live_connection_tests(
472
427
  workflow_inputs["control_image"] = control_image
473
428
  if connector_name:
474
429
  workflow_inputs["connector_name"] = connector_name
430
+ if airbyte_ref:
431
+ workflow_inputs["airbyte_ref"] = airbyte_ref
475
432
 
476
433
  try:
477
- workflow_url = _trigger_workflow_dispatch(
434
+ dispatch_result = trigger_workflow_dispatch(
478
435
  owner=LIVE_TEST_REPO_OWNER,
479
436
  repo=LIVE_TEST_REPO_NAME,
480
437
  workflow_file=REGRESSION_TEST_WORKFLOW_FILE,
@@ -491,12 +448,15 @@ def run_live_connection_tests(
491
448
  workflow_url=None,
492
449
  )
493
450
 
451
+ view_url = dispatch_result.run_url or dispatch_result.workflow_url
494
452
  return RunLiveConnectionTestsResponse(
495
453
  run_id=run_id,
496
454
  status=TestRunStatus.QUEUED,
497
455
  message=f"Regression-test workflow triggered for connection {connection_id}. "
498
- f"View progress at: {workflow_url}",
499
- workflow_url=workflow_url,
456
+ f"View progress at: {view_url}",
457
+ workflow_url=dispatch_result.workflow_url,
458
+ github_run_id=dispatch_result.run_id,
459
+ github_run_url=dispatch_result.run_url,
500
460
  )
501
461
 
502
462
 
@@ -8,7 +8,6 @@ workflow in the airbytehq/airbyte repository via GitHub's workflow dispatch API.
8
8
  from __future__ import annotations
9
9
 
10
10
  import base64
11
- import os
12
11
  from typing import Annotated, Literal
13
12
 
14
13
  import requests
@@ -16,15 +15,22 @@ import yaml
16
15
  from fastmcp import FastMCP
17
16
  from pydantic import BaseModel, Field
18
17
 
18
+ from airbyte_ops_mcp.github_actions import GITHUB_API_BASE, resolve_github_token
19
19
  from airbyte_ops_mcp.mcp._mcp_utils import mcp_tool, register_mcp_tools
20
20
 
21
- GITHUB_API_BASE = "https://api.github.com"
22
21
  DEFAULT_REPO_OWNER = "airbytehq"
23
22
  DEFAULT_REPO_NAME = "airbyte"
24
23
  DEFAULT_BRANCH = "master"
25
24
  PRERELEASE_WORKFLOW_FILE = "publish-connectors-prerelease-command.yml"
26
25
  CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors"
27
26
 
27
+ # Token env vars for prerelease publishing (in order of preference)
28
+ PRERELEASE_TOKEN_ENV_VARS = [
29
+ "GITHUB_CONNECTOR_PUBLISHING_PAT",
30
+ "GITHUB_CI_WORKFLOW_TRIGGER_PAT",
31
+ "GITHUB_TOKEN",
32
+ ]
33
+
28
34
 
29
35
  class PRHeadInfo(BaseModel):
30
36
  """Information about a PR's head commit."""
@@ -46,34 +52,6 @@ class PrereleaseWorkflowResult(BaseModel):
46
52
  docker_image_tag: str | None = None
47
53
 
48
54
 
49
- def _get_github_token() -> str:
50
- """Get GitHub token from environment.
51
-
52
- Checks for tokens in order of specificity:
53
- 1. GITHUB_CONNECTOR_PUBLISHING_PAT (most specific)
54
- 2. GITHUB_CI_WORKFLOW_TRIGGER_PAT (general workflow triggering)
55
- 3. GITHUB_TOKEN (fallback)
56
-
57
- Returns:
58
- GitHub token string.
59
-
60
- Raises:
61
- ValueError: If no GitHub token environment variable is set.
62
- """
63
- token = (
64
- os.getenv("GITHUB_CONNECTOR_PUBLISHING_PAT")
65
- or os.getenv("GITHUB_CI_WORKFLOW_TRIGGER_PAT")
66
- or os.getenv("GITHUB_TOKEN")
67
- )
68
- if not token:
69
- raise ValueError(
70
- "No GitHub token found. Set GITHUB_CONNECTOR_PUBLISHING_PAT, "
71
- "GITHUB_CI_WORKFLOW_TRIGGER_PAT, or GITHUB_TOKEN environment variable "
72
- "with 'actions:write' permission."
73
- )
74
- return token
75
-
76
-
77
55
  def _get_pr_head_info(
78
56
  owner: str,
79
57
  repo: str,
@@ -248,7 +226,7 @@ def publish_connector_to_airbyte_registry(
248
226
  )
249
227
 
250
228
  # Guard: Check for required token
251
- token = _get_github_token()
229
+ token = resolve_github_token(PRERELEASE_TOKEN_ENV_VARS)
252
230
 
253
231
  # Get the PR's head ref and SHA
254
232
  head_info = _get_pr_head_info(