diff --git a/docs/docs/usage/scripts.md b/docs/docs/usage/scripts.md index 2e0374cd5e..61315f5c8b 100644 --- a/docs/docs/usage/scripts.md +++ b/docs/docs/usage/scripts.md @@ -105,6 +105,19 @@ all = {composite = ["lint", "test"]} Running `pdm run all` will run `lint` first and then `test` if `lint` succeeded. +To override the default behavior and continue the execution of the remaining +scripts after a failure, set the `keep_going` option to `true`: + +```toml +[tool.pdm.scripts] +lint = "flake8" +test = "pytest" +all = {composite = ["lint", "test"]} +all.keep_going = true +``` + +If `keep_going` set to `true` return code of composite script is either '0' if all succeeded or the code of last failed individual script. + You can also provide arguments to the called scripts: ```toml diff --git a/src/pdm/cli/commands/run.py b/src/pdm/cli/commands/run.py index 01587002af..36f7ed64a3 100644 --- a/src/pdm/cli/commands/run.py +++ b/src/pdm/cli/commands/run.py @@ -34,6 +34,7 @@ class TaskOptions(TypedDict, total=False): env: Mapping[str, str] env_file: EnvFileOptions | str | None help: str + keep_going: bool site_packages: bool @@ -103,7 +104,7 @@ class TaskRunner: """The task runner for pdm project""" TYPES = ("cmd", "shell", "call", "composite") - OPTIONS = ("env", "env_file", "help", "site_packages") + OPTIONS = ("env", "env_file", "help", "keep_going", "site_packages") def __init__(self, project: Project, hooks: HookManager) -> None: self.project = project @@ -270,7 +271,8 @@ def run_task(self, task: Task, args: Sequence[str] = (), opts: TaskOptions | Non args = list(args) should_interpolate = any(RE_ARGS_PLACEHOLDER.search(script) for script in value) should_interpolate = should_interpolate or any(RE_PDM_PLACEHOLDER.search(script) for script in value) - code = 0 + composite_code = 0 + keep_going = options.pop("keep_going", False) if options else False for script in value: if should_interpolate: script, _ = interpolate(script, args) @@ -279,8 +281,10 @@ def run_task(self, task: Task, args: Sequence[str] = (), opts: TaskOptions | Non subargs = split[1:] + ([] if should_interpolate else args) code = self.run(cmd, subargs, options, chdir=True) if code != 0: - return code - return code + if not keep_going: + return code + composite_code = code + return composite_code return self._run_process( args, chdir=True, diff --git a/tests/cli/test_run.py b/tests/cli/test_run.py index c2536822fb..e2d4c4d99a 100644 --- a/tests/cli/test_run.py +++ b/tests/cli/test_run.py @@ -614,6 +614,22 @@ def test_composite_stops_on_first_failure(project, pdm, capfd): assert "Second CALLED" not in out +def test_composite_keep_going_on_failure(project, pdm, capfd): + project.pyproject.settings["scripts"] = { + "first": {"cmd": ["python", "-c", "print('First CALLED')"]}, + "fail": "python -c 'raise Exception'", + "second": "echo 'Second CALLED'", + "test": {"composite": ["first", "fail", "second"], "keep_going": True}, + } + project.pyproject.write() + capfd.readouterr() + result = pdm(["run", "test"], obj=project) + assert result.exit_code == 1 + out, err = capfd.readouterr() + assert "First CALLED" in out + assert "Second CALLED" in out + + def test_composite_inherit_env(project, pdm, capfd, _echo): project.pyproject.settings["scripts"] = { "first": {