Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@

### Linting

- Warn if `manifest.diagram` is not set, and fail if it points at a file that does not exist ([#4460](https://github.com/nf-core/tools/pull/4460))

### Modules

### Subworkflows

### Template

- Add a commented-out `manifest.diagram` to `nextflow.config`, for the pipeline metro map ([#4460](https://github.com/nf-core/tools/pull/4460))

### Version updates

## [v4.1.0 - Marshalled Mamba](https://github.com/nf-core/tools/releases/tag/4.1.0) - [2026-07-29]
Expand Down
4 changes: 4 additions & 0 deletions nf_core/pipeline-template/nextflow.config
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,10 @@ manifest {
nextflowVersion = '!>=25.10.4'
version = '{{ version }}'
doi = ''
// TODO nf-core: Draw a metro map for your pipeline, save it to `docs/images/` and uncomment the line below.
// Any SVG works, including hand-drawn ones. If you'd like a hand, nf-metro can generate one
// from a config file: https://seqeralabs.github.io/nf-metro/latest/
Comment thread
ewels marked this conversation as resolved.
Outdated
// diagram = 'docs/images/metro_map.svg'
}

{% if nf_schema -%}
Expand Down
23 changes: 23 additions & 0 deletions nf_core/pipelines/lint/nextflow_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,23 @@ def nextflow_config(self) -> dict[str, list[str]]:
**The following variables throw warnings if missing:**

* ``manifest.mainScript``: The filename of the main pipeline script (should be ``main.nf``)
* ``manifest.diagram``

* A relative path to a workflow diagram (metro map) for the pipeline, eg. ``docs/images/metro_map.svg``
* Any SVG works, including hand-drawn ones. `nf-metro <https://seqeralabs.github.io/nf-metro/latest/>`_
can generate one from a config file, if you'd like a hand.
Comment thread
pinin4fjords marked this conversation as resolved.
* Requires Nextflow ``26.10.0`` or later, but is safe to set for older versions - they ignore it.
* If set, the file must exist in the pipeline or the test **fails** (see below)

* ``timeline.file``, ``trace.file``, ``report.file``, ``dag.file``

* Default filenames for the timeline, trace and report
* The DAG file path should end with ``.svg`` (If Graphviz is not installed, Nextflow will generate a ``.dot`` file instead)

**The following variables fail the test if they are set to an invalid value:**

* ``manifest.diagram``: must be a relative path to a file that exists in the pipeline, not a URL

**The following variables are depreciated and fail the test if they are still present:**

* ``params.version``: The old method for specifying the pipeline version. Replaced by ``manifest.version``
Expand Down Expand Up @@ -150,6 +162,7 @@ def nextflow_config(self) -> dict[str, list[str]]:
# Throw a warning if these are missing
config_warn = [
["manifest.mainScript"],
["manifest.diagram"],
["timeline.file"],
["trace.file"],
["report.file"],
Expand Down Expand Up @@ -272,6 +285,16 @@ def _config_has_key(key: str) -> bool:
else:
failed.append(f"Config ``dag.file`` did not end with ``{default_dag_format}``")

# Check that manifest.diagram points at a file that exists in the pipeline
diagram = manifest.get("diagram", "")
if diagram and "manifest.diagram" not in ignore_configs:
Comment thread
ewels marked this conversation as resolved.
if re.match(r"^\w+://", diagram):
failed.append(f"Config ``manifest.diagram`` should be a relative path, not a URL: ``{diagram}``")
elif (Path(self.wf_path) / diagram).is_file():
Comment thread
ewels marked this conversation as resolved.
passed.append(f"Config ``manifest.diagram`` file found: ``{diagram}``")
else:
failed.append(f"Config ``manifest.diagram`` file not found: ``{diagram}``")

# Check that the minimum nextflowVersion is set properly
nextflow_version = manifest.get("nextflowVersion", "")
if nextflow_version:
Expand Down
56 changes: 49 additions & 7 deletions tests/pipelines/lint/test_nextflow_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

from ..test_lint import TestLint

# The template ships `manifest.diagram` commented out, so a new pipeline always warns about it
MISSING_DIAGRAM_WARNING = "Config variable not found: `manifest.diagram`"


class TestLintNextflowConfig(TestLint):
def setUp(self) -> None:
Expand All @@ -18,15 +21,15 @@ def test_nextflow_config_example_pass(self):
self.lint_obj.load_pipeline_config()
result = self.lint_obj.nextflow_config()
assert len(result["failed"]) == 0
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]

def test_default_values_match(self):
"""Test that the default values in nextflow.config match the default values defined in the nextflow_schema.json."""
lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline)
lint_obj.load_pipeline_config()
result = lint_obj.nextflow_config()
assert len(result["failed"]) == 0
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]
assert "Config default value correct: params.validate_params" in str(result["passed"])

def test_nextflow_config_bad_name_fail(self):
Expand All @@ -37,7 +40,7 @@ def test_nextflow_config_bad_name_fail(self):
lint_obj.nf_config["manifest"]["name"] = "bad_name"
result = lint_obj.nextflow_config()
assert len(result["failed"]) > 0
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]

def test_nextflow_config_dev_in_release_mode_failed(self):
"""Tests that config variable existence test fails with dev version in release mode"""
Expand All @@ -48,7 +51,7 @@ def test_nextflow_config_dev_in_release_mode_failed(self):
lint_obj.nf_config["manifest"]["version"] = "dev_is_bad_name"
result = lint_obj.nextflow_config()
assert len(result["failed"]) > 0
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]

def test_nextflow_config_missing_test_profile_failed(self):
"""Test failure if config file does not contain `test` profile."""
Expand All @@ -64,7 +67,7 @@ def test_nextflow_config_missing_test_profile_failed(self):
lint_obj.load_pipeline_config()
result = lint_obj.nextflow_config()
assert len(result["failed"]) > 0
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]

def test_default_values_fail(self):
"""Test linting fails if the default values in nextflow.config do not match the ones defined in the nextflow_schema.json."""
Expand Down Expand Up @@ -181,7 +184,7 @@ def test_default_values_float(self):
lint_obj.load_pipeline_config()
result = lint_obj.nextflow_config()
assert len(result["failed"]) == 0
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]
assert "Config default value correct: params.dummy" in str(result["passed"])

def test_default_values_float_fail(self):
Expand Down Expand Up @@ -214,5 +217,44 @@ def test_default_values_float_fail(self):
result = lint_obj.nextflow_config()

assert len(result["failed"]) == 1
assert len(result["warned"]) == 0
assert result["warned"] == [MISSING_DIAGRAM_WARNING]
assert "Config default value incorrect: `params.dummy" in str(result["failed"])

def test_manifest_diagram_pass(self):
"""Test that a `manifest.diagram` pointing at an existing file passes."""
diagram = Path(self.new_pipeline) / "docs" / "images" / "metro_map.svg"
Comment thread
ewels marked this conversation as resolved.
Outdated
diagram.parent.mkdir(parents=True, exist_ok=True)
diagram.write_text("<svg></svg>")
self._set_manifest_diagram("docs/images/metro_map.svg")

result = self._lint_new_pipeline()
assert len(result["failed"]) == 0
assert len(result["warned"]) == 0
assert "Config ``manifest.diagram`` file found: ``docs/images/metro_map.svg``" in result["passed"]

def test_manifest_diagram_missing_file_fail(self):
"""Test that a `manifest.diagram` pointing at a non-existent file fails."""
self._set_manifest_diagram("docs/images/metro_map.svg")

result = self._lint_new_pipeline()
assert "Config ``manifest.diagram`` file not found: ``docs/images/metro_map.svg``" in result["failed"]

def test_manifest_diagram_url_fail(self):
"""Test that a `manifest.diagram` set to a URL fails - it should be a relative path."""
url = "https://example.com/metro_map.svg"
self._set_manifest_diagram(url)

result = self._lint_new_pipeline()
assert f"Config ``manifest.diagram`` should be a relative path, not a URL: ``{url}``" in result["failed"]

def _set_manifest_diagram(self, value: str) -> None:
Comment thread
ewels marked this conversation as resolved.
Outdated
"""Uncomment the templated `manifest.diagram` line and set it to `value`."""
nf_conf_file = Path(self.new_pipeline) / "nextflow.config"
content = nf_conf_file.read_text()
assert "// diagram" in content
nf_conf_file.write_text(content.replace("// diagram", f"diagram = '{value}' //"))
Comment thread
ewels marked this conversation as resolved.
Outdated

def _lint_new_pipeline(self) -> dict:
lint_obj = nf_core.pipelines.lint.PipelineLint(self.new_pipeline)
lint_obj.load_pipeline_config()
return lint_obj.nextflow_config()
Loading