Skip to content

ci-connector-ops: Check test strictness level on GA source connectors #19383

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Nov 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .github/workflows/connector_ops_ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
name: Connector Ops CI

on:
pull_request:
paths:
- "airbyte-integrations/connectors/source-**"
Comment on lines +4 to +6
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 only runs on connector changes

jobs:
test-strictness-level:
name: "Check test strictness level"
runs-on: ubuntu-latest
steps:
- name: Checkout Airbyte
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install Python
uses: actions/setup-python@v4
with:
python-version: "3.9"
- name: Install ci-connector-ops package
run: pip install --quiet -e ./tools/ci_connector_ops
Comment on lines +20 to +21
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 running a local package on /this/ branch

- name: Check test strictness level
run: check-test-strictness-level
3 changes: 3 additions & 0 deletions tools/ci_connector_ops/ci_connector_ops/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
#
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
#

import logging
import sys
from typing import List

from ci_connector_ops import utils

RELEASE_STAGE_TO_STRICTNESS_LEVEL_MAPPING = {"generally_available": "high"}


def find_connectors_with_bad_strictness_level() -> List[str]:
"""Check if changed connectors have the expected SAT test strictness level according to their release stage.
1. Identify changed connectors
2. Retrieve their release stage from the catalog
3. Parse their acceptance test config file
4. Check if the test strictness level matches the strictness level expected for their release stage.

Returns:
List[str]: List of changed connector names that are not matching test strictness level expectations.
"""
connectors_with_bad_strictness_level = []
changed_connector_names = utils.get_changed_connector_names()
for connector_name in changed_connector_names:
connector_release_stage = utils.get_connector_release_stage(connector_name)
expected_test_strictness_level = RELEASE_STAGE_TO_STRICTNESS_LEVEL_MAPPING.get(connector_release_stage)
_, acceptance_test_config = utils.get_acceptance_test_config(connector_name)
can_check_strictness_level = all(
[item is not None for item in [connector_release_stage, expected_test_strictness_level, acceptance_test_config]]
)
if can_check_strictness_level:
try:
assert acceptance_test_config.get("test_strictness_level") == expected_test_strictness_level
except AssertionError:
connectors_with_bad_strictness_level.append(connector_name)
return connectors_with_bad_strictness_level


def main():
connectors_with_bad_strictness_level = find_connectors_with_bad_strictness_level()
if connectors_with_bad_strictness_level:
logging.error(
f"The following GA connectors must enable high test strictness level: {connectors_with_bad_strictness_level}. Please check this documentation for details: https://docs.airbyte.com/connector-development/testing-connectors/source-acceptance-tests-reference/#strictness-level"
)
sys.exit(1)
else:
sys.exit(0)


if __name__ == "__main__":
main()
105 changes: 105 additions & 0 deletions tools/ci_connector_ops/ci_connector_ops/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
#
import logging
from typing import Dict, Optional, Set, Tuple

import git
import requests
import yaml

AIRBYTE_REPO = git.Repo(".")
OSS_CATALOG_URL = "https://storage.googleapis.com/prod-airbyte-cloud-connector-metadata-service/oss_catalog.json"
CONNECTOR_PATH_PREFIX = "airbyte-integrations/connectors"
SOURCE_CONNECTOR_PATH_PREFIX = CONNECTOR_PATH_PREFIX + "/source-"
ACCEPTANCE_TEST_CONFIG_FILE_NAME = "acceptance-test-config.yml"
AIRBYTE_DOCKER_REPO = "airbyte"
SOURCE_DEFINITIONS_FILE_PATH = "airbyte-config/init/src/main/resources/seed/source_definitions.yaml"
DESTINATION_DEFINITIONS_FILE_PATH = "airbyte-config/init/src/main/resources/seed/destination_definitions.yaml"
DEFINITIONS_FILE_PATH = {"source": SOURCE_DEFINITIONS_FILE_PATH, "destination": DESTINATION_DEFINITIONS_FILE_PATH}


def download_catalog(catalog_url):
response = requests.get(catalog_url)
return response.json()


OSS_CATALOG = download_catalog(OSS_CATALOG_URL)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Since you are using the published catalog, there's a race-condition if /this/ PR is changing the release stage that the test won't run. Could we look at the local YML files in this branch instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yes, you're right. Moreover, the approach I took to compute the diff would also detect connector changes made on master after the PR was opened and not rebased... I improved the behavior in 929e3b0



def read_definitions(definitions_file_path: str) -> Dict:
with open(definitions_file_path) as definitions_file:
return yaml.safe_load(definitions_file)


def get_changed_connector_names() -> Set[str]:
"""Retrieve a list of connector names that were changed in the current branch (compared to master).

Returns:
Set[str]: Set of connector names e.g ["source-pokeapi"]
"""
changed_source_connector_files = {
file_path
for file_path in AIRBYTE_REPO.git.diff("--name-only", "origin/master...").split("\n")
if file_path.startswith(SOURCE_CONNECTOR_PATH_PREFIX)
}

def get_connector_name_from_path(path):
return path.split("/")[2]

return {get_connector_name_from_path(changed_file) for changed_file in changed_source_connector_files}


def get_connector_definition(connector_name: str) -> Optional[Dict]:
"""Find a connector definition from the catalog.

Args:
connector_name (str): The connector name. E.G. 'source-pokeapi'

Raises:
Exception: Raised if the definition type (source/destination) could not be determined from connector name.

Returns:
Optional[Dict]: The definition if the connector was found in the catalo. Returns None otherwise.
"""
try:
definition_type = connector_name.split("-")[0]
assert definition_type in ["source", "destination"]
except AssertionError:
raise Exception(f"Could not determine the definition type for {connector_name}.")
definitions = read_definitions(DEFINITIONS_FILE_PATH[definition_type])
for definition in definitions:
if definition["dockerRepository"].replace(f"{AIRBYTE_DOCKER_REPO}/", "") == connector_name:
return definition


def get_connector_release_stage(connector_name: str) -> Optional[str]:
"""Retrieve the connector release stage (E.G. alpha/beta/generally_available).

Args:
connector_name (str): The connector name. E.G. 'source-pokeapi'

Returns:
Optional[str]: The connector release stage if it was defined. Returns None otherwise.
"""
definition = get_connector_definition(connector_name)
return definition.get("releaseStage")


def get_acceptance_test_config(connector_name: str) -> Tuple[str, Dict]:
"""Retrieve the acceptance test config file path and its content as dict.

Args:
connector_name (str): The connector name. E.G. 'source-pokeapi'


Returns:
Tuple(str, Dict): The acceptance test config file path and its content as dict.
"""
acceptance_test_config_path = f"{CONNECTOR_PATH_PREFIX}/{connector_name}/{ACCEPTANCE_TEST_CONFIG_FILE_NAME}"
try:
with open(acceptance_test_config_path) as acceptance_test_config_file:
return acceptance_test_config_path, yaml.safe_load(acceptance_test_config_file)
except FileNotFoundError:
logging.warning(f"No {ACCEPTANCE_TEST_CONFIG_FILE_NAME} file found for {connector_name}")
return None, None
25 changes: 25 additions & 0 deletions tools/ci_connector_ops/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
#


from setuptools import find_packages, setup

MAIN_REQUIREMENTS = ["requests", "PyYAML~=6.0", "GitPython~=3.1.29"]


setup(
version="0.1.0",
name="ci_connector_ops",
description="Packaged maintained by the connector operations team to perform CI for connectors",
author="Airbyte",
author_email="[email protected]",
packages=find_packages(),
install_requires=MAIN_REQUIREMENTS,
python_requires=">=3.9",
entry_points={
"console_scripts": [
"check-test-strictness-level = ci_connector_ops.check_test_strictness_level:main",
],
},
)