-
Notifications
You must be signed in to change notification settings - Fork 2.1k
feat(cli-e2e-v2): Slice 1 — framework + MySQL pilot connector #27949
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
base: main
Are you sure you want to change the base?
Changes from all commits
191f31f
2970a52
71f98e7
a62a4ef
8cc4771
e545128
9d9a84f
641adc8
708d552
deb80ff
9be5b61
7b1b833
9ed76b6
0742401
8fc818d
e4e82b5
d3fe8c7
c2ee0ab
357e1f5
32e22f9
943bab4
4628226
8e74c7e
93a2846
74a989d
dcc3b22
daea036
5d77f0f
d8b080d
c6506f5
aa13878
c560af3
8afb388
7fd5f66
9498f53
1ea1ec8
a3f6eb8
6745fcb
20034cc
0c94b90
d689f73
63f8900
c02b44b
9c608bc
5fd72b7
3d6de3c
bbd3c46
2f3f104
4dea125
2f29657
b926457
1ae1c32
bca81a9
6148095
e3354a7
f5aeade
38948ce
0440cee
a8f57c6
0cfce8a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,99 @@ | ||
| # Copyright 2026 Collate | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| # CLI E2E v2 — strangler-fig replacement for py-cli-e2e-tests.yml. | ||
| # | ||
| # Each connector lives under ingestion/tests/cli_e2e_v2/<connector>/ and | ||
| # is a self-contained pytest module (no inheritance). The matrix below | ||
| # grows by one entry per connector migration PR; the connector's | ||
| # corresponding entry is removed from py-cli-e2e-tests.yml in the same PR. | ||
| # | ||
| # Triggers: workflow_dispatch only during the stabilization window for | ||
| # the MySQL pilot. The schedule cron will be added once the pilot is | ||
| # consistently green (see spec §7.1). | ||
|
|
||
| name: py-cli-e2e-tests-v2 | ||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| connectors: | ||
| description: "Connectors to run (JSON array)" | ||
| required: true | ||
| default: '["mysql"]' | ||
|
|
||
| permissions: | ||
| id-token: write | ||
| contents: read | ||
|
|
||
| jobs: | ||
| py-cli-e2e-tests-v2: | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 60 | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| connector: ${{ fromJSON(inputs.connectors || '["mysql"]') }} | ||
| environment: test | ||
|
|
||
| steps: | ||
| - name: Free Disk Space (Ubuntu) | ||
| uses: jlumbroso/free-disk-space@main | ||
| with: | ||
| tool-cache: false | ||
| android: true | ||
| dotnet: true | ||
| haskell: true | ||
| large-packages: false | ||
| swap-storage: true | ||
| docker-images: false | ||
|
|
||
| - name: Checkout | ||
| uses: actions/checkout@v4 | ||
|
|
||
| - name: Setup Openmetadata Test Environment | ||
| uses: ./.github/actions/setup-openmetadata-test-environment | ||
| with: | ||
| python-version: '3.10' | ||
|
|
||
| - name: Run CLI E2E v2 tests | ||
| id: e2e-v2-test | ||
| env: | ||
| # MySQL test data lives in a dedicated MySQL container that the | ||
| # session-scoped `mysql_container` pytest fixture (testcontainers) | ||
| # boots, bootstraps with the OM-doc minimum grants, and tears | ||
| # down. Teammates run the same way locally — no env plumbing. | ||
| # Only OM-server admin creds (used to mint the ingestion-bot | ||
| # JWT) need to be exported here; they come from the bundled | ||
| # docker-compose and are not secrets. | ||
| OM_ADMIN_EMAIL: admin@open-metadata.org | ||
| OM_ADMIN_PASSWORD: admin | ||
| run: | | ||
| source env/bin/activate | ||
| cd ingestion | ||
| mkdir -p junit | ||
| pytest -v \ | ||
| --junitxml=junit/test-results-v2-${{ matrix.connector }}.xml \ | ||
| tests/cli_e2e_v2/${{ matrix.connector }} | ||
| shell: bash | ||
|
|
||
| - name: Upload tests artifact | ||
| if: always() | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: tests-v2-${{ matrix.connector }} | ||
| path: ingestion/junit/test-results-v2-*.xml | ||
|
|
||
| - name: Clean Up | ||
| if: always() | ||
| run: | | ||
| cd ./docker/development | ||
| docker compose down --remove-orphans | ||
| sudo rm -rf ${PWD}/docker-volume |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,14 +13,23 @@ | |
| Handle workflow execution | ||
| """ | ||
|
|
||
| from pathlib import Path | ||
| from typing import Any, Dict # noqa: UP035 | ||
|
|
||
| from metadata.workflow.base import BaseWorkflow | ||
|
|
||
|
|
||
| def execute_workflow(workflow: BaseWorkflow, config_dict: Dict[str, Any]) -> None: # noqa: UP006 | ||
| """Execute the workflow and raise if needed""" | ||
| workflow.execute() | ||
| workflow.stop() | ||
| def execute_workflow( | ||
| workflow: BaseWorkflow, | ||
| config_dict: Dict[str, Any], # noqa: UP006 | ||
| status_file: Path | None = None, | ||
| ) -> None: | ||
| """Execute the workflow, write status file if requested, raise on failure if configured.""" | ||
| try: | ||
| workflow.execute() | ||
| finally: | ||
| workflow.stop() | ||
| if status_file is not None: | ||
| workflow.write_status_file(status_file) | ||
|
Comment on lines
+30
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Edge Case: write_status_file in finally can suppress the original exceptionIn Suggested fix: Was this helpful? React with 👍 / 👎 | Reply |
||
| if config_dict.get("workflowConfig", {}).get("raiseOnError", True): | ||
| workflow.raise_from_status() | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,10 +12,12 @@ | |
| Base workflow definition. | ||
| """ | ||
|
|
||
| import json | ||
| import traceback | ||
| import uuid | ||
| from abc import ABC, abstractmethod | ||
| from datetime import datetime | ||
| from pathlib import Path | ||
| from statistics import mean | ||
| from typing import Any, Dict, List, Optional, TypeVar, Union # noqa: UP035 | ||
|
|
||
|
|
@@ -212,13 +214,21 @@ def get_failures(self) -> List[StackTraceError]: # noqa: UP006 | |
| def workflow_steps(self) -> List[Step]: # noqa: UP006 | ||
| """Steps to report status from""" | ||
|
|
||
| def _step_meets_success_threshold(self, step: Step) -> bool: | ||
| """True iff the step has no failures, or its success ratio meets the workflow's threshold. | ||
|
|
||
| Shared by `raise_from_status_internal` (which raises on failure) and | ||
| `write_status_file` (which reports the CLI's observable success/failure state). | ||
| """ | ||
| status = step.get_status() | ||
| if not status.failures: | ||
| return True | ||
| return status.calculate_success() >= self.workflow_config.successThreshold # pyright: ignore[reportOperatorIssue] | ||
|
|
||
| def raise_from_status_internal(self, raise_warnings=False) -> None: | ||
| """Based on the internal workflow status, raise a WorkflowExecutionError""" | ||
| for step in self.workflow_steps(): | ||
| if ( | ||
| step.get_status().failures | ||
| and step.get_status().calculate_success() < self.workflow_config.successThreshold | ||
| ): | ||
| if not self._step_meets_success_threshold(step): | ||
| raise WorkflowExecutionError(f"{step.name} reported errors: {Summary.from_step(step)}") | ||
|
|
||
| if raise_warnings and step.status.warnings: | ||
|
|
@@ -400,3 +410,28 @@ def print_status(self): | |
| start_time, | ||
| self._is_debug_enabled(), | ||
| ) | ||
|
|
||
| def write_status_file(self, path: Path) -> None: | ||
| """Serialize per-step status to JSON at the given path. | ||
|
|
||
| The `success` field mirrors the CLI's exit-code semantic: True iff every | ||
| step meets its success threshold (the same condition under which | ||
| `raise_from_status_internal` does NOT raise). | ||
|
|
||
| Shape: | ||
| { | ||
| "pipeline_type": str, | ||
| "ingestion_pipeline_fqn": str | None, | ||
| "success": bool, | ||
| "steps": [<StepSummary dicts>] | ||
| } | ||
| """ | ||
| ingestion_status = self.build_ingestion_status() | ||
| success = all(self._step_meets_success_threshold(step) for step in self.workflow_steps()) | ||
| payload = { | ||
| "pipeline_type": self.config.source.type, # pyright: ignore[reportAttributeAccessIssue] | ||
| "ingestion_pipeline_fqn": self.config.ingestionPipelineFQN, # pyright: ignore[reportAttributeAccessIssue] | ||
|
Comment on lines
+432
to
+433
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| "success": success, | ||
| "steps": ingestion_status.model_dump(), | ||
| } | ||
| path.write_text(json.dumps(payload, indent=2, default=str)) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The old
run_appexplicitly calledworkflow.print_status()betweenstop()andraise_from_status(), giving operators a human-readable summary on stdout. The new unifiedexecute_workflowincommon.pydoes not callprint_status(), so theappcommand now runs silently on success. Otherrun_*functions (ingest, profile, etc.) may have had their ownprint_status()calls that were similarly dropped during unification.Suggested fix:
Was this helpful? React with 👍 / 👎 | Reply
gitar fixto apply this suggestion