airbyte-internal-ops 0.1.3__py3-none-any.whl → 0.1.5__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.
Files changed (35) hide show
  1. {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/METADATA +8 -5
  2. {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/RECORD +35 -15
  3. airbyte_ops_mcp/_legacy/airbyte_ci/connector_pipelines/airbyte_ci/connectors/test/steps/common.py +1 -1
  4. airbyte_ops_mcp/airbyte_repo/list_connectors.py +44 -4
  5. airbyte_ops_mcp/airbyte_repo/utils.py +5 -3
  6. airbyte_ops_mcp/cli/cloud.py +317 -47
  7. airbyte_ops_mcp/cli/repo.py +15 -0
  8. airbyte_ops_mcp/cloud_admin/connection_config.py +131 -0
  9. airbyte_ops_mcp/live_tests/__init__.py +16 -0
  10. airbyte_ops_mcp/live_tests/_connection_retriever/__init__.py +35 -0
  11. airbyte_ops_mcp/live_tests/_connection_retriever/audit_logging.py +88 -0
  12. airbyte_ops_mcp/live_tests/_connection_retriever/consts.py +33 -0
  13. airbyte_ops_mcp/live_tests/_connection_retriever/db_access.py +82 -0
  14. airbyte_ops_mcp/live_tests/_connection_retriever/retrieval.py +391 -0
  15. airbyte_ops_mcp/live_tests/_connection_retriever/secrets_resolution.py +130 -0
  16. airbyte_ops_mcp/live_tests/config.py +190 -0
  17. airbyte_ops_mcp/live_tests/connection_fetcher.py +159 -2
  18. airbyte_ops_mcp/live_tests/connection_secret_retriever.py +173 -0
  19. airbyte_ops_mcp/live_tests/evaluation_modes.py +45 -0
  20. airbyte_ops_mcp/live_tests/http_metrics.py +81 -0
  21. airbyte_ops_mcp/live_tests/message_cache/__init__.py +15 -0
  22. airbyte_ops_mcp/live_tests/message_cache/duckdb_cache.py +415 -0
  23. airbyte_ops_mcp/live_tests/obfuscation.py +126 -0
  24. airbyte_ops_mcp/live_tests/regression/__init__.py +29 -0
  25. airbyte_ops_mcp/live_tests/regression/comparators.py +466 -0
  26. airbyte_ops_mcp/live_tests/schema_generation.py +154 -0
  27. airbyte_ops_mcp/live_tests/validation/__init__.py +43 -0
  28. airbyte_ops_mcp/live_tests/validation/catalog_validators.py +389 -0
  29. airbyte_ops_mcp/live_tests/validation/record_validators.py +227 -0
  30. airbyte_ops_mcp/mcp/_mcp_utils.py +3 -0
  31. airbyte_ops_mcp/mcp/live_tests.py +515 -0
  32. airbyte_ops_mcp/mcp/server.py +3 -0
  33. airbyte_ops_mcp/mcp/server_info.py +2 -2
  34. {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/WHEEL +0 -0
  35. {airbyte_internal_ops-0.1.3.dist-info → airbyte_internal_ops-0.1.5.dist-info}/entry_points.txt +0 -0
@@ -7,6 +7,7 @@ Commands:
7
7
  airbyte-ops cloud connector clear-version-override - Clear connector version override
8
8
  airbyte-ops cloud connector live-test - Run live validation tests on a connector
9
9
  airbyte-ops cloud connector regression-test - Run regression tests comparing connector versions
10
+ airbyte-ops cloud connector fetch-connection-config - Fetch connection config to local file
10
11
  """
11
12
 
12
13
  from __future__ import annotations
@@ -15,11 +16,20 @@ import json
15
16
  from pathlib import Path
16
17
  from typing import Annotated, Literal
17
18
 
19
+ from airbyte_cdk.models.connector_metadata import MetadataFile
20
+ from airbyte_cdk.utils.connector_paths import find_connector_root_from_name
21
+ from airbyte_cdk.utils.docker import build_connector_image, verify_docker_installation
18
22
  from airbyte_protocol.models import ConfiguredAirbyteCatalog
19
23
  from cyclopts import App, Parameter
20
24
 
21
25
  from airbyte_ops_mcp.cli._base import app
22
- from airbyte_ops_mcp.cli._shared import print_error, print_json, print_success
26
+ from airbyte_ops_mcp.cli._shared import (
27
+ exit_with_error,
28
+ print_error,
29
+ print_json,
30
+ print_success,
31
+ )
32
+ from airbyte_ops_mcp.cloud_admin.connection_config import fetch_connection_config
23
33
  from airbyte_ops_mcp.live_tests.ci_output import (
24
34
  generate_regression_report,
25
35
  get_report_summary,
@@ -52,6 +62,9 @@ from airbyte_ops_mcp.mcp.cloud_connector_versions import (
52
62
  set_cloud_connector_version_override,
53
63
  )
54
64
 
65
+ # Path to connectors directory within the airbyte repo
66
+ CONNECTORS_SUBDIR = Path("airbyte-integrations") / "connectors"
67
+
55
68
  # Create the cloud sub-app
56
69
  cloud_app = App(name="cloud", help="Airbyte Cloud operations.")
57
70
  app.command(cloud_app)
@@ -250,14 +263,83 @@ def _run_connector_command(
250
263
  }
251
264
 
252
265
 
266
+ def _build_connector_image_from_source(
267
+ connector_name: str,
268
+ repo_root: Path | None = None,
269
+ tag: str = "dev",
270
+ ) -> str | None:
271
+ """Build a connector image from source code.
272
+
273
+ Args:
274
+ connector_name: Name of the connector (e.g., 'source-pokeapi').
275
+ repo_root: Optional path to the airbyte repo root. If not provided,
276
+ will attempt to auto-detect from current directory.
277
+ tag: Tag to apply to the built image (default: 'dev').
278
+
279
+ Returns:
280
+ The full image name with tag if successful, None if build fails.
281
+ """
282
+ if not verify_docker_installation():
283
+ print_error("Docker is not installed or not running")
284
+ return None
285
+
286
+ try:
287
+ connector_directory = find_connector_root_from_name(connector_name)
288
+ except FileNotFoundError:
289
+ if repo_root:
290
+ connector_directory = repo_root / CONNECTORS_SUBDIR / connector_name
291
+ if not connector_directory.exists():
292
+ print_error(f"Connector directory not found: {connector_directory}")
293
+ return None
294
+ else:
295
+ print_error(
296
+ f"Could not find connector '{connector_name}'. "
297
+ "Try providing --repo-root to specify the airbyte repo location."
298
+ )
299
+ return None
300
+
301
+ metadata_file_path = connector_directory / "metadata.yaml"
302
+ if not metadata_file_path.exists():
303
+ print_error(f"metadata.yaml not found at {metadata_file_path}")
304
+ return None
305
+
306
+ metadata = MetadataFile.from_file(metadata_file_path)
307
+ print_success(f"Building image for connector: {connector_name}")
308
+
309
+ built_image = build_connector_image(
310
+ connector_name=connector_name,
311
+ connector_directory=connector_directory,
312
+ metadata=metadata,
313
+ tag=tag,
314
+ no_verify=False,
315
+ )
316
+ print_success(f"Successfully built image: {built_image}")
317
+ return built_image
318
+
319
+
253
320
  @connector_app.command(name="live-test")
254
321
  def live_test(
255
322
  connector_image: Annotated[
256
- str,
323
+ str | None,
257
324
  Parameter(
258
- help="Full connector image name with tag (e.g., airbyte/source-github:1.0.0)."
325
+ help="Full connector image name with tag (e.g., airbyte/source-github:1.0.0). "
326
+ "Optional if connector_name or connection_id is provided."
259
327
  ),
260
- ],
328
+ ] = None,
329
+ connector_name: Annotated[
330
+ str | None,
331
+ Parameter(
332
+ help="Connector name to build from source (e.g., 'source-pokeapi'). "
333
+ "If provided, builds the image locally with tag 'dev'."
334
+ ),
335
+ ] = None,
336
+ repo_root: Annotated[
337
+ str | None,
338
+ Parameter(
339
+ help="Path to the airbyte repo root. Required if connector_name is provided "
340
+ "and the repo cannot be auto-detected."
341
+ ),
342
+ ] = None,
261
343
  command: Annotated[
262
344
  Literal["spec", "check", "discover", "read"],
263
345
  Parameter(help="The Airbyte command to run."),
@@ -266,7 +348,8 @@ def live_test(
266
348
  str | None,
267
349
  Parameter(
268
350
  help="Airbyte Cloud connection ID to fetch config/catalog from. "
269
- "Mutually exclusive with config-path/catalog-path."
351
+ "Mutually exclusive with config-path/catalog-path. "
352
+ "If provided, connector_image can be auto-detected."
270
353
  ),
271
354
  ] = None,
272
355
  config_path: Annotated[
@@ -292,6 +375,11 @@ def live_test(
292
375
  and validates the output. Results are written to the output directory and
293
376
  to GitHub Actions outputs if running in CI.
294
377
 
378
+ You can provide the connector image in three ways:
379
+ 1. --connector-image: Use a pre-built image from Docker registry
380
+ 2. --connector-name: Build the image locally from source code
381
+ 3. --connection-id: Auto-detect from an Airbyte Cloud connection
382
+
295
383
  You can provide config/catalog either via file paths OR via a connection_id
296
384
  that fetches them from Airbyte Cloud.
297
385
  """
@@ -300,26 +388,41 @@ def live_test(
300
388
 
301
389
  cmd = Command(command)
302
390
 
303
- if not ensure_image_available(connector_image):
304
- print_error(f"Failed to pull connector image: {connector_image}")
305
- write_github_output("success", False)
306
- write_github_output("error", f"Failed to pull image: {connector_image}")
307
- return
308
-
309
391
  config_file: Path | None = None
310
392
  catalog_file: Path | None = None
311
393
  state_file = Path(state_path) if state_path else None
394
+ resolved_connector_image: str | None = connector_image
395
+
396
+ # If connector_name is provided, build the image from source
397
+ if connector_name:
398
+ if connector_image:
399
+ write_github_output("success", False)
400
+ write_github_output(
401
+ "error", "Cannot specify both connector_image and connector_name"
402
+ )
403
+ exit_with_error("Cannot specify both connector_image and connector_name")
404
+
405
+ repo_root_path = Path(repo_root) if repo_root else None
406
+ built_image = _build_connector_image_from_source(
407
+ connector_name=connector_name,
408
+ repo_root=repo_root_path,
409
+ tag="dev",
410
+ )
411
+ if not built_image:
412
+ write_github_output("success", False)
413
+ write_github_output("error", f"Failed to build image for {connector_name}")
414
+ exit_with_error(f"Failed to build image for {connector_name}")
415
+ resolved_connector_image = built_image
312
416
 
313
417
  if connection_id:
314
418
  if config_path or catalog_path:
315
- print_error(
316
- "Cannot specify both connection_id and config_path/catalog_path"
317
- )
318
419
  write_github_output("success", False)
319
420
  write_github_output(
320
421
  "error", "Cannot specify both connection_id and file paths"
321
422
  )
322
- return
423
+ exit_with_error(
424
+ "Cannot specify both connection_id and config_path/catalog_path"
425
+ )
323
426
 
324
427
  print_success(f"Fetching config/catalog from connection: {connection_id}")
325
428
  connection_data = fetch_connection_data(connection_id)
@@ -330,12 +433,35 @@ def live_test(
330
433
  f"Fetched config for source: {connection_data.source_name} "
331
434
  f"with {len(connection_data.stream_names)} streams"
332
435
  )
436
+
437
+ if not resolved_connector_image and connection_data.connector_image:
438
+ resolved_connector_image = connection_data.connector_image
439
+ print_success(f"Auto-detected connector image: {resolved_connector_image}")
333
440
  else:
334
441
  config_file = Path(config_path) if config_path else None
335
442
  catalog_file = Path(catalog_path) if catalog_path else None
336
443
 
444
+ if not resolved_connector_image:
445
+ write_github_output("success", False)
446
+ write_github_output("error", "Missing connector image")
447
+ exit_with_error(
448
+ "You must provide one of the following: a connector_image, a connector_name, "
449
+ "or a connection_id for a connection that has an associated connector image. "
450
+ "If using connection_id, ensure the connection has a connector image configured."
451
+ )
452
+
453
+ # If connector_name was provided, we just built the image locally and it is already
454
+ # available in Docker, so we skip the image availability check/pull. Only try to pull
455
+ # if we didn't just build it (i.e., using a pre-built image from registry).
456
+ if not connector_name and not ensure_image_available(resolved_connector_image):
457
+ write_github_output("success", False)
458
+ write_github_output(
459
+ "error", f"Failed to pull image: {resolved_connector_image}"
460
+ )
461
+ exit_with_error(f"Failed to pull connector image: {resolved_connector_image}")
462
+
337
463
  result = _run_connector_command(
338
- connector_image=connector_image,
464
+ connector_image=resolved_connector_image,
339
465
  command=cmd,
340
466
  output_dir=output_path,
341
467
  target_or_control=TargetOrControl.TARGET,
@@ -349,14 +475,14 @@ def live_test(
349
475
  write_github_outputs(
350
476
  {
351
477
  "success": result["success"],
352
- "connector": connector_image,
478
+ "connector": resolved_connector_image,
353
479
  "command": command,
354
480
  "exit_code": result["exit_code"],
355
481
  }
356
482
  )
357
483
 
358
484
  write_test_summary(
359
- connector_image=connector_image,
485
+ connector_image=resolved_connector_image,
360
486
  test_type="live-test",
361
487
  success=result["success"],
362
488
  results={
@@ -367,9 +493,9 @@ def live_test(
367
493
  )
368
494
 
369
495
  if result["success"]:
370
- print_success(f"Live test passed for {connector_image}")
496
+ print_success(f"Live test passed for {resolved_connector_image}")
371
497
  else:
372
- print_error(f"Live test failed for {connector_image}")
498
+ exit_with_error(f"Live test failed for {resolved_connector_image}")
373
499
 
374
500
 
375
501
  def _run_with_optional_http_metrics(
@@ -451,17 +577,33 @@ def _run_with_optional_http_metrics(
451
577
  @connector_app.command(name="regression-test")
452
578
  def regression_test(
453
579
  target_image: Annotated[
454
- str,
580
+ str | None,
455
581
  Parameter(
456
- help="Target connector image (new version) with tag (e.g., airbyte/source-github:2.0.0)."
582
+ help="Target connector image (new version) with tag (e.g., airbyte/source-github:2.0.0). "
583
+ "Optional if connector_name is provided."
457
584
  ),
458
- ],
585
+ ] = None,
459
586
  control_image: Annotated[
460
- str,
587
+ str | None,
461
588
  Parameter(
462
- help="Control connector image (baseline version) with tag (e.g., airbyte/source-github:1.0.0)."
589
+ help="Control connector image (baseline version) with tag (e.g., airbyte/source-github:1.0.0). "
590
+ "Optional if connection_id is provided (auto-detected from connection)."
463
591
  ),
464
- ],
592
+ ] = None,
593
+ connector_name: Annotated[
594
+ str | None,
595
+ Parameter(
596
+ help="Connector name to build target image from source (e.g., 'source-pokeapi'). "
597
+ "If provided, builds the target image locally with tag 'dev'."
598
+ ),
599
+ ] = None,
600
+ repo_root: Annotated[
601
+ str | None,
602
+ Parameter(
603
+ help="Path to the airbyte repo root. Required if connector_name is provided "
604
+ "and the repo cannot be auto-detected."
605
+ ),
606
+ ] = None,
465
607
  command: Annotated[
466
608
  Literal["spec", "check", "discover", "read"],
467
609
  Parameter(help="The Airbyte command to run."),
@@ -470,7 +612,8 @@ def regression_test(
470
612
  str | None,
471
613
  Parameter(
472
614
  help="Airbyte Cloud connection ID to fetch config/catalog from. "
473
- "Mutually exclusive with config-path/catalog-path."
615
+ "Mutually exclusive with config-path/catalog-path. "
616
+ "If provided, control_image can be auto-detected."
474
617
  ),
475
618
  ] = None,
476
619
  config_path: Annotated[
@@ -506,6 +649,14 @@ def regression_test(
506
649
  Results are written to the output directory and to GitHub Actions outputs
507
650
  if running in CI.
508
651
 
652
+ You can provide the target image in two ways:
653
+ 1. --target-image: Use a pre-built image from Docker registry
654
+ 2. --connector-name: Build the target image locally from source code
655
+
656
+ You can provide the control image in two ways:
657
+ 1. --control-image: Use a pre-built image from Docker registry
658
+ 2. --connection-id: Auto-detect from an Airbyte Cloud connection
659
+
509
660
  You can provide config/catalog either via file paths OR via a connection_id
510
661
  that fetches them from Airbyte Cloud.
511
662
  """
@@ -514,27 +665,42 @@ def regression_test(
514
665
 
515
666
  cmd = Command(command)
516
667
 
517
- for image in [target_image, control_image]:
518
- if not ensure_image_available(image):
519
- print_error(f"Failed to pull connector image: {image}")
520
- write_github_output("success", False)
521
- write_github_output("error", f"Failed to pull image: {image}")
522
- return
523
-
524
668
  config_file: Path | None = None
525
669
  catalog_file: Path | None = None
526
670
  state_file = Path(state_path) if state_path else None
671
+ resolved_target_image: str | None = target_image
672
+ resolved_control_image: str | None = control_image
673
+
674
+ # If connector_name is provided, build the target image from source
675
+ if connector_name:
676
+ if target_image:
677
+ write_github_output("success", False)
678
+ write_github_output(
679
+ "error", "Cannot specify both target_image and connector_name"
680
+ )
681
+ exit_with_error("Cannot specify both target_image and connector_name")
682
+
683
+ repo_root_path = Path(repo_root) if repo_root else None
684
+ built_image = _build_connector_image_from_source(
685
+ connector_name=connector_name,
686
+ repo_root=repo_root_path,
687
+ tag="dev",
688
+ )
689
+ if not built_image:
690
+ write_github_output("success", False)
691
+ write_github_output("error", f"Failed to build image for {connector_name}")
692
+ exit_with_error(f"Failed to build image for {connector_name}")
693
+ resolved_target_image = built_image
527
694
 
528
695
  if connection_id:
529
696
  if config_path or catalog_path:
530
- print_error(
531
- "Cannot specify both connection_id and config_path/catalog_path"
532
- )
533
697
  write_github_output("success", False)
534
698
  write_github_output(
535
699
  "error", "Cannot specify both connection_id and file paths"
536
700
  )
537
- return
701
+ exit_with_error(
702
+ "Cannot specify both connection_id and config_path/catalog_path"
703
+ )
538
704
 
539
705
  print_success(f"Fetching config/catalog from connection: {connection_id}")
540
706
  connection_data = fetch_connection_data(connection_id)
@@ -545,15 +711,53 @@ def regression_test(
545
711
  f"Fetched config for source: {connection_data.source_name} "
546
712
  f"with {len(connection_data.stream_names)} streams"
547
713
  )
714
+
715
+ # Auto-detect control_image from connection if not provided
716
+ if not resolved_control_image and connection_data.connector_image:
717
+ resolved_control_image = connection_data.connector_image
718
+ print_success(f"Auto-detected control image: {resolved_control_image}")
548
719
  else:
549
720
  config_file = Path(config_path) if config_path else None
550
721
  catalog_file = Path(catalog_path) if catalog_path else None
551
722
 
723
+ # Validate that we have both images
724
+ if not resolved_target_image:
725
+ write_github_output("success", False)
726
+ write_github_output("error", "No target image specified")
727
+ exit_with_error(
728
+ "You must provide one of the following: a target_image or a connector_name "
729
+ "to build the target image from source."
730
+ )
731
+
732
+ if not resolved_control_image:
733
+ write_github_output("success", False)
734
+ write_github_output("error", "No control image specified")
735
+ exit_with_error(
736
+ "You must provide one of the following: a control_image or a connection_id "
737
+ "for a connection that has an associated connector image."
738
+ )
739
+
740
+ # Pull images if they weren't just built locally
741
+ # If connector_name was provided, we just built the target image locally
742
+ if not connector_name and not ensure_image_available(resolved_target_image):
743
+ write_github_output("success", False)
744
+ write_github_output("error", f"Failed to pull image: {resolved_target_image}")
745
+ exit_with_error(
746
+ f"Failed to pull target connector image: {resolved_target_image}"
747
+ )
748
+
749
+ if not ensure_image_available(resolved_control_image):
750
+ write_github_output("success", False)
751
+ write_github_output("error", f"Failed to pull image: {resolved_control_image}")
752
+ exit_with_error(
753
+ f"Failed to pull control connector image: {resolved_control_image}"
754
+ )
755
+
552
756
  target_output = output_path / "target"
553
757
  control_output = output_path / "control"
554
758
 
555
759
  target_result = _run_with_optional_http_metrics(
556
- connector_image=target_image,
760
+ connector_image=resolved_target_image,
557
761
  command=cmd,
558
762
  output_dir=target_output,
559
763
  target_or_control=TargetOrControl.TARGET,
@@ -564,7 +768,7 @@ def regression_test(
564
768
  )
565
769
 
566
770
  control_result = _run_with_optional_http_metrics(
567
- connector_image=control_image,
771
+ connector_image=resolved_control_image,
568
772
  command=cmd,
569
773
  output_dir=control_output,
570
774
  target_or_control=TargetOrControl.CONTROL,
@@ -589,8 +793,8 @@ def regression_test(
589
793
  write_github_outputs(
590
794
  {
591
795
  "success": both_succeeded and not regression_detected,
592
- "target_image": target_image,
593
- "control_image": control_image,
796
+ "target_image": resolved_target_image,
797
+ "control_image": resolved_control_image,
594
798
  "command": command,
595
799
  "target_exit_code": target_result["exit_code"],
596
800
  "control_exit_code": control_result["exit_code"],
@@ -601,8 +805,8 @@ def regression_test(
601
805
  write_json_output("regression_report", combined_result)
602
806
 
603
807
  report_path = generate_regression_report(
604
- target_image=target_image,
605
- control_image=control_image,
808
+ target_image=resolved_target_image,
809
+ control_image=resolved_control_image,
606
810
  command=command,
607
811
  target_result=target_result,
608
812
  control_result=control_result,
@@ -614,8 +818,74 @@ def regression_test(
614
818
  write_github_summary(summary)
615
819
 
616
820
  if regression_detected:
617
- print_error(f"Regression detected between {target_image} and {control_image}")
821
+ exit_with_error(
822
+ f"Regression detected between {resolved_target_image} and {resolved_control_image}"
823
+ )
618
824
  elif both_succeeded:
619
- print_success(f"Regression test passed for {target_image} vs {control_image}")
825
+ print_success(
826
+ f"Regression test passed for {resolved_target_image} vs {resolved_control_image}"
827
+ )
828
+ else:
829
+ exit_with_error(
830
+ f"Both versions failed for {resolved_target_image} vs {resolved_control_image}"
831
+ )
832
+
833
+
834
+ @connector_app.command(name="fetch-connection-config")
835
+ def fetch_connection_config_cmd(
836
+ connection_id: Annotated[
837
+ str,
838
+ Parameter(help="The UUID of the Airbyte Cloud connection."),
839
+ ],
840
+ output_path: Annotated[
841
+ str | None,
842
+ Parameter(
843
+ help="Path to output file or directory. "
844
+ "If directory, writes connection-<id>-config.json. "
845
+ "Default: ./connection-<id>-config.json"
846
+ ),
847
+ ] = None,
848
+ with_secrets: Annotated[
849
+ bool,
850
+ Parameter(
851
+ name="--with-secrets",
852
+ negative="--no-secrets",
853
+ help="If set, fetches unmasked secrets from the internal database. "
854
+ "Requires GCP_PROD_DB_ACCESS_CREDENTIALS env var or `gcloud auth application-default login`. "
855
+ "Must be used with --oc-issue-url.",
856
+ ),
857
+ ] = False,
858
+ oc_issue_url: Annotated[
859
+ str | None,
860
+ Parameter(
861
+ help="OC issue URL for audit logging. Required when using --with-secrets."
862
+ ),
863
+ ] = None,
864
+ ) -> None:
865
+ """Fetch connection configuration from Airbyte Cloud to a local file.
866
+
867
+ This command retrieves the source configuration for a given connection ID
868
+ and writes it to a local JSON file.
869
+
870
+ Requires authentication via AIRBYTE_CLOUD_CLIENT_ID and
871
+ AIRBYTE_CLOUD_CLIENT_SECRET environment variables.
872
+
873
+ When --with-secrets is specified, the command fetches unmasked secrets from
874
+ the internal database using the connection-retriever. This additionally requires:
875
+ - An OC issue URL for audit logging (--oc-issue-url)
876
+ - GCP credentials via `GCP_PROD_DB_ACCESS_CREDENTIALS` env var or `gcloud auth application-default login`
877
+ - If `CI=true`: expects `cloud-sql-proxy` running on localhost, or
878
+ direct network access to the Cloud SQL instance.
879
+ """
880
+ path = Path(output_path) if output_path else None
881
+ result = fetch_connection_config(
882
+ connection_id=connection_id,
883
+ output_path=path,
884
+ with_secrets=with_secrets,
885
+ oc_issue_url=oc_issue_url,
886
+ )
887
+ if result.success:
888
+ print_success(result.message)
620
889
  else:
621
- print_error(f"Both versions failed for {target_image} vs {control_image}")
890
+ print_error(result.message)
891
+ print_json(result.model_dump())
@@ -27,6 +27,7 @@ from airbyte_ops_mcp.airbyte_repo.list_connectors import (
27
27
  CONNECTOR_PATH_PREFIX,
28
28
  METADATA_FILE_NAME,
29
29
  _detect_connector_language,
30
+ get_connectors_with_local_cdk,
30
31
  )
31
32
  from airbyte_ops_mcp.cli._base import app
32
33
  from airbyte_ops_mcp.cli._shared import exit_with_error, print_json
@@ -160,6 +161,15 @@ def list_connectors(
160
161
  bool,
161
162
  Parameter(help="Include only modified connectors (requires PR context)."),
162
163
  ] = False,
164
+ local_cdk: Annotated[
165
+ bool,
166
+ Parameter(
167
+ help=(
168
+ "Include connectors using local CDK reference. "
169
+ "When combined with --modified-only, adds local-CDK connectors to the modified set."
170
+ )
171
+ ),
172
+ ] = False,
163
173
  language: Annotated[
164
174
  list[str] | None,
165
175
  Parameter(help="Languages to include (python, java, low-code, manifest-only)."),
@@ -286,6 +296,11 @@ def list_connectors(
286
296
  connectors = list(result.connectors)
287
297
  repo_path_obj = Path(repo_path)
288
298
 
299
+ # Add connectors with local CDK reference if --local-cdk flag is set
300
+ if local_cdk:
301
+ local_cdk_connectors = get_connectors_with_local_cdk(repo_path)
302
+ connectors = sorted(set(connectors) | local_cdk_connectors)
303
+
289
304
  # Apply connector type filter
290
305
  if connector_type_filter:
291
306
  connectors = [