Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
28 changes: 28 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,34 @@ development source code and as such may not be routinely kept up to date.

# __NEXT__

## Improvements

* `nextstrain run` now resolves workflow names by looking in the pathogen
registration (`nextstrain-pathogen.yaml`) for an explicitly registered path.
If no path is registered for a workflow, `nextstrain run` still falls back to
using the workflow name for the workflow path.

This allows for workflow names that are not also directory paths within the
pathogen source, which is useful for pathogens that are structured
non-conventionally for one reason or another. The decoupling of workflow
names from paths also means that the workflow can be relocated within the
pathogen repo without breaking the name (i.e. the external interface to the
workflow).

As an example, the following workflow registration:

```yaml
workflows:
phylogenetic:
path: .
compatibility:
nextstrain run: yes
```

would allow invocation of a `phylogenetic` workflow located at the top-level
of the pathogen source, such as in [zika-tutorial](https://github.com/nextstrain/zika-tutorial).
([#481](https://github.com/nextstrain/cli/pull/481))


# 10.3.0 (26 September 2025)

Expand Down
29 changes: 29 additions & 0 deletions doc/changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,35 @@ development source code and as such may not be routinely kept up to date.
(v-next)=
## __NEXT__

(v-next-improvements)=
### Improvements

* `nextstrain run` now resolves workflow names by looking in the pathogen
registration (`nextstrain-pathogen.yaml`) for an explicitly registered path.
If no path is registered for a workflow, `nextstrain run` still falls back to
using the workflow name for the workflow path.

This allows for workflow names that are not also directory paths within the
pathogen source, which is useful for pathogens that are structured
non-conventionally for one reason or another. The decoupling of workflow
names from paths also means that the workflow can be relocated within the
pathogen repo without breaking the name (i.e. the external interface to the
workflow).

As an example, the following workflow registration:

```yaml
workflows:
phylogenetic:
path: .
compatibility:
nextstrain run: yes
```

would allow invocation of a `phylogenetic` workflow located at the top-level
of the pathogen source, such as in [zika-tutorial](https://github.com/nextstrain/zika-tutorial).
([#481](https://github.com/nextstrain/cli/pull/481))


(v10-3-0)=
## 10.3.0 (26 September 2025)
Expand Down
4 changes: 3 additions & 1 deletion doc/commands/run.rst
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ positional arguments
for valid workflow names.

Workflow names conventionally correspond directly to directory
paths in the pathogen source, but this may not always be the case.
paths in the pathogen source, but this may not always be the case:
the pathogen's registration info can provide an explicit path for a
workflow name.

Required.

Expand Down
4 changes: 3 additions & 1 deletion nextstrain/cli/command/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ def register_parser(subparser):
for valid workflow names.

Workflow names conventionally correspond directly to directory
paths in the pathogen source, but this may not always be the case.
paths in the pathogen source, but this may not always be the case:
the pathogen's registration info can provide an explicit path for a
workflow name.

Required.
"""))
Expand Down
70 changes: 68 additions & 2 deletions nextstrain/cli/pathogens.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,8 +342,73 @@ def compatible_workflows(self, feature: str) -> Dict[str, Dict]:
}


def workflow_path(self, workflow: str) -> Path:
return self.path / workflow
def workflow_registration(self, name: str) -> Optional[dict]:
"""
Returns the registration dictionary for the workflow *name*.

Returns ``None`` if the workflow is not registered, does not have
registration information, or the registered information is not a
dictionary.
"""
if (info := self.registered_workflows().get(name)) and not isinstance(info, dict):
debug(f"pathogen registration.workflows[{name!r}] is not a dict (got a {type(info).__name__})")
return None

return info


def workflow_path(self, name: str) -> Path:
if (info := self.workflow_registration(name)) and (path := info.get("path")):
debug(f"pathogen registration specifies {path!r} for workflow {name!r}")

# Forbid anchored paths in registration info, as it's never correct
# practice. An anchored path is just an absolute path on POSIX
# systems but covers more "absolute-like" cases on Windows systems
# too.
if PurePath(path).anchor:
raise UserError(f"""
The {self.registration_path.name} file for {str(self)!r}
registers an anchored path for the workflow {name!r}:

{path}

Registered workflow paths must be relative to (and within)
the pathogen source itself. This is a mistake that the
pathogen author(s) must fix.
""")

# Ensure the relative path resolves _within_ the pathogen repo to
# avoid shenanigans.
resolved_pathogen_path = self.path.resolve()
resolved_workflow_path = (resolved_pathogen_path / path).resolve()

# Path.is_relative_to() was added in Python 3.9, so implement it
# ourselves around .relative_to().
try:
resolved_workflow_path.relative_to(resolved_pathogen_path)
except ValueError:
raise UserError(f"""
The {self.registration_path.name} file for {str(self)!r}
registers an out-of-bounds path for the workflow {name!r}:

{path}

which resolves to:

{str(resolved_workflow_path)}

which is outside of the pathogen's source.

Registered workflow paths must be within the pathogen
source itself. This is a mistake that the pathogen
author(s) must fix.
""")

debug(f"resolved workflow {name!r} to {str(resolved_workflow_path)!r}")
return resolved_workflow_path

debug(f"pathogen registration does not specify path for workflow {name!r}; using name as path")
return self.path / name


def setup(self, dry_run: bool = False, force: bool = False) -> SetupStatus:
Expand Down Expand Up @@ -747,6 +812,7 @@ def __init__(self, path: str):

registered_workflows = PathogenVersion.registered_workflows
compatible_workflows = PathogenVersion.compatible_workflows
workflow_registration = PathogenVersion.workflow_registration
workflow_path = PathogenVersion.workflow_path

def __str__(self) -> str:
Expand Down
Loading