airbyte-internal-ops 0.3.1__py3-none-any.whl → 0.4.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: airbyte-internal-ops
3
- Version: 0.3.1
3
+ Version: 0.4.1
4
4
  Summary: MCP and API interfaces that let the agents do the admin work
5
5
  Author-email: Aaron Steers <aj@airbyte.io>
6
6
  Keywords: admin,airbyte,api,mcp
@@ -2,8 +2,8 @@ airbyte_ops_mcp/__init__.py,sha256=tuzdlMkfnWBnsri5KGHM2M_xuNnzFk2u_aR79mmN7Yg,7
2
2
  airbyte_ops_mcp/_annotations.py,sha256=MO-SBDnbykxxHDESG7d8rviZZ4WlZgJKv0a8eBqcEzQ,1757
3
3
  airbyte_ops_mcp/constants.py,sha256=khcv9W3WkApIyPygEGgE2noBIqLomjoOMLxFBU1ArjA,5308
4
4
  airbyte_ops_mcp/gcp_auth.py,sha256=i0cm1_xX4fj_31iKlfARpNvTaSr85iGTSw9KMf4f4MU,7206
5
- airbyte_ops_mcp/github_actions.py,sha256=wKnuIVmF4u1gMYNdSoryD_PUmvMz5SaHgOvbU0dsolA,9957
6
- airbyte_ops_mcp/github_api.py,sha256=uupbYKAkm7yLHK_1cDXYKl1bOYhUygZhG5IHspS7duE,8104
5
+ airbyte_ops_mcp/github_actions.py,sha256=FSi_tjS9TbwRVp8dwlDZhFOi7lJXEZQLhPm2KpcjNlY,7022
6
+ airbyte_ops_mcp/github_api.py,sha256=ezpMR1vjqQ-1f5yOLBVbxW70OPtUferl1uA0u_gUVo8,12733
7
7
  airbyte_ops_mcp/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  airbyte_ops_mcp/_legacy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  airbyte_ops_mcp/_legacy/airbyte_ci/README.md,sha256=qEYx4geDR8AEDjrcA303h7Nol-CMDLojxUyiGzQprM8,236
@@ -354,8 +354,8 @@ airbyte_ops_mcp/cli/_base.py,sha256=I8tWnyQf0ks4r3J8N8h-5GZxyn37T-55KsbuHnxYlcg,
354
354
  airbyte_ops_mcp/cli/_shared.py,sha256=jg-xMyGzTCGPqKd8VTfE_3kGPIyO_3Kx5sQbG4rPc0Y,1311
355
355
  airbyte_ops_mcp/cli/app.py,sha256=SEdBpqFUG2O8zGV5ifwptxrLGFph_dLr66-MX9d69gQ,789
356
356
  airbyte_ops_mcp/cli/cloud.py,sha256=OmeJPW8ME82PLJSqzoU_tz_3iqsTA-MY4QBO-ad8gfo,44141
357
- airbyte_ops_mcp/cli/gh.py,sha256=91b1AxFXvHQCFyXhrrym-756ZjnMCqvxFdmwCtma1zI,2046
358
- airbyte_ops_mcp/cli/registry.py,sha256=tcf_CDiUVJpSdBRNqlEL3zFKMqK53AhFpJjAETM4gLs,9781
357
+ airbyte_ops_mcp/cli/gh.py,sha256=koJPu0MDB6AW7mJq2z4dZV65ofvsZTkqoeitGF8KJR8,5364
358
+ airbyte_ops_mcp/cli/registry.py,sha256=L4nDKhlegr31gSE-GUvDFSq10KgDz5kJuZXgLIxYIyg,9785
359
359
  airbyte_ops_mcp/cli/repo.py,sha256=G1hoQpH0XYhUH3FFOsia9xabGB0LP9o3XcwBuqvFVo0,16331
360
360
  airbyte_ops_mcp/cloud_admin/__init__.py,sha256=cqE96Q10Kp6elhH9DAi6TVsIwSUy3sooDLLrxTaktGk,816
361
361
  airbyte_ops_mcp/cloud_admin/api_client.py,sha256=ysTztSbLX0SZSK3qneHTSKVODRzVmLbHBC3ND0j_LTc,38020
@@ -376,21 +376,21 @@ airbyte_ops_mcp/mcp/cloud_connector_versions.py,sha256=5qUYRZapYBprmmc5J3lKQzeQ3
376
376
  airbyte_ops_mcp/mcp/connector_analysis.py,sha256=OC4KrOSkMkKPkOisWnSv96BDDE5TQYHq-Jxa2vtjJpo,298
377
377
  airbyte_ops_mcp/mcp/connector_qa.py,sha256=aImpqdnqBPDrz10BS0owsV4kuIU2XdalzgbaGZsbOL0,258
378
378
  airbyte_ops_mcp/mcp/gcp_logs.py,sha256=IPtq4098_LN1Cgeba4jATO1iYFFFpL2-aRO0pGcOdzs,2689
379
- airbyte_ops_mcp/mcp/github.py,sha256=h3M3VJrq09y_F9ueQVCq3bUbVBNFuTNKprHtGU_ttio,8045
379
+ airbyte_ops_mcp/mcp/github_actions.py,sha256=_mAVTl6UX3F7S_HeV1-M5R4jMNzNQGI3ADs3sBzden8,11760
380
380
  airbyte_ops_mcp/mcp/github_repo_ops.py,sha256=PiERpt8abo20Gz4CfXhrDNlVM4o4FOt5sweZJND2a0s,5314
381
381
  airbyte_ops_mcp/mcp/metadata.py,sha256=fwGW97WknR5lfKcQnFtK6dU87aA6TmLj1NkKyqDAV9g,270
382
- airbyte_ops_mcp/mcp/prerelease.py,sha256=nc6VU03ADVHWM3OjGKxbS5XqY4VoyRyrZNU_fyAtaOI,10465
383
- airbyte_ops_mcp/mcp/prod_db_queries.py,sha256=DPzyHCT3yxj2kjkucefoVpsR71vscuJQ8tGgLs_lhv0,32068
382
+ airbyte_ops_mcp/mcp/prerelease.py,sha256=fEZwqtyFQC9nKBF6MJf0WcHoiEoCiouFbBG2bqBtuRY,10701
383
+ airbyte_ops_mcp/mcp/prod_db_queries.py,sha256=VsiBBnVbOjc8lBb2Xr1lmcH3wu7QHQfjd4lORarEE1s,42700
384
384
  airbyte_ops_mcp/mcp/prompts.py,sha256=mJld9mdPECXYZffWXGSvNs4Xevx3rxqUGNlzGKVC2_s,1599
385
385
  airbyte_ops_mcp/mcp/registry.py,sha256=PW-VYUj42qx2pQ_apUkVaoUFq7VgB9zEU7-aGrkSCCw,290
386
- airbyte_ops_mcp/mcp/regression_tests.py,sha256=S1h-5S5gcZA4WEtIZyAQ836hd04tjSRRqMiYMx0S93g,16079
387
- airbyte_ops_mcp/mcp/server.py,sha256=lKAXxt4u4bz7dsKvAYFFHziMbun2pOnxYmrMtRxsZvM,5317
386
+ airbyte_ops_mcp/mcp/regression_tests.py,sha256=dmM22ODwUTbVisKiRcJunzEgMKrZOkpsbkUm0_hFWYk,16752
387
+ airbyte_ops_mcp/mcp/server.py,sha256=dMOFXPFeHBIqicOWs8UsPfzgsWnzsWDsZJ79E_OYjT0,5341
388
388
  airbyte_ops_mcp/mcp/server_info.py,sha256=Yi4B1auW64QZGBDas5mro_vwTjvrP785TFNSBP7GhRg,2361
389
389
  airbyte_ops_mcp/prod_db_access/__init__.py,sha256=5pxouMPY1beyWlB0UwPnbaLTKTHqU6X82rbbgKY2vYU,1069
390
390
  airbyte_ops_mcp/prod_db_access/db_engine.py,sha256=VUqEWZtharJUR-Cri_pMwtGh1C4Neu4s195mbEXlm-w,9190
391
391
  airbyte_ops_mcp/prod_db_access/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
392
- airbyte_ops_mcp/prod_db_access/queries.py,sha256=TNxTY5Hf3ImHBX0_e_20-VbF3yzYm2mX3ykWzQXgpno,17754
393
- airbyte_ops_mcp/prod_db_access/sql.py,sha256=xB7SJGnBSlY-ZB7ku_9QfvNIEldGEmCn-jJcAdes_LY,30407
392
+ airbyte_ops_mcp/prod_db_access/queries.py,sha256=pyW5GxDZ5ibwXawxyI_IR7VFcmoX7pZyZ2jdADqhJRY,20276
393
+ airbyte_ops_mcp/prod_db_access/sql.py,sha256=lzFOYfkb-rFTaZ6vrAK9G8Ym4KTUdhPMBzK44NSRzcg,35362
394
394
  airbyte_ops_mcp/registry/__init__.py,sha256=iEaPlt9GrnlaLbc__98TguNeZG8wuQu7S-_2QkhHcbA,858
395
395
  airbyte_ops_mcp/registry/models.py,sha256=B4L4TKr52wo0xs0CqvCBrpowqjShzVnZ5eTr2-EyhNs,2346
396
396
  airbyte_ops_mcp/registry/publish.py,sha256=VoPxsM2_0zJ829orzCRN-kjgcJtuBNyXgW4I9J680ro,12717
@@ -400,7 +400,7 @@ airbyte_ops_mcp/regression_tests/ci_output.py,sha256=rrvCVKKShc1iVPMuQJDBqSbsiAH
400
400
  airbyte_ops_mcp/regression_tests/config.py,sha256=dwWeY0tatdbwl9BqbhZ7EljoZDCtKmGO5fvOAIxeXmA,5873
401
401
  airbyte_ops_mcp/regression_tests/connection_fetcher.py,sha256=5wIiA0VvCFNEc-fr6Po18gZMX3E5fyPOGf2SuVOqv5U,12799
402
402
  airbyte_ops_mcp/regression_tests/connection_secret_retriever.py,sha256=FhWNVWq7sON4nwUmVJv8BgXBOqg1YV4b5WuWyCzZ0LU,4695
403
- airbyte_ops_mcp/regression_tests/connector_runner.py,sha256=bappfBSq8dn3IyVAMS_XuzYEwWus23hkDCHLa2RFysI,9920
403
+ airbyte_ops_mcp/regression_tests/connector_runner.py,sha256=OZzUa2aLh0sHaEARsDePOA-e3qEX4cvh3Jhnvi8S1rY,10130
404
404
  airbyte_ops_mcp/regression_tests/evaluation_modes.py,sha256=lAL6pEDmy_XCC7_m4_NXjt_f6Z8CXeAhMkc0FU8bm_M,1364
405
405
  airbyte_ops_mcp/regression_tests/http_metrics.py,sha256=oTD7f2MnQOvx4plOxHop2bInQ0-whvuToSsrC7TIM-M,12469
406
406
  airbyte_ops_mcp/regression_tests/models.py,sha256=brtAT9oO1TwjFcP91dFcu0XcUNqQb-jf7di1zkoVEuo,8782
@@ -414,7 +414,7 @@ airbyte_ops_mcp/regression_tests/regression/comparators.py,sha256=MJkLZEKHivgrG0
414
414
  airbyte_ops_mcp/regression_tests/validation/__init__.py,sha256=MBEwGOoNuqT4_oCahtoK62OKWIjUCfWa7vZTxNj_0Ek,1532
415
415
  airbyte_ops_mcp/regression_tests/validation/catalog_validators.py,sha256=jqqVAMOk0mtdPgwu4d0hA0ZEjtsNh5gapvGydRv3_qk,12553
416
416
  airbyte_ops_mcp/regression_tests/validation/record_validators.py,sha256=RjauAhKWNwxMBTu0eNS2hMFNQVs5CLbQU51kp6FOVDk,7432
417
- airbyte_internal_ops-0.3.1.dist-info/METADATA,sha256=kx1iQ0YE42LjpsFpjJD7SECaYMHEjo36VjvSVf3BwHk,5679
418
- airbyte_internal_ops-0.3.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
419
- airbyte_internal_ops-0.3.1.dist-info/entry_points.txt,sha256=WxP0l7bRFss4Cr5uQqVj9mTEKwnRKouNuphXQF0lotA,171
420
- airbyte_internal_ops-0.3.1.dist-info/RECORD,,
417
+ airbyte_internal_ops-0.4.1.dist-info/METADATA,sha256=mjk54F-EL71ItP6D4BxFg8_eZVZlbfe8bW1wTgno10g,5679
418
+ airbyte_internal_ops-0.4.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
419
+ airbyte_internal_ops-0.4.1.dist-info/entry_points.txt,sha256=WxP0l7bRFss4Cr5uQqVj9mTEKwnRKouNuphXQF0lotA,171
420
+ airbyte_internal_ops-0.4.1.dist-info/RECORD,,
airbyte_ops_mcp/cli/gh.py CHANGED
@@ -3,17 +3,23 @@
3
3
 
4
4
  Commands:
5
5
  airbyte-ops gh workflow status - Check GitHub Actions workflow status
6
+ airbyte-ops gh workflow trigger - Trigger a GitHub Actions CI workflow
6
7
  """
7
8
 
8
9
  from __future__ import annotations
9
10
 
11
+ import json
12
+ import time
10
13
  from typing import Annotated
11
14
 
12
15
  from cyclopts import App, Parameter
13
16
 
14
17
  from airbyte_ops_mcp.cli._base import app
15
18
  from airbyte_ops_mcp.cli._shared import exit_with_error, print_json
16
- from airbyte_ops_mcp.mcp.github import check_workflow_status
19
+ from airbyte_ops_mcp.mcp.github_actions import (
20
+ check_ci_workflow_status,
21
+ trigger_ci_workflow,
22
+ )
17
23
 
18
24
  # Create the gh sub-app
19
25
  gh_app = App(name="gh", help="GitHub operations.")
@@ -62,10 +68,107 @@ def workflow_status(
62
68
  "Must provide either --url OR all of (--owner, --repo, --run-id)."
63
69
  )
64
70
 
65
- result = check_workflow_status(
71
+ result = check_ci_workflow_status(
66
72
  workflow_url=url,
67
73
  owner=owner,
68
74
  repo=repo,
69
75
  run_id=run_id,
70
76
  )
71
77
  print_json(result.model_dump())
78
+
79
+
80
+ @workflow_app.command(name="trigger")
81
+ def workflow_trigger(
82
+ owner: Annotated[
83
+ str,
84
+ Parameter(help="Repository owner (e.g., 'airbytehq')."),
85
+ ],
86
+ repo: Annotated[
87
+ str,
88
+ Parameter(help="Repository name (e.g., 'airbyte')."),
89
+ ],
90
+ workflow_file: Annotated[
91
+ str,
92
+ Parameter(help="Workflow file name (e.g., 'connector-regression-test.yml')."),
93
+ ],
94
+ workflow_definition_ref: Annotated[
95
+ str | None,
96
+ Parameter(
97
+ help="Branch name or PR number for the workflow definition to use. "
98
+ "If a PR number is provided, it resolves to the PR's head branch name. "
99
+ "Defaults to 'main' if not specified."
100
+ ),
101
+ ] = None,
102
+ inputs: Annotated[
103
+ str | None,
104
+ Parameter(
105
+ help='Workflow inputs as a JSON string (e.g., \'{"key": "value"}\').'
106
+ ),
107
+ ] = None,
108
+ wait: Annotated[
109
+ bool,
110
+ Parameter(help="Wait for the workflow to complete before returning."),
111
+ ] = False,
112
+ wait_seconds: Annotated[
113
+ int,
114
+ Parameter(
115
+ help="Maximum seconds to wait for workflow completion (default: 600)."
116
+ ),
117
+ ] = 600,
118
+ ) -> None:
119
+ """Trigger a GitHub Actions CI workflow via workflow_dispatch.
120
+
121
+ This command triggers a workflow in any GitHub repository that has workflow_dispatch
122
+ enabled. It resolves PR numbers to branch names automatically.
123
+ """
124
+ # Parse inputs JSON if provided
125
+ parsed_inputs: dict[str, str] | None = None
126
+ if inputs:
127
+ try:
128
+ parsed_inputs = json.loads(inputs)
129
+ except json.JSONDecodeError as e:
130
+ exit_with_error(f"Invalid JSON for --inputs: {e}")
131
+
132
+ # Trigger the workflow
133
+ result = trigger_ci_workflow(
134
+ owner=owner,
135
+ repo=repo,
136
+ workflow_file=workflow_file,
137
+ workflow_definition_ref=workflow_definition_ref,
138
+ inputs=parsed_inputs,
139
+ )
140
+
141
+ print_json(result.model_dump())
142
+
143
+ # If wait is enabled and we have a run_id, poll for completion
144
+ if wait and result.run_id:
145
+ print(f"\nWaiting for workflow to complete (timeout: {wait_seconds}s)...")
146
+ start_time = time.time()
147
+ poll_interval = 10 # seconds
148
+
149
+ while time.time() - start_time < wait_seconds:
150
+ status_result = check_ci_workflow_status(
151
+ owner=owner,
152
+ repo=repo,
153
+ run_id=result.run_id,
154
+ )
155
+
156
+ if status_result.status == "completed":
157
+ print(
158
+ f"\nWorkflow completed with conclusion: {status_result.conclusion}"
159
+ )
160
+ print_json(status_result.model_dump())
161
+ return
162
+
163
+ elapsed = int(time.time() - start_time)
164
+ print(f" Status: {status_result.status} (elapsed: {elapsed}s)")
165
+ time.sleep(poll_interval)
166
+
167
+ print(f"\nTimeout reached after {wait_seconds}s. Workflow still running.")
168
+ # Print final status
169
+ final_status = check_ci_workflow_status(
170
+ owner=owner,
171
+ repo=repo,
172
+ run_id=result.run_id,
173
+ )
174
+ print_json(final_status.model_dump())
@@ -28,11 +28,11 @@ from airbyte_ops_mcp.cli._shared import (
28
28
  print_json,
29
29
  print_success,
30
30
  )
31
- from airbyte_ops_mcp.github_actions import (
31
+ from airbyte_ops_mcp.github_api import (
32
32
  get_file_contents_at_ref,
33
33
  resolve_github_token,
34
34
  )
35
- from airbyte_ops_mcp.mcp.github import get_docker_image_info
35
+ from airbyte_ops_mcp.mcp.github_actions import get_docker_image_info
36
36
  from airbyte_ops_mcp.mcp.prerelease import (
37
37
  compute_prerelease_docker_image_tag,
38
38
  publish_connector_to_airbyte_registry,
@@ -1,72 +1,23 @@
1
1
  # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
2
  """GitHub Actions API utilities.
3
3
 
4
- This module provides core utilities for interacting with GitHub Actions workflows,
5
- including workflow dispatch, run discovery, and authentication. These utilities
4
+ This module provides utilities for interacting with GitHub Actions workflows,
5
+ including workflow dispatch, run discovery, and job status. These utilities
6
6
  are used by MCP tools but are not MCP-specific.
7
+
8
+ For general GitHub API utilities (authentication, PR info, file contents),
9
+ see the github_api module.
7
10
  """
8
11
 
9
12
  from __future__ import annotations
10
13
 
11
- import os
12
- import shutil
13
- import subprocess
14
14
  import time
15
15
  from dataclasses import dataclass
16
16
  from datetime import datetime, timedelta
17
17
 
18
18
  import requests
19
19
 
20
- GITHUB_API_BASE = "https://api.github.com"
21
-
22
-
23
- def resolve_github_token(preferred_env_vars: list[str] | None = None) -> str:
24
- """Resolve GitHub token from environment variables or gh CLI.
25
-
26
- Checks environment variables in order of preference, returning the first
27
- non-empty value found. If no environment variables are set, attempts to
28
- get a token from the gh CLI tool using 'gh auth token'.
29
-
30
- Args:
31
- preferred_env_vars: List of environment variable names to check in order.
32
- Defaults to ["GITHUB_CI_WORKFLOW_TRIGGER_PAT", "GITHUB_TOKEN"].
33
-
34
- Returns:
35
- GitHub token string.
36
-
37
- Raises:
38
- ValueError: If no GitHub token is found in env vars or gh CLI.
39
- """
40
- if preferred_env_vars is None:
41
- preferred_env_vars = ["GITHUB_CI_WORKFLOW_TRIGGER_PAT", "GITHUB_TOKEN"]
42
-
43
- # Check environment variables first
44
- for env_var in preferred_env_vars:
45
- token = os.getenv(env_var)
46
- if token:
47
- return token
48
-
49
- # Fall back to gh CLI if available
50
- gh_path = shutil.which("gh")
51
- if gh_path:
52
- try:
53
- result = subprocess.run(
54
- [gh_path, "auth", "token"],
55
- capture_output=True,
56
- text=True,
57
- timeout=5,
58
- check=False,
59
- )
60
- if result.returncode == 0 and result.stdout.strip():
61
- return result.stdout.strip()
62
- except (subprocess.TimeoutExpired, subprocess.SubprocessError):
63
- pass
64
-
65
- env_var_list = ", ".join(preferred_env_vars)
66
- raise ValueError(
67
- f"No GitHub token found. Set one of: {env_var_list} environment variable, "
68
- "or authenticate with 'gh auth login'."
69
- )
20
+ from airbyte_ops_mcp.github_api import GITHUB_API_BASE, resolve_github_token
70
21
 
71
22
 
72
23
  @dataclass
@@ -106,51 +57,6 @@ class WorkflowJobInfo:
106
57
  """ISO 8601 timestamp when the job completed"""
107
58
 
108
59
 
109
- def get_file_contents_at_ref(
110
- owner: str,
111
- repo: str,
112
- path: str,
113
- ref: str,
114
- token: str | None = None,
115
- ) -> str | None:
116
- """Fetch file contents from GitHub at a specific ref.
117
-
118
- Uses the GitHub Contents API to retrieve file contents at a specific
119
- commit SHA, branch, or tag. This allows reading files without having
120
- the repository checked out locally.
121
-
122
- Args:
123
- owner: Repository owner (e.g., "airbytehq")
124
- repo: Repository name (e.g., "airbyte")
125
- path: Path to the file within the repository
126
- ref: Git ref (commit SHA, branch name, or tag)
127
- token: GitHub API token (optional for public repos, but recommended
128
- to avoid rate limiting)
129
-
130
- Returns:
131
- File contents as a string, or None if the file doesn't exist.
132
-
133
- Raises:
134
- requests.HTTPError: If API request fails (except 404).
135
- """
136
- url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/contents/{path}"
137
- headers = {
138
- "Accept": "application/vnd.github.raw+json",
139
- "X-GitHub-Api-Version": "2022-11-28",
140
- }
141
- if token:
142
- headers["Authorization"] = f"Bearer {token}"
143
-
144
- params = {"ref": ref}
145
-
146
- response = requests.get(url, headers=headers, params=params, timeout=30)
147
- if response.status_code == 404:
148
- return None
149
- response.raise_for_status()
150
-
151
- return response.text
152
-
153
-
154
60
  def get_workflow_jobs(
155
61
  owner: str,
156
62
  repo: str,
@@ -1,20 +1,178 @@
1
1
  # Copyright (c) 2025 Airbyte, Inc., all rights reserved.
2
- """GitHub API utilities for user and comment operations.
2
+ """GitHub API utilities.
3
3
 
4
- This module provides utilities for interacting with GitHub's REST API
5
- to retrieve user information and comment details. These utilities are
6
- used by MCP tools for authorization and audit purposes.
4
+ This module provides core utilities for interacting with GitHub's REST API,
5
+ including authentication, user/comment operations, PR information retrieval,
6
+ and file content fetching. These utilities are used by MCP tools and other
7
+ modules but are not MCP-specific.
7
8
  """
8
9
 
9
10
  from __future__ import annotations
10
11
 
12
+ import os
11
13
  import re
14
+ import shutil
15
+ import subprocess
12
16
  from dataclasses import dataclass
13
17
  from urllib.parse import urlparse
14
18
 
15
19
  import requests
16
20
 
17
- from airbyte_ops_mcp.github_actions import GITHUB_API_BASE, resolve_github_token
21
+ GITHUB_API_BASE = "https://api.github.com"
22
+
23
+
24
+ def resolve_github_token(preferred_env_vars: list[str] | None = None) -> str:
25
+ """Resolve GitHub token from environment variables or gh CLI.
26
+
27
+ Checks environment variables in order of preference, returning the first
28
+ non-empty value found. If no environment variables are set, attempts to
29
+ get a token from the gh CLI tool using 'gh auth token'.
30
+
31
+ Args:
32
+ preferred_env_vars: List of environment variable names to check in order.
33
+ Defaults to ["GITHUB_CI_WORKFLOW_TRIGGER_PAT", "GITHUB_TOKEN"].
34
+
35
+ Returns:
36
+ GitHub token string.
37
+
38
+ Raises:
39
+ ValueError: If no GitHub token is found in env vars or gh CLI.
40
+ """
41
+ if preferred_env_vars is None:
42
+ preferred_env_vars = ["GITHUB_CI_WORKFLOW_TRIGGER_PAT", "GITHUB_TOKEN"]
43
+
44
+ # Check environment variables first
45
+ for env_var in preferred_env_vars:
46
+ token = os.getenv(env_var)
47
+ if token:
48
+ return token
49
+
50
+ # Fall back to gh CLI if available
51
+ gh_path = shutil.which("gh")
52
+ if gh_path:
53
+ try:
54
+ result = subprocess.run(
55
+ [gh_path, "auth", "token"],
56
+ capture_output=True,
57
+ text=True,
58
+ timeout=5,
59
+ check=False,
60
+ )
61
+ if result.returncode == 0 and result.stdout.strip():
62
+ return result.stdout.strip()
63
+ except (subprocess.TimeoutExpired, subprocess.SubprocessError):
64
+ pass
65
+
66
+ env_var_list = ", ".join(preferred_env_vars)
67
+ raise ValueError(
68
+ f"No GitHub token found. Set one of: {env_var_list} environment variable, "
69
+ "or authenticate with 'gh auth login'."
70
+ )
71
+
72
+
73
+ @dataclass
74
+ class PRHeadInfo:
75
+ """Information about a PR's head commit."""
76
+
77
+ ref: str
78
+ """Branch name of the PR's head"""
79
+
80
+ sha: str
81
+ """Full commit SHA of the PR's head"""
82
+
83
+ short_sha: str
84
+ """First 7 characters of the commit SHA"""
85
+
86
+
87
+ def get_pr_head_ref(
88
+ owner: str,
89
+ repo: str,
90
+ pr_number: int,
91
+ token: str,
92
+ ) -> PRHeadInfo:
93
+ """Get the head ref (branch name) and SHA for a PR.
94
+
95
+ This is useful for resolving a PR number to the actual branch name,
96
+ which is required for workflow_dispatch API calls (which don't accept
97
+ refs/pull/{pr}/head format).
98
+
99
+ Args:
100
+ owner: Repository owner (e.g., "airbytehq")
101
+ repo: Repository name (e.g., "airbyte")
102
+ pr_number: Pull request number
103
+ token: GitHub API token
104
+
105
+ Returns:
106
+ PRHeadInfo with ref (branch name), sha, and short_sha.
107
+
108
+ Raises:
109
+ ValueError: If PR not found.
110
+ requests.HTTPError: If API request fails.
111
+ """
112
+ url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/pulls/{pr_number}"
113
+ headers = {
114
+ "Authorization": f"Bearer {token}",
115
+ "Accept": "application/vnd.github+json",
116
+ "X-GitHub-Api-Version": "2022-11-28",
117
+ }
118
+
119
+ response = requests.get(url, headers=headers, timeout=30)
120
+ if response.status_code == 404:
121
+ raise ValueError(f"PR {owner}/{repo}#{pr_number} not found")
122
+ response.raise_for_status()
123
+
124
+ pr_data = response.json()
125
+ sha = pr_data["head"]["sha"]
126
+ return PRHeadInfo(
127
+ ref=pr_data["head"]["ref"],
128
+ sha=sha,
129
+ short_sha=sha[:7],
130
+ )
131
+
132
+
133
+ def get_file_contents_at_ref(
134
+ owner: str,
135
+ repo: str,
136
+ path: str,
137
+ ref: str,
138
+ token: str | None = None,
139
+ ) -> str | None:
140
+ """Fetch file contents from GitHub at a specific ref.
141
+
142
+ Uses the GitHub Contents API to retrieve file contents at a specific
143
+ commit SHA, branch, or tag. This allows reading files without having
144
+ the repository checked out locally.
145
+
146
+ Args:
147
+ owner: Repository owner (e.g., "airbytehq")
148
+ repo: Repository name (e.g., "airbyte")
149
+ path: Path to the file within the repository
150
+ ref: Git ref (commit SHA, branch name, or tag)
151
+ token: GitHub API token (optional for public repos, but recommended
152
+ to avoid rate limiting)
153
+
154
+ Returns:
155
+ File contents as a string, or None if the file doesn't exist.
156
+
157
+ Raises:
158
+ requests.HTTPError: If API request fails (except 404).
159
+ """
160
+ url = f"{GITHUB_API_BASE}/repos/{owner}/{repo}/contents/{path}"
161
+ headers = {
162
+ "Accept": "application/vnd.github.raw+json",
163
+ "X-GitHub-Api-Version": "2022-11-28",
164
+ }
165
+ if token:
166
+ headers["Authorization"] = f"Bearer {token}"
167
+
168
+ params = {"ref": ref}
169
+
170
+ response = requests.get(url, headers=headers, params=params, timeout=30)
171
+ if response.status_code == 404:
172
+ return None
173
+ response.raise_for_status()
174
+
175
+ return response.text
18
176
 
19
177
 
20
178
  class GitHubCommentParseError(Exception):