From 7495f3466f448bbcfe438050438653d868b1c4b3 Mon Sep 17 00:00:00 2001 From: tzing Date: Thu, 23 Oct 2025 23:52:47 +0800 Subject: [PATCH 01/18] add: dag101 --- docs/index.rst | 1 + docs/rules/dag.rst | 39 +++++++++++++++++++++++ tests/analyzers/test_template.py | 43 ++++++++++++++++++++++++++ tugboat/analyzers/template/__init__.py | 23 ++++++++++++++ 4 files changed, 106 insertions(+) create mode 100644 docs/rules/dag.rst diff --git a/docs/index.rst b/docs/index.rst index 0da3a44..05de4e2 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -155,6 +155,7 @@ For more information on how to use Tugboat, runs its help command: :caption: Rules rules/cron-workflow + rules/dag rules/fatal-errors rules/manifest-errors rules/step diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst new file mode 100644 index 0000000..f993755 --- /dev/null +++ b/docs/rules/dag.rst @@ -0,0 +1,39 @@ +DAG Rules (``DAG``) +=================== + +Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. + +.. _dag: https://argo-workflows.readthedocs.io/en/latest/walk-through/dag/ +.. _template: https://argo-workflows.readthedocs.io/en/latest/fields/#template + +.. DAG1xx duplicated items + +.. rule:: DAG101 Duplicate task names + + The template contains multiple tasks with the same name. + + .. code-block:: yaml + :emphasize-lines: 11,17 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: steps- + spec: + entrypoint: hello-hello + templates: + - name: hello-hello + dag: + tasks: + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello-1" + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello-2" diff --git a/tests/analyzers/test_template.py b/tests/analyzers/test_template.py index 72741c8..6f0ab59 100644 --- a/tests/analyzers/test_template.py +++ b/tests/analyzers/test_template.py @@ -74,6 +74,24 @@ def test_check_duplicate_step_names(self, diagnoses_logger): in diagnoses ) + def test_check_duplicate_task_names(self, diagnoses_logger): + diagnoses = analyze_yaml_stream(MANIFEST_DUPLICATE_TASK_NAMES) + diagnoses_logger(diagnoses) + assert ( + IsPartialModel( + code="DAG101", + loc=("spec", "templates", 0, "dag", "tasks", 0, "name"), + ) + in diagnoses + ) + assert ( + IsPartialModel( + code="DAG101", + loc=("spec", "templates", 0, "dag", "tasks", 1, "name"), + ) + in diagnoses + ) + MANIFEST_AMBIGUOUS_TYPE = """ apiVersion: argoproj.io/v1alpha1 @@ -147,6 +165,31 @@ def test_check_duplicate_step_names(self, diagnoses_logger): value: "hello-2" """ +MANIFEST_DUPLICATE_TASK_NAMES = """ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: steps- +spec: + entrypoint: hello-hello + templates: + - name: hello-hello + dag: + tasks: + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello-1" + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello-2" +""" + def test_check_input_parameters_1(diagnoses_logger): diagnoses = analyze_yaml_stream( diff --git a/tugboat/analyzers/template/__init__.py b/tugboat/analyzers/template/__init__.py index 58a19f9..bf23687 100644 --- a/tugboat/analyzers/template/__init__.py +++ b/tugboat/analyzers/template/__init__.py @@ -78,6 +78,29 @@ def check_steps( ) +@hookimpl(specname="analyze_template") +def check_dag(template: Template, workflow: WorkflowCompatible) -> Iterable[Diagnosis]: + if not template.dag: + return + + # check for duplicate task names + task_names = collections.defaultdict(list) + for idx, task in enumerate(template.dag.tasks or ()): + if task.name: + task_names[task.name].append(("dag", "tasks", idx, "name")) + + for name, locs in task_names.items(): + if len(locs) > 1: + for loc in locs: + yield { + "code": "DAG101", + "loc": loc, + "summary": "Duplicate task name", + "msg": f"Task name '{name}' is duplicated.", + "input": name, + } + + @hookimpl(specname="analyze_template") def check_metrics( template: Template, workflow: WorkflowCompatible From 464d5fb65ce2f3718520f81e397db3e88e0c3fb4 Mon Sep 17 00:00:00 2001 From: tzing Date: Thu, 23 Oct 2025 23:54:11 +0800 Subject: [PATCH 02/18] add: analyze_task hook --- tugboat/analyzers/template/__init__.py | 9 +++++++++ tugboat/hookspecs/workflow.py | 24 +++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/tugboat/analyzers/template/__init__.py b/tugboat/analyzers/template/__init__.py index bf23687..e5175c2 100644 --- a/tugboat/analyzers/template/__init__.py +++ b/tugboat/analyzers/template/__init__.py @@ -100,6 +100,15 @@ def check_dag(template: Template, workflow: WorkflowCompatible) -> Iterable[Diag "input": name, } + # investigate tasks + pm = get_plugin_manager() + + for idx, task in enumerate(template.dag.tasks or ()): + yield from prepend_loc.from_iterables( + ["dag", "tasks", idx], + pm.hook.analyze_task(task=task, template=template, workflow=workflow), + ) + @hookimpl(specname="analyze_template") def check_metrics( diff --git a/tugboat/hookspecs/workflow.py b/tugboat/hookspecs/workflow.py index 473cf85..4abaf0b 100644 --- a/tugboat/hookspecs/workflow.py +++ b/tugboat/hookspecs/workflow.py @@ -15,7 +15,7 @@ if typing.TYPE_CHECKING: from collections.abc import Iterable - from tugboat.schemas import Step, Template, Workflow, WorkflowTemplate + from tugboat.schemas import DagTask, Step, Template, Workflow, WorkflowTemplate from tugboat.types import Diagnosis @@ -84,3 +84,25 @@ def analyze_step( workflow : Workflow | WorkflowTemplate The workflow that the template is part of. """ + + +@hookspec +def analyze_task( + task: DagTask, template: Template, workflow: Workflow | WorkflowTemplate +) -> Iterable[Diagnosis]: # type: ignore[reportReturnType] + """ + Analyze a DAG template task. + + For issues reported by this hook, the ``loc`` attribute of the diagnosis + should start from the ``task`` object itself. The framework will manage + the outer structure and its location path. + + Parameters + ---------- + task : DagTask + The task to analyze. + template : Template + The template that the task is part of. + workflow : Workflow | WorkflowTemplate + The workflow that the template is part of. + """ From 7c9b5839e9ba0a9cba91ee51fc8303649a314f75 Mon Sep 17 00:00:00 2001 From: tzing Date: Mon, 27 Oct 2025 22:47:24 +0800 Subject: [PATCH 03/18] add: basic analyze_task --- tests/analyzers/test_dag.py | 34 ++++++++++++ tugboat/analyzers/dag.py | 104 ++++++++++++++++++++++++++++++++++++ tugboat/core.py | 2 + 3 files changed, 140 insertions(+) create mode 100644 tests/analyzers/test_dag.py create mode 100644 tugboat/analyzers/dag.py diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py new file mode 100644 index 0000000..019d928 --- /dev/null +++ b/tests/analyzers/test_dag.py @@ -0,0 +1,34 @@ +import pytest + +from tests.dirty_equals import ContainsSubStrings, IsPartialModel +from tugboat.engine import analyze_yaml_stream + + +def test_analyze_task(diagnoses_logger): + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: task-2 + depends: task-1 + dependencies: + - task-1 + template: test + - name: deprecated + onExit: exit + template: print-message + """ + ) + diagnoses_logger(diagnoses) + + loc = ("spec", "templates", 0, "dag", "tasks", 0) + assert IsPartialModel({"code": "M201", "loc": (*loc, "depends")}) in diagnoses + assert IsPartialModel({"code": "M201", "loc": (*loc, "dependencies")}) in diagnoses diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py new file mode 100644 index 0000000..bc1e14d --- /dev/null +++ b/tugboat/analyzers/dag.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +import logging +import re +import typing + +from rapidfuzz.process import extractOne + +from tugboat.constraints import ( + accept_none, + mutually_exclusive, + require_exactly_one, + require_non_empty, +) +from tugboat.core import get_plugin_manager, hookimpl +from tugboat.references import get_task_context +from tugboat.types import Field +from tugboat.utils import ( + check_model_fields_references, + check_value_references, + critique_relaxed_artifact, + critique_relaxed_parameter, + find_duplicate_names, + join_with_or, + prepend_loc, +) + +if typing.TYPE_CHECKING: + from collections.abc import Iterable + + from tugboat.references import Context + from tugboat.schemas import DagTask, Step, Template, Workflow, WorkflowTemplate + from tugboat.schemas.arguments import RelaxedArtifact, RelaxedParameter + from tugboat.types import Diagnosis + + type WorkflowCompatible = Workflow | WorkflowTemplate + +logger = logging.getLogger(__name__) + + +@hookimpl +def analyze_task( + task: DagTask, template: Template, workflow: Workflow | WorkflowTemplate +) -> Iterable[Diagnosis]: + yield from require_exactly_one( + model=task, + loc=(), + fields=["template", "templateRef", "inline"], + ) + yield from mutually_exclusive( + model=task, + loc=(), + fields=["withItems", "withParam", "withSequence"], + ) + yield from mutually_exclusive( + model=task, + loc=(), + fields=["depends", "dependencies"], + ) + + if task.onExit: + yield { + "code": "DAG901", + "loc": ("onExit",), + "summary": "Deprecated field", + "msg": "Field 'onExit' is deprecated. Please use 'hooks[exit].template' instead.", + "input": Field("onExit"), + } + + +@hookimpl(specname="analyze_task") +def check_argument_parameters( + task: DagTask, template: Template, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + if not task.arguments: + return + + # report duplicate names + for idx, name in find_duplicate_names(task.arguments.parameters or ()): + yield { + "code": "DAG102", + "loc": ("arguments", "parameters", idx, "name"), + "summary": "Duplicate parameter name", + "msg": f"Parameter name '{name}' is duplicated.", + "input": name, + } + + +@hookimpl(specname="analyze_task") +def check_argument_artifacts( + task: DagTask, template: Template, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + if not task.arguments: + return + + # report duplicate names + for idx, name in find_duplicate_names(task.arguments.artifacts or ()): + yield { + "code": "DAG103", + "loc": ("arguments", "artifacts", idx, "name"), + "summary": "Duplicate artifact name", + "msg": f"Artifact name '{name}' is duplicated.", + "input": name, + } diff --git a/tugboat/core.py b/tugboat/core.py index 64e22ed..d88b536 100644 --- a/tugboat/core.py +++ b/tugboat/core.py @@ -23,6 +23,7 @@ def get_plugin_manager() -> pluggy.PluginManager: # built-in hook implementations import tugboat.analyzers.container import tugboat.analyzers.cron_workflow + import tugboat.analyzers.dag import tugboat.analyzers.step import tugboat.analyzers.template import tugboat.analyzers.template.inputs @@ -33,6 +34,7 @@ def get_plugin_manager() -> pluggy.PluginManager: pm.register(tugboat.analyzers.container) pm.register(tugboat.analyzers.cron_workflow) + pm.register(tugboat.analyzers.dag) pm.register(tugboat.analyzers.step) pm.register(tugboat.analyzers.template) pm.register(tugboat.analyzers.template.inputs) From d84d6a8394e9e84bfde500ed5326cb666a2e184f Mon Sep 17 00:00:00 2001 From: tzing Date: Mon, 27 Oct 2025 22:51:43 +0800 Subject: [PATCH 04/18] add: dag102, dag103 --- docs/rules/dag.rst | 56 ++++++++++++++++++++++++++++++++ tests/analyzers/test_dag.py | 65 +++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index f993755..b8bffa0 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -37,3 +37,59 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. parameters: - name: message value: "hello-2" + +.. rule:: DAG102 Duplicate input parameters + + The task contains several input parameters (``.arguments.parameters``) that share the same name, + which means the parameter was set multiple times. + + .. code-block:: yaml + :emphasize-lines: 15,17 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + name: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello" + - name: message + value: "world" + +.. rule:: DAG103 Duplicate input artifacts + + The task includes several input artifacts (``.arguments.artifacts``) that share the same name. + The artifact was set multiple times. + + .. code-block:: yaml + :emphasize-lines: 15,18 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + name: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process-data + template: data-processor + arguments: + artifacts: + - name: input-data + raw: + data: "data-1" + - name: input-data + raw: + data: "data-2" diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index 019d928..9a12f7c 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -32,3 +32,68 @@ def test_analyze_task(diagnoses_logger): loc = ("spec", "templates", 0, "dag", "tasks", 0) assert IsPartialModel({"code": "M201", "loc": (*loc, "depends")}) in diagnoses assert IsPartialModel({"code": "M201", "loc": (*loc, "dependencies")}) in diagnoses + + +def test_check_argument_parameters(diagnoses_logger): + diagnoses = analyze_yaml_stream(MANIFEST_INVALID_INPUT_PARAMETERS) + diagnoses_logger(diagnoses) + + loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "parameters") + + # DAG102: Duplicate parameter name + assert IsPartialModel(code="DAG102", loc=(*loc_prefix, 0, "name")) in diagnoses + assert IsPartialModel(code="DAG102", loc=(*loc_prefix, 1, "name")) in diagnoses + + +MANIFEST_INVALID_INPUT_PARAMETERS = """ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: test- +spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: step1 + arguments: + parameters: + - name: param1 + value: "value1" + - name: param1 + value: "value2" +""" + + +def test_check_argument_artifacts(diagnoses_logger): + diagnoses = analyze_yaml_stream(MANIFEST_INVALID_INPUT_ARTIFACTS) + diagnoses_logger(diagnoses) + + loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "artifacts") + + # DAG103: Duplicate artifact name + assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 0, "name")) in diagnoses + assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 1, "name")) in diagnoses + + +MANIFEST_INVALID_INPUT_ARTIFACTS = """ +apiVersion: argoproj.io/v1alpha1 +kind: Workflow +metadata: + generateName: test- +spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: step1 + arguments: + artifacts: + - name: artifact1 + from: "{{workflow.invalid}}" + - name: artifact1 + raw: + data: "some data" +""" From bb49f9abc5e9fca29401f9b9545806145e786ac0 Mon Sep 17 00:00:00 2001 From: tzing Date: Mon, 3 Nov 2025 22:56:09 +0800 Subject: [PATCH 05/18] add: dag201 --- docs/rules/dag.rst | 24 ++++++++++++++++++ tests/analyzers/test_dag.py | 50 +++++++++++++++++++++++++++++++++++++ tugboat/analyzers/dag.py | 41 ++++++++++++++++++++++++++++++ 3 files changed, 115 insertions(+) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index b8bffa0..fcffd17 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -93,3 +93,27 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. - name: input-data raw: data: "data-2" + +.. DAG2xx template reference issues + +.. rule:: DAG201 Self-referencing task + + The task references the current template in the ``template`` field. This may cause an infinite loop. + + Since this can still be intentional, this rule defaults to warning level. + + .. code-block:: yaml + :emphasize-lines: 12 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: main diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index 9a12f7c..dd552ac 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -97,3 +97,53 @@ def test_check_argument_artifacts(diagnoses_logger): raw: data: "some data" """ + + +def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_logger): + with caplog.at_level("DEBUG", "tugboat.analyzers.dag"): + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: WorkflowTemplate + metadata: + name: demo + spec: + templates: + - name: main + dag: + tasks: + - name: loop + template: main + + - name: invalid-reference + dag: + tasks: + - name: missing + templateRef: + name: demo + template: not-exist-template + - name: external + templateRef: + name: another-workflow + template: whatever + + - name: another + suspend: {} + """ + ) + + diagnoses_logger(diagnoses) + + assert ( + IsPartialModel( + { + "code": "DAG201", + "loc": ("spec", "templates", 0, "dag", "tasks", 0, "template"), + } + ) + in diagnoses + ) + assert ( + "Task 'external': Referenced template 'another-workflow' is not the same as current workflow 'demo'. Skipping." + in caplog.text + ) diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index bc1e14d..ae665ac 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -102,3 +102,44 @@ def check_argument_artifacts( "msg": f"Artifact name '{name}' is duplicated.", "input": name, } + + +@hookimpl(specname="analyze_task") +def check_referenced_template( + task: DagTask, template: Template, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + if task.template: + yield from prepend_loc( + ("template",), + _check_referenced_template(task.template, template, workflow), + ) + + elif task.templateRef: + if task.templateRef.name == workflow.metadata.name: + yield from prepend_loc( + ("templateRef", "template"), + _check_referenced_template( + task.templateRef.template, template, workflow + ), + ) + else: + logger.debug( + "Task '%s': Referenced template '%s' is not the same as current workflow '%s'. Skipping.", + task.name, + task.templateRef.name, + workflow.name, + ) + + +def _check_referenced_template( + target_template_name: str, template: Template, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + if target_template_name == template.name: + yield { + "type": "warning", + "code": "DAG201", + "loc": (), + "summary": "Self-referencing", + "msg": "Self-referencing may cause infinite recursion.", + "input": target_template_name, + } From 2f60f4b31fd04934936f40e4813b70b86a6bfd35 Mon Sep 17 00:00:00 2001 From: tzing Date: Mon, 3 Nov 2025 23:13:36 +0800 Subject: [PATCH 06/18] add: dag202 --- docs/rules/dag.rst | 20 ++++++++++++++++++++ tests/analyzers/test_dag.py | 24 +++++++++++++++++++++++- tugboat/analyzers/dag.py | 22 ++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index fcffd17..f6d2714 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -117,3 +117,23 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. tasks: - name: process template: main + +.. rule:: DAG202 Reference to a non-existent template + + The task references a template that does not exist in the workflow. + + .. code-block:: yaml + :emphasize-lines: 12 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: not-exist-template diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index dd552ac..aa72ae7 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -1,6 +1,6 @@ import pytest -from tests.dirty_equals import ContainsSubStrings, IsPartialModel +from tests.dirty_equals import ContainsSubStrings, IsPartialModel, HasSubstring from tugboat.engine import analyze_yaml_stream @@ -143,6 +143,28 @@ def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_l ) in diagnoses ) + assert ( + IsPartialModel( + { + "code": "DAG202", + "loc": ( + "spec", + "templates", + 1, + "dag", + "tasks", + 0, + "templateRef", + "template", + ), + "msg": HasSubstring( + "Template 'not-exist-template' does not exist in the workflow." + ) + & HasSubstring("Available templates: 'another' or 'main'"), + } + ) + in diagnoses + ) assert ( "Task 'external': Referenced template 'another-workflow' is not the same as current workflow 'demo'. Skipping." in caplog.text diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index ae665ac..fa7ad35 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -143,3 +143,25 @@ def _check_referenced_template( "msg": "Self-referencing may cause infinite recursion.", "input": target_template_name, } + + if target_template_name not in workflow.template_dict: + templates = set(workflow.template_dict) + templates -= {template.name} + + suggestion = None + if result := extractOne(target_template_name, templates): + suggestion, _, _ = result + + yield { + "code": "DAG202", + "loc": (), + "summary": "Template not found", + "msg": ( + f""" + Template '{target_template_name}' does not exist in the workflow. + Available templates: {join_with_or(templates)} + """ + ), + "input": target_template_name, + "fix": suggestion, + } From ede319c36bed3c222225c5c7dc08042f08639396 Mon Sep 17 00:00:00 2001 From: tzing Date: Mon, 3 Nov 2025 23:22:40 +0800 Subject: [PATCH 07/18] doc: dag901 --- docs/rules/dag.rst | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index f6d2714..c5d3bcd 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -137,3 +137,54 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. tasks: - name: process template: not-exist-template + +.. DAG9xx deprecated items + +.. rule:: DAG901 Deprecated Field: ``onExit`` + + The ``onExit`` field in a task definition is deprecated. + + As of Argo Workflows 3.1, ``onExit`` is deprecated by the :py:class:`~tugboat.schemas.DagTask.hooks` field. + This rule is a variant of :rule:`STP901` specific to DAG templates. + + .. code-block:: yaml + :caption: ❌ Example manifest that violates this rule + :emphasize-lines: 18 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: build + onExit: exit + template: run-build + + - name: run-build + container: + image: alpine + + .. code-block:: yaml + :caption: ✅ Example manifest that complies with this rule + :emphasize-lines: 13-15 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: build + template: run-build + hooks: + exit: + template: cleanup From 18059433588a32d8e81785b7c930a2f389fd93bc Mon Sep 17 00:00:00 2001 From: tzing Date: Mon, 3 Nov 2025 23:45:50 +0800 Subject: [PATCH 08/18] add: dag basic field checks --- docs/rules/dag.rst | 7 + tests/analyzers/test_dag.py | 183 +++++++++++++++++---------- tugboat/analyzers/dag.py | 20 +++ tugboat/analyzers/step.py | 4 +- tugboat/schemas/template/__init__.py | 1 + 5 files changed, 144 insertions(+), 71 deletions(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index c5d3bcd..b77d56f 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -138,6 +138,13 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. - name: process template: not-exist-template +.. DAG3xx argument reference issues + +.. rule:: DAG301 Invalid parameter reference + + A task input parameter contains a reference that cannot be resolved. + This rule builds on :rule:`VAR002` but narrows the report to the task argument where the typo or mismatch occurs. + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index aa72ae7..b12655d 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -1,6 +1,7 @@ import pytest +from dirty_equals import AnyThing -from tests.dirty_equals import ContainsSubStrings, IsPartialModel, HasSubstring +from tests.dirty_equals import HasSubstring, IsPartialModel from tugboat.engine import analyze_yaml_stream @@ -30,75 +31,120 @@ def test_analyze_task(diagnoses_logger): diagnoses_logger(diagnoses) loc = ("spec", "templates", 0, "dag", "tasks", 0) - assert IsPartialModel({"code": "M201", "loc": (*loc, "depends")}) in diagnoses - assert IsPartialModel({"code": "M201", "loc": (*loc, "dependencies")}) in diagnoses + assert IsPartialModel(code="M201", loc=(*loc, "depends")) in diagnoses + assert IsPartialModel(code="M201", loc=(*loc, "dependencies")) in diagnoses def test_check_argument_parameters(diagnoses_logger): - diagnoses = analyze_yaml_stream(MANIFEST_INVALID_INPUT_PARAMETERS) + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: step1 + arguments: + parameters: + - name: param1 + value: "value1" + - name: param1 + value: "value2" + """ + ) diagnoses_logger(diagnoses) loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "parameters") - - # DAG102: Duplicate parameter name assert IsPartialModel(code="DAG102", loc=(*loc_prefix, 0, "name")) in diagnoses assert IsPartialModel(code="DAG102", loc=(*loc_prefix, 1, "name")) in diagnoses -MANIFEST_INVALID_INPUT_PARAMETERS = """ -apiVersion: argoproj.io/v1alpha1 -kind: Workflow -metadata: - generateName: test- -spec: - entrypoint: main - templates: - - name: main - dag: - tasks: - - name: step1 - arguments: - parameters: - - name: param1 - value: "value1" - - name: param1 - value: "value2" -""" +def test_check_argument_parameter_fields(diagnoses_logger): + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: task1 + template: compute + arguments: + parameters: + - name: message + valueFrom: + path: /tmp/message + - name: invalid-ref + value: "{{ workflow.invalid }}" + - name: structured + value: + foo: bar + - name: "" + value: foo + - name: both + value: baz + valueFrom: + parameter: message + - name: compute + container: + image: alpine:3.19 + """ + ) + diagnoses_logger(diagnoses) + + loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "parameters") + assert ( + IsPartialModel(code="M102", loc=(*loc_prefix, 0, "valueFrom", "path")) + in diagnoses + ) + assert IsPartialModel(code="M101", loc=(*loc_prefix, 0, "valueFrom")) in diagnoses + assert IsPartialModel(code="DAG301", loc=(*loc_prefix, 1, "value")) in diagnoses + assert IsPartialModel(code="M103", loc=(*loc_prefix, 2, "value")) in diagnoses + assert IsPartialModel(code="M202", loc=(*loc_prefix, 3, "name")) in diagnoses + assert IsPartialModel(code="M201", loc=(*loc_prefix, 4, "value")) in diagnoses + assert IsPartialModel(code="M201", loc=(*loc_prefix, 4, "valueFrom")) in diagnoses def test_check_argument_artifacts(diagnoses_logger): - diagnoses = analyze_yaml_stream(MANIFEST_INVALID_INPUT_ARTIFACTS) + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: step1 + arguments: + artifacts: + - name: artifact1 + from: "{{workflow.invalid}}" + - name: artifact1 + raw: + data: "some data" + """ + ) diagnoses_logger(diagnoses) loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "artifacts") - - # DAG103: Duplicate artifact name assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 0, "name")) in diagnoses assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 1, "name")) in diagnoses -MANIFEST_INVALID_INPUT_ARTIFACTS = """ -apiVersion: argoproj.io/v1alpha1 -kind: Workflow -metadata: - generateName: test- -spec: - entrypoint: main - templates: - - name: main - dag: - tasks: - - name: step1 - arguments: - artifacts: - - name: artifact1 - from: "{{workflow.invalid}}" - - name: artifact1 - raw: - data: "some data" -""" - - def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_logger): with caplog.at_level("DEBUG", "tugboat.analyzers.dag"): diagnoses = analyze_yaml_stream( @@ -136,32 +182,31 @@ def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_l assert ( IsPartialModel( - { - "code": "DAG201", - "loc": ("spec", "templates", 0, "dag", "tasks", 0, "template"), - } + code="DAG201", + loc=("spec", "templates", 0, "dag", "tasks", 0, "template"), ) in diagnoses ) assert ( IsPartialModel( - { - "code": "DAG202", - "loc": ( - "spec", - "templates", - 1, - "dag", - "tasks", - 0, - "templateRef", - "template", - ), - "msg": HasSubstring( + code="DAG202", + loc=( + "spec", + "templates", + 1, + "dag", + "tasks", + 0, + "templateRef", + "template", + ), + msg=( + AnyThing + & HasSubstring( "Template 'not-exist-template' does not exist in the workflow." ) - & HasSubstring("Available templates: 'another' or 'main'"), - } + & HasSubstring("Available templates: 'another' or 'main'") + ), ) in diagnoses ) diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index fa7ad35..eb9bfdb 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -6,6 +6,7 @@ from rapidfuzz.process import extractOne +from tugboat.analyzers.step import check_argument_parameter_fields from tugboat.constraints import ( accept_none, mutually_exclusive, @@ -85,6 +86,25 @@ def check_argument_parameters( "input": name, } + # check fields for each parameter + ctx = get_task_context(workflow, template, task) + + for idx, param in enumerate(task.arguments.parameters or ()): + yield from prepend_loc( + ("arguments", "parameters", idx), + _check_argument_parameter_fields(param, ctx), + ) + + +def _check_argument_parameter_fields( + param: RelaxedParameter, context: Context +) -> Iterable[Diagnosis]: + for diag in check_argument_parameter_fields(param, context): + match diag["code"]: + case "STP301": + diag["code"] = "DAG301" + yield diag + @hookimpl(specname="analyze_task") def check_argument_artifacts( diff --git a/tugboat/analyzers/step.py b/tugboat/analyzers/step.py index 6141333..22e8d28 100644 --- a/tugboat/analyzers/step.py +++ b/tugboat/analyzers/step.py @@ -88,11 +88,11 @@ def check_argument_parameters( for idx, param in enumerate(step.arguments.parameters or ()): yield from prepend_loc( ("arguments", "parameters", idx), - _check_argument_parameter_fields(param, ctx), + check_argument_parameter_fields(param, ctx), ) -def _check_argument_parameter_fields( +def check_argument_parameter_fields( param: RelaxedParameter, context: Context ) -> Iterable[Diagnosis]: yield from require_non_empty( diff --git a/tugboat/schemas/template/__init__.py b/tugboat/schemas/template/__init__.py index cf7bdf4..dcc0b7f 100644 --- a/tugboat/schemas/template/__init__.py +++ b/tugboat/schemas/template/__init__.py @@ -173,6 +173,7 @@ class DagTask(_StepBase): .. _DAGTask: https://argo-workflows.readthedocs.io/en/latest/fields/#dagtask """ + arguments: RelaxedArguments | None = None # type: ignore[reportIncompatibleVariableOverride] dependencies: Array[str] | None = None depends: str | None = None From 6714de962212f1ee0b8ee0c8725f23d2a0a465f0 Mon Sep 17 00:00:00 2001 From: tzing Date: Tue, 4 Nov 2025 00:06:03 +0800 Subject: [PATCH 09/18] add: dag302 --- docs/rules/dag.rst | 25 +++++++++++++++++++++++++ tests/analyzers/test_dag.py | 4 +++- tugboat/analyzers/dag.py | 24 +++++++++++++++++++++++- tugboat/analyzers/step.py | 4 ++-- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index b77d56f..6a8edec 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -145,6 +145,31 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. A task input parameter contains a reference that cannot be resolved. This rule builds on :rule:`VAR002` but narrows the report to the task argument where the typo or mismatch occurs. +.. rule:: DAG302 Invalid artifact reference + + A task input artifact points to an artifact that is not available in the current context. + This can happen with bare references (``inputs.artifacts.foo``) or templated expressions (``{{ inputs.artifacts.foo }}``) when the name is misspelled or simply not defined. + + .. code-block:: yaml + :emphasize-lines: 16 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: compute + arguments: + artifacts: + - name: data + from: "{{ inputs.artifacts.not-exist }}" + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index b12655d..1c0fc74 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -129,10 +129,11 @@ def test_check_argument_artifacts(diagnoses_logger): dag: tasks: - name: step1 + template: compute arguments: artifacts: - name: artifact1 - from: "{{workflow.invalid}}" + from: workflow.invalid - name: artifact1 raw: data: "some data" @@ -143,6 +144,7 @@ def test_check_argument_artifacts(diagnoses_logger): loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "artifacts") assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 0, "name")) in diagnoses assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 1, "name")) in diagnoses + assert IsPartialModel(code="DAG302", loc=(*loc_prefix, 0, "from")) in diagnoses def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_logger): diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index eb9bfdb..322e9a7 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -6,7 +6,10 @@ from rapidfuzz.process import extractOne -from tugboat.analyzers.step import check_argument_parameter_fields +from tugboat.analyzers.step import ( + check_argument_artifact_fields, + check_argument_parameter_fields, +) from tugboat.constraints import ( accept_none, mutually_exclusive, @@ -123,6 +126,25 @@ def check_argument_artifacts( "input": name, } + # check fields for each parameter + ctx = get_task_context(workflow, template, task) + + for idx, artifact in enumerate(task.arguments.artifacts or ()): + yield from prepend_loc( + ("arguments", "artifacts", idx), + _check_argument_artifact_fields(artifact, ctx), + ) + + +def _check_argument_artifact_fields( + artifact: RelaxedArtifact, context: Context +) -> Iterable[Diagnosis]: + for diag in check_argument_artifact_fields(artifact, context): + match diag["code"]: + case "STP302": + diag["code"] = "DAG302" + yield diag + @hookimpl(specname="analyze_task") def check_referenced_template( diff --git a/tugboat/analyzers/step.py b/tugboat/analyzers/step.py index 22e8d28..d9ef952 100644 --- a/tugboat/analyzers/step.py +++ b/tugboat/analyzers/step.py @@ -230,11 +230,11 @@ def check_argument_artifacts( for idx, artifact in enumerate(step.arguments.artifacts or []): yield from prepend_loc( ("arguments", "artifacts", idx), - _check_argument_artifact_fields(artifact, ctx), + check_argument_artifact_fields(artifact, ctx), ) -def _check_argument_artifact_fields( +def check_argument_artifact_fields( artifact: RelaxedArtifact, context: Context ) -> Iterable[Diagnosis]: yield from require_non_empty( From 6f7228ab6c3913e7a3d270465fc711b52d2a37ba Mon Sep 17 00:00:00 2001 From: tzing Date: Tue, 4 Nov 2025 00:12:24 +0800 Subject: [PATCH 10/18] add: dag303 --- docs/rules/dag.rst | 27 +++++++++++++++++++++++++++ tests/analyzers/test_dag.py | 7 +++++-- tugboat/analyzers/dag.py | 2 ++ 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index 6a8edec..e30e0f9 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -170,6 +170,33 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. - name: data from: "{{ inputs.artifacts.not-exist }}" +.. rule:: DAG303 Invalid raw artifact reference + + A task input artifact uses the :py:attr:`~tugboat.schemas.Artifact.raw` block to embed a template expression, but the expression does not resolve to a known parameter. + + Raw artifacts only support parameter references (e.g. ``{{ inputs.parameters.foo }}``); pointing at artifacts leaves the expression unresolved. + + .. code-block:: yaml + :emphasize-lines: 17 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: compute + arguments: + artifacts: + - name: data + raw: + data: "{{ inputs.artifacts.any }}" + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index 1c0fc74..d3ac729 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -133,10 +133,10 @@ def test_check_argument_artifacts(diagnoses_logger): arguments: artifacts: - name: artifact1 - from: workflow.invalid + from: "{{ workflow.invalid }}" - name: artifact1 raw: - data: "some data" + data: "{{ workflow.invalid }}" """ ) diagnoses_logger(diagnoses) @@ -145,6 +145,9 @@ def test_check_argument_artifacts(diagnoses_logger): assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 0, "name")) in diagnoses assert IsPartialModel(code="DAG103", loc=(*loc_prefix, 1, "name")) in diagnoses assert IsPartialModel(code="DAG302", loc=(*loc_prefix, 0, "from")) in diagnoses + assert ( + IsPartialModel(code="DAG303", loc=(*loc_prefix, 1, "raw", "data")) in diagnoses + ) def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_logger): diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index 322e9a7..8dd3ce8 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -143,6 +143,8 @@ def _check_argument_artifact_fields( match diag["code"]: case "STP302": diag["code"] = "DAG302" + case "STP303": + diag["code"] = "DAG303" yield diag From 8e2fcc87dcadc8b1614c3e89891cde0a4444c792 Mon Sep 17 00:00:00 2001 From: tzing Date: Tue, 4 Nov 2025 00:33:24 +0800 Subject: [PATCH 11/18] add: dag304 --- docs/rules/dag.rst | 34 +++++++++++++++++++++++++++++++++ tests/analyzers/test_dag.py | 38 +++++++++++++++++++++++++++++++++++++ tugboat/analyzers/dag.py | 36 +++++++++++++++++++++++++++++++++++ tugboat/analyzers/step.py | 11 +++++++---- 4 files changed, 115 insertions(+), 4 deletions(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index e30e0f9..b762b8a 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -197,6 +197,40 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. raw: data: "{{ inputs.artifacts.any }}" +.. rule:: DAG304 Unexpected argument parameters + + The task provides parameters that the referenced template does not declare. + Remove the extra entries or update the target template so both sides align. + + .. code-block:: yaml + :emphasize-lines: 17 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: print-message + arguments: + parameters: + - name: message + value: hello + - name: extra-param + value: ignored + + - name: print-message + inputs: + parameters: + - name: message + - name: role + default: user + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index d3ac729..1f1cad0 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -115,6 +115,44 @@ def test_check_argument_parameter_fields(diagnoses_logger): assert IsPartialModel(code="M201", loc=(*loc_prefix, 4, "valueFrom")) in diagnoses +def test_check_argument_parameters_usage(diagnoses_logger): + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: hello + template: print-message + arguments: + parameters: + - name: role + value: admin + - name: extra-param + value: blah + - name: print-message + inputs: + parameters: + - name: message + - name: role + default: user + """ + ) + diagnoses_logger(diagnoses) + + loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "parameters") + assert ( + IsPartialModel(code="DAG304", loc=(*loc_prefix, 1, "name")) + in diagnoses + ) + + def test_check_argument_artifacts(diagnoses_logger): diagnoses = analyze_yaml_stream( """ diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index 8dd3ce8..d5d0f7d 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -9,6 +9,7 @@ from tugboat.analyzers.step import ( check_argument_artifact_fields, check_argument_parameter_fields, + get_template_by_ref, ) from tugboat.constraints import ( accept_none, @@ -18,6 +19,7 @@ ) from tugboat.core import get_plugin_manager, hookimpl from tugboat.references import get_task_context +from tugboat.schemas import Arguments from tugboat.types import Field from tugboat.utils import ( check_model_fields_references, @@ -148,6 +150,40 @@ def _check_argument_artifact_fields( yield diag +@hookimpl(specname="analyze_task") +def check_argument_parameters_usage( + task: DagTask, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + ref_template = get_template_by_ref(task, workflow) + if not ref_template: + return + + arguments = task.arguments or Arguments() + + if ref_template.inputs: + expected_params = set(ref_template.inputs.parameter_dict) + else: + expected_params = set() + + for idx, param in enumerate(arguments.parameters or ()): + if param.name and param.name not in expected_params: + suggestion = None + if result := extractOne(param.name, expected_params): + suggestion, _, _ = result + + yield { + "type": "warning", + "code": "DAG304", + "loc": ("arguments", "parameters", idx, "name"), + "summary": "Unexpected parameter", + "msg": ( + f"Parameter '{param.name}' is not expected by the template '{ref_template.name}'." + ), + "input": param.name, + "fix": suggestion, + } + + @hookimpl(specname="analyze_task") def check_referenced_template( task: DagTask, template: Template, workflow: WorkflowCompatible diff --git a/tugboat/analyzers/step.py b/tugboat/analyzers/step.py index d9ef952..22dd7e6 100644 --- a/tugboat/analyzers/step.py +++ b/tugboat/analyzers/step.py @@ -31,10 +31,11 @@ from collections.abc import Iterable from tugboat.references import Context - from tugboat.schemas import Step, Template, Workflow, WorkflowTemplate + from tugboat.schemas import DagTask, Step, Template, Workflow, WorkflowTemplate from tugboat.schemas.arguments import RelaxedArtifact, RelaxedParameter from tugboat.types import Diagnosis + type TaskCompatible = DagTask | Step type WorkflowCompatible = Workflow | WorkflowTemplate logger = logging.getLogger(__name__) @@ -147,7 +148,7 @@ def check_argument_parameters_usage( step: Step, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: # early exit: referenced template not found - ref_template = _get_template_by_ref(step, workflow) + ref_template = get_template_by_ref(step, workflow) if not ref_template: return @@ -199,7 +200,9 @@ def check_argument_parameters_usage( } -def _get_template_by_ref(step: Step, workflow: WorkflowCompatible) -> Template | None: +def get_template_by_ref( + step: TaskCompatible, workflow: WorkflowCompatible +) -> Template | None: if step.template: return workflow.template_dict.get(step.template) if step.templateRef and step.templateRef.name == workflow.metadata.name: @@ -332,7 +335,7 @@ def check_argument_artifact_usage( step: Step, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: # early exit: referenced template not found - ref_template = _get_template_by_ref(step, workflow) + ref_template = get_template_by_ref(step, workflow) if not ref_template: return From bdc383d422e2fbe5ad8d65293b9c0dda7f733102 Mon Sep 17 00:00:00 2001 From: tzing Date: Tue, 4 Nov 2025 06:22:17 +0800 Subject: [PATCH 12/18] add: dag305 --- docs/rules/dag.rst | 26 ++++++++++++ tests/analyzers/test_dag.py | 1 + tugboat/analyzers/dag.py | 83 ++++++++++--------------------------- tugboat/analyzers/step.py | 8 ++-- 4 files changed, 53 insertions(+), 65 deletions(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index b762b8a..aafdfed 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -231,6 +231,32 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. - name: role default: user +.. rule:: DAG305 Missing argument parameters + + The task skips parameters that are required by the referenced template. + Ensure every parameter without a default or static value on the template side is provided by the task. + + .. code-block:: yaml + :emphasize-lines: 11-12 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: print-message + + - name: print-message + inputs: + parameters: + - name: message + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index 1f1cad0..d51a52e 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -151,6 +151,7 @@ def test_check_argument_parameters_usage(diagnoses_logger): IsPartialModel(code="DAG304", loc=(*loc_prefix, 1, "name")) in diagnoses ) + assert IsPartialModel(code="DAG305", loc=loc_prefix) in diagnoses def test_check_argument_artifacts(diagnoses_logger): diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index d5d0f7d..6f11fa7 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -6,11 +6,7 @@ from rapidfuzz.process import extractOne -from tugboat.analyzers.step import ( - check_argument_artifact_fields, - check_argument_parameter_fields, - get_template_by_ref, -) +import tugboat.analyzers.step as step_analyzer from tugboat.constraints import ( accept_none, mutually_exclusive, @@ -95,20 +91,13 @@ def check_argument_parameters( ctx = get_task_context(workflow, template, task) for idx, param in enumerate(task.arguments.parameters or ()): - yield from prepend_loc( - ("arguments", "parameters", idx), - _check_argument_parameter_fields(param, ctx), - ) + for diag in step_analyzer.check_argument_parameter_fields(param, ctx): + diag["loc"] = ["arguments", "parameters", idx, *diag["loc"]] - -def _check_argument_parameter_fields( - param: RelaxedParameter, context: Context -) -> Iterable[Diagnosis]: - for diag in check_argument_parameter_fields(param, context): - match diag["code"]: - case "STP301": - diag["code"] = "DAG301" - yield diag + match diag["code"]: + case "STP301": + diag["code"] = "DAG301" + yield diag @hookimpl(specname="analyze_task") @@ -132,56 +121,28 @@ def check_argument_artifacts( ctx = get_task_context(workflow, template, task) for idx, artifact in enumerate(task.arguments.artifacts or ()): - yield from prepend_loc( - ("arguments", "artifacts", idx), - _check_argument_artifact_fields(artifact, ctx), - ) + for diag in step_analyzer.check_argument_artifact_fields(artifact, ctx): + diag["loc"] = ["arguments", "artifacts", idx, *diag["loc"]] - -def _check_argument_artifact_fields( - artifact: RelaxedArtifact, context: Context -) -> Iterable[Diagnosis]: - for diag in check_argument_artifact_fields(artifact, context): - match diag["code"]: - case "STP302": - diag["code"] = "DAG302" - case "STP303": - diag["code"] = "DAG303" - yield diag + match diag["code"]: + case "STP302": + diag["code"] = "DAG302" + case "STP303": + diag["code"] = "DAG303" + yield diag @hookimpl(specname="analyze_task") def check_argument_parameters_usage( task: DagTask, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: - ref_template = get_template_by_ref(task, workflow) - if not ref_template: - return - - arguments = task.arguments or Arguments() - - if ref_template.inputs: - expected_params = set(ref_template.inputs.parameter_dict) - else: - expected_params = set() - - for idx, param in enumerate(arguments.parameters or ()): - if param.name and param.name not in expected_params: - suggestion = None - if result := extractOne(param.name, expected_params): - suggestion, _, _ = result - - yield { - "type": "warning", - "code": "DAG304", - "loc": ("arguments", "parameters", idx, "name"), - "summary": "Unexpected parameter", - "msg": ( - f"Parameter '{param.name}' is not expected by the template '{ref_template.name}'." - ), - "input": param.name, - "fix": suggestion, - } + for diag in step_analyzer.check_argument_parameters_usage(task, workflow): + match diag["code"]: + case "STP304": + diag["code"] = "DAG304" + case "STP305": + diag["code"] = "DAG305" + yield diag @hookimpl(specname="analyze_task") diff --git a/tugboat/analyzers/step.py b/tugboat/analyzers/step.py index 22dd7e6..050468b 100644 --- a/tugboat/analyzers/step.py +++ b/tugboat/analyzers/step.py @@ -145,10 +145,10 @@ def check_argument_parameter_fields( @hookimpl(specname="analyze_step") def check_argument_parameters_usage( - step: Step, workflow: WorkflowCompatible + step: TaskCompatible, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: # early exit: referenced template not found - ref_template = get_template_by_ref(step, workflow) + ref_template = _get_template_by_ref(step, workflow) if not ref_template: return @@ -200,7 +200,7 @@ def check_argument_parameters_usage( } -def get_template_by_ref( +def _get_template_by_ref( step: TaskCompatible, workflow: WorkflowCompatible ) -> Template | None: if step.template: @@ -335,7 +335,7 @@ def check_argument_artifact_usage( step: Step, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: # early exit: referenced template not found - ref_template = get_template_by_ref(step, workflow) + ref_template = _get_template_by_ref(step, workflow) if not ref_template: return From 352afd14b9ece40bd97cfd032a0de03bf5ac37f4 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 17:19:16 +0800 Subject: [PATCH 13/18] chore: apply changes from main --- tests/analyzers/test_dag.py | 15 ++++----------- tugboat/analyzers/dag.py | 32 +++++++++----------------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index d51a52e..988bb78 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -86,9 +86,6 @@ def test_check_argument_parameter_fields(diagnoses_logger): path: /tmp/message - name: invalid-ref value: "{{ workflow.invalid }}" - - name: structured - value: - foo: bar - name: "" value: foo - name: both @@ -109,10 +106,9 @@ def test_check_argument_parameter_fields(diagnoses_logger): ) assert IsPartialModel(code="M101", loc=(*loc_prefix, 0, "valueFrom")) in diagnoses assert IsPartialModel(code="DAG301", loc=(*loc_prefix, 1, "value")) in diagnoses - assert IsPartialModel(code="M103", loc=(*loc_prefix, 2, "value")) in diagnoses - assert IsPartialModel(code="M202", loc=(*loc_prefix, 3, "name")) in diagnoses - assert IsPartialModel(code="M201", loc=(*loc_prefix, 4, "value")) in diagnoses - assert IsPartialModel(code="M201", loc=(*loc_prefix, 4, "valueFrom")) in diagnoses + assert IsPartialModel(code="M202", loc=(*loc_prefix, 2, "name")) in diagnoses + assert IsPartialModel(code="M201", loc=(*loc_prefix, 3, "value")) in diagnoses + assert IsPartialModel(code="M201", loc=(*loc_prefix, 3, "valueFrom")) in diagnoses def test_check_argument_parameters_usage(diagnoses_logger): @@ -147,10 +143,7 @@ def test_check_argument_parameters_usage(diagnoses_logger): diagnoses_logger(diagnoses) loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "parameters") - assert ( - IsPartialModel(code="DAG304", loc=(*loc_prefix, 1, "name")) - in diagnoses - ) + assert IsPartialModel(code="DAG304", loc=(*loc_prefix, 1, "name")) in diagnoses assert IsPartialModel(code="DAG305", loc=loc_prefix) in diagnoses diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index 6f11fa7..3b3a86f 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -1,27 +1,16 @@ from __future__ import annotations import logging -import re import typing from rapidfuzz.process import extractOne import tugboat.analyzers.step as step_analyzer -from tugboat.constraints import ( - accept_none, - mutually_exclusive, - require_exactly_one, - require_non_empty, -) -from tugboat.core import get_plugin_manager, hookimpl +from tugboat.constraints import mutually_exclusive +from tugboat.core import hookimpl from tugboat.references import get_task_context -from tugboat.schemas import Arguments from tugboat.types import Field from tugboat.utils import ( - check_model_fields_references, - check_value_references, - critique_relaxed_artifact, - critique_relaxed_parameter, find_duplicate_names, join_with_or, prepend_loc, @@ -30,9 +19,7 @@ if typing.TYPE_CHECKING: from collections.abc import Iterable - from tugboat.references import Context - from tugboat.schemas import DagTask, Step, Template, Workflow, WorkflowTemplate - from tugboat.schemas.arguments import RelaxedArtifact, RelaxedParameter + from tugboat.schemas import DagTask, Template, Workflow, WorkflowTemplate from tugboat.types import Diagnosis type WorkflowCompatible = Workflow | WorkflowTemplate @@ -44,24 +31,23 @@ def analyze_task( task: DagTask, template: Template, workflow: Workflow | WorkflowTemplate ) -> Iterable[Diagnosis]: - yield from require_exactly_one( - model=task, - loc=(), + yield from mutually_exclusive( + task, fields=["template", "templateRef", "inline"], + require_one=True, ) yield from mutually_exclusive( - model=task, - loc=(), + task, fields=["withItems", "withParam", "withSequence"], ) yield from mutually_exclusive( - model=task, - loc=(), + task, fields=["depends", "dependencies"], ) if task.onExit: yield { + "type": "warning", "code": "DAG901", "loc": ("onExit",), "summary": "Deprecated field", From b6654676c26d2c6152d4efe926e9bd6abf67ed15 Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 17:47:21 +0800 Subject: [PATCH 14/18] enh: reduce repeated logic --- tugboat/analyzers/dag.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index 3b3a86f..40f87cb 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -62,9 +62,11 @@ def check_argument_parameters( ) -> Iterable[Diagnosis]: if not task.arguments: return + if not task.arguments.parameters: + return # report duplicate names - for idx, name in find_duplicate_names(task.arguments.parameters or ()): + for idx, name in find_duplicate_names(task.arguments.parameters): yield { "code": "DAG102", "loc": ("arguments", "parameters", idx, "name"), @@ -76,7 +78,7 @@ def check_argument_parameters( # check fields for each parameter ctx = get_task_context(workflow, template, task) - for idx, param in enumerate(task.arguments.parameters or ()): + for idx, param in enumerate(task.arguments.parameters): for diag in step_analyzer.check_argument_parameter_fields(param, ctx): diag["loc"] = ["arguments", "parameters", idx, *diag["loc"]] @@ -92,9 +94,11 @@ def check_argument_artifacts( ) -> Iterable[Diagnosis]: if not task.arguments: return + if not task.arguments.artifacts: + return # report duplicate names - for idx, name in find_duplicate_names(task.arguments.artifacts or ()): + for idx, name in find_duplicate_names(task.arguments.artifacts): yield { "code": "DAG103", "loc": ("arguments", "artifacts", idx, "name"), @@ -106,7 +110,7 @@ def check_argument_artifacts( # check fields for each parameter ctx = get_task_context(workflow, template, task) - for idx, artifact in enumerate(task.arguments.artifacts or ()): + for idx, artifact in enumerate(task.arguments.artifacts): for diag in step_analyzer.check_argument_artifact_fields(artifact, ctx): diag["loc"] = ["arguments", "artifacts", idx, *diag["loc"]] From a5d44e4775f80ecb93817fbba24dfa86ba2de2ba Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 17:59:40 +0800 Subject: [PATCH 15/18] add: dag306, dag307 --- docs/rules/dag.rst | 61 +++++++++++++++++++++++++++++++++++++ tests/analyzers/test_dag.py | 38 +++++++++++++++++++++++ tugboat/analyzers/dag.py | 19 +++++++++--- tugboat/analyzers/step.py | 2 +- 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index aafdfed..269be10 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -257,6 +257,67 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. parameters: - name: message +.. rule:: DAG306 Unexpected argument artifacts + + The task provides artifacts that the referenced template does not declare. + Remove the extra entries or update the target template so both sides align. + + .. code-block:: yaml + :emphasize-lines: 17 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: data-processor + arguments: + artifacts: + - name: input-data + raw: + data: "data" + - name: extra-artifact + raw: + data: "ignored" + + - name: data-processor + inputs: + artifacts: + - name: input-data + +.. rule:: DAG307 Missing required argument artifacts + + The task skips artifacts that are required by the referenced template. + Ensure every artifact without a default or static value on the template side is provided by the task. + + .. code-block:: yaml + :emphasize-lines: 11-12 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + template: data-processor + + - name: data-processor + inputs: + artifacts: + - name: input-data + + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index 988bb78..2dc30f1 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -182,6 +182,44 @@ def test_check_argument_artifacts(diagnoses_logger): ) +def test_check_argument_artifact_usage(diagnoses_logger): + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: hello + template: print-message + arguments: + artifacts: + - name: role + from: "{{ workflow.artifact-role }}" + - name: extra-artifact + from: "{{ workflow.artifact-extra }}" + + - name: print-message + inputs: + artifacts: + - name: message + - name: role + optional: true + """ + ) + diagnoses_logger(diagnoses) + + loc_prefix = ("spec", "templates", 0, "dag", "tasks", 0, "arguments", "artifacts") + + assert IsPartialModel(code="DAG306", loc=(*loc_prefix, 1, "name")) in diagnoses + assert IsPartialModel(code="DAG307", loc=loc_prefix) in diagnoses + + def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_logger): with caplog.at_level("DEBUG", "tugboat.analyzers.dag"): diagnoses = analyze_yaml_stream( diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index 40f87cb..cb793a1 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -10,11 +10,7 @@ from tugboat.core import hookimpl from tugboat.references import get_task_context from tugboat.types import Field -from tugboat.utils import ( - find_duplicate_names, - join_with_or, - prepend_loc, -) +from tugboat.utils import find_duplicate_names, join_with_or, prepend_loc if typing.TYPE_CHECKING: from collections.abc import Iterable @@ -135,6 +131,19 @@ def check_argument_parameters_usage( yield diag +@hookimpl(specname="analyze_task") +def check_argument_artifact_usage( + task: DagTask, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + for diag in step_analyzer.check_argument_artifact_usage(task, workflow): + match diag["code"]: + case "STP306": + diag["code"] = "DAG306" + case "STP307": + diag["code"] = "DAG307" + yield diag + + @hookimpl(specname="analyze_task") def check_referenced_template( task: DagTask, template: Template, workflow: WorkflowCompatible diff --git a/tugboat/analyzers/step.py b/tugboat/analyzers/step.py index a8950f6..231622a 100644 --- a/tugboat/analyzers/step.py +++ b/tugboat/analyzers/step.py @@ -322,7 +322,7 @@ def check_argument_artifact_fields( @hookimpl(specname="analyze_step") def check_argument_artifact_usage( - step: Step, workflow: WorkflowCompatible + step: TaskCompatible, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: # early exit: referenced template not found ref_template = _get_template_by_ref(step, workflow) From e797686821512156c4f6313878b6ebd5f57eb43f Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 19:23:49 +0800 Subject: [PATCH 16/18] add: dag401 --- docs/rules/dag.rst | 28 ++++++++++++++++++++++++++ tests/analyzers/test_dag.py | 39 +++++++++++++++++++++++++++++++++++++ tugboat/analyzers/dag.py | 11 +++++++++++ tugboat/analyzers/step.py | 2 +- 4 files changed, 79 insertions(+), 1 deletion(-) diff --git a/docs/rules/dag.rst b/docs/rules/dag.rst index 269be10..ea45597 100644 --- a/docs/rules/dag.rst +++ b/docs/rules/dag.rst @@ -318,6 +318,34 @@ Code ``DAG`` is used for errors related to the `DAG`_ in a `template`_. - name: input-data +.. STP4xx definition issues + +.. rule:: DAG401 Invalid task definition + + The task definition is invalid according to the schema. + This can happen when fields have the wrong type or unexpected values. + + For example, nested a dag inside a task is not allowed: + + .. code-block:: yaml + :emphasize-lines: 12-14 + + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: dag- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: process + inline: + dag: + tasks: [] + + .. DAG9xx deprecated items .. rule:: DAG901 Deprecated Field: ``onExit`` diff --git a/tests/analyzers/test_dag.py b/tests/analyzers/test_dag.py index 2dc30f1..adcf228 100644 --- a/tests/analyzers/test_dag.py +++ b/tests/analyzers/test_dag.py @@ -289,3 +289,42 @@ def test_check_referenced_template(caplog: pytest.LogCaptureFixture, diagnoses_l "Task 'external': Referenced template 'another-workflow' is not the same as current workflow 'demo'. Skipping." in caplog.text ) + + +def test_check_inline_template(diagnoses_logger): + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: test- + spec: + entrypoint: main + templates: + - name: main + dag: + tasks: + - name: task1 + inline: + dag: + tasks: [] + """ + ) + diagnoses_logger(diagnoses) + + assert ( + IsPartialModel( + code="DAG401", + loc=( + "spec", + "templates", + 0, + "dag", + "tasks", + 0, + "inline", + "dag", + ), + ) + in diagnoses + ) diff --git a/tugboat/analyzers/dag.py b/tugboat/analyzers/dag.py index cb793a1..2ad33cb 100644 --- a/tugboat/analyzers/dag.py +++ b/tugboat/analyzers/dag.py @@ -205,3 +205,14 @@ def _check_referenced_template( "input": target_template_name, "fix": suggestion, } + + +@hookimpl(specname="analyze_task") +def check_inline_template( + task: DagTask, workflow: WorkflowCompatible +) -> Iterable[Diagnosis]: + for diagnosis in step_analyzer.check_inline_template(task, workflow): + match diagnosis["code"]: + case "STP401": + diagnosis["code"] = "DAG401" + yield diagnosis diff --git a/tugboat/analyzers/step.py b/tugboat/analyzers/step.py index 231622a..eeeab5e 100644 --- a/tugboat/analyzers/step.py +++ b/tugboat/analyzers/step.py @@ -467,7 +467,7 @@ def check_fields_references( @hookimpl(specname="analyze_step") def check_inline_template( - step: Step, workflow: WorkflowCompatible + step: TaskCompatible, workflow: WorkflowCompatible ) -> Iterable[Diagnosis]: if not step.inline: return From e621d57abf692388f15115bae9fdd0fdace936aa Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 20:40:54 +0800 Subject: [PATCH 17/18] doc --- docs/changelog.rst | 5 +++++ docs/rules/template.rst | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 542a8b7..0e792e3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,11 @@ Changelog Unreleased ---------- +:octicon:`codescan-checkmark` Rules ++++++++++++++++++++++++++++++++++++ + +* New rule set :doc:`rules/dag` to validate DAG templates and their tasks. + :octicon:`plug` Enhancements ++++++++++++++++++++++++++++ diff --git a/docs/rules/template.rst b/docs/rules/template.rst index bb058b3..9cc6d0f 100644 --- a/docs/rules/template.rst +++ b/docs/rules/template.rst @@ -25,7 +25,7 @@ Argo Workflows offers various types of templates. However, Tugboat currently sup * - `DAG template `_ - :octicon:`alert` Partial (:py:class:`~tugboat.schemas.DagTask`) - - :octicon:`x` + - :octicon:`check` :doc:`dag` * - `Data template `_ - :octicon:`x` From a39c0eef98863ba8e704b5308aded0c945f84eaf Mon Sep 17 00:00:00 2001 From: tzing Date: Sun, 30 Nov 2025 20:43:15 +0800 Subject: [PATCH 18/18] enh: merge test fixture --- tests/analyzers/test_template.py | 52 ++++++++++++++++---------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/tests/analyzers/test_template.py b/tests/analyzers/test_template.py index cb555b3..d6a6732 100644 --- a/tests/analyzers/test_template.py +++ b/tests/analyzers/test_template.py @@ -75,7 +75,32 @@ def test_check_duplicate_step_names(self, diagnoses_logger): ) def test_check_duplicate_task_names(self, diagnoses_logger): - diagnoses = analyze_yaml_stream(MANIFEST_DUPLICATE_TASK_NAMES) + diagnoses = analyze_yaml_stream( + """ + apiVersion: argoproj.io/v1alpha1 + kind: Workflow + metadata: + generateName: steps- + spec: + entrypoint: hello-hello + templates: + - name: hello-hello + dag: + tasks: + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello-1" + - name: hello + template: print-message + arguments: + parameters: + - name: message + value: "hello-2" + """ + ) diagnoses_logger(diagnoses) assert ( IsPartialModel( @@ -165,31 +190,6 @@ def test_check_duplicate_task_names(self, diagnoses_logger): value: "hello-2" """ -MANIFEST_DUPLICATE_TASK_NAMES = """ -apiVersion: argoproj.io/v1alpha1 -kind: Workflow -metadata: - generateName: steps- -spec: - entrypoint: hello-hello - templates: - - name: hello-hello - dag: - tasks: - - name: hello - template: print-message - arguments: - parameters: - - name: message - value: "hello-1" - - name: hello - template: print-message - arguments: - parameters: - - name: message - value: "hello-2" -""" - def test_check_input_parameters_1(diagnoses_logger): diagnoses = analyze_yaml_stream(