airbyte-internal-ops 0.4.2__py3-none-any.whl → 0.5.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.
Files changed (53) hide show
  1. {airbyte_internal_ops-0.4.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/METADATA +1 -1
  2. {airbyte_internal_ops-0.4.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/RECORD +13 -52
  3. airbyte_ops_mcp/cli/cloud.py +27 -0
  4. airbyte_ops_mcp/cloud_admin/api_client.py +473 -0
  5. airbyte_ops_mcp/cloud_admin/models.py +56 -0
  6. airbyte_ops_mcp/mcp/cloud_connector_versions.py +460 -0
  7. airbyte_ops_mcp/mcp/prerelease.py +5 -44
  8. airbyte_ops_mcp/regression_tests/ci_output.py +8 -4
  9. airbyte_ops_mcp/regression_tests/http_metrics.py +21 -2
  10. airbyte_ops_mcp/regression_tests/models.py +6 -0
  11. airbyte_ops_mcp/telemetry.py +162 -0
  12. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/.gitignore +0 -1
  13. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/README.md +0 -420
  14. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/__init__.py +0 -2
  15. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/__init__.py +0 -1
  16. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/__init__.py +0 -8
  17. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/base_backend.py +0 -16
  18. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/duckdb_backend.py +0 -87
  19. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/backends/file_backend.py +0 -165
  20. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connection_objects_retrieval.py +0 -377
  21. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/connector_runner.py +0 -247
  22. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/errors.py +0 -7
  23. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/evaluation_modes.py +0 -25
  24. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/hacks.py +0 -23
  25. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/json_schema_helper.py +0 -384
  26. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/mitm_addons.py +0 -37
  27. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/models.py +0 -595
  28. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/proxy.py +0 -207
  29. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/secret_access.py +0 -47
  30. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/segment_tracking.py +0 -45
  31. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/commons/utils.py +0 -214
  32. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/conftest.py.disabled +0 -751
  33. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/consts.py +0 -4
  34. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/poetry.lock +0 -4480
  35. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/pytest.ini +0 -9
  36. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/__init__.py +0 -1
  37. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_check.py +0 -61
  38. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_discover.py +0 -117
  39. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_read.py +0 -627
  40. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/regression_tests/test_spec.py +0 -43
  41. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/report.py +0 -542
  42. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/stash_keys.py +0 -38
  43. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/__init__.py +0 -0
  44. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/private_details.html.j2 +0 -305
  45. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/templates/report.html.j2 +0 -515
  46. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/utils.py +0 -187
  47. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/__init__.py +0 -0
  48. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_check.py +0 -61
  49. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_discover.py +0 -217
  50. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_read.py +0 -177
  51. airbyte_ops_mcp/_legacy/airbyte_ci/connector_live_tests/validation_tests/test_spec.py +0 -631
  52. {airbyte_internal_ops-0.4.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/WHEEL +0 -0
  53. {airbyte_internal_ops-0.4.2.dist-info → airbyte_internal_ops-0.5.0.dist-info}/entry_points.txt +0 -0
@@ -71,3 +71,59 @@ class VersionOverrideOperationResult(BaseModel):
71
71
  if self.success:
72
72
  return f"✓ {self.message}"
73
73
  return f"✗ {self.message}"
74
+
75
+
76
+ class WorkspaceVersionOverrideResult(BaseModel):
77
+ """Result of a workspace-level version override operation.
78
+
79
+ This model provides detailed information about the outcome of a workspace-level
80
+ version pinning or unpinning operation.
81
+ """
82
+
83
+ success: bool = Field(description="Whether the operation succeeded")
84
+ message: str = Field(description="Human-readable message describing the result")
85
+ workspace_id: str = Field(description="The workspace ID")
86
+ connector_name: str = Field(
87
+ description="The connector name (e.g., 'source-github')"
88
+ )
89
+ connector_type: Literal["source", "destination"] = Field(
90
+ description="The type of connector (source or destination)"
91
+ )
92
+ version: str | None = Field(
93
+ default=None,
94
+ description="The version that was pinned (None if cleared or failed)",
95
+ )
96
+
97
+ def __str__(self) -> str:
98
+ """Return a string representation of the operation result."""
99
+ if self.success:
100
+ return f"✓ {self.message}"
101
+ return f"✗ {self.message}"
102
+
103
+
104
+ class OrganizationVersionOverrideResult(BaseModel):
105
+ """Result of an organization-level version override operation.
106
+
107
+ This model provides detailed information about the outcome of an organization-level
108
+ version pinning or unpinning operation.
109
+ """
110
+
111
+ success: bool = Field(description="Whether the operation succeeded")
112
+ message: str = Field(description="Human-readable message describing the result")
113
+ organization_id: str = Field(description="The organization ID")
114
+ connector_name: str = Field(
115
+ description="The connector name (e.g., 'source-github')"
116
+ )
117
+ connector_type: Literal["source", "destination"] = Field(
118
+ description="The type of connector (source or destination)"
119
+ )
120
+ version: str | None = Field(
121
+ default=None,
122
+ description="The version that was pinned (None if cleared or failed)",
123
+ )
124
+
125
+ def __str__(self) -> str:
126
+ """Return a string representation of the operation result."""
127
+ if self.success:
128
+ return f"✓ {self.message}"
129
+ return f"✗ {self.message}"
@@ -25,7 +25,9 @@ from airbyte_ops_mcp.cloud_admin.auth import (
25
25
  )
26
26
  from airbyte_ops_mcp.cloud_admin.models import (
27
27
  ConnectorVersionInfo,
28
+ OrganizationVersionOverrideResult,
28
29
  VersionOverrideOperationResult,
30
+ WorkspaceVersionOverrideResult,
29
31
  )
30
32
  from airbyte_ops_mcp.github_api import (
31
33
  GitHubAPIError,
@@ -434,6 +436,464 @@ def set_cloud_connector_version_override(
434
436
  )
435
437
 
436
438
 
439
+ @mcp_tool(
440
+ destructive=True,
441
+ idempotent=False,
442
+ open_world=True,
443
+ )
444
+ def set_workspace_connector_version_override(
445
+ workspace_id: Annotated[
446
+ str,
447
+ Field(description="The Airbyte Cloud workspace ID."),
448
+ ],
449
+ connector_name: Annotated[
450
+ str,
451
+ Field(
452
+ description="The connector name (e.g., 'source-github', 'destination-bigquery')."
453
+ ),
454
+ ],
455
+ connector_type: Annotated[
456
+ Literal["source", "destination"],
457
+ "The type of connector (source or destination)",
458
+ ],
459
+ approval_comment_url: Annotated[
460
+ str,
461
+ Field(
462
+ description="URL to a GitHub comment where the admin has explicitly "
463
+ "requested or authorized this deployment. Must be a valid GitHub comment URL. "
464
+ "Required for authorization. The admin email is automatically derived from "
465
+ "the comment author's GitHub profile.",
466
+ ),
467
+ ],
468
+ version: Annotated[
469
+ str | None,
470
+ Field(
471
+ description="The semver version string to pin to (e.g., '0.1.0'). "
472
+ "Must be None if unset is True.",
473
+ default=None,
474
+ ),
475
+ ],
476
+ unset: Annotated[
477
+ bool,
478
+ Field(
479
+ description="If True, removes any existing version override. "
480
+ "Cannot be True if version is provided.",
481
+ default=False,
482
+ ),
483
+ ],
484
+ override_reason: Annotated[
485
+ str | None,
486
+ Field(
487
+ description="Required when setting a version. "
488
+ "Explanation for the override (min 10 characters).",
489
+ default=None,
490
+ ),
491
+ ],
492
+ override_reason_reference_url: Annotated[
493
+ str | None,
494
+ Field(
495
+ description="Optional URL with more context (e.g., issue link).",
496
+ default=None,
497
+ ),
498
+ ],
499
+ issue_url: Annotated[
500
+ str | None,
501
+ Field(
502
+ description="URL to the GitHub issue providing context for this operation. "
503
+ "Must be a valid GitHub URL (https://github.com/...). Required for authorization.",
504
+ default=None,
505
+ ),
506
+ ],
507
+ ai_agent_session_url: Annotated[
508
+ str | None,
509
+ Field(
510
+ description="URL to the AI agent session driving this operation, if applicable. "
511
+ "Provides additional auditability for AI-driven operations.",
512
+ default=None,
513
+ ),
514
+ ],
515
+ ) -> WorkspaceVersionOverrideResult:
516
+ """Set or clear a workspace-level version override for a connector type.
517
+
518
+ This pins ALL instances of a connector type within a workspace to a specific version.
519
+ For example, pinning 'source-github' at workspace level means all GitHub sources
520
+ in that workspace will use the pinned version.
521
+
522
+ **Admin-only operation** - Requires:
523
+ - AIRBYTE_INTERNAL_ADMIN_FLAG=airbyte.io environment variable
524
+ - issue_url parameter (GitHub issue URL for context)
525
+ - approval_comment_url parameter (GitHub comment URL with approval from an @airbyte.io user)
526
+
527
+ You must specify EXACTLY ONE of `version` OR `unset=True`, but not both.
528
+ When setting a version, `override_reason` is required.
529
+ """
530
+ # Validate admin access (check env var flag)
531
+ try:
532
+ require_internal_admin_flag_only()
533
+ except CloudAuthError as e:
534
+ return WorkspaceVersionOverrideResult(
535
+ success=False,
536
+ message=f"Admin authentication failed: {e}",
537
+ workspace_id=workspace_id,
538
+ connector_name=connector_name,
539
+ connector_type=connector_type,
540
+ )
541
+
542
+ # Validate authorization parameters
543
+ validation_errors: list[str] = []
544
+
545
+ if not issue_url:
546
+ validation_errors.append(
547
+ "issue_url is required for authorization (GitHub issue URL)"
548
+ )
549
+ elif not issue_url.startswith("https://github.com/"):
550
+ validation_errors.append(
551
+ f"issue_url must be a valid GitHub URL (https://github.com/...), got: {issue_url}"
552
+ )
553
+
554
+ if not approval_comment_url.startswith("https://github.com/"):
555
+ validation_errors.append(
556
+ f"approval_comment_url must be a valid GitHub URL, got: {approval_comment_url}"
557
+ )
558
+ elif (
559
+ "#issuecomment-" not in approval_comment_url
560
+ and "#discussion_r" not in approval_comment_url
561
+ ):
562
+ validation_errors.append(
563
+ "approval_comment_url must be a GitHub comment URL "
564
+ "(containing #issuecomment- or #discussion_r)"
565
+ )
566
+
567
+ if validation_errors:
568
+ return WorkspaceVersionOverrideResult(
569
+ success=False,
570
+ message="Authorization validation failed: " + "; ".join(validation_errors),
571
+ workspace_id=workspace_id,
572
+ connector_name=connector_name,
573
+ connector_type=connector_type,
574
+ )
575
+
576
+ # Derive admin email from approval comment URL
577
+ try:
578
+ admin_user_email = get_admin_email_from_approval_comment(approval_comment_url)
579
+ except GitHubCommentParseError as e:
580
+ return WorkspaceVersionOverrideResult(
581
+ success=False,
582
+ message=f"Failed to parse approval comment URL: {e}",
583
+ workspace_id=workspace_id,
584
+ connector_name=connector_name,
585
+ connector_type=connector_type,
586
+ )
587
+ except GitHubAPIError as e:
588
+ return WorkspaceVersionOverrideResult(
589
+ success=False,
590
+ message=f"Failed to fetch approval comment from GitHub: {e}",
591
+ workspace_id=workspace_id,
592
+ connector_name=connector_name,
593
+ connector_type=connector_type,
594
+ )
595
+ except GitHubUserEmailNotFoundError as e:
596
+ return WorkspaceVersionOverrideResult(
597
+ success=False,
598
+ message=str(e),
599
+ workspace_id=workspace_id,
600
+ connector_name=connector_name,
601
+ connector_type=connector_type,
602
+ )
603
+
604
+ # Build enhanced override reason with audit fields (only for 'set' operations)
605
+ enhanced_override_reason = override_reason
606
+ if not unset and override_reason:
607
+ audit_parts = [override_reason]
608
+ audit_parts.append(f"Issue: {issue_url}")
609
+ audit_parts.append(f"Approval: {approval_comment_url}")
610
+ if ai_agent_session_url:
611
+ audit_parts.append(f"AI Session: {ai_agent_session_url}")
612
+ enhanced_override_reason = " | ".join(audit_parts)
613
+
614
+ # Resolve auth and call API client
615
+ try:
616
+ auth = _resolve_cloud_auth()
617
+
618
+ result = api_client.set_workspace_connector_version_override(
619
+ workspace_id=workspace_id,
620
+ connector_name=connector_name,
621
+ connector_type=connector_type,
622
+ api_root=constants.CLOUD_CONFIG_API_ROOT,
623
+ client_id=auth.client_id,
624
+ client_secret=auth.client_secret,
625
+ bearer_token=auth.bearer_token,
626
+ version=version,
627
+ unset=unset,
628
+ override_reason=enhanced_override_reason,
629
+ override_reason_reference_url=override_reason_reference_url,
630
+ user_email=admin_user_email,
631
+ )
632
+
633
+ if unset:
634
+ if result:
635
+ message = f"Successfully cleared workspace-level version override for {connector_name}."
636
+ else:
637
+ message = f"No workspace-level version override was active for {connector_name} (nothing to clear)"
638
+ else:
639
+ message = f"Successfully pinned {connector_name} to version {version} at workspace level."
640
+
641
+ return WorkspaceVersionOverrideResult(
642
+ success=True,
643
+ message=message,
644
+ workspace_id=workspace_id,
645
+ connector_name=connector_name,
646
+ connector_type=connector_type,
647
+ version=version if not unset else None,
648
+ )
649
+
650
+ except PyAirbyteInputError as e:
651
+ return WorkspaceVersionOverrideResult(
652
+ success=False,
653
+ message=str(e),
654
+ workspace_id=workspace_id,
655
+ connector_name=connector_name,
656
+ connector_type=connector_type,
657
+ )
658
+ except CloudAuthError as e:
659
+ return WorkspaceVersionOverrideResult(
660
+ success=False,
661
+ message=f"Authentication failed: {e}",
662
+ workspace_id=workspace_id,
663
+ connector_name=connector_name,
664
+ connector_type=connector_type,
665
+ )
666
+
667
+
668
+ @mcp_tool(
669
+ destructive=True,
670
+ idempotent=False,
671
+ open_world=True,
672
+ )
673
+ def set_organization_connector_version_override(
674
+ organization_id: Annotated[
675
+ str,
676
+ Field(description="The Airbyte Cloud organization ID."),
677
+ ],
678
+ connector_name: Annotated[
679
+ str,
680
+ Field(
681
+ description="The connector name (e.g., 'source-github', 'destination-bigquery')."
682
+ ),
683
+ ],
684
+ connector_type: Annotated[
685
+ Literal["source", "destination"],
686
+ "The type of connector (source or destination)",
687
+ ],
688
+ approval_comment_url: Annotated[
689
+ str,
690
+ Field(
691
+ description="URL to a GitHub comment where the admin has explicitly "
692
+ "requested or authorized this deployment. Must be a valid GitHub comment URL. "
693
+ "Required for authorization. The admin email is automatically derived from "
694
+ "the comment author's GitHub profile.",
695
+ ),
696
+ ],
697
+ version: Annotated[
698
+ str | None,
699
+ Field(
700
+ description="The semver version string to pin to (e.g., '0.1.0'). "
701
+ "Must be None if unset is True.",
702
+ default=None,
703
+ ),
704
+ ],
705
+ unset: Annotated[
706
+ bool,
707
+ Field(
708
+ description="If True, removes any existing version override. "
709
+ "Cannot be True if version is provided.",
710
+ default=False,
711
+ ),
712
+ ],
713
+ override_reason: Annotated[
714
+ str | None,
715
+ Field(
716
+ description="Required when setting a version. "
717
+ "Explanation for the override (min 10 characters).",
718
+ default=None,
719
+ ),
720
+ ],
721
+ override_reason_reference_url: Annotated[
722
+ str | None,
723
+ Field(
724
+ description="Optional URL with more context (e.g., issue link).",
725
+ default=None,
726
+ ),
727
+ ],
728
+ issue_url: Annotated[
729
+ str | None,
730
+ Field(
731
+ description="URL to the GitHub issue providing context for this operation. "
732
+ "Must be a valid GitHub URL (https://github.com/...). Required for authorization.",
733
+ default=None,
734
+ ),
735
+ ],
736
+ ai_agent_session_url: Annotated[
737
+ str | None,
738
+ Field(
739
+ description="URL to the AI agent session driving this operation, if applicable. "
740
+ "Provides additional auditability for AI-driven operations.",
741
+ default=None,
742
+ ),
743
+ ],
744
+ ) -> OrganizationVersionOverrideResult:
745
+ """Set or clear an organization-level version override for a connector type.
746
+
747
+ This pins ALL instances of a connector type across an entire organization to a
748
+ specific version. For example, pinning 'source-github' at organization level means
749
+ all GitHub sources in all workspaces within that organization will use the pinned version.
750
+
751
+ **Admin-only operation** - Requires:
752
+ - AIRBYTE_INTERNAL_ADMIN_FLAG=airbyte.io environment variable
753
+ - issue_url parameter (GitHub issue URL for context)
754
+ - approval_comment_url parameter (GitHub comment URL with approval from an @airbyte.io user)
755
+
756
+ You must specify EXACTLY ONE of `version` OR `unset=True`, but not both.
757
+ When setting a version, `override_reason` is required.
758
+ """
759
+ # Validate admin access (check env var flag)
760
+ try:
761
+ require_internal_admin_flag_only()
762
+ except CloudAuthError as e:
763
+ return OrganizationVersionOverrideResult(
764
+ success=False,
765
+ message=f"Admin authentication failed: {e}",
766
+ organization_id=organization_id,
767
+ connector_name=connector_name,
768
+ connector_type=connector_type,
769
+ )
770
+
771
+ # Validate authorization parameters
772
+ validation_errors: list[str] = []
773
+
774
+ if not issue_url:
775
+ validation_errors.append(
776
+ "issue_url is required for authorization (GitHub issue URL)"
777
+ )
778
+ elif not issue_url.startswith("https://github.com/"):
779
+ validation_errors.append(
780
+ f"issue_url must be a valid GitHub URL (https://github.com/...), got: {issue_url}"
781
+ )
782
+
783
+ if not approval_comment_url.startswith("https://github.com/"):
784
+ validation_errors.append(
785
+ f"approval_comment_url must be a valid GitHub URL, got: {approval_comment_url}"
786
+ )
787
+ elif (
788
+ "#issuecomment-" not in approval_comment_url
789
+ and "#discussion_r" not in approval_comment_url
790
+ ):
791
+ validation_errors.append(
792
+ "approval_comment_url must be a GitHub comment URL "
793
+ "(containing #issuecomment- or #discussion_r)"
794
+ )
795
+
796
+ if validation_errors:
797
+ return OrganizationVersionOverrideResult(
798
+ success=False,
799
+ message="Authorization validation failed: " + "; ".join(validation_errors),
800
+ organization_id=organization_id,
801
+ connector_name=connector_name,
802
+ connector_type=connector_type,
803
+ )
804
+
805
+ # Derive admin email from approval comment URL
806
+ try:
807
+ admin_user_email = get_admin_email_from_approval_comment(approval_comment_url)
808
+ except GitHubCommentParseError as e:
809
+ return OrganizationVersionOverrideResult(
810
+ success=False,
811
+ message=f"Failed to parse approval comment URL: {e}",
812
+ organization_id=organization_id,
813
+ connector_name=connector_name,
814
+ connector_type=connector_type,
815
+ )
816
+ except GitHubAPIError as e:
817
+ return OrganizationVersionOverrideResult(
818
+ success=False,
819
+ message=f"Failed to fetch approval comment from GitHub: {e}",
820
+ organization_id=organization_id,
821
+ connector_name=connector_name,
822
+ connector_type=connector_type,
823
+ )
824
+ except GitHubUserEmailNotFoundError as e:
825
+ return OrganizationVersionOverrideResult(
826
+ success=False,
827
+ message=str(e),
828
+ organization_id=organization_id,
829
+ connector_name=connector_name,
830
+ connector_type=connector_type,
831
+ )
832
+
833
+ # Build enhanced override reason with audit fields (only for 'set' operations)
834
+ enhanced_override_reason = override_reason
835
+ if not unset and override_reason:
836
+ audit_parts = [override_reason]
837
+ audit_parts.append(f"Issue: {issue_url}")
838
+ audit_parts.append(f"Approval: {approval_comment_url}")
839
+ if ai_agent_session_url:
840
+ audit_parts.append(f"AI Session: {ai_agent_session_url}")
841
+ enhanced_override_reason = " | ".join(audit_parts)
842
+
843
+ # Resolve auth and call API client
844
+ try:
845
+ auth = _resolve_cloud_auth()
846
+
847
+ result = api_client.set_organization_connector_version_override(
848
+ organization_id=organization_id,
849
+ connector_name=connector_name,
850
+ connector_type=connector_type,
851
+ api_root=constants.CLOUD_CONFIG_API_ROOT,
852
+ client_id=auth.client_id,
853
+ client_secret=auth.client_secret,
854
+ bearer_token=auth.bearer_token,
855
+ version=version,
856
+ unset=unset,
857
+ override_reason=enhanced_override_reason,
858
+ override_reason_reference_url=override_reason_reference_url,
859
+ user_email=admin_user_email,
860
+ )
861
+
862
+ if unset:
863
+ if result:
864
+ message = f"Successfully cleared organization-level version override for {connector_name}."
865
+ else:
866
+ message = f"No organization-level version override was active for {connector_name} (nothing to clear)"
867
+ else:
868
+ message = f"Successfully pinned {connector_name} to version {version} at organization level."
869
+
870
+ return OrganizationVersionOverrideResult(
871
+ success=True,
872
+ message=message,
873
+ organization_id=organization_id,
874
+ connector_name=connector_name,
875
+ connector_type=connector_type,
876
+ version=version if not unset else None,
877
+ )
878
+
879
+ except PyAirbyteInputError as e:
880
+ return OrganizationVersionOverrideResult(
881
+ success=False,
882
+ message=str(e),
883
+ organization_id=organization_id,
884
+ connector_name=connector_name,
885
+ connector_type=connector_type,
886
+ )
887
+ except CloudAuthError as e:
888
+ return OrganizationVersionOverrideResult(
889
+ success=False,
890
+ message=f"Authentication failed: {e}",
891
+ organization_id=organization_id,
892
+ connector_name=connector_name,
893
+ connector_type=connector_type,
894
+ )
895
+
896
+
437
897
  def register_cloud_connector_version_tools(app: FastMCP) -> None:
438
898
  """Register cloud connector version management tools with the FastMCP app.
439
899
 
@@ -18,6 +18,7 @@ import yaml
18
18
  from fastmcp import FastMCP
19
19
  from pydantic import BaseModel, Field
20
20
 
21
+ from airbyte_ops_mcp.github_actions import trigger_workflow_dispatch
21
22
  from airbyte_ops_mcp.github_api import (
22
23
  GITHUB_API_BASE,
23
24
  get_pr_head_ref,
@@ -149,49 +150,6 @@ def _get_connector_metadata(
149
150
  return yaml.safe_load(content)
150
151
 
151
152
 
152
- def _trigger_workflow_dispatch(
153
- owner: str,
154
- repo: str,
155
- workflow_file: str,
156
- ref: str,
157
- inputs: dict,
158
- token: str,
159
- ) -> str:
160
- """Trigger a GitHub Actions workflow via workflow_dispatch.
161
-
162
- Args:
163
- owner: Repository owner (e.g., "airbytehq")
164
- repo: Repository name (e.g., "airbyte")
165
- workflow_file: Workflow file name (e.g., "publish-connectors-prerelease-command.yml")
166
- ref: Git ref to run the workflow on (branch name)
167
- inputs: Workflow inputs dictionary
168
- token: GitHub API token
169
-
170
- Returns:
171
- URL to view workflow runs.
172
-
173
- Raises:
174
- requests.HTTPError: If API request fails.
175
- """
176
- url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/actions/workflows/{workflow_file}/dispatches"
177
- headers = {
178
- "Authorization": f"Bearer {token}",
179
- "Accept": "application/vnd.github+json",
180
- "X-GitHub-Api-Version": "2022-11-28",
181
- }
182
- payload = {
183
- "ref": ref,
184
- "inputs": inputs,
185
- }
186
-
187
- response = requests.post(url, headers=headers, json=payload, timeout=30)
188
- response.raise_for_status()
189
-
190
- # workflow_dispatch returns 204 No Content on success
191
- # Return URL to view workflow runs
192
- return f"https://github.com/{owner}/{repo}/actions/workflows/{workflow_file}"
193
-
194
-
195
153
  @mcp_tool(
196
154
  read_only=False,
197
155
  destructive=False,
@@ -271,14 +229,17 @@ def publish_connector_to_airbyte_registry(
271
229
 
272
230
  # Trigger the workflow on the default branch
273
231
  # The workflow will checkout the PR branch via inputs.gitref
274
- workflow_url = _trigger_workflow_dispatch(
232
+ dispatch_result = trigger_workflow_dispatch(
275
233
  owner=DEFAULT_REPO_OWNER,
276
234
  repo=target_repo_name,
277
235
  workflow_file=target_workflow,
278
236
  ref=target_branch,
279
237
  inputs=workflow_inputs,
280
238
  token=token,
239
+ find_run=True,
281
240
  )
241
+ # Use the specific run URL if found, otherwise fall back to the workflow URL
242
+ workflow_url = dispatch_result.run_url or dispatch_result.workflow_url
282
243
 
283
244
  # Try to compute docker_image and docker_image_tag from connector metadata
284
245
  docker_image: str | None = None
@@ -308,10 +308,10 @@ def generate_action_test_comparison_report(
308
308
  [
309
309
  "#### HTTP Metrics",
310
310
  "",
311
- "| Version | Flow Count | Duplicate Flows |",
312
- "|---------|------------|-----------------|",
313
- f"| Control | {control_http.get('flow_count', 0)} | {control_http.get('duplicate_flow_count', 0)} |",
314
- f"| Target | {target_http.get('flow_count', 0)} | {target_http.get('duplicate_flow_count', 0)} |",
311
+ "| Version | Flow Count | Duplicate Flows | Cache Hit Ratio |",
312
+ "|---------|------------|-----------------|-----------------|",
313
+ f"| Control | {control_http.get('flow_count', 0)} | {control_http.get('duplicate_flow_count', 0)} | {control_http.get('cache_hit_ratio', 'N/A')} |",
314
+ f"| Target | {target_http.get('flow_count', 0)} | {target_http.get('duplicate_flow_count', 0)} | {target_http.get('cache_hit_ratio', 'N/A')} |",
315
315
  "",
316
316
  ]
317
317
  )
@@ -370,6 +370,9 @@ def generate_single_version_report(
370
370
  run_url = _get_github_run_url()
371
371
  artifacts_url = _get_github_artifacts_url()
372
372
 
373
+ # Get tester identity from environment (GitHub Actions sets GITHUB_ACTOR)
374
+ tester = os.getenv("GITHUB_ACTOR") or os.getenv("USER") or "unknown"
375
+
373
376
  # Start with L2 header containing the command name (no L1 header)
374
377
  lines: list[str] = [
375
378
  f"## `{command.upper()}` Test Results",
@@ -377,6 +380,7 @@ def generate_single_version_report(
377
380
  "### Context",
378
381
  "",
379
382
  f"- **Test Date:** {datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M:%S UTC')}",
383
+ f"- **Tester:** `{tester}`",
380
384
  f"- **Connector:** `{connector_name}`",
381
385
  f"- **Version:** `{version}`",
382
386
  f"- **Command:** `{command.upper()}`",