airbyte-internal-ops 0.2.0__py3-none-any.whl → 0.2.2__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.
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/METADATA +19 -3
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/RECORD +41 -41
- airbyte_ops_mcp/__init__.py +2 -2
- airbyte_ops_mcp/cli/cloud.py +207 -306
- airbyte_ops_mcp/cloud_admin/api_client.py +51 -26
- airbyte_ops_mcp/cloud_admin/connection_config.py +2 -2
- airbyte_ops_mcp/constants.py +61 -1
- airbyte_ops_mcp/github_actions.py +69 -1
- airbyte_ops_mcp/mcp/_http_headers.py +56 -0
- airbyte_ops_mcp/mcp/_mcp_utils.py +2 -2
- airbyte_ops_mcp/mcp/cloud_connector_versions.py +57 -43
- airbyte_ops_mcp/mcp/github.py +34 -1
- airbyte_ops_mcp/mcp/prerelease.py +3 -3
- airbyte_ops_mcp/mcp/prod_db_queries.py +293 -50
- airbyte_ops_mcp/mcp/{live_tests.py → regression_tests.py} +158 -176
- airbyte_ops_mcp/mcp/server.py +3 -3
- airbyte_ops_mcp/prod_db_access/db_engine.py +7 -11
- airbyte_ops_mcp/prod_db_access/queries.py +79 -0
- airbyte_ops_mcp/prod_db_access/sql.py +86 -0
- airbyte_ops_mcp/{live_tests → regression_tests}/__init__.py +3 -3
- airbyte_ops_mcp/{live_tests → regression_tests}/cdk_secrets.py +1 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/connection_secret_retriever.py +3 -3
- airbyte_ops_mcp/{live_tests → regression_tests}/connector_runner.py +1 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/__init__.py +3 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/regression/__init__.py +1 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/schema_generation.py +3 -1
- airbyte_ops_mcp/{live_tests → regression_tests}/validation/__init__.py +2 -2
- airbyte_ops_mcp/{live_tests → regression_tests}/validation/record_validators.py +4 -2
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.2.0.dist-info → airbyte_internal_ops-0.2.2.dist-info}/entry_points.txt +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/ci_output.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/commons/__init__.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/config.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/connection_fetcher.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/evaluation_modes.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/http_metrics.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/message_cache/duckdb_cache.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/models.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/obfuscation.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/regression/comparators.py +0 -0
- /airbyte_ops_mcp/{live_tests → regression_tests}/validation/catalog_validators.py +0 -0
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
-
"""MCP tools for
|
|
2
|
+
"""MCP tools for connector regression tests.
|
|
3
3
|
|
|
4
|
-
This module provides MCP tools for triggering
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
This module provides MCP tools for triggering regression tests on Airbyte Cloud
|
|
5
|
+
connections via GitHub Actions workflows. Regression tests can run in two modes:
|
|
6
|
+
- Single version mode: Tests a connector version against a connection config
|
|
7
|
+
- Comparison mode: Compares a target version against a control (baseline) version
|
|
8
|
+
|
|
9
|
+
Tests run asynchronously in GitHub Actions and results can be polled via workflow status.
|
|
10
|
+
|
|
11
|
+
Note: The term "regression tests" encompasses all connector validation testing.
|
|
12
|
+
The term "live tests" is reserved for scenarios where actual Cloud connections
|
|
13
|
+
are pinned to pre-release versions for real-world validation.
|
|
7
14
|
"""
|
|
8
15
|
|
|
9
16
|
from __future__ import annotations
|
|
@@ -17,6 +24,10 @@ from typing import Annotated, Any
|
|
|
17
24
|
import requests
|
|
18
25
|
from airbyte.cloud import CloudWorkspace
|
|
19
26
|
from airbyte.cloud.auth import resolve_cloud_client_id, resolve_cloud_client_secret
|
|
27
|
+
from airbyte.exceptions import (
|
|
28
|
+
AirbyteMissingResourceError,
|
|
29
|
+
AirbyteWorkspaceMismatchError,
|
|
30
|
+
)
|
|
20
31
|
from fastmcp import FastMCP
|
|
21
32
|
from pydantic import BaseModel, Field
|
|
22
33
|
|
|
@@ -33,13 +44,49 @@ logger = logging.getLogger(__name__)
|
|
|
33
44
|
# GitHub Workflow Configuration
|
|
34
45
|
# =============================================================================
|
|
35
46
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
REGRESSION_TEST_REPO_OWNER = "airbytehq"
|
|
48
|
+
REGRESSION_TEST_REPO_NAME = "airbyte-ops-mcp"
|
|
49
|
+
REGRESSION_TEST_DEFAULT_BRANCH = "main"
|
|
50
|
+
# Unified regression test workflow (handles both single-version and comparison modes)
|
|
40
51
|
REGRESSION_TEST_WORKFLOW_FILE = "connector-regression-test.yml"
|
|
41
52
|
|
|
42
53
|
|
|
54
|
+
# =============================================================================
|
|
55
|
+
# Workspace Validation Helpers
|
|
56
|
+
# =============================================================================
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def validate_connection_workspace(
|
|
60
|
+
connection_id: str,
|
|
61
|
+
workspace_id: str,
|
|
62
|
+
) -> None:
|
|
63
|
+
"""Validate that a connection belongs to the expected workspace.
|
|
64
|
+
|
|
65
|
+
Uses PyAirbyte's CloudConnection.check_is_valid() method to verify that
|
|
66
|
+
the connection exists and belongs to the specified workspace.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ValueError: If Airbyte Cloud credentials are missing.
|
|
70
|
+
AirbyteWorkspaceMismatchError: If connection belongs to a different workspace.
|
|
71
|
+
AirbyteMissingResourceError: If connection is not found.
|
|
72
|
+
"""
|
|
73
|
+
client_id = resolve_cloud_client_id()
|
|
74
|
+
client_secret = resolve_cloud_client_secret()
|
|
75
|
+
if not client_id or not client_secret:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"Missing Airbyte Cloud credentials. "
|
|
78
|
+
"Set AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET env vars."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
workspace = CloudWorkspace(
|
|
82
|
+
workspace_id=workspace_id,
|
|
83
|
+
client_id=client_id,
|
|
84
|
+
client_secret=client_secret,
|
|
85
|
+
)
|
|
86
|
+
connection = workspace.get_connection(connection_id)
|
|
87
|
+
connection.check_is_valid()
|
|
88
|
+
|
|
89
|
+
|
|
43
90
|
def _get_workflow_run_status(
|
|
44
91
|
owner: str,
|
|
45
92
|
repo: str,
|
|
@@ -90,8 +137,8 @@ class TestRunStatus(str, Enum):
|
|
|
90
137
|
FAILED = "failed"
|
|
91
138
|
|
|
92
139
|
|
|
93
|
-
class
|
|
94
|
-
"""
|
|
140
|
+
class TestOutcome(str, Enum):
|
|
141
|
+
"""Outcome of a test (execution or comparison)."""
|
|
95
142
|
|
|
96
143
|
PENDING = "pending"
|
|
97
144
|
RUNNING = "running"
|
|
@@ -130,10 +177,10 @@ class StreamComparisonResultModel(BaseModel):
|
|
|
130
177
|
message: str = Field(description="Human-readable comparison summary")
|
|
131
178
|
|
|
132
179
|
|
|
133
|
-
class
|
|
134
|
-
"""Results from the
|
|
180
|
+
class RegressionTestExecutionResult(BaseModel):
|
|
181
|
+
"""Results from executing the connector (validations and record counts)."""
|
|
135
182
|
|
|
136
|
-
|
|
183
|
+
outcome: TestOutcome = Field(description="Outcome of the execution")
|
|
137
184
|
catalog_validations: list[ValidationResultModel] = Field(
|
|
138
185
|
default_factory=list,
|
|
139
186
|
description="Results of catalog validation checks",
|
|
@@ -148,14 +195,14 @@ class LivePhaseResult(BaseModel):
|
|
|
148
195
|
)
|
|
149
196
|
error_message: str | None = Field(
|
|
150
197
|
default=None,
|
|
151
|
-
description="Error message if the
|
|
198
|
+
description="Error message if the execution failed",
|
|
152
199
|
)
|
|
153
200
|
|
|
154
201
|
|
|
155
|
-
class
|
|
156
|
-
"""Results from
|
|
202
|
+
class RegressionTestComparisonResult(BaseModel):
|
|
203
|
+
"""Results from comparing target vs control connector versions."""
|
|
157
204
|
|
|
158
|
-
|
|
205
|
+
outcome: TestOutcome = Field(description="Outcome of the comparison")
|
|
159
206
|
baseline_version: str | None = Field(
|
|
160
207
|
default=None,
|
|
161
208
|
description="Version of the baseline (control) connector",
|
|
@@ -166,12 +213,12 @@ class RegressionPhaseResult(BaseModel):
|
|
|
166
213
|
)
|
|
167
214
|
error_message: str | None = Field(
|
|
168
215
|
default=None,
|
|
169
|
-
description="Error message if the
|
|
216
|
+
description="Error message if the comparison failed",
|
|
170
217
|
)
|
|
171
218
|
|
|
172
219
|
|
|
173
|
-
class
|
|
174
|
-
"""Complete result of a
|
|
220
|
+
class RegressionTestResult(BaseModel):
|
|
221
|
+
"""Complete result of a regression test run."""
|
|
175
222
|
|
|
176
223
|
run_id: str = Field(description="Unique identifier for this test run")
|
|
177
224
|
connection_id: str = Field(description="The connection being tested")
|
|
@@ -183,23 +230,23 @@ class LiveConnectionTestResult(BaseModel):
|
|
|
183
230
|
)
|
|
184
231
|
baseline_version: str | None = Field(
|
|
185
232
|
default=None,
|
|
186
|
-
description="Version of the baseline connector (if
|
|
233
|
+
description="Version of the baseline connector (if comparison mode)",
|
|
187
234
|
)
|
|
188
235
|
evaluation_mode: str = Field(
|
|
189
236
|
default="diagnostic",
|
|
190
237
|
description="Evaluation mode used (diagnostic or strict)",
|
|
191
238
|
)
|
|
192
|
-
|
|
239
|
+
compare_versions: bool = Field(
|
|
193
240
|
default=False,
|
|
194
|
-
description="Whether
|
|
241
|
+
description="Whether comparison mode was used (target vs control)",
|
|
195
242
|
)
|
|
196
|
-
|
|
243
|
+
execution_result: RegressionTestExecutionResult | None = Field(
|
|
197
244
|
default=None,
|
|
198
|
-
description="Results from the
|
|
245
|
+
description="Results from executing the connector (validations and record counts)",
|
|
199
246
|
)
|
|
200
|
-
|
|
247
|
+
comparison_result: RegressionTestComparisonResult | None = Field(
|
|
201
248
|
default=None,
|
|
202
|
-
description="Results from
|
|
249
|
+
description="Results from comparing target vs control connector versions",
|
|
203
250
|
)
|
|
204
251
|
artifacts: dict[str, str] = Field(
|
|
205
252
|
default_factory=dict,
|
|
@@ -223,8 +270,8 @@ class LiveConnectionTestResult(BaseModel):
|
|
|
223
270
|
)
|
|
224
271
|
|
|
225
272
|
|
|
226
|
-
class
|
|
227
|
-
"""Response from starting a
|
|
273
|
+
class RunRegressionTestsResponse(BaseModel):
|
|
274
|
+
"""Response from starting a regression test via GitHub Actions workflow."""
|
|
228
275
|
|
|
229
276
|
run_id: str = Field(
|
|
230
277
|
description="Unique identifier for the test run (internal tracking ID)"
|
|
@@ -255,66 +302,57 @@ class RunLiveConnectionTestsResponse(BaseModel):
|
|
|
255
302
|
idempotent=False,
|
|
256
303
|
open_world=True,
|
|
257
304
|
)
|
|
258
|
-
def
|
|
259
|
-
|
|
260
|
-
command: Annotated[
|
|
305
|
+
def run_regression_tests(
|
|
306
|
+
connector_name: Annotated[
|
|
261
307
|
str,
|
|
262
|
-
"
|
|
263
|
-
]
|
|
264
|
-
|
|
308
|
+
"Connector name to build from source (e.g., 'source-pokeapi'). Required.",
|
|
309
|
+
],
|
|
310
|
+
pr: Annotated[
|
|
311
|
+
int,
|
|
312
|
+
"PR number from the airbyte monorepo to checkout and build from (e.g., 70847). Required.",
|
|
313
|
+
],
|
|
314
|
+
connection_id: Annotated[
|
|
265
315
|
str | None,
|
|
266
|
-
"
|
|
267
|
-
"
|
|
316
|
+
"Airbyte Cloud connection ID to fetch config/catalog from. "
|
|
317
|
+
"If not provided, uses GSM integration test secrets.",
|
|
268
318
|
] = None,
|
|
269
|
-
|
|
319
|
+
skip_compare: Annotated[
|
|
270
320
|
bool,
|
|
271
|
-
"If True,
|
|
272
|
-
"If False, run
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
target_image: Annotated[
|
|
321
|
+
"If True, skip comparison and run single-version tests only. "
|
|
322
|
+
"If False (default), run comparison tests (target vs control versions).",
|
|
323
|
+
] = False,
|
|
324
|
+
skip_read_action: Annotated[
|
|
325
|
+
bool,
|
|
326
|
+
"If True, skip the read action (run only spec, check, discover). "
|
|
327
|
+
"If False (default), run all verbs including read.",
|
|
328
|
+
] = False,
|
|
329
|
+
override_test_image: Annotated[
|
|
281
330
|
str | None,
|
|
282
|
-
"
|
|
283
|
-
"
|
|
284
|
-
"Only used when skip_regression_tests=False.",
|
|
331
|
+
"Override test connector image with tag (e.g., 'airbyte/source-github:1.0.0'). "
|
|
332
|
+
"Ignored if skip_compare=False.",
|
|
285
333
|
] = None,
|
|
286
|
-
|
|
334
|
+
override_control_image: Annotated[
|
|
287
335
|
str | None,
|
|
288
|
-
"
|
|
289
|
-
"
|
|
290
|
-
"(auto-detected from connection). Only used when skip_regression_tests=False.",
|
|
336
|
+
"Override control connector image (baseline version) with tag. "
|
|
337
|
+
"Ignored if skip_compare=True.",
|
|
291
338
|
] = None,
|
|
292
|
-
|
|
339
|
+
workspace_id: Annotated[
|
|
293
340
|
str | None,
|
|
294
|
-
"
|
|
295
|
-
"
|
|
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.",
|
|
341
|
+
"Optional Airbyte Cloud workspace ID. If provided with connection_id, validates "
|
|
342
|
+
"that the connection belongs to this workspace before triggering tests.",
|
|
298
343
|
] = None,
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
"PR number from the airbyte monorepo to checkout and build from "
|
|
302
|
-
"(e.g., 70847). Only used when connector_name is provided. "
|
|
303
|
-
"If not specified, builds from the default branch (master).",
|
|
304
|
-
] = None,
|
|
305
|
-
) -> RunLiveConnectionTestsResponse:
|
|
306
|
-
"""Start a live connection test run via GitHub Actions workflow.
|
|
344
|
+
) -> RunRegressionTestsResponse:
|
|
345
|
+
"""Start a regression test run via GitHub Actions workflow.
|
|
307
346
|
|
|
308
|
-
This tool triggers
|
|
309
|
-
|
|
347
|
+
This tool triggers the regression test workflow which builds the connector
|
|
348
|
+
from the specified PR and runs tests against it.
|
|
310
349
|
|
|
311
|
-
-
|
|
312
|
-
|
|
350
|
+
- skip_compare=False (default): Comparison mode - compares the PR version
|
|
351
|
+
against the baseline (control) version.
|
|
352
|
+
- skip_compare=True: Single-version mode - runs tests without comparison.
|
|
313
353
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
For regression tests, provide either target_image or connector_name to specify
|
|
317
|
-
the target version.
|
|
354
|
+
If connection_id is provided, config/catalog are fetched from Airbyte Cloud.
|
|
355
|
+
Otherwise, GSM integration test secrets are used.
|
|
318
356
|
|
|
319
357
|
Returns immediately with a run_id and workflow URL. Check the workflow URL
|
|
320
358
|
to monitor progress and view results.
|
|
@@ -329,133 +367,77 @@ def run_live_connection_tests(
|
|
|
329
367
|
try:
|
|
330
368
|
token = resolve_github_token()
|
|
331
369
|
except ValueError as e:
|
|
332
|
-
return
|
|
370
|
+
return RunRegressionTestsResponse(
|
|
333
371
|
run_id=run_id,
|
|
334
372
|
status=TestRunStatus.FAILED,
|
|
335
373
|
message=str(e),
|
|
336
374
|
workflow_url=None,
|
|
337
375
|
)
|
|
338
376
|
|
|
339
|
-
# Validate workspace membership if workspace_id
|
|
340
|
-
if workspace_id:
|
|
341
|
-
client_id = resolve_cloud_client_id()
|
|
342
|
-
client_secret = resolve_cloud_client_secret()
|
|
343
|
-
if not client_id or not client_secret:
|
|
344
|
-
return RunLiveConnectionTestsResponse(
|
|
345
|
-
run_id=run_id,
|
|
346
|
-
status=TestRunStatus.FAILED,
|
|
347
|
-
message=(
|
|
348
|
-
"Missing Airbyte Cloud credentials. "
|
|
349
|
-
"Set AIRBYTE_CLOUD_CLIENT_ID and AIRBYTE_CLOUD_CLIENT_SECRET env vars."
|
|
350
|
-
),
|
|
351
|
-
workflow_url=None,
|
|
352
|
-
)
|
|
353
|
-
workspace = CloudWorkspace(
|
|
354
|
-
workspace_id=workspace_id,
|
|
355
|
-
client_id=client_id,
|
|
356
|
-
client_secret=client_secret,
|
|
357
|
-
)
|
|
358
|
-
connection = workspace.get_connection(connection_id)
|
|
359
|
-
if connection is None:
|
|
360
|
-
return RunLiveConnectionTestsResponse(
|
|
361
|
-
run_id=run_id,
|
|
362
|
-
status=TestRunStatus.FAILED,
|
|
363
|
-
message=f"Connection {connection_id} not found in workspace {workspace_id}",
|
|
364
|
-
workflow_url=None,
|
|
365
|
-
)
|
|
366
|
-
|
|
367
|
-
if skip_regression_tests:
|
|
368
|
-
# Live test workflow
|
|
369
|
-
workflow_inputs: dict[str, str] = {
|
|
370
|
-
"connection_id": connection_id,
|
|
371
|
-
"command": command,
|
|
372
|
-
}
|
|
373
|
-
if connector_image:
|
|
374
|
-
workflow_inputs["connector_image"] = connector_image
|
|
375
|
-
if connector_name:
|
|
376
|
-
workflow_inputs["connector_name"] = connector_name
|
|
377
|
-
if pr:
|
|
378
|
-
workflow_inputs["pr"] = str(pr)
|
|
379
|
-
|
|
377
|
+
# Validate workspace membership if workspace_id and connection_id are provided
|
|
378
|
+
if workspace_id and connection_id:
|
|
380
379
|
try:
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
)
|
|
389
|
-
except Exception as e:
|
|
390
|
-
logger.exception("Failed to trigger live test workflow")
|
|
391
|
-
return RunLiveConnectionTestsResponse(
|
|
380
|
+
validate_connection_workspace(connection_id, workspace_id)
|
|
381
|
+
except (
|
|
382
|
+
ValueError,
|
|
383
|
+
AirbyteWorkspaceMismatchError,
|
|
384
|
+
AirbyteMissingResourceError,
|
|
385
|
+
) as e:
|
|
386
|
+
return RunRegressionTestsResponse(
|
|
392
387
|
run_id=run_id,
|
|
393
388
|
status=TestRunStatus.FAILED,
|
|
394
|
-
message=
|
|
389
|
+
message=str(e),
|
|
395
390
|
workflow_url=None,
|
|
396
391
|
)
|
|
397
392
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
message=f"Live-test workflow triggered for connection {connection_id}. "
|
|
403
|
-
f"View progress at: {view_url}",
|
|
404
|
-
workflow_url=dispatch_result.workflow_url,
|
|
405
|
-
github_run_id=dispatch_result.run_id,
|
|
406
|
-
github_run_url=dispatch_result.run_url,
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
# Regression test workflow (skip_regression_tests=False)
|
|
410
|
-
# Validate that we have enough info to run regression tests
|
|
411
|
-
if not target_image and not connector_name:
|
|
412
|
-
return RunLiveConnectionTestsResponse(
|
|
413
|
-
run_id=run_id,
|
|
414
|
-
status=TestRunStatus.FAILED,
|
|
415
|
-
message=(
|
|
416
|
-
"For regression tests (skip_regression_tests=False), provide either "
|
|
417
|
-
"target_image or connector_name so the workflow can determine the target image."
|
|
418
|
-
),
|
|
419
|
-
workflow_url=None,
|
|
420
|
-
)
|
|
421
|
-
|
|
422
|
-
workflow_inputs = {
|
|
423
|
-
"connection_id": connection_id,
|
|
424
|
-
"command": command,
|
|
393
|
+
# Build workflow inputs - connector_name and pr are required
|
|
394
|
+
workflow_inputs: dict[str, str] = {
|
|
395
|
+
"connector_name": connector_name,
|
|
396
|
+
"pr": str(pr),
|
|
425
397
|
}
|
|
426
|
-
if target_image:
|
|
427
|
-
workflow_inputs["target_image"] = target_image
|
|
428
|
-
if control_image:
|
|
429
|
-
workflow_inputs["control_image"] = control_image
|
|
430
|
-
if connector_name:
|
|
431
|
-
workflow_inputs["connector_name"] = connector_name
|
|
432
|
-
if pr:
|
|
433
|
-
workflow_inputs["pr"] = str(pr)
|
|
434
398
|
|
|
399
|
+
# Add optional inputs
|
|
400
|
+
if connection_id:
|
|
401
|
+
workflow_inputs["connection_id"] = connection_id
|
|
402
|
+
if skip_compare:
|
|
403
|
+
workflow_inputs["skip_compare"] = "true"
|
|
404
|
+
if skip_read_action:
|
|
405
|
+
workflow_inputs["skip_read_action"] = "true"
|
|
406
|
+
if override_test_image:
|
|
407
|
+
workflow_inputs["override_test_image"] = override_test_image
|
|
408
|
+
if override_control_image:
|
|
409
|
+
workflow_inputs["override_control_image"] = override_control_image
|
|
410
|
+
|
|
411
|
+
mode_description = "single-version" if skip_compare else "comparison"
|
|
435
412
|
try:
|
|
436
413
|
dispatch_result = trigger_workflow_dispatch(
|
|
437
|
-
owner=
|
|
438
|
-
repo=
|
|
414
|
+
owner=REGRESSION_TEST_REPO_OWNER,
|
|
415
|
+
repo=REGRESSION_TEST_REPO_NAME,
|
|
439
416
|
workflow_file=REGRESSION_TEST_WORKFLOW_FILE,
|
|
440
|
-
ref=
|
|
417
|
+
ref=REGRESSION_TEST_DEFAULT_BRANCH,
|
|
441
418
|
inputs=workflow_inputs,
|
|
442
419
|
token=token,
|
|
443
420
|
)
|
|
444
421
|
except Exception as e:
|
|
445
|
-
logger.exception(
|
|
446
|
-
|
|
422
|
+
logger.exception(
|
|
423
|
+
f"Failed to trigger {mode_description} regression test workflow"
|
|
424
|
+
)
|
|
425
|
+
return RunRegressionTestsResponse(
|
|
447
426
|
run_id=run_id,
|
|
448
427
|
status=TestRunStatus.FAILED,
|
|
449
|
-
message=f"Failed to trigger regression
|
|
428
|
+
message=f"Failed to trigger {mode_description} regression test workflow: {e}",
|
|
450
429
|
workflow_url=None,
|
|
451
430
|
)
|
|
452
431
|
|
|
453
432
|
view_url = dispatch_result.run_url or dispatch_result.workflow_url
|
|
454
|
-
|
|
433
|
+
connection_info = f" for connection {connection_id}" if connection_id else ""
|
|
434
|
+
return RunRegressionTestsResponse(
|
|
455
435
|
run_id=run_id,
|
|
456
436
|
status=TestRunStatus.QUEUED,
|
|
457
|
-
message=
|
|
458
|
-
|
|
437
|
+
message=(
|
|
438
|
+
f"{mode_description.capitalize()} regression test workflow triggered "
|
|
439
|
+
f"for {connector_name} (PR #{pr}){connection_info}. View progress at: {view_url}"
|
|
440
|
+
),
|
|
459
441
|
workflow_url=dispatch_result.workflow_url,
|
|
460
442
|
github_run_id=dispatch_result.run_id,
|
|
461
443
|
github_run_url=dispatch_result.run_url,
|
|
@@ -467,8 +449,8 @@ def run_live_connection_tests(
|
|
|
467
449
|
# =============================================================================
|
|
468
450
|
|
|
469
451
|
|
|
470
|
-
def
|
|
471
|
-
"""Register
|
|
452
|
+
def register_regression_tests_tools(app: FastMCP) -> None:
|
|
453
|
+
"""Register regression tests tools with the FastMCP app.
|
|
472
454
|
|
|
473
455
|
Args:
|
|
474
456
|
app: FastMCP application instance
|
airbyte_ops_mcp/mcp/server.py
CHANGED
|
@@ -26,10 +26,10 @@ from airbyte_ops_mcp.mcp.cloud_connector_versions import (
|
|
|
26
26
|
)
|
|
27
27
|
from airbyte_ops_mcp.mcp.github import register_github_tools
|
|
28
28
|
from airbyte_ops_mcp.mcp.github_repo_ops import register_github_repo_ops_tools
|
|
29
|
-
from airbyte_ops_mcp.mcp.live_tests import register_live_tests_tools
|
|
30
29
|
from airbyte_ops_mcp.mcp.prerelease import register_prerelease_tools
|
|
31
30
|
from airbyte_ops_mcp.mcp.prod_db_queries import register_prod_db_query_tools
|
|
32
31
|
from airbyte_ops_mcp.mcp.prompts import register_prompts
|
|
32
|
+
from airbyte_ops_mcp.mcp.regression_tests import register_regression_tests_tools
|
|
33
33
|
from airbyte_ops_mcp.mcp.server_info import register_server_info_resources
|
|
34
34
|
|
|
35
35
|
# Default HTTP server configuration
|
|
@@ -47,7 +47,7 @@ def register_server_assets(app: FastMCP) -> None:
|
|
|
47
47
|
- REPO: GitHub repository operations
|
|
48
48
|
- CLOUD: Cloud connector version management
|
|
49
49
|
- PROMPTS: Prompt templates for common workflows
|
|
50
|
-
-
|
|
50
|
+
- REGRESSION_TESTS: Connector regression tests (single-version and comparison)
|
|
51
51
|
- REGISTRY: Connector registry operations (future)
|
|
52
52
|
- METADATA: Connector metadata operations (future)
|
|
53
53
|
- QA: Connector quality assurance (future)
|
|
@@ -63,7 +63,7 @@ def register_server_assets(app: FastMCP) -> None:
|
|
|
63
63
|
register_cloud_connector_version_tools(app)
|
|
64
64
|
register_prod_db_query_tools(app)
|
|
65
65
|
register_prompts(app)
|
|
66
|
-
|
|
66
|
+
register_regression_tests_tools(app)
|
|
67
67
|
|
|
68
68
|
|
|
69
69
|
register_server_assets(app)
|
|
@@ -166,24 +166,20 @@ def _get_connector() -> Connector:
|
|
|
166
166
|
|
|
167
167
|
|
|
168
168
|
def _get_secret_value(
|
|
169
|
-
gsm_client: secretmanager.SecretManagerServiceClient,
|
|
169
|
+
gsm_client: secretmanager.SecretManagerServiceClient,
|
|
170
|
+
secret_id: str,
|
|
170
171
|
) -> str:
|
|
171
|
-
"""Get the value of the
|
|
172
|
+
"""Get the value of the latest version of a secret.
|
|
172
173
|
|
|
173
174
|
Args:
|
|
174
175
|
gsm_client: GCP Secret Manager client
|
|
175
|
-
secret_id: The
|
|
176
|
+
secret_id: The full resource ID of the secret
|
|
177
|
+
(e.g., "projects/123/secrets/my-secret")
|
|
176
178
|
|
|
177
179
|
Returns:
|
|
178
|
-
The value of the
|
|
180
|
+
The value of the latest version of the secret
|
|
179
181
|
"""
|
|
180
|
-
response = gsm_client.
|
|
181
|
-
request={"parent": secret_id, "filter": "state:ENABLED"}
|
|
182
|
-
)
|
|
183
|
-
if len(response.versions) == 0:
|
|
184
|
-
raise ValueError(f"No enabled version of secret {secret_id} found")
|
|
185
|
-
enabled_version = response.versions[0]
|
|
186
|
-
response = gsm_client.access_secret_version(name=enabled_version.name)
|
|
182
|
+
response = gsm_client.access_secret_version(name=f"{secret_id}/versions/latest")
|
|
187
183
|
return response.payload.data.decode("UTF-8")
|
|
188
184
|
|
|
189
185
|
|
|
@@ -22,6 +22,8 @@ from airbyte_ops_mcp.prod_db_access.sql import (
|
|
|
22
22
|
SELECT_ACTORS_PINNED_TO_VERSION,
|
|
23
23
|
SELECT_CONNECTIONS_BY_CONNECTOR,
|
|
24
24
|
SELECT_CONNECTIONS_BY_CONNECTOR_AND_ORG,
|
|
25
|
+
SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR,
|
|
26
|
+
SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR_AND_ORG,
|
|
25
27
|
SELECT_CONNECTOR_VERSIONS,
|
|
26
28
|
SELECT_DATAPLANES_LIST,
|
|
27
29
|
SELECT_FAILED_SYNC_ATTEMPTS_FOR_CONNECTOR,
|
|
@@ -30,6 +32,7 @@ from airbyte_ops_mcp.prod_db_access.sql import (
|
|
|
30
32
|
SELECT_SUCCESSFUL_SYNCS_FOR_VERSION,
|
|
31
33
|
SELECT_SYNC_RESULTS_FOR_VERSION,
|
|
32
34
|
SELECT_WORKSPACE_INFO,
|
|
35
|
+
SELECT_WORKSPACES_BY_EMAIL_DOMAIN,
|
|
33
36
|
)
|
|
34
37
|
|
|
35
38
|
logger = logging.getLogger(__name__)
|
|
@@ -113,6 +116,48 @@ def query_connections_by_connector(
|
|
|
113
116
|
)
|
|
114
117
|
|
|
115
118
|
|
|
119
|
+
def query_connections_by_destination_connector(
|
|
120
|
+
connector_definition_id: str,
|
|
121
|
+
organization_id: str | None = None,
|
|
122
|
+
limit: int = 1000,
|
|
123
|
+
*,
|
|
124
|
+
gsm_client: secretmanager.SecretManagerServiceClient | None = None,
|
|
125
|
+
) -> list[dict[str, Any]]:
|
|
126
|
+
"""Query connections by destination connector type, optionally filtered by organization.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
connector_definition_id: Destination connector definition UUID to filter by
|
|
130
|
+
organization_id: Optional organization UUID to search within
|
|
131
|
+
limit: Maximum number of results (default: 1000)
|
|
132
|
+
gsm_client: GCP Secret Manager client. If None, a new client will be instantiated.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
List of connection records with workspace and dataplane info
|
|
136
|
+
"""
|
|
137
|
+
# Use separate queries to avoid pg8000 NULL parameter type issues
|
|
138
|
+
if organization_id is None:
|
|
139
|
+
return _run_sql_query(
|
|
140
|
+
SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR,
|
|
141
|
+
parameters={
|
|
142
|
+
"connector_definition_id": connector_definition_id,
|
|
143
|
+
"limit": limit,
|
|
144
|
+
},
|
|
145
|
+
query_name="SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR",
|
|
146
|
+
gsm_client=gsm_client,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return _run_sql_query(
|
|
150
|
+
SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR_AND_ORG,
|
|
151
|
+
parameters={
|
|
152
|
+
"connector_definition_id": connector_definition_id,
|
|
153
|
+
"organization_id": organization_id,
|
|
154
|
+
"limit": limit,
|
|
155
|
+
},
|
|
156
|
+
query_name="SELECT_CONNECTIONS_BY_DESTINATION_CONNECTOR_AND_ORG",
|
|
157
|
+
gsm_client=gsm_client,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
116
161
|
def query_connector_versions(
|
|
117
162
|
connector_definition_id: str,
|
|
118
163
|
*,
|
|
@@ -337,3 +382,37 @@ def query_org_workspaces(
|
|
|
337
382
|
query_name="SELECT_ORG_WORKSPACES",
|
|
338
383
|
gsm_client=gsm_client,
|
|
339
384
|
)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def query_workspaces_by_email_domain(
|
|
388
|
+
email_domain: str,
|
|
389
|
+
limit: int = 100,
|
|
390
|
+
*,
|
|
391
|
+
gsm_client: secretmanager.SecretManagerServiceClient | None = None,
|
|
392
|
+
) -> list[dict[str, Any]]:
|
|
393
|
+
"""Query workspaces by email domain.
|
|
394
|
+
|
|
395
|
+
This is useful for identifying workspaces based on user email domains.
|
|
396
|
+
For example, searching for "motherduck.com" will find workspaces where users have
|
|
397
|
+
@motherduck.com email addresses, which may belong to partner accounts.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
email_domain: Email domain to search for (e.g., "motherduck.com", "fivetran.com").
|
|
401
|
+
Do not include the "@" symbol.
|
|
402
|
+
limit: Maximum number of results (default: 100)
|
|
403
|
+
gsm_client: GCP Secret Manager client. If None, a new client will be instantiated.
|
|
404
|
+
|
|
405
|
+
Returns:
|
|
406
|
+
List of workspace records with organization_id, workspace_id, workspace_name,
|
|
407
|
+
slug, email, dataplane_group_id, dataplane_name, and created_at.
|
|
408
|
+
Results are ordered by organization_id and workspace_name.
|
|
409
|
+
"""
|
|
410
|
+
# Strip leading @ if provided
|
|
411
|
+
clean_domain = email_domain.lstrip("@")
|
|
412
|
+
|
|
413
|
+
return _run_sql_query(
|
|
414
|
+
SELECT_WORKSPACES_BY_EMAIL_DOMAIN,
|
|
415
|
+
parameters={"email_domain": clean_domain, "limit": limit},
|
|
416
|
+
query_name="SELECT_WORKSPACES_BY_EMAIL_DOMAIN",
|
|
417
|
+
gsm_client=gsm_client,
|
|
418
|
+
)
|