Skip to content

Commit eb4724e

Browse files
geoHeilclaude
andauthored
ci: prototype tach-based modular skipping (#3333)
* ci: prototype tach-based modular skipping Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: modularize ubuntu setup and refine gating Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: adopt metaxy-inspired governance helpers - replace custom aggregate check with re-actors/alls-green - set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24 on every workflow - keep PR concurrency alive when the graphite:merge label is present Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: tune checks and pin action versions Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: split CI suites and heavy examples Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * DCO Remediation Commit for Georg Heiler <georg.kf.heiler@gmail.com> I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: ecaa477 I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: d15416f Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: sharpen tach graph and per-suite path filters - Split docling.pipeline into per-pipeline tach modules (asr, vlm, standard_pdf, threaded_standard_pdf, legacy_standard_pdf, extraction_vlm, base, base_extraction, simple) so pytest --tach-base impact analysis can attribute changes to a specific pipeline rather than the whole package. - Split the asr- and vlm-specific docling.datamodel option files (asr_model_specs, pipeline_options_asr_model, vlm_engine_options, vlm_model_specs, pipeline_options_vlm_model, layout_model_specs, stage_model_specs, backend_options) into their own tach modules so a narrow spec/options change no longer marks the full datamodel as impacted. - Narrow the per-suite pipeline path filters in checks.yml to the concrete pipeline files relevant to each suite, so editing vlm_pipeline.py only triggers the vlm matrix cell and editing asr_pipeline.py only the asr one. - Rekey the model cache in setup-ubuntu-ci to include runner.os and hashFiles(uv.lock, pyproject.toml), with ordered restore-keys fallbacks so a lockfile bump no longer silently stales the cache. Metaxy parity note: layered tach enforcement (layer = "...") is blocked by existing backend<->datamodel and utils<->stages cycles; depot runners, nox dynamic matrices, devenv/nix, dprint and ty are not applicable to docling's stack. All pinned action SHAs are on their latest release as of this commit. Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: introduce pipeline and orchestration tach layers Earlier notes claimed layers were blocked. That was only true for the cyclic core (backend<->datamodel, utils<->stages). The boundary *above* core is clean: - No module under docling/backend, docling/datamodel, docling/models, docling/utils, docling/exceptions, or docling/chunking imports anything from docling.pipeline (verified by grep). - No module anywhere in docling/ imports from docling.cli, docling.document_converter, docling.document_extractor, or docling.service_client (also verified). So we can introduce two real layers on top of the cyclic core: - "pipeline" — docling.pipeline and all nine concrete pipelines (base, simple, base_extraction, asr, vlm, extraction_vlm, standard_pdf, threaded_standard_pdf, legacy_standard_pdf). - "orchestration" — docling.cli, docling.document_converter, docling.document_extractor, and docling.experimental.pipeline. Unlayered modules stay "below" both layers (tach allows them to be depended on freely) and continue to carry the declared-but-cyclic backend<->datamodel and utils<->stages edges. A VLM-only layer was explored but rejected: only docling.pipeline.vlm_pipeline and docling.pipeline.extraction_vlm_pipeline could be cleanly layered as "vlm", because the matching datamodel options (pipeline_options_vlm_model, vlm_engine_options, vlm_model_specs) and model stages (vlm_convert, vlm_pipeline_models) sit inside the datamodel/models cycle and cannot be promoted to a higher layer without first breaking that cycle. Layering only the two pipeline files is not worth the extra config. Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: expand tach layers to entrypoints/pipeline/models/core Follow-up to the two-layer attempt. After verifying via grep that nothing in datamodel/utils/backend imports from docling.models.{extraction,factories,plugins,vlm_pipeline_models} or from the "upper" stages (page_assemble, page_preprocessing, reading_order, picture_description, vlm_convert), those nine modules can be promoted out of the cyclic core into a dedicated "models" layer. The resulting order (highest first): - entrypoints — cli, document_converter, document_extractor, experimental.pipeline - pipeline — docling.pipeline + the nine concrete pipelines - models — model factories, extraction, plugins, vlm_pipeline_models, and the five "upper" stages - core — datamodel*, backend*, utils, exceptions, chunking, models (base), models.utils, inference_engines.*, the six "core stages" that utils cycles with (chart_extraction, code_formula, layout, ocr, picture_classifier, table_structure), and the experimental.* and service_client modules Rename the previous "orchestration" layer to "entrypoints" to match the common docling vocabulary. Every module now carries an explicit layer tag instead of relying on implicit unlayered behaviour, so future additions must pick a layer deliberately. A VLM layer, a stand-alone inference-engines layer, and separating datamodel from backend all remain blocked by the bidirectional backend<->datamodel and utils<->core-stages edges; those need a code-level refactor first. Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: refine tach client and foundation layers Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: add optional windows and macos smoke lanes Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: normalize reusable workflow boolean inputs Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: replace external all-green action Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: use org-allowed setup-uv action Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: install compiler toolchain for ML tests Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * DCO Remediation Commit for Georg Heiler <georg.kf.heiler@gmail.com> I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: bb714af I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: a1f2761 Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * DCO Remediation Commit for Georg Heiler <georg.kf.heiler@gmail.com> I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: cc6551b I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: b21b0e7 Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: simplify ML pytest suite patterns Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: gate heavy examples on label, add job timeouts - ci-heavy-examples: run only on main push, schedule, workflow_dispatch, or when a PR is labeled tests:full / tests:heavy-examples. Drops the path-based auto-trigger so that common edits to pyproject.toml, uv.lock, or .github/actions do not kick off the 45-60min matrix on every PR push. Collapses the changes job into a job-level if gate and adds timeout-minutes: 90. - checks.yml: add timeout-minutes to every job so stuck runners cannot burn the full 6h default. Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: tolerate cancelled allowed-skip jobs in check aggregator Intentional cancellations (manual cancel, concurrency replacement) on jobs that are already in ALLOWED_SKIPS should not mark the overall workflow red. Treat `cancelled` the same as `skipped` when the job is listed as an allowed skip; any unexpected cancellation of a required job still fails. Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * docs: make minimal vlm example portable Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * DCO Remediation Commit for Georg Heiler <georg.kf.heiler@gmail.com> I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: 2135051 I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: 4f6d1d7 Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: install workspace packages in CI syncs Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * DCO Remediation Commit for Georg Heiler <georg.kf.heiler@gmail.com> I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: 492fa98 I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: 3eefae7 Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * DCO Remediation Commit for Georg Heiler <georg.kf.heiler@gmail.com> I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: fe8c9689a0ee94f36eb826da8e2177ef87404f5e I, Georg Heiler <georg.kf.heiler@gmail.com>, hereby add my Signed-off-by to this commit: eabdd24a6734ec873cdaac857718aef2473677e7 Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: remove unused graphite concurrency exception Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: document test labels and gate cross-platform lanes Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: select ml tests with pytest markers Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: fix marker selector typing Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: simplify ml suite scheduling Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: mark cross-platform smoke tests Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: reuse test trigger for ml matrix Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: tighten full ci aggregation Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> * ci: share required job result check Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> --------- Signed-off-by: Georg Heiler <georg.kf.heiler@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 05e0a4d commit eb4724e

54 files changed

Lines changed: 1971 additions & 295 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CI_LABELS.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# CI labels
2+
3+
The pull request workflows recognize these optional maintainer labels:
4+
5+
- `tests:full`: run the full Linux CI matrix for the PR, including all ML
6+
suites and package compatibility lanes.
7+
- `tests:heavy-examples`: run the heavy examples workflow for the PR.
8+
9+
Windows and macOS smoke lanes are intentionally not label-triggered. Run them
10+
from the `Run CI` or `Run CI Main` workflow dispatch inputs when cross-platform
11+
verification is needed.
12+
13+
## ML test segmentation
14+
15+
Expensive ML tests are selected with module-level pytest markers, not workflow
16+
file globs:
17+
18+
- `pytest.mark.ml_ocr`
19+
- `pytest.mark.ml_pdf_model`
20+
- `pytest.mark.ml_vlm`
21+
- `pytest.mark.ml_asr`
22+
23+
New tests run in the core lane by default. If a new test belongs in an ML lane,
24+
add the matching module-level `pytestmark`; do not add per-test file globs to
25+
the workflow.
26+
27+
The workflow intentionally uses a broad ML trigger for code, test, and tooling
28+
changes. Tach performs the fine-grained affected-test selection inside the ML
29+
lanes.
30+
31+
Path filters still decide whether a CI lane should be created at all. Pytest
32+
markers only select which test modules run after a test lane has started.
33+
34+
## Cross-platform smoke tests
35+
36+
Windows and macOS smoke tests are selected with `pytest.mark.cross_platform`.
37+
Use this marker for lightweight modules that should be exercised by the
38+
workflow-dispatch cross-platform lanes; do not maintain a separate test-file
39+
list in the workflow.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: Setup Ubuntu CI
2+
description: Set up Python, uv, and optional Docling CI dependencies on Ubuntu.
3+
4+
inputs:
5+
python_version:
6+
description: Python version passed to astral-sh/setup-uv.
7+
required: true
8+
enable_cache:
9+
description: Whether to enable the uv cache.
10+
default: "true"
11+
uv_sync_args:
12+
description: Arguments passed to `uv sync`. Leave empty to skip syncing.
13+
default: ""
14+
install_system_deps:
15+
description: Whether to install Ubuntu OCR and office dependencies.
16+
default: "false"
17+
cache_models:
18+
description: Whether to restore and save the shared model cache.
19+
default: "false"
20+
21+
runs:
22+
using: composite
23+
steps:
24+
- name: Grant permissions to APT cache directory # allows restore
25+
if: ${{ inputs.install_system_deps == 'true' }}
26+
shell: bash
27+
run: sudo chown -R "$USER:$USER" /var/cache/apt/archives
28+
29+
- name: Cache APT packages
30+
if: ${{ inputs.install_system_deps == 'true' }}
31+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
32+
with:
33+
path: /var/cache/apt/archives
34+
key: apt-packages-${{ runner.os }}-${{ hashFiles('.github/actions/setup-ubuntu-ci/action.yml') }}
35+
restore-keys: |
36+
apt-packages-${{ runner.os }}-
37+
38+
- name: Install system dependencies
39+
if: ${{ inputs.install_system_deps == 'true' }}
40+
shell: bash
41+
run: |
42+
sudo apt-get -qq update
43+
sudo apt-get -qq install -y build-essential ffmpeg tesseract-ocr tesseract-ocr-eng tesseract-ocr-fra tesseract-ocr-deu tesseract-ocr-spa tesseract-ocr-script-latn libleptonica-dev libtesseract-dev libreoffice pkg-config
44+
45+
- name: Set TESSDATA_PREFIX
46+
if: ${{ inputs.install_system_deps == 'true' }}
47+
shell: bash
48+
run: echo "TESSDATA_PREFIX=$(dpkg -L tesseract-ocr-eng | grep tessdata$)" >> "$GITHUB_ENV"
49+
50+
- name: Install uv and set the python version
51+
uses: astral-sh/setup-uv@v7
52+
with:
53+
python-version: ${{ inputs.python_version }}
54+
enable-cache: ${{ inputs.enable_cache }}
55+
56+
- name: Install Python dependencies
57+
if: ${{ inputs.uv_sync_args != '' }}
58+
shell: bash
59+
run: uv sync ${{ inputs.uv_sync_args }}
60+
61+
- name: Cache models
62+
if: ${{ inputs.cache_models == 'true' }}
63+
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
64+
with:
65+
path: |
66+
~/.cache/huggingface
67+
~/.cache/modelscope
68+
~/.EasyOCR/
69+
key: models-cache-${{ runner.os }}-${{ hashFiles('uv.lock', 'pyproject.toml') }}
70+
restore-keys: |
71+
models-cache-${{ runner.os }}-
72+
models-cache-
73+
74+
- name: Grant permissions to APT cache directory # allows backup
75+
if: ${{ inputs.install_system_deps == 'true' }}
76+
shell: bash
77+
run: sudo chown -R "$USER:$USER" /var/cache/apt/archives
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import json
5+
from typing import Any
6+
7+
SUCCESS = "success"
8+
SKIPPED = "skipped"
9+
10+
11+
def parse_allowed_skips(raw_allowed_skips: str) -> set[str]:
12+
return {job for job in raw_allowed_skips.split() if job}
13+
14+
15+
def parse_needs(raw_needs: str) -> dict[str, Any]:
16+
loaded = json.loads(raw_needs)
17+
if not isinstance(loaded, dict):
18+
msg = "--needs-json must decode to a JSON object."
19+
raise ValueError(msg)
20+
return loaded
21+
22+
23+
def result_for_job(job: str, value: Any) -> str:
24+
if not isinstance(value, dict):
25+
msg = f"{job}: needs entry must be a JSON object."
26+
raise ValueError(msg)
27+
28+
result = value.get("result")
29+
if not isinstance(result, str):
30+
msg = f"{job}: needs entry must contain a string result."
31+
raise ValueError(msg)
32+
33+
return result
34+
35+
36+
def collect_failures(needs: dict[str, Any], allowed_skips: set[str]) -> list[str]:
37+
failures: list[str] = []
38+
for job, value in needs.items():
39+
result = result_for_job(job, value)
40+
if result == SUCCESS:
41+
continue
42+
if result == SKIPPED and job in allowed_skips:
43+
print(f"::notice title=Allowed skipped job::{job}")
44+
continue
45+
failures.append(f"{job}={result}")
46+
return failures
47+
48+
49+
def parse_args() -> argparse.Namespace:
50+
parser = argparse.ArgumentParser(
51+
description="Fail unless all required GitHub Actions needs succeeded."
52+
)
53+
parser.add_argument("--needs-json", required=True)
54+
parser.add_argument("--allowed-skips", default="")
55+
return parser.parse_args()
56+
57+
58+
def main() -> int:
59+
args = parse_args()
60+
needs = parse_needs(args.needs_json)
61+
allowed_skips = parse_allowed_skips(args.allowed_skips)
62+
failures = collect_failures(needs, allowed_skips)
63+
if failures:
64+
print(f"::error title=Required jobs failed::{', '.join(failures)}")
65+
return 1
66+
return 0
67+
68+
69+
if __name__ == "__main__":
70+
raise SystemExit(main())
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import ast
5+
import json
6+
import os
7+
from pathlib import Path
8+
9+
ML_MARKERS = ("ml_ocr", "ml_pdf_model", "ml_vlm", "ml_asr")
10+
CROSS_PLATFORM_MARKER = "cross_platform"
11+
CI_FILE_MARKERS = (*ML_MARKERS, CROSS_PLATFORM_MARKER)
12+
SUITE_MARKERS = {
13+
"ocr": "ml_ocr",
14+
"pdf-model": "ml_pdf_model",
15+
"vlm": "ml_vlm",
16+
"asr": "ml_asr",
17+
}
18+
MARKER_SUITES = {marker: suite for suite, marker in SUITE_MARKERS.items()}
19+
20+
21+
def parse_bool(value: str) -> bool:
22+
return value.lower() == "true"
23+
24+
25+
def is_pytest_mark_attribute(node: ast.AST, marker: str) -> bool:
26+
if not isinstance(node, ast.Attribute) or node.attr != marker:
27+
return False
28+
if not isinstance(node.value, ast.Attribute) or node.value.attr != "mark":
29+
return False
30+
return isinstance(node.value.value, ast.Name) and node.value.value.id == "pytest"
31+
32+
33+
def markers_in_node(node: ast.AST) -> set[str]:
34+
markers: set[str] = set()
35+
for child in ast.walk(node):
36+
for marker in CI_FILE_MARKERS:
37+
if is_pytest_mark_attribute(child, marker):
38+
markers.add(marker)
39+
return markers
40+
41+
42+
def module_level_ci_markers(tree: ast.Module) -> set[str]:
43+
markers: set[str] = set()
44+
for statement in tree.body:
45+
value: ast.AST | None = None
46+
if isinstance(statement, ast.Assign) and any(
47+
isinstance(target, ast.Name) and target.id == "pytestmark"
48+
for target in statement.targets
49+
):
50+
value = statement.value
51+
elif (
52+
isinstance(statement, ast.AnnAssign)
53+
and isinstance(statement.target, ast.Name)
54+
and statement.target.id == "pytestmark"
55+
):
56+
value = statement.value
57+
58+
if value is not None:
59+
markers.update(markers_in_node(value))
60+
61+
return markers
62+
63+
64+
def detect_ci_markers(path: Path) -> set[str]:
65+
if not path.exists() or path.suffix != ".py":
66+
return set()
67+
68+
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
69+
all_markers = markers_in_node(tree)
70+
module_markers = module_level_ci_markers(tree)
71+
if all_markers != module_markers:
72+
raise ValueError(
73+
f"{path}: CI pytest markers must be declared with module-level "
74+
"`pytestmark` so CI can select whole test modules."
75+
)
76+
return module_markers
77+
78+
79+
def detect_ml_markers(path: Path) -> set[str]:
80+
return detect_ci_markers(path) & set(ML_MARKERS)
81+
82+
83+
def discover_test_markers(repo_root: Path) -> dict[str, list[Path]]:
84+
discovered: dict[str, list[Path]] = {marker: [] for marker in CI_FILE_MARKERS}
85+
tests_dir = repo_root / "tests"
86+
if not tests_dir.exists():
87+
return discovered
88+
89+
for path in sorted(tests_dir.rglob("*.py")):
90+
markers = detect_ci_markers(path)
91+
for marker in markers:
92+
discovered[marker].append(path.relative_to(repo_root))
93+
94+
return discovered
95+
96+
97+
def build_ml_suites(*, run_all_ml: bool) -> list[str]:
98+
if not run_all_ml:
99+
return []
100+
101+
return [MARKER_SUITES[marker] for marker in ML_MARKERS]
102+
103+
104+
def write_github_output(name: str, value: str) -> None:
105+
output_path = os.environ.get("GITHUB_OUTPUT")
106+
if output_path is None:
107+
print(f"{name}={value}")
108+
return
109+
110+
with Path(output_path).open("a", encoding="utf-8") as output_file:
111+
output_file.write(f"{name}={value}\n")
112+
113+
114+
def print_paths(paths: list[Path]) -> None:
115+
for path in paths:
116+
print(path.as_posix())
117+
118+
119+
def run_matrix(args: argparse.Namespace) -> None:
120+
suites = build_ml_suites(run_all_ml=parse_bool(args.run_all_ml))
121+
write_github_output("ml_suites", json.dumps(suites, separators=(",", ":")))
122+
123+
124+
def run_core_ignore_args(args: argparse.Namespace) -> None:
125+
discovered = discover_test_markers(args.repo_root)
126+
marked_paths = sorted(
127+
{path for marker in ML_MARKERS for path in discovered[marker]}
128+
)
129+
for path in marked_paths:
130+
print(f"--ignore={path.as_posix()}")
131+
132+
133+
def run_suite_args(args: argparse.Namespace) -> None:
134+
if args.suite not in SUITE_MARKERS:
135+
raise ValueError(f"Unknown ML suite: {args.suite}")
136+
137+
discovered = discover_test_markers(args.repo_root)
138+
print_paths(discovered[SUITE_MARKERS[args.suite]])
139+
140+
141+
def run_suite_marker(args: argparse.Namespace) -> None:
142+
if args.suite not in SUITE_MARKERS:
143+
raise ValueError(f"Unknown ML suite: {args.suite}")
144+
145+
print(SUITE_MARKERS[args.suite])
146+
147+
148+
def run_marker_args(args: argparse.Namespace) -> None:
149+
if args.marker not in CI_FILE_MARKERS:
150+
raise ValueError(f"Unknown CI marker: {args.marker}")
151+
152+
discovered = discover_test_markers(args.repo_root)
153+
print_paths(discovered[args.marker])
154+
155+
156+
def parse_args() -> argparse.Namespace:
157+
parser = argparse.ArgumentParser(
158+
description="Select pytest modules for Docling's marker-based CI lanes."
159+
)
160+
parser.add_argument("--repo-root", type=Path, default=Path("."))
161+
subparsers = parser.add_subparsers(dest="command", required=True)
162+
163+
matrix_parser = subparsers.add_parser("matrix")
164+
matrix_parser.add_argument("--run-all-ml", default="false")
165+
matrix_parser.set_defaults(func=run_matrix)
166+
167+
core_parser = subparsers.add_parser("core-ignore-args")
168+
core_parser.set_defaults(func=run_core_ignore_args)
169+
170+
suite_parser = subparsers.add_parser("suite-args")
171+
suite_parser.add_argument("suite")
172+
suite_parser.set_defaults(func=run_suite_args)
173+
174+
marker_parser = subparsers.add_parser("suite-marker")
175+
marker_parser.add_argument("suite")
176+
marker_parser.set_defaults(func=run_suite_marker)
177+
178+
marker_args_parser = subparsers.add_parser("marker-args")
179+
marker_args_parser.add_argument("marker")
180+
marker_args_parser.set_defaults(func=run_marker_args)
181+
182+
return parser.parse_args()
183+
184+
185+
def main() -> None:
186+
args = parse_args()
187+
args.func(args)
188+
189+
190+
if __name__ == "__main__":
191+
main()

0 commit comments

Comments
 (0)