Skip to content

Commit 5410d79

Browse files
committed
run: Resolve workflow names using pathogen registration, if available
This decoupling of workflow names from paths was always expected and intended to be possible, and now it is. Related-to: <nextstrain/zika-tutorial#19> Related-to: <nextstrain/public#1>
1 parent 9b505c4 commit 5410d79

4 files changed

Lines changed: 128 additions & 3 deletions

File tree

CHANGES.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ development source code and as such may not be routinely kept up to date.
1313

1414
# __NEXT__
1515

16+
## Improvements
17+
18+
* `nextstrain run` now resolves workflow names by looking in the pathogen
19+
registration (`nextstrain-pathogen.yaml`) for an explicitly registered path.
20+
If no path is registered for a workflow, `nextstrain run` still falls back to
21+
using the workflow name for the workflow path.
22+
23+
This allows for workflow names that are not also directory paths within the
24+
pathogen source, which is useful for pathogens that are structured
25+
non-conventionally for one reason or another. The decoupling of workflow
26+
names from paths also means that the workflow can be relocated within the
27+
pathogen repo without breaking the name (i.e. the external interface to the
28+
workflow).
29+
30+
As an example, the following workflow registration:
31+
32+
```yaml
33+
workflows:
34+
phylogenetic:
35+
path: .
36+
compatibility:
37+
nextstrain run: yes
38+
```
39+
40+
would allow invocation of a `phylogenetic` workflow located at the top-level
41+
of the pathogen source, such as in [zika-tutorial](https://github.com/nextstrain/zika-tutorial).
42+
([#481](https://github.com/nextstrain/cli/pull/481))
43+
1644

1745
# 10.3.0 (26 September 2025)
1846

doc/changes.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,35 @@ development source code and as such may not be routinely kept up to date.
1616
(v-next)=
1717
## __NEXT__
1818

19+
(v-next-improvements)=
20+
### Improvements
21+
22+
* `nextstrain run` now resolves workflow names by looking in the pathogen
23+
registration (`nextstrain-pathogen.yaml`) for an explicitly registered path.
24+
If no path is registered for a workflow, `nextstrain run` still falls back to
25+
using the workflow name for the workflow path.
26+
27+
This allows for workflow names that are not also directory paths within the
28+
pathogen source, which is useful for pathogens that are structured
29+
non-conventionally for one reason or another. The decoupling of workflow
30+
names from paths also means that the workflow can be relocated within the
31+
pathogen repo without breaking the name (i.e. the external interface to the
32+
workflow).
33+
34+
As an example, the following workflow registration:
35+
36+
```yaml
37+
workflows:
38+
phylogenetic:
39+
path: .
40+
compatibility:
41+
nextstrain run: yes
42+
```
43+
44+
would allow invocation of a `phylogenetic` workflow located at the top-level
45+
of the pathogen source, such as in [zika-tutorial](https://github.com/nextstrain/zika-tutorial).
46+
([#481](https://github.com/nextstrain/cli/pull/481))
47+
1948

2049
(v10-3-0)=
2150
## 10.3.0 (26 September 2025)

nextstrain/cli/command/run.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ def register_parser(subparser):
8080
for valid workflow names.
8181
8282
Workflow names conventionally correspond directly to directory
83-
paths in the pathogen source, but this may not always be the case.
83+
paths in the pathogen source, but this may not always be the case:
84+
the pathogen's registration info can provide an explicit path for a
85+
workflow name.
8486
8587
Required.
8688
"""))

nextstrain/cli/pathogens.py

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,73 @@ def compatible_workflows(self, feature: str) -> Dict[str, Dict]:
342342
}
343343

344344

345-
def workflow_path(self, workflow: str) -> Path:
346-
return self.path / workflow
345+
def workflow_registration(self, name: str) -> Optional[dict]:
346+
"""
347+
Returns the registration dictionary for the workflow *name*.
348+
349+
Returns ``None`` if the workflow is not registered, does not have
350+
registration information, or the registered information is not a
351+
dictionary.
352+
"""
353+
if (info := self.registered_workflows().get(name)) and not isinstance(info, dict):
354+
debug(f"pathogen registration.workflows[{name!r}] is not a dict (got a {type(info).__name__})")
355+
return None
356+
357+
return info
358+
359+
360+
def workflow_path(self, name: str) -> Path:
361+
if (info := self.workflow_registration(name)) and (path := info.get("path")):
362+
debug(f"pathogen registration specifies {path!r} for workflow {name!r}")
363+
364+
# Forbid anchored paths in registration info, as it's never correct
365+
# practice. An anchored path is just an absolute path on POSIX
366+
# systems but covers more "absolute-like" cases on Windows systems
367+
# too.
368+
if PurePath(path).anchor:
369+
raise UserError(f"""
370+
The {self.registration_path.name} file for {str(self)!r}
371+
registers an anchored path for the workflow {name!r}:
372+
373+
{path}
374+
375+
Registered workflow paths must be relative to (and within)
376+
the pathogen source itself. This is a mistake that the
377+
pathogen author(s) must fix.
378+
""")
379+
380+
# Ensure the relative path resolves _within_ the pathogen repo to
381+
# avoid shenanigans.
382+
resolved_pathogen_path = self.path.resolve()
383+
resolved_workflow_path = (resolved_pathogen_path / path).resolve()
384+
385+
# Path.is_relative_to() was added in Python 3.9, so implement it
386+
# ourselves around .relative_to().
387+
try:
388+
resolved_workflow_path.relative_to(resolved_pathogen_path)
389+
except ValueError:
390+
raise UserError(f"""
391+
The {self.registration_path.name} file for {str(self)!r}
392+
registers an out-of-bounds path for the workflow {name!r}:
393+
394+
{path}
395+
396+
which resolves to:
397+
398+
{str(resolved_workflow_path)}
399+
400+
which is outside of the pathogen's source.
401+
402+
Registered workflow paths must be within the pathogen
403+
source itself. This is a mistake that the pathogen
404+
author(s) must fix.
405+
""")
406+
407+
debug(f"resolved workflow {name!r} to {str(resolved_workflow_path)!r}")
408+
return resolved_workflow_path
409+
410+
debug(f"pathogen registration does not specify path for workflow {name!r}; using name as path")
411+
return self.path / name
347412

348413

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

748813
registered_workflows = PathogenVersion.registered_workflows
749814
compatible_workflows = PathogenVersion.compatible_workflows
815+
workflow_registration = PathogenVersion.workflow_registration
750816
workflow_path = PathogenVersion.workflow_path
751817

752818
def __str__(self) -> str:

0 commit comments

Comments
 (0)