diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6704d65b..f8695f41 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,12 +35,31 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} - + - name: "Install dependencies" run: pip install ".[dev]" - + - name: Run tests run: pytest --cov=. --cov-report html --cov-report term --cov-fail-under=25 + + schema-compatibility: + name: Check schema compatibility with demo server + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.13" + + - name: Install dependencies + run: pip install ".[dev]" + + - name: Check schema compatibility + run: python scripts/check_schema_compatibility.py + conda-build: name: Conda build check runs-on: ubuntu-latest diff --git a/openhexa/cli/api.py b/openhexa/cli/api.py index 539e172b..cbdfe3dd 100644 --- a/openhexa/cli/api.py +++ b/openhexa/cli/api.py @@ -24,7 +24,7 @@ from jinja2 import Template from openhexa.cli.settings import settings -from openhexa.graphql import BaseOpenHexaClient +from openhexa.graphql import BUNDLED_SCHEMA_PATH, BaseOpenHexaClient from openhexa.sdk.pipelines import get_local_workspace_config from openhexa.sdk.pipelines.runtime import get_pipeline from openhexa.utils import create_requests_session, stringcase @@ -134,9 +134,7 @@ def _detect_graphql_breaking_changes_if_needed(token): def _detect_graphql_breaking_changes(token): """Detect breaking changes between the schema referenced in the SDK and the server using graphql-core.""" - stored_schema_obj = build_schema( - (Path(__file__).parent.parent / "graphql" / "schema.generated.graphql").open().read() - ) + stored_schema_obj = build_schema(BUNDLED_SCHEMA_PATH.read_text()) server_schema_obj = build_client_schema( _query_graphql(get_introspection_query(input_value_deprecation=True), token=token) ) diff --git a/openhexa/graphql/__init__.py b/openhexa/graphql/__init__.py index 42359acc..c97da20f 100644 --- a/openhexa/graphql/__init__.py +++ b/openhexa/graphql/__init__.py @@ -1,5 +1,8 @@ """GraphQL package.""" +from pathlib import Path from .base_openhexa_client import BaseOpenHexaClient # noqa: F401 -> Expose base client class from .graphql_client import * # noqa: F403 -> Expose autogenerated types + +BUNDLED_SCHEMA_PATH = Path(__file__).parent / "schema.generated.graphql" diff --git a/scripts/check_schema_compatibility.py b/scripts/check_schema_compatibility.py new file mode 100644 index 00000000..28f6fe92 --- /dev/null +++ b/scripts/check_schema_compatibility.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python +""" +Check for breaking changes between the SDK's bundled GraphQL schema and live server schemas for our production and demo environments. + +Exits 0 regardless of outcome. Emits ::warning:: annotations when running in GitHub Actions +so breaking changes are visible without affecting the job status. +""" + +import os +from pathlib import Path + +import requests +from graphql import build_client_schema, build_schema, get_introspection_query +from graphql.utilities import find_breaking_changes + +from openhexa.graphql import BUNDLED_SCHEMA_PATH + +GITHUB_ACTIONS = os.getenv("GITHUB_ACTIONS") == "true" +REPO_ROOT = Path(__file__).parent.parent +SCHEMA_RELATIVE_PATH = BUNDLED_SCHEMA_PATH.relative_to(REPO_ROOT) + +URLS = [ + "https://api.demo.openhexa.org", + "https://api.openhexa.org", +] + + +def fetch_server_schema(graphql_url: str): + """Fetch the live GraphQL schema from the server via introspection.""" + response = requests.post( + graphql_url, + json={"query": get_introspection_query(input_value_deprecation=True)}, + timeout=30, + ) + response.raise_for_status() + body = response.json() + if "errors" in body: + raise RuntimeError(f"Introspection query returned errors: {body['errors']}") + return build_client_schema(body["data"]) + + +def check_url(stored_schema, url: str) -> list: + """Check breaking changes for a single server URL. Returns the list of breaking changes.""" + graphql_url = url.rstrip("/") + "/graphql/" + server_schema = fetch_server_schema(graphql_url) + + breaking_changes = find_breaking_changes(stored_schema, server_schema) + if breaking_changes: + print(f" ⚠️ {len(breaking_changes)} breaking change(s) detected:") + for change in breaking_changes: + print(f" - {change.description}") + if GITHUB_ACTIONS: + print(f"::warning file={SCHEMA_RELATIVE_PATH},line=1,title=GraphQL schema breaking change ({url})::{change.description}") + else: + print(" ✅ No breaking changes detected.") + return breaking_changes + + +def main(): + """Execute main function.""" + stored_schema = build_schema(BUNDLED_SCHEMA_PATH.read_text()) + + all_breaking_changes = [] + for url in URLS: + all_breaking_changes.extend(check_url(stored_schema, url)) + + if all_breaking_changes: + print( + "\nThe server schema has diverged from openhexa/graphql/schema.generated.graphql." + "\nUpdate the bundled schema by copying the latest schema from the OpenHEXA monorepo" + " and re-running: python -m ariadne_codegen" + ) + + +if __name__ == "__main__": + main()