Skip to content

Commit 71e8bd8

Browse files
authored
Add ability to "group" pipeline steps to control inclusion #1045 (#1055)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 3d33285 commit 71e8bd8

26 files changed

+543
-139
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ Changelog
44
v33.2.0 (unreleased)
55
--------------------
66

7+
- Add ability to "group" pipeline steps to control their inclusion in a pipeline run.
8+
The groups can be selected in the UI, or provided using the
9+
"pipeline_name:group1,group2" syntax in CLI and REST API.
10+
https://github.com/nexB/scancode.io/issues/1045
11+
712
- Refine pipeline choices in the "Add pipeline" modal based on the project context.
813
* When there is at least one existing pipeline in the project, the modal now includes
914
all addon pipelines along with the existing pipeline for selection.
1015
* In cases where no pipelines are assigned to the project, the modal displays all
1116
base (non-addon) pipelines for user selection.
1217

13-
https://github.com/nexB/scancode.io/issues/
18+
https://github.com/nexB/scancode.io/issues/1071
1419

1520
v33.1.0 (2024-02-02)
1621
--------------------

docs/command-line-interface.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ Optional arguments:
8484

8585
- ``--pipeline PIPELINES`` Pipelines names to add on the project.
8686

87+
.. tip::
88+
Use the "pipeline_name:group1,group2" syntax to select steps groups:
89+
90+
``--pipeline map_deploy_to_develop:Java,JavaScript``
91+
8792
- ``--input-file INPUTS_FILES`` Input file locations to copy in the :guilabel:`input/`
8893
work directory.
8994

@@ -190,6 +195,11 @@ add the docker pipeline to your project::
190195

191196
$ scanpipe add-pipeline --project foo analyze_docker_image
192197

198+
.. tip::
199+
Use the "pipeline_name:group1,group2" syntax to select steps groups:
200+
201+
``--pipeline map_deploy_to_develop:Java,JavaScript``
202+
193203

194204
`$ scanpipe execute --project PROJECT`
195205
--------------------------------------
@@ -201,6 +211,7 @@ Optional arguments:
201211
- ``--async`` Add the pipeline run to the tasks queue for execution by a worker instead
202212
of running in the current thread.
203213

214+
204215
`$ scanpipe show-pipeline --project PROJECT`
205216
--------------------------------------------
206217

docs/rest-api.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,11 @@ Data:
278278
- ``pipeline``: The pipeline name
279279
- ``execute_now``: ``true`` or ``false``
280280

281+
.. tip::
282+
Use the "pipeline_name:group1,group2" syntax to select steps groups:
283+
284+
``"pipeline": "map_deploy_to_develop:Java,JavaScript"``
285+
281286
Using cURL:
282287

283288
.. code-block:: console

scanpipe/api/views.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,11 @@ def add_pipeline(self, request, *args, **kwargs):
247247

248248
pipeline = request.data.get("pipeline")
249249
if pipeline:
250-
pipeline = scanpipe_app.get_new_pipeline_name(pipeline)
251-
if pipeline in scanpipe_app.pipelines:
250+
pipeline_name, groups = scanpipe_app.extract_group_from_pipeline(pipeline)
251+
pipeline_name = scanpipe_app.get_new_pipeline_name(pipeline_name)
252+
if pipeline_name in scanpipe_app.pipelines:
252253
execute_now = request.data.get("execute_now")
253-
project.add_pipeline(pipeline, execute_now)
254+
project.add_pipeline(pipeline_name, execute_now, selected_groups=groups)
254255
return Response({"status": "Pipeline added."})
255256

256257
message = {"status": f"{pipeline} is not a valid pipeline."}

scanpipe/apps.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,17 @@ def get_new_pipeline_name(pipeline_name):
189189
return new_name
190190
return pipeline_name
191191

192+
@staticmethod
193+
def extract_group_from_pipeline(pipeline):
194+
pipeline_name = pipeline
195+
groups = None
196+
197+
if ":" in pipeline:
198+
pipeline_name, value = pipeline.split(":", maxsplit=1)
199+
groups = value.split(",") if value else []
200+
201+
return pipeline_name, groups
202+
192203
def get_scancode_licenses(self):
193204
"""
194205
Load licenses-related information from the ScanCode-toolkit ``licensedcode``

scanpipe/forms.py

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,14 @@ def handle_inputs(self, project):
100100
project.add_input_source(download_url=url)
101101

102102

103+
class GroupChoiceField(forms.MultipleChoiceField):
104+
widget = forms.CheckboxSelectMultiple
105+
106+
def valid_value(self, value):
107+
"""Accept all values."""
108+
return True
109+
110+
103111
class PipelineBaseForm(forms.Form):
104112
pipeline = forms.ChoiceField(
105113
choices=scanpipe_app.get_pipeline_choices(),
@@ -110,12 +118,14 @@ class PipelineBaseForm(forms.Form):
110118
initial=True,
111119
required=False,
112120
)
121+
selected_groups = GroupChoiceField(required=False)
113122

114123
def handle_pipeline(self, project):
115124
pipeline = self.cleaned_data["pipeline"]
116125
execute_now = self.cleaned_data["execute_now"]
126+
selected_groups = self.cleaned_data.get("selected_groups", [])
117127
if pipeline:
118-
project.add_pipeline(pipeline, execute_now)
128+
project.add_pipeline(pipeline, execute_now, selected_groups)
119129

120130

121131
class ProjectForm(InputsBaseForm, PipelineBaseForm, forms.ModelForm):
@@ -127,6 +137,7 @@ class Meta:
127137
"input_urls",
128138
"pipeline",
129139
"execute_now",
140+
"selected_groups",
130141
]
131142

132143
def __init__(self, *args, **kwargs):
@@ -158,32 +169,11 @@ def save(self, project):
158169

159170
class AddPipelineForm(PipelineBaseForm):
160171
pipeline = forms.ChoiceField(
172+
choices=scanpipe_app.get_pipeline_choices(),
161173
widget=forms.RadioSelect(),
162174
required=True,
163175
)
164176

165-
def __init__(self, project_runs=None, *args, **kwargs):
166-
super().__init__(*args, **kwargs)
167-
168-
# The pipeline choices are determined based on the project context:
169-
# 1. If no pipelines are assigned to the project:
170-
# Include all base (non-addon) pipelines.
171-
# 2. If at least one pipeline already exists on the project:
172-
# Include all addon pipelines and the existing pipeline (useful for
173-
# potential re-runs in debug mode).
174-
project_run_names = {run.pipeline_name for run in project_runs or []}
175-
176-
pipeline_choices = [
177-
(name, pipeline_class.get_summary())
178-
for name, pipeline_class in scanpipe_app.pipelines.items()
179-
# no pipelines are assigned to the project
180-
if (not project_runs and not pipeline_class.is_addon)
181-
# at least one pipeline already exists on the project
182-
or (project_runs and (name in project_run_names or pipeline_class.is_addon))
183-
]
184-
185-
self.fields["pipeline"].choices = pipeline_choices
186-
187177
def save(self, project):
188178
self.handle_pipeline(project)
189179
return project

scanpipe/management/commands/__init__.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -152,14 +152,22 @@ def add_arguments(self, parser):
152152
action="append",
153153
dest="input_files",
154154
default=list(),
155-
help="Input file locations to copy in the input/ work directory.",
155+
help=(
156+
"Input file locations to copy in the input/ work directory. "
157+
'Use the "filename:tag" syntax to tag input files such as '
158+
'"path/filename:tag"'
159+
),
156160
)
157161
parser.add_argument(
158162
"--input-url",
159163
action="append",
160164
dest="input_urls",
161165
default=list(),
162-
help="Input URLs to download in the input/ work directory.",
166+
help=(
167+
"Input URLs to download in the input/ work directory. "
168+
'Use the "url#tag" syntax to tag downloaded files such as '
169+
'"https://url.com/filename#tag"'
170+
),
163171
)
164172
parser.add_argument(
165173
"--copy-codebase",
@@ -251,19 +259,32 @@ def validate_copy_from(copy_from):
251259
raise CommandError(f"{copy_from} is not a directory")
252260

253261

254-
def validate_pipelines(pipeline_names):
262+
def extract_group_from_pipelines(pipelines):
263+
"""
264+
Add support for the ":group1,group2" suffix in pipeline data.
265+
266+
For example: "map_deploy_to_develop:Java,JavaScript"
267+
"""
268+
pipelines_data = {}
269+
for pipeline in pipelines:
270+
pipeline_name, groups = scanpipe_app.extract_group_from_pipeline(pipeline)
271+
pipelines_data[pipeline_name] = groups
272+
return pipelines_data
273+
274+
275+
def validate_pipelines(pipelines_data):
255276
"""Raise an error if one of the `pipeline_names` is not available."""
256277
# Backward compatibility with old pipeline names.
257-
pipeline_names = [
258-
scanpipe_app.get_new_pipeline_name(pipeline_name)
259-
for pipeline_name in pipeline_names
260-
]
278+
pipelines_data = {
279+
scanpipe_app.get_new_pipeline_name(pipeline_name): groups
280+
for pipeline_name, groups in pipelines_data.items()
281+
}
261282

262-
for pipeline_name in pipeline_names:
283+
for pipeline_name in pipelines_data.keys():
263284
if pipeline_name not in scanpipe_app.pipelines:
264285
raise CommandError(
265286
f"{pipeline_name} is not a valid pipeline. \n"
266287
f"Available: {', '.join(scanpipe_app.pipelines.keys())}"
267288
)
268289

269-
return pipeline_names
290+
return pipelines_data

scanpipe/management/commands/add-pipeline.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from django.template.defaultfilters import pluralize
2424

2525
from scanpipe.management.commands import ProjectCommand
26+
from scanpipe.management.commands import extract_group_from_pipelines
2627
from scanpipe.management.commands import validate_pipelines
2728

2829

@@ -38,13 +39,16 @@ def add_arguments(self, parser):
3839
help="One or more pipeline names.",
3940
)
4041

41-
def handle(self, *pipeline_names, **options):
42-
super().handle(*pipeline_names, **options)
42+
def handle(self, *pipelines, **options):
43+
super().handle(*pipelines, **options)
4344

44-
pipeline_names = validate_pipelines(pipeline_names)
45-
for pipeline_name in pipeline_names:
46-
self.project.add_pipeline(pipeline_name)
45+
pipelines_data = extract_group_from_pipelines(pipelines)
46+
pipelines_data = validate_pipelines(pipelines_data)
4747

48+
for pipeline_name, selected_groups in pipelines_data.items():
49+
self.project.add_pipeline(pipeline_name, selected_groups=selected_groups)
50+
51+
pipeline_names = pipelines_data.keys()
4852
msg = (
4953
f"Pipeline{pluralize(pipeline_names)} {', '.join(pipeline_names)} "
5054
f"added to the project"

scanpipe/management/commands/create-project.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from django.core.management.base import BaseCommand
2727

2828
from scanpipe.management.commands import AddInputCommandMixin
29+
from scanpipe.management.commands import extract_group_from_pipelines
2930
from scanpipe.management.commands import validate_copy_from
3031
from scanpipe.management.commands import validate_pipelines
3132
from scanpipe.models import Project
@@ -43,8 +44,9 @@ def add_arguments(self, parser):
4344
dest="pipelines",
4445
default=list(),
4546
help=(
46-
"Pipelines names to add to the project."
47-
"The pipelines are added and executed based on their given order."
47+
"Pipelines names to add to the project. "
48+
"The pipelines are added and executed based on their given order. "
49+
'Groups can be provided using the "pipeline_name:group1,group2" syntax.'
4850
),
4951
)
5052
parser.add_argument(
@@ -68,7 +70,7 @@ def add_arguments(self, parser):
6870

6971
def handle(self, *args, **options):
7072
name = options["name"]
71-
pipeline_names = options["pipelines"]
73+
pipelines = options["pipelines"]
7274
input_files = options["input_files"]
7375
input_urls = options["input_urls"]
7476
copy_from = options["copy_codebase"]
@@ -84,22 +86,24 @@ def handle(self, *args, **options):
8486
raise CommandError("\n".join(e.messages))
8587

8688
# Run validation before creating the project in the database
87-
pipeline_names = validate_pipelines(pipeline_names)
89+
pipelines_data = extract_group_from_pipelines(pipelines)
90+
pipelines_data = validate_pipelines(pipelines_data)
91+
8892
input_files_data = self.extract_tag_from_input_files(input_files)
8993
self.validate_input_files(input_files=input_files_data.keys())
9094
validate_copy_from(copy_from)
9195

92-
if execute and not pipeline_names:
96+
if execute and not pipelines:
9397
raise CommandError("The --execute option requires one or more pipelines.")
9498

9599
project.save()
100+
self.project = project
96101
msg = f"Project {name} created with work directory {project.work_directory}"
97102
self.stdout.write(msg, self.style.SUCCESS)
98103

99-
for pipeline_name in pipeline_names:
100-
project.add_pipeline(pipeline_name)
104+
for pipeline_name, selected_groups in pipelines_data.items():
105+
self.project.add_pipeline(pipeline_name, selected_groups=selected_groups)
101106

102-
self.project = project
103107
if input_files:
104108
self.handle_input_files(input_files_data)
105109

scanpipe/management/commands/show-pipeline.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,7 @@ def handle(self, *args, **options):
3232

3333
for run in self.project.runs.all():
3434
status_code = self.get_run_status_code(run)
35-
self.stdout.write(f" [{status_code}] {run.pipeline_name}")
35+
output = f" [{status_code}] {run.pipeline_name}"
36+
if run.selected_groups:
37+
output += f" ({','.join(run.selected_groups)})"
38+
self.stdout.write(output)

0 commit comments

Comments
 (0)