Skip to content

Commit 18abf99

Browse files
committed
cloud-availability-updater: implement git interaction
1 parent 80d5f69 commit 18abf99

File tree

6 files changed

+214
-61
lines changed

6 files changed

+214
-61
lines changed

tools/ci_connector_ops/ci_connector_ops/qa_engine/cloud_availability_updater.py

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,88 +7,93 @@
77
import logging
88
from pathlib import Path
99
import subprocess
10-
from typing import Iterable, Optional
10+
from typing import Optional
1111

1212
import git
13-
from github import Github
1413

1514
from .models import ConnectorQAReport
15+
from .constants import (
16+
AIRBYTE_CLOUD_GITHUB_REPO_URL,
17+
AIRBYTE_CLOUD_MAIN_BRANCH_NAME
18+
)
1619

17-
logging.basicConfig(level="INFO")
20+
logger = logging.getLogger(__name__)
21+
logger.setLevel(logging.INFO)
1822

19-
AIRBYTE_CLOUD_REPO_PATH = Path(os.environ["AIRBYTE_CLOUD_REPO_PATH"])
20-
GITHUB_ACCESS_TOKEN = os.environ["GITHUB_ACCESS_TOKEN"]
21-
AIRBYTE_CLOUD_REPO = git.Repo(AIRBYTE_CLOUD_REPO_PATH)
22-
PATH_TO_DEFINITIONS_MASKS = {
23-
"source": AIRBYTE_CLOUD_REPO_PATH / "cloud-config/cloud-config-seed/src/main/resources/seed/source_definitions_mask.yaml",
24-
"destination": AIRBYTE_CLOUD_REPO_PATH / "cloud-config/cloud-config-seed/src/main/resources/seed/destination_definitions_mask.yaml"
25-
}
2623

27-
def checkout_new_branch(connector: ConnectorQAReport) -> git.Head:
28-
new_branch_name = f"cloud-availability-updater/{connector.connector_technical_name}-to-cloud"
29-
new_branch = AIRBYTE_CLOUD_REPO.create_head(new_branch_name)
24+
def clone_airbyte_cloud_repo(local_repo_path: Path) -> git.Repo:
25+
logging.info(f"Cloning {AIRBYTE_CLOUD_GITHUB_REPO_URL} to {local_repo_path}")
26+
return git.Repo.clone_from(AIRBYTE_CLOUD_GITHUB_REPO_URL, local_repo_path, branch=AIRBYTE_CLOUD_MAIN_BRANCH_NAME)
27+
28+
def get_definitions_mask_path(local_repo_path, definition_type: str) -> Path:
29+
definitions_mask_path = local_repo_path / f"cloud-config/cloud-config-seed/src/main/resources/seed/{definition_type}_definitions_mask.yaml"
30+
if not definitions_mask_path.exists():
31+
raise FileNotFoundError(f"Can't find the {definition_type} definitions mask")
32+
return definitions_mask_path
33+
34+
def checkout_new_branch(airbyte_cloud_repo: git.Repo, new_branch_name: str) -> git.Head:
35+
new_branch = airbyte_cloud_repo.create_head(new_branch_name)
3036
new_branch.checkout()
3137
logging.info(f"Checked out branch {new_branch_name}.")
3238
return new_branch
3339

34-
def update_definition_mask(connector: ConnectorQAReport) -> Optional[Path]:
35-
definition_mask_path = PATH_TO_DEFINITIONS_MASKS[connector.connector_type]
36-
with open(definition_mask_path, "r") as definition_mask:
40+
def update_definitions_mask(connector: ConnectorQAReport, definitions_mask_path: Path) -> Optional[Path]:
41+
with open(definitions_mask_path, "r") as definition_mask:
3742
connector_already_in_mask = connector.connector_definition_id in definition_mask.read()
3843
if connector_already_in_mask:
39-
logging.warning(f"{connector.connector_name}'s definition id is already in {definition_mask_path}.")
44+
logging.warning(f"{connector.connector_name}'s definition id is already in {definitions_mask_path}.")
4045
return None
4146

4247
to_append = f"""# {connector.connector_name} (from cloud availability updater)
4348
- {connector.connector_type}DefinitionId: {connector.connector_definition_id}
4449
"""
4550

46-
with open(definition_mask_path, "a") as f:
51+
with open(definitions_mask_path, "a") as f:
4752
f.write(to_append)
48-
logging.info(f"Updated {definition_mask_path} with {connector.connector_name}'s definition id.")
49-
return definition_mask_path
53+
logging.info(f"Updated {definitions_mask_path} with {connector.connector_name}'s definition id.")
54+
return definitions_mask_path
5055

51-
def run_generate_cloud_connector_catalog() -> str:
56+
def run_generate_cloud_connector_catalog(airbyte_cloud_repo_path: Path) -> str:
5257
result = subprocess.check_output(
53-
f"cd {AIRBYTE_CLOUD_REPO_PATH} && ./gradlew :cloud-config:cloud-config-seed:generateCloudConnectorCatalog",
58+
f"cd {airbyte_cloud_repo_path} && ./gradlew :cloud-config:cloud-config-seed:generateCloudConnectorCatalog",
5459
shell=True
5560
)
5661
logging.info("Ran generateCloudConnectorCatalog Gradle Task")
5762
return result.decode()
5863

59-
def commit_files(connector: ConnectorQAReport):
60-
AIRBYTE_CLOUD_REPO.git.add('--all')
61-
AIRBYTE_CLOUD_REPO.git.commit(m=f"🤖 Add {connector.connector_technical_name} to cloud")
64+
def commit_all_files(airbyte_cloud_repo: git.Repo, commit_message: str):
65+
airbyte_cloud_repo.git.add('--all')
66+
airbyte_cloud_repo.git.commit(m=commit_message)
6267
logging.info(f"Committed file changes.")
6368

64-
def push_branch(branch, dry_run=True):
65-
if not dry_run:
66-
AIRBYTE_CLOUD_REPO.git.push("--set-upstream", "origin", branch)
67-
logging.info(f"Pushed branch {branch} to origin")
68-
69-
def create_pr(connector: ConnectorQAReport, branch: str, dry_run=True):
70-
g = Github(GITHUB_ACCESS_TOKEN)
71-
72-
repo = g.get_repo("airbytehq/airbyte-cloud")
73-
body = f"""
74-
The Cloud Availability Updater decided that it's the right time to make {connector.connector_name} available on Cloud!
75-
```
76-
{connector}
77-
```
69+
def push_branch(airbyte_cloud_repo: git.Repo, branch:str):
70+
airbyte_cloud_repo.git.push("--set-upstream", "origin", branch)
71+
logging.info(f"Pushed branch {branch} to origin")
72+
73+
def deploy_new_connector_to_cloud_repo(
74+
airbyte_cloud_repo_path: Path,
75+
airbyte_cloud_repo: git.Repo,
76+
connector: ConnectorQAReport
77+
):
78+
"""Updates the local definitions mask on Airbyte cloud repo.
79+
Calls the generateCloudConnectorCatalog gradle task.
80+
Commits these changes on a new branch.
81+
Pushes these new branch to the origin.
82+
83+
Args:
84+
airbyte_cloud_repo_path (Path): The local path to Airbyte Cloud repository.
85+
airbyte_cloud_repo (git.Repo): The Airbyte Cloud repo instance.
86+
connector (ConnectorQAReport): The connector to add to a definitions mask.
7887
"""
79-
if not dry_run:
80-
return repo.create_pull(title=f"🤖 Add {connector.connector_technical_name} to cloud", body=body, head=branch, base="master")
81-
82-
def deploy_eligible_connector(connector: ConnectorQAReport, dry_run=True):
83-
AIRBYTE_CLOUD_REPO.heads.master.checkout()
84-
new_branch = checkout_new_branch(connector)
85-
update_definition_mask(connector)
86-
run_generate_cloud_connector_catalog()
87-
commit_files(connector)
88-
push_branch(new_branch, dry_run=dry_run)
89-
create_pr(connector, new_branch, dry_run=dry_run)
90-
91-
def deploy_eligible_connectors(connectors_eligible_for_cloud: Iterable[ConnectorQAReport]):
92-
for connector in connectors_eligible_for_cloud:
93-
deploy_eligible_connector(connector)
94-
AIRBYTE_CLOUD_REPO.heads.master.checkout()
88+
airbyte_cloud_repo.git.checkout(AIRBYTE_CLOUD_MAIN_BRANCH_NAME)
89+
new_branch_name = f"cloud-availability-updater/deploy-{connector.connector_technical_name}"
90+
checkout_new_branch(airbyte_cloud_repo, new_branch_name)
91+
definitions_mask_path = get_definitions_mask_path(airbyte_cloud_repo_path, connector.connector_type)
92+
update_definitions_mask(connector, definitions_mask_path)
93+
run_generate_cloud_connector_catalog(airbyte_cloud_repo_path)
94+
commit_all_files(
95+
airbyte_cloud_repo,
96+
f"🤖 Add {connector.connector_name} connector to cloud"
97+
)
98+
push_branch(airbyte_cloud_repo, new_branch_name)
99+
airbyte_cloud_repo.git.checkout(AIRBYTE_CLOUD_MAIN_BRANCH_NAME)

tools/ci_connector_ops/ci_connector_ops/qa_engine/constants.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@
1313
]
1414

1515
GCS_QA_REPORT_PATH = "gs://prod-airbyte-cloud-connector-metadata-service/qa_report.json"
16+
AIRBYTE_CLOUD_GITHUB_REPO_URL = "https://github.com/airbytehq/airbyte-cloud.git"
17+
AIRBYTE_CLOUD_MAIN_BRANCH_NAME = "master"

tools/ci_connector_ops/ci_connector_ops/qa_engine/models.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@
33
#
44

55

6-
from datetime import datetime
76
from enum import Enum
8-
from typing import List, Optional
7+
from typing import List
98

109
from pydantic import BaseModel
1110

@@ -30,8 +29,7 @@ class ConnectorQAReport(BaseModel):
3029
is_appropriate_for_cloud_use: bool
3130
latest_build_is_successful: bool
3231
documentation_is_available: bool
33-
is_eligible_for_cloud: bool
34-
report_generation_datetime: datetime
32+
3533

3634
class QAReport(BaseModel):
3735
connectors_qa_report: List[ConnectorQAReport]

tools/ci_connector_ops/pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
markers =
3+
slow: marks tests as slow (deselect with '-m "not slow"')
4+
serial

tools/ci_connector_ops/setup.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"pandas~=1.5.3",
1313
"pandas-gbq~=0.19.0",
1414
"pydantic~=1.10.4",
15-
"PyGithub"
1615
"fsspec~=2023.1.0",
1716
"gcsfs~=2023.1.0"
1817
]
@@ -24,7 +23,7 @@
2423

2524

2625
setup(
27-
version="0.1.6",
26+
version="0.1.9",
2827
name="ci_connector_ops",
2928
description="Packaged maintained by the connector operations team to perform CI for connectors",
3029
author="Airbyte",
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#
2+
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
3+
#
4+
5+
6+
from datetime import datetime
7+
from pathlib import Path
8+
9+
import pytest
10+
import git
11+
import yaml
12+
13+
from ci_connector_ops.qa_engine import cloud_availability_updater, models
14+
15+
@pytest.fixture(scope="module")
16+
def dummy_repo_path(tmp_path_factory) -> Path:
17+
repo_path = tmp_path_factory.mktemp("cloud_availability_updater_tests") / "airbyte-cloud"
18+
repo_path.mkdir()
19+
return repo_path
20+
21+
@pytest.fixture(scope="module")
22+
def dummy_repo(dummy_repo_path) -> git.Repo:
23+
seed_dir = dummy_repo_path / "cloud-config/cloud-config-seed/src/main/resources/seed"
24+
seed_dir.mkdir(parents=True)
25+
repo = git.Repo.init(dummy_repo_path)
26+
source_definitions_mask_path = seed_dir / "source_definitions_mask.yaml"
27+
destination_definitions_mask_path = seed_dir / "destination_definitions_mask.yaml"
28+
source_definitions_mask_path.touch()
29+
destination_definitions_mask_path.touch()
30+
repo.git.add("--all")
31+
repo.git.commit(m=f"🤖 Initialized the repo")
32+
return repo
33+
34+
35+
@pytest.fixture
36+
def checkout_master(dummy_repo):
37+
"""
38+
Ensure we're always on dummy repo master before and after each test using this fixture
39+
"""
40+
yield dummy_repo.heads.master.checkout()
41+
dummy_repo.heads.master.checkout()
42+
43+
def test_get_definitions_mask_path(checkout_master, dummy_repo_path: Path):
44+
path = cloud_availability_updater.get_definitions_mask_path(dummy_repo_path, "source")
45+
assert path.exists() and path.name == "source_definitions_mask.yaml"
46+
path = cloud_availability_updater.get_definitions_mask_path(dummy_repo_path, "destination")
47+
assert path.exists() and path.name == "destination_definitions_mask.yaml"
48+
with pytest.raises(FileNotFoundError):
49+
cloud_availability_updater.get_definitions_mask_path(dummy_repo_path, "foobar")
50+
51+
def test_checkout_new_branch(mocker, checkout_master, dummy_repo):
52+
new_branch = cloud_availability_updater.checkout_new_branch(dummy_repo, "test-branch")
53+
assert new_branch.name == dummy_repo.active_branch.name == "test-branch"
54+
55+
56+
@pytest.mark.parametrize(
57+
"definitions_mask_content_before_update, definition_id, expect_update",
58+
[
59+
("", "abcdefg", True),
60+
("abcdefg", "abcdefg", False),
61+
]
62+
63+
)
64+
def test_update_definitions_mask(
65+
mocker,
66+
tmp_path,
67+
definitions_mask_content_before_update,
68+
definition_id,
69+
expect_update
70+
):
71+
connector = mocker.Mock(
72+
connector_name="foobar",
73+
connector_definition_id=definition_id,
74+
connector_type="unknown"
75+
)
76+
definitions_mask_path = tmp_path / "definitions_mask.yaml"
77+
with open(definitions_mask_path, "w") as definitions_mask:
78+
definitions_mask.write(definitions_mask_content_before_update)
79+
updated_path = cloud_availability_updater.update_definitions_mask(connector, definitions_mask_path)
80+
if not expect_update:
81+
assert updated_path is None
82+
else:
83+
with open(updated_path, "r") as definitions_mask:
84+
raw_content = definitions_mask.read()
85+
definitions = yaml.safe_load(raw_content)
86+
assert isinstance(definitions, list)
87+
assert definitions[0]["unknownDefinitionId"] == definition_id
88+
assert len(
89+
[
90+
d for d in definitions
91+
if d["unknownDefinitionId"] == definition_id
92+
]) == 1
93+
assert "# foobar (from cloud availability updater)" in raw_content
94+
assert raw_content[-1] == "\n"
95+
96+
def test_commit_files(checkout_master, dummy_repo, dummy_repo_path):
97+
cloud_availability_updater.checkout_new_branch(dummy_repo, "test-commit-files")
98+
commit_message = "🤖 Add new connector to cloud"
99+
with open(dummy_repo_path / "test_file.txt", "w") as f:
100+
f.write(".")
101+
102+
cloud_availability_updater.commit_all_files(dummy_repo, commit_message)
103+
104+
assert dummy_repo.head.reference.commit.message == commit_message + "\n"
105+
edited_files = dummy_repo.git.diff("--name-only", checkout_master.name).split("\n")
106+
assert "test_file.txt" in edited_files
107+
108+
def test_push_branch(mocker):
109+
mock_repo = mocker.Mock()
110+
cloud_availability_updater.push_branch(mock_repo, "new_branch")
111+
mock_repo.git.push.assert_called_once_with("--set-upstream", "origin", "new_branch")
112+
113+
@pytest.mark.slow
114+
def test_deploy_new_connector_to_cloud_repo(mocker, tmp_path):
115+
mocker.patch.object(cloud_availability_updater, "push_branch")
116+
mocker.patch.object(cloud_availability_updater, "run_generate_cloud_connector_catalog")
117+
118+
repo_path = tmp_path / "airbyte-cloud"
119+
repo_path.mkdir()
120+
airbyte_cloud_repo = cloud_availability_updater.clone_airbyte_cloud_repo(repo_path)
121+
source_definitions_mask_path = repo_path / "cloud-config/cloud-config-seed/src/main/resources/seed/source_definitions_mask.yaml"
122+
destination_definitions_mask_path = repo_path / "cloud-config/cloud-config-seed/src/main/resources/seed/destination_definitions_mask.yaml"
123+
assert source_definitions_mask_path.exists() and destination_definitions_mask_path.exists()
124+
125+
connector = models.ConnectorQAReport(
126+
connector_type="source",
127+
connector_name="foobar",
128+
connector_technical_name="source-foobar",
129+
connector_definition_id="abcdefg",
130+
connector_version="0.0.0",
131+
release_stage="alpha",
132+
is_on_cloud=False,
133+
is_appropriate_for_cloud_use=True,
134+
latest_build_is_successful=True,
135+
documentation_is_available=True
136+
)
137+
cloud_availability_updater.deploy_new_connector_to_cloud_repo(repo_path, airbyte_cloud_repo, connector)
138+
new_branch_name = f"cloud-availability-updater/deploy-{connector.connector_technical_name}"
139+
140+
cloud_availability_updater.push_branch.assert_called_once_with(airbyte_cloud_repo, new_branch_name)
141+
cloud_availability_updater.run_generate_cloud_connector_catalog.assert_called_once_with(repo_path)
142+
airbyte_cloud_repo.git.checkout(new_branch_name)
143+
edited_files = airbyte_cloud_repo.git.diff("--name-only", "master").split("\n")
144+
assert edited_files == ['cloud-config/cloud-config-seed/src/main/resources/seed/source_definitions_mask.yaml']
145+
assert airbyte_cloud_repo.head.reference.commit.message == "🤖 Add foobar connector to cloud\n"

0 commit comments

Comments
 (0)