Skip to content
Closed
4 changes: 4 additions & 0 deletions airbyte_cdk/cli/airbyte_cdk/_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ def test(
)


from airbyte_cdk.cli.airbyte_cdk._qa import pre_release_check

connector_cli_group.add_command(pre_release_check)

__all__ = [
"connector_cli_group",
]
94 changes: 94 additions & 0 deletions airbyte_cdk/cli/airbyte_cdk/_qa.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
"""CLI command for running QA checks on connectors using pytest."""

import os
import subprocess
import sys
from pathlib import Path
from typing import List, Optional

import rich_click as click

from airbyte_cdk.cli.airbyte_cdk._util import resolve_connector_name_and_directory


@click.command(name="pre-release-check")
@click.option(
"-c",
"--check",
"selected_checks",
multiple=True,
help="The name of the check to run. If not provided, all checks will be run.",
)
@click.option(
"--connector-name",
type=str,
help="Name of the connector to check. Ignored if --connector-directory is provided.",
)
@click.option(
"--connector-directory",
type=click.Path(exists=True, file_okay=False, path_type=Path),
help="Path to the connector directory.",
)
@click.option(
"-r",
"--report-path",
"report_path",
type=click.Path(file_okay=True, path_type=Path, writable=True, dir_okay=False),
help="The path to the report file to write the results to as JSON.",
)
def pre_release_check(
selected_checks: List[str],
connector_name: Optional[str] = None,
connector_directory: Optional[Path] = None,
report_path: Optional[Path] = None,
) -> None:
"""Run pre-release checks on a connector using pytest.

This command runs quality assurance checks on a connector to ensure it meets
Airbyte's standards for release. The checks include:

- Documentation checks
- Metadata checks
- Packaging checks
- Security checks
- Asset checks
- Testing checks

If no connector name or directory is provided, we will look within the current working
directory. If the current working directory is not a connector directory (e.g. starting
with 'source-') and no connector name or path is provided, the process will fail.
"""
connector_name, connector_directory = resolve_connector_name_and_directory(
connector_name=connector_name,
connector_directory=connector_directory,
)

pytest_args = ["-xvs"]

if connector_name:
pytest_args.extend(["--connector-name", connector_name])
if connector_directory:
pytest_args.extend(["--connector-directory", str(connector_directory)])

if report_path:
pytest_args.extend(["--report-path", str(report_path)])

if selected_checks:
for check in selected_checks:
pytest_args.extend(["-k", check])

qa_module_path = Path(__file__).parent.parent.parent / "qa"
pytest_args.extend(["-p", "airbyte_cdk.qa.pytest_plugin"])

test_paths = []
for root, _, files in os.walk(qa_module_path / "checks"):
for file in files:
if file.endswith("_test.py"):
test_paths.append(os.path.join(root, file))

cmd = [sys.executable, "-m", "pytest"] + pytest_args + test_paths
click.echo(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd)

if result.returncode != 0:
raise click.ClickException(f"Pytest failed with exit code {result.returncode}")
69 changes: 69 additions & 0 deletions airbyte_cdk/qa/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""The `airbyte_cdk.qa` module provides quality assurance checks for Airbyte connectors.

This module includes a framework for running pre-release checks on connectors to ensure
they meet Airbyte's quality standards. The checks are organized into categories and can
be run individually or as a group.


The QA module includes the following check categories:

- **Packaging**: Checks related to connector packaging, including dependency management,
versioning, and licensing.
- **Metadata**: Checks related to connector metadata, including language tags, CDK tags,
and breaking changes deadlines.
- **Security**: Checks related to connector security, including HTTPS usage and base image
requirements.
- **Assets**: Checks related to connector assets, including icons and other visual elements.
- **Documentation**: Checks related to connector documentation, ensuring it exists and is
properly formatted.
- **Testing**: Checks related to connector testing, ensuring acceptance tests are present.


Checks can be configured based on various connector attributes:

- **Connector Language**: Checks can be configured to run only on connectors of specific
languages (Python, Java, Low-Code, Manifest-Only).
- **Connector Type**: Checks can be configured to run only on specific connector types
(source, destination).
- **Support Level**: Checks can be configured to run only on connectors with specific
support levels (certified, community, etc.).
- **Cloud Usage**: Checks can be configured to run only on connectors with specific
cloud usage settings (enabled, disabled, etc.).
- **Internal SL**: Checks can be configured to run only on connectors with specific
internal service level requirements.


Checks can be run using the `airbyte-cdk connector pre-release-check` command:

```bash
airbyte-cdk connector pre-release-check --connector-name source-example

airbyte-cdk connector pre-release-check --connector-name source-example --check CheckConnectorUsesPoetry --check CheckVersionBump

airbyte-cdk connector pre-release-check --connector-directory /path/to/connector

airbyte-cdk connector pre-release-check --connector-name source-example --report-path report.json
```


The QA module is designed to be extensible. New checks can be added by creating a new
class that inherits from the `Check` base class and implementing the required methods.

Example:

```python
from airbyte_cdk.qa.models import Check, CheckCategory, CheckResult
from airbyte_cdk.qa.connector import Connector

class MyCustomCheck(Check):
name = "My custom check"
description = "Description of what my check verifies"
category = CheckCategory.TESTING

def _run(self, connector: Connector) -> CheckResult:
if some_condition:
return self.pass_(connector, "Check passed message")
else:
return self.fail(connector, "Check failed message")
```
"""
17 changes: 17 additions & 0 deletions airbyte_cdk/qa/checks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""QA checks for Airbyte connectors."""

from airbyte_cdk.qa.checks.assets import ENABLED_CHECKS as ASSETS_CHECKS
from airbyte_cdk.qa.checks.documentation import ENABLED_CHECKS as DOCUMENTATION_CHECKS
from airbyte_cdk.qa.checks.metadata import ENABLED_CHECKS as METADATA_CORRECTNESS_CHECKS
from airbyte_cdk.qa.checks.packaging import ENABLED_CHECKS as PACKAGING_CHECKS
from airbyte_cdk.qa.checks.security import ENABLED_CHECKS as SECURITY_CHECKS
from airbyte_cdk.qa.checks.testing import ENABLED_CHECKS as TESTING_CHECKS

ENABLED_CHECKS = (
DOCUMENTATION_CHECKS
+ METADATA_CORRECTNESS_CHECKS
+ PACKAGING_CHECKS
+ ASSETS_CHECKS
+ SECURITY_CHECKS
+ TESTING_CHECKS
)
45 changes: 45 additions & 0 deletions airbyte_cdk/qa/checks/assets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Asset checks for Airbyte connectors."""

from airbyte_cdk.qa import consts
from airbyte_cdk.qa.connector import Connector
from airbyte_cdk.qa.models import Check, CheckCategory, CheckResult


class AssetCheck(Check):
"""Base class for asset checks."""

category = CheckCategory.ASSETS


class CheckConnectorHasIcon(AssetCheck):
"""Check that connectors have an icon."""

name = "Connectors must have an icon"
description = f"Connectors must have an icon file named `{consts.ICON_FILE_NAME}` in their code directory. This is to ensure that all connectors have a visual representation in the UI."

def _run(self, connector: Connector) -> CheckResult:
"""Run the check.

Args:
connector: The connector to check

Returns:
CheckResult: The result of the check
"""
icon_path = connector.code_directory / consts.ICON_FILE_NAME
if not icon_path.exists():
return self.create_check_result(
connector=connector,
passed=False,
message=f"Icon file {consts.ICON_FILE_NAME} does not exist",
)
return self.create_check_result(
connector=connector,
passed=True,
message=f"Icon file {consts.ICON_FILE_NAME} exists",
)


ENABLED_CHECKS = [
CheckConnectorHasIcon(),
]
9 changes: 9 additions & 0 deletions airbyte_cdk/qa/checks/documentation/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Documentation checks for Airbyte connectors."""

from airbyte_cdk.qa.checks.documentation.documentation import (
CheckDocumentationExists,
)

ENABLED_CHECKS = [
CheckDocumentationExists(),
]
54 changes: 54 additions & 0 deletions airbyte_cdk/qa/checks/documentation/documentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""Documentation checks for Airbyte connectors."""

from pathlib import Path

from airbyte_cdk.qa.connector import Connector
from airbyte_cdk.qa.models import Check, CheckCategory, CheckResult


class DocumentationCheck(Check):
"""Base class for documentation checks."""

category = CheckCategory.DOCUMENTATION


class CheckDocumentationExists(DocumentationCheck):
"""Check that connectors have documentation."""

name = "Connectors must have documentation"
description = (
"Connectors must have documentation to ensure that users can understand how to use them."
)

def _run(self, connector: Connector) -> CheckResult:
"""Run the check.

Args:
connector: The connector to check

Returns:
CheckResult: The result of the check
"""
docs_dir = Path("/home/ubuntu/repos/airbyte/docs/integrations")
connector_type_dir = docs_dir / (connector.connector_type + "s")

doc_file = connector_type_dir / (
connector.technical_name.replace("source-", "").replace("destination-", "") + ".md"
)

if not doc_file.exists():
return self.create_check_result(
connector=connector,
passed=False,
message=f"Documentation file {doc_file} does not exist",
)
return self.create_check_result(
connector=connector,
passed=True,
message=f"Documentation file {doc_file} exists",
)


ENABLED_CHECKS = [
CheckDocumentationExists(),
]
Loading
Loading