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.
- {airbyte_internal_ops-0.3.1.dist-info → airbyte_internal_ops-0.4.1.dist-info}/METADATA +1 -1
- {airbyte_internal_ops-0.3.1.dist-info → airbyte_internal_ops-0.4.1.dist-info}/RECORD +16 -16
- airbyte_ops_mcp/cli/gh.py +105 -2
- airbyte_ops_mcp/cli/registry.py +2 -2
- airbyte_ops_mcp/github_actions.py +6 -100
- airbyte_ops_mcp/github_api.py +163 -5
- airbyte_ops_mcp/mcp/{github.py → github_actions.py} +117 -5
- airbyte_ops_mcp/mcp/prerelease.py +53 -63
- airbyte_ops_mcp/mcp/prod_db_queries.py +298 -1
- airbyte_ops_mcp/mcp/regression_tests.py +20 -9
- airbyte_ops_mcp/mcp/server.py +2 -2
- airbyte_ops_mcp/prod_db_access/queries.py +68 -0
- airbyte_ops_mcp/prod_db_access/sql.py +108 -0
- airbyte_ops_mcp/regression_tests/connector_runner.py +8 -4
- {airbyte_internal_ops-0.3.1.dist-info → airbyte_internal_ops-0.4.1.dist-info}/WHEEL +0 -0
- {airbyte_internal_ops-0.3.1.dist-info → airbyte_internal_ops-0.4.1.dist-info}/entry_points.txt +0 -0
|
@@ -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=
|
|
6
|
-
airbyte_ops_mcp/github_api.py,sha256=
|
|
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=
|
|
358
|
-
airbyte_ops_mcp/cli/registry.py,sha256=
|
|
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/
|
|
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=
|
|
383
|
-
airbyte_ops_mcp/mcp/prod_db_queries.py,sha256=
|
|
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=
|
|
387
|
-
airbyte_ops_mcp/mcp/server.py,sha256=
|
|
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=
|
|
393
|
-
airbyte_ops_mcp/prod_db_access/sql.py,sha256=
|
|
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=
|
|
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.
|
|
418
|
-
airbyte_internal_ops-0.
|
|
419
|
-
airbyte_internal_ops-0.
|
|
420
|
-
airbyte_internal_ops-0.
|
|
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.
|
|
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 =
|
|
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())
|
airbyte_ops_mcp/cli/registry.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
5
|
-
including workflow dispatch, run discovery, and
|
|
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
|
|
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,
|
airbyte_ops_mcp/github_api.py
CHANGED
|
@@ -1,20 +1,178 @@
|
|
|
1
1
|
# Copyright (c) 2025 Airbyte, Inc., all rights reserved.
|
|
2
|
-
"""GitHub API utilities
|
|
2
|
+
"""GitHub API utilities.
|
|
3
3
|
|
|
4
|
-
This module provides utilities for interacting with GitHub's REST API
|
|
5
|
-
|
|
6
|
-
used by MCP tools
|
|
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
|
-
|
|
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):
|