Skip to content

Commit 90828d4

Browse files
authored
Version connector build statuses (#22029)
* Refactor the job log json to include the docker_version * Output to versioned folder * Handle the case where people call the action without connector prefixed * Retrieve status of each connector * Use build report statuses in the QA Engine * Cast build status as an enum
1 parent 9c7decc commit 90828d4

File tree

15 files changed

+442
-216
lines changed

15 files changed

+442
-216
lines changed

airbyte-integrations/bases/source-acceptance-test/README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# Source Acceptance Tests
2-
This package gathers multiple test suites to assess the sanity of any Airbyte **source** connector.
2+
This package gathers multiple test suites to assess the sanity of any Airbyte **source** connector.
33
It is shipped as a [pytest](https://docs.pytest.org/en/7.1.x/) plugin and relies on pytest to discover, configure and execute tests.
44
Test-specific documentation can be found [here](https://docs.airbyte.com/connector-development/testing-connectors/source-acceptance-tests-reference/)).
55

66
## Running the acceptance tests on a source connector:
7-
1. `cd` into your connector project (e.g. `airbyte-integrations/connectors/source-pokeapi`)
7+
1. `cd` into your connector project (e.g. `airbyte-integrations/connectors/source-pokeapi`)
88
2. Edit `acceptance-test-config.yml` according to your need. Please refer to our [Source Acceptance Test Reference](https://docs.airbyte.com/connector-development/testing-connectors/source-acceptance-tests-reference/) if you need details about the available options.
99
3. Build the connector docker image ( e.g.: `docker build . -t airbyte/source-pokeapi:dev`)
1010
4. Use one of the following ways to run tests (**from your connector project directory**)
@@ -33,9 +33,9 @@ _Note: this will use the latest docker image for source-acceptance-test and will
3333
* When running or `./acceptance-test-docker.sh` in a connector project
3434
* When running `/test` command on a GitHub pull request.
3535
* When running `/publish` command on a GitHub pull request.
36-
* When running ` integration-test` GitHub action that is creating the [connector builds summary](https://github.com/airbytehq/airbyte/blob/master/airbyte-integrations/builds.md).
36+
* When running ` integration-test` GitHub action that is creating the JSON files linked to from [connector builds summary](https://github.com/airbytehq/airbyte/blob/master/airbyte-integrations/builds.md).
3737

38-
## Developing on the SAT
38+
## Developing on the SAT
3939
You may want to iterate on the SAT project: adding new tests, fixing a bug etc.
4040
These iterations are more conveniently achieved by remaining in the current directory.
4141

airbyte-integrations/builds.md

Lines changed: 173 additions & 173 deletions
Large diffs are not rendered by default.

tools/bin/build_report.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
DESTINATION_DEFINITIONS_YAML = f"{CONNECTOR_DEFINITIONS_DIR}/destination_definitions.yaml"
3131
CONNECTORS_ROOT_PATH = "./airbyte-integrations/connectors"
3232
RELEVANT_BASE_MODULES = ["base-normalization", "source-acceptance-test"]
33+
CONNECTOR_BUILD_OUTPUT_URL = "https://dnsgjos7lj2fu.cloudfront.net/tests/history/connectors"
3334

3435
# Global vars
3536
TESTED_SOURCE = []
@@ -42,7 +43,7 @@
4243

4344

4445
def get_status_page(connector) -> str:
45-
response = requests.get(f"https://dnsgjos7lj2fu.cloudfront.net/tests/summary/{connector}/index.html")
46+
response = requests.get(f"{CONNECTOR_BUILD_OUTPUT_URL}/{connector}/index.html")
4647
if response.status_code == 200:
4748
return response.text
4849

tools/ci_connector_ops/ci_connector_ops/qa_checks.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010

1111
def check_documentation_file_exists(connector: Connector) -> bool:
12-
"""Check if a markdown file with connector documentation is available
12+
"""Check if a markdown file with connector documentation is available
1313
in docs/integrations/<connector-type>s/<connector-name>.md
1414
1515
Args:
@@ -38,7 +38,7 @@ def check_documentation_follows_guidelines(connector: Connector) -> bool:
3838
follows_guidelines = False
3939

4040
expected_sections = [
41-
"## Prerequisites",
41+
"## Prerequisites",
4242
"## Setup guide",
4343
"## Supported sync modes",
4444
"## Supported streams",
@@ -112,7 +112,7 @@ def check_connector_has_no_critical_vulnerabilities(connector: Connector) -> boo
112112
QA_CHECKS = [
113113
check_documentation_file_exists,
114114
# Disabling the following check because it's likely to not pass on a lot of connectors.
115-
# check_documentation_follows_guidelines,
115+
# check_documentation_follows_guidelines,
116116
check_changelog_entry_is_updated,
117117
check_connector_icon_is_available,
118118
check_connector_https_url_only,

tools/ci_connector_ops/ci_connector_ops/qa_engine/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
33
#
44

5-
5+
CONNECTOR_BUILD_OUTPUT_URL = "https://dnsgjos7lj2fu.cloudfront.net/tests/history/connectors"
66
CLOUD_CATALOG_URL = "https://storage.googleapis.com/prod-airbyte-cloud-connector-metadata-service/cloud_catalog.json"
77
OSS_CATALOG_URL = "https://storage.googleapis.com/prod-airbyte-cloud-connector-metadata-service/oss_catalog.json"
88

tools/ci_connector_ops/ci_connector_ops/qa_engine/enrichments.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
import pandas as pd
77

88
def get_enriched_catalog(
9-
oss_catalog: pd.DataFrame,
10-
cloud_catalog: pd.DataFrame,
9+
oss_catalog: pd.DataFrame,
10+
cloud_catalog: pd.DataFrame,
1111
adoption_metrics_per_connector_version: pd.DataFrame) -> pd.DataFrame:
1212
"""Merge OSS and Cloud catalog in a single dataframe on their definition id.
1313
Transformations:
@@ -35,7 +35,7 @@ def get_enriched_catalog(
3535
indicator=True,
3636
suffixes=("", "_cloud"),
3737
)
38-
38+
3939
enriched_catalog.columns = enriched_catalog.columns.str.replace(
4040
"(?<=[a-z])(?=[A-Z])", "_", regex=True
4141
).str.lower() # column names to snake case

tools/ci_connector_ops/ci_connector_ops/qa_engine/inputs.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,57 @@
55
import os
66
from importlib.resources import files
77
import json
8+
import logging
9+
10+
from .constants import CONNECTOR_BUILD_OUTPUT_URL
811

912
from google.oauth2 import service_account
1013
import requests
1114
import pandas as pd
15+
from typing import Optional
16+
17+
from enum import Enum
18+
19+
LOGGER = logging.getLogger(__name__)
20+
21+
class BUILD_STATUSES(str, Enum):
22+
SUCCESS = "success"
23+
FAILURE = "failure"
24+
NOT_FOUND = None
25+
26+
@classmethod
27+
def from_string(cls, string_value: Optional[str]) -> "BUILD_STATUSES":
28+
if string_value is None:
29+
return BUILD_STATUSES.NOT_FOUND
30+
31+
return BUILD_STATUSES[string_value.upper()]
32+
33+
34+
35+
def get_connector_build_output_url(connector_technical_name: str, connector_version: str) -> str:
36+
return f"{CONNECTOR_BUILD_OUTPUT_URL}/{connector_technical_name}/{connector_version}.json"
37+
38+
def fetch_latest_build_status_for_connector_version(connector_technical_name: str, connector_version: str) ->BUILD_STATUSES:
39+
"""Fetch the latest build status for a given connector version."""
40+
connector_build_output_url = get_connector_build_output_url(connector_technical_name, connector_version)
41+
connector_build_output_response = requests.get(connector_build_output_url)
42+
43+
# if the connector returned successfully, return the outcome
44+
if connector_build_output_response.status_code == 200:
45+
connector_build_output = connector_build_output_response.json()
46+
outcome = connector_build_output.get("outcome")
47+
48+
try:
49+
return BUILD_STATUSES.from_string(outcome)
50+
except KeyError:
51+
LOGGER.error(f"Error: Unexpected build status value: {outcome} for connector {connector_technical_name}:{connector_version}")
52+
return BUILD_STATUSES.NOT_FOUND
1253

54+
else:
55+
return BUILD_STATUSES.NOT_FOUND
1356

1457
def fetch_remote_catalog(catalog_url: str) -> pd.DataFrame:
15-
"""Fetch a combined remote catalog and return a single DataFrame
58+
"""Fetch a combined remote catalog and return a single DataFrame
1659
with sources and destinations defined by the connector_type column.
1760
1861
Args:

tools/ci_connector_ops/ci_connector_ops/qa_engine/main.py

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

55

6-
from .constants import CLOUD_CATALOG_URL, GCS_QA_REPORT_PATH, OSS_CATALOG_URL
7-
from . import enrichments, inputs, validations, outputs
6+
from .constants import CLOUD_CATALOG_URL, OSS_CATALOG_URL
7+
from . import enrichments, inputs, validations
88

99

1010
def main():
1111
oss_catalog = inputs.fetch_remote_catalog(OSS_CATALOG_URL)
1212
cloud_catalog = inputs.fetch_remote_catalog(CLOUD_CATALOG_URL)
1313
adoption_metrics_per_connector_version = inputs.fetch_adoption_metrics_per_connector_version()
1414
enriched_catalog = enrichments.get_enriched_catalog(
15-
oss_catalog,
16-
cloud_catalog,
15+
oss_catalog,
16+
cloud_catalog,
1717
adoption_metrics_per_connector_version
1818
)
1919
validations.get_qa_report(enriched_catalog, len(oss_catalog))

tools/ci_connector_ops/ci_connector_ops/qa_engine/validations.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Copyright (c) 2023 Airbyte, Inc., all rights reserved.
33
#
44

5-
65
from datetime import datetime
76
from typing import Iterable
87

@@ -11,6 +10,7 @@
1110

1211
from .constants import INAPPROPRIATE_FOR_CLOUD_USE_CONNECTORS
1312
from .models import ConnectorQAReport, QAReport
13+
from .inputs import fetch_latest_build_status_for_connector_version, BUILD_STATUSES
1414

1515
TRUTHY_COLUMNS_TO_BE_ELIGIBLE = [
1616
"documentation_is_available",
@@ -32,26 +32,31 @@ def is_eligible_for_promotion_to_cloud(connector_qa_data: pd.Series) -> bool:
3232
if connector_qa_data["is_on_cloud"]:
3333
return False
3434
return all([
35-
connector_qa_data[col]
35+
connector_qa_data[col]
3636
for col in TRUTHY_COLUMNS_TO_BE_ELIGIBLE
3737
])
3838

39+
def latest_build_is_successful(connector_qa_data: pd.Series) -> bool:
40+
connector_technical_name = connector_qa_data["connector_technical_name"]
41+
connector_version = connector_qa_data["connector_version"]
42+
latest_build_status = fetch_latest_build_status_for_connector_version(connector_technical_name, connector_version)
43+
return latest_build_status == BUILD_STATUSES.SUCCESS
3944

4045
def get_qa_report(enriched_catalog: pd.DataFrame, oss_catalog_length: int) -> pd.DataFrame:
4146
"""Perform validation steps on top of the enriched catalog.
4247
Adds the following columns:
4348
- documentation_is_available:
4449
GET the documentation URL and expect a 200 status code.
45-
- is_appropriate_for_cloud_use:
50+
- is_appropriate_for_cloud_use:
4651
Determined from an hardcoded list of definition ids inappropriate for cloud use.
4752
- latest_build_is_successful:
4853
Check if the latest build for the current connector version is successful.
4954
- number_of_connections:
5055
Get the number of connections using this connector version from our datawarehouse.
5156
- number_of_users:
52-
Get the number of users using this connector version from our datawarehouse.
57+
Get the number of users using this connector version from our datawarehouse.
5358
- sync_success_rate:
54-
Get the sync success rate of the connections with this connector version from our datawarehouse.
59+
Get the sync success rate of the connections with this connector version from our datawarehouse.
5560
Args:
5661
enriched_catalog (pd.DataFrame): The enriched catalog.
5762
oss_catalog_length (pd.DataFrame): The length of the OSS catalog, for sanity check.
@@ -62,12 +67,8 @@ def get_qa_report(enriched_catalog: pd.DataFrame, oss_catalog_length: int) -> pd
6267
qa_report = enriched_catalog.copy(deep=True)
6368
qa_report["documentation_is_available"] = qa_report.documentation_url.apply(url_is_reachable)
6469
qa_report["is_appropriate_for_cloud_use"] = qa_report.connector_definition_id.apply(is_appropriate_for_cloud_use)
65-
66-
# TODO YET TO IMPLEMENT VALIDATIONS
67-
qa_report["latest_build_is_successful"] = False # TODO, tracked in https://github.com/airbytehq/airbyte/issues/21720
6870

69-
qa_report["is_eligible_for_promotion_to_cloud"] = qa_report.apply(is_eligible_for_promotion_to_cloud, axis="columns")
70-
qa_report["report_generation_datetime"] = datetime.utcnow()
71+
qa_report["latest_build_is_successful"] = qa_report.apply(latest_build_is_successful, axis="columns")
7172

7273
qa_report["is_eligible_for_promotion_to_cloud"] = qa_report.apply(is_eligible_for_promotion_to_cloud, axis="columns")
7374
qa_report["report_generation_datetime"] = datetime.utcnow()

tools/ci_connector_ops/ci_connector_ops/utils.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,6 @@ def download_catalog(catalog_url):
3636
OSS_CATALOG = download_catalog(OSS_CATALOG_URL)
3737

3838

39-
40-
41-
4239
class ConnectorInvalidNameError(Exception):
4340
pass
4441

@@ -108,15 +105,15 @@ def icon_path(self) -> Path:
108105
@property
109106
def code_directory(self) -> Path:
110107
return Path(f"./airbyte-integrations/connectors/{self.technical_name}")
111-
108+
112109
@property
113110
def version(self) -> str:
114111
with open(self.code_directory / "Dockerfile") as f:
115112
for line in f:
116113
if "io.airbyte.version" in line:
117114
return line.split("=")[1].strip()
118115
raise ConnectorVersionNotFound("""
119-
Could not find the connector version from its Dockerfile.
116+
Could not find the connector version from its Dockerfile.
120117
The io.airbyte.version tag is missing.
121118
""")
122119

tools/ci_connector_ops/setup.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# Copyright (c) 2022 Airbyte, Inc., all rights reserved.
33
#
44

5-
65
from setuptools import find_packages, setup
76

87
MAIN_REQUIREMENTS = [
@@ -21,7 +20,6 @@
2120
"pytest-mock~=3.10.0",
2221
]
2322

24-
2523
setup(
2624
version="0.1.10",
2725
name="ci_connector_ops",

tools/ci_connector_ops/tests/test_qa_engine/test_inputs.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
import pandas as pd
99
import pytest
10+
from unittest.mock import MagicMock, call
11+
import requests
1012

1113
from ci_connector_ops.qa_engine import inputs, constants
1214

@@ -56,3 +58,86 @@ def test_fetch_adoption_metrics_per_connector_version(mocker):
5658
project_id=expected_project_id,
5759
credentials=inputs.service_account.Credentials.from_service_account_info.return_value
5860
)
61+
62+
@pytest.mark.parametrize("connector_name, connector_version, mocked_json_payload, mocked_status_code, expected_status", [
63+
(
64+
"connectors/source-pokeapi",
65+
"0.1.5",
66+
{
67+
"link": "https://github.com/airbytehq/airbyte/actions/runs/4029659593",
68+
"outcome": "success",
69+
"docker_version": "0.1.5",
70+
"timestamp": "1674872401",
71+
"connector": "connectors/source-pokeapi"
72+
},
73+
200,
74+
inputs.BUILD_STATUSES.SUCCESS
75+
),
76+
(
77+
"connectors/source-pokeapi",
78+
"0.1.5",
79+
{
80+
"link": "https://github.com/airbytehq/airbyte/actions/runs/4029659593",
81+
"outcome": "failure",
82+
"docker_version": "0.1.5",
83+
"timestamp": "1674872401",
84+
"connector": "connectors/source-pokeapi"
85+
},
86+
200,
87+
inputs.BUILD_STATUSES.FAILURE
88+
),
89+
(
90+
"connectors/source-pokeapi",
91+
"0.1.5",
92+
None,
93+
404,
94+
inputs.BUILD_STATUSES.NOT_FOUND
95+
),
96+
(
97+
"connectors/source-pokeapi",
98+
"0.1.5",
99+
{
100+
"link": "https://github.com/airbytehq/airbyte/actions/runs/4029659593",
101+
"docker_version": "0.1.5",
102+
"timestamp": "1674872401",
103+
"connector": "connectors/source-pokeapi"
104+
},
105+
200,
106+
inputs.BUILD_STATUSES.NOT_FOUND
107+
),
108+
(
109+
"connectors/source-pokeapi",
110+
"0.1.5",
111+
None,
112+
404,
113+
inputs.BUILD_STATUSES.NOT_FOUND
114+
),
115+
])
116+
def test_fetch_latest_build_status_for_connector_version(mocker, connector_name, connector_version, mocked_json_payload, mocked_status_code, expected_status):
117+
# Mock the api call to get the latest build status for a connector version
118+
mock_response = MagicMock()
119+
mock_response.json.return_value = mocked_json_payload
120+
mock_response.status_code = mocked_status_code
121+
mock_get = mocker.patch.object(requests, 'get', return_value=mock_response)
122+
123+
assert inputs.fetch_latest_build_status_for_connector_version(connector_name, connector_version) == expected_status
124+
assert mock_get.call_args == call(f"{constants.CONNECTOR_BUILD_OUTPUT_URL}/{connector_name}/{connector_version}.json")
125+
126+
def test_fetch_latest_build_status_for_connector_version_invalid_status(mocker, caplog):
127+
connector_name = "connectors/source-pokeapi"
128+
connector_version = "0.1.5"
129+
mocked_json_payload = {
130+
"link": "https://github.com/airbytehq/airbyte/actions/runs/4029659593",
131+
"outcome": "unknown_outcome_123",
132+
"docker_version": "0.1.5",
133+
"timestamp": "1674872401",
134+
"connector": "connectors/source-pokeapi"
135+
}
136+
# Mock the api call to get the latest build status for a connector version
137+
mock_response = MagicMock()
138+
mock_response.json.return_value = mocked_json_payload
139+
mock_response.status_code = 200
140+
mocker.patch.object(requests, 'get', return_value=mock_response)
141+
142+
assert inputs.fetch_latest_build_status_for_connector_version(connector_name, connector_version) == inputs.BUILD_STATUSES.NOT_FOUND
143+
assert 'Error: Unexpected build status value: unknown_outcome_123 for connector connectors/source-pokeapi:0.1.5' in caplog.text

0 commit comments

Comments
 (0)