Repository: denoland/deno
Pull request: fix(task): only pass args to root task

When we run `deno task` with args like `deno task foo arg` the argument should only be passed to the root task, not to its dependencies.

Fixes https://github.com/denoland/deno/issues/27206

Changed files:
filename: cli/tools/task.rs
patch: @@ -241,12 +241,15 @@ pub async fn execute_script(
             description: None,
           },
           kill_signal,
+          cli_options.argv(),
         )
         .await;
     }
 
     for task_config in &packages_task_configs {
-      let exit_code = task_runner.run_tasks(task_config, &kill_signal).await?;
+      let exit_code = task_runner
+        .run_tasks(task_config, &kill_signal, cli_options.argv())
+        .await?;
       if exit_code > 0 {
         return Ok(exit_code);
       }
@@ -263,6 +266,7 @@ struct RunSingleOptions<'a> {
   cwd: &'a Path,
   custom_commands: HashMap<String, Rc<dyn ShellCommand>>,
   kill_signal: KillSignal,
+  argv: &'a [String],
 }
 
 struct TaskRunner<'a> {
@@ -279,9 +283,10 @@ impl<'a> TaskRunner<'a> {
     &self,
     pkg_tasks_config: &PackageTaskInfo,
     kill_signal: &KillSignal,
+    argv: &[String],
   ) -> Result<i32, deno_core::anyhow::Error> {
     match sort_tasks_topo(pkg_tasks_config) {
-      Ok(sorted) => self.run_tasks_in_parallel(sorted, kill_signal).await,
+      Ok(sorted) => self.run_tasks_in_parallel(sorted, kill_signal, argv).await,
       Err(err) => match err {
         TaskError::NotFound(name) => {
           if self.task_flags.is_run {
@@ -317,6 +322,7 @@ impl<'a> TaskRunner<'a> {
     &self,
     tasks: Vec<ResolvedTask<'a>>,
     kill_signal: &KillSignal,
+    args: &[String],
   ) -> Result<i32, deno_core::anyhow::Error> {
     struct PendingTasksContext<'a> {
       completed: HashSet<usize>,
@@ -338,13 +344,21 @@ impl<'a> TaskRunner<'a> {
         &mut self,
         runner: &'b TaskRunner<'b>,
         kill_signal: &KillSignal,
+        argv: &'a [String],
       ) -> Option<
         LocalBoxFuture<'b, Result<(i32, &'a ResolvedTask<'a>), AnyError>>,
       >
       where
         'a: 'b,
       {
-        for task in self.tasks.iter() {
+        let mut tasks_iter = self.tasks.iter().peekable();
+        while let Some(task) = tasks_iter.next() {
+          let args = if tasks_iter.peek().is_none() {
+            argv
+          } else {
+            &[]
+          };
+
           if self.completed.contains(&task.id)
             || self.running.contains(&task.id)
           {
@@ -366,7 +380,13 @@ impl<'a> TaskRunner<'a> {
               match task.task_or_script {
                 TaskOrScript::Task(_, def) => {
                   runner
-                    .run_deno_task(task.folder_url, task.name, def, kill_signal)
+                    .run_deno_task(
+                      task.folder_url,
+                      task.name,
+                      def,
+                      kill_signal,
+                      args,
+                    )
                     .await
                 }
                 TaskOrScript::Script(scripts, _) => {
@@ -376,6 +396,7 @@ impl<'a> TaskRunner<'a> {
                       task.name,
                       scripts,
                       kill_signal,
+                      args,
                     )
                     .await
                 }
@@ -399,7 +420,7 @@ impl<'a> TaskRunner<'a> {
 
     while context.has_remaining_tasks() {
       while queue.len() < self.concurrency {
-        if let Some(task) = context.get_next_task(self, kill_signal) {
+        if let Some(task) = context.get_next_task(self, kill_signal, args) {
           queue.push(task);
         } else {
           break;
@@ -429,6 +450,7 @@ impl<'a> TaskRunner<'a> {
     task_name: &str,
     definition: &TaskDefinition,
     kill_signal: KillSignal,
+    argv: &'a [String],
   ) -> Result<i32, deno_core::anyhow::Error> {
     let cwd = match &self.task_flags.cwd {
       Some(path) => canonicalize_path(&PathBuf::from(path))
@@ -447,6 +469,7 @@ impl<'a> TaskRunner<'a> {
         cwd: &cwd,
         custom_commands,
         kill_signal,
+        argv,
       })
       .await
   }
@@ -457,6 +480,7 @@ impl<'a> TaskRunner<'a> {
     task_name: &str,
     scripts: &IndexMap<String, String>,
     kill_signal: KillSignal,
+    argv: &[String],
   ) -> Result<i32, deno_core::anyhow::Error> {
     // ensure the npm packages are installed if using a managed resolver
     if let Some(npm_resolver) = self.npm_resolver.as_managed() {
@@ -489,6 +513,7 @@ impl<'a> TaskRunner<'a> {
             cwd: &cwd,
             custom_commands: custom_commands.clone(),
             kill_signal: kill_signal.clone(),
+            argv,
           })
           .await?;
         if exit_code > 0 {
@@ -510,11 +535,12 @@ impl<'a> TaskRunner<'a> {
       cwd,
       custom_commands,
       kill_signal,
+      argv,
     } = opts;
 
     output_task(
       opts.task_name,
-      &task_runner::get_script_with_args(script, self.cli_options.argv()),
+      &task_runner::get_script_with_args(script, argv),
     );
 
     Ok(
@@ -525,7 +551,7 @@ impl<'a> TaskRunner<'a> {
         env_vars: self.env_vars.clone(),
         custom_commands,
         init_cwd: self.cli_options.initial_cwd(),
-        argv: self.cli_options.argv(),
+        argv,
         root_node_modules_dir: self.npm_resolver.root_node_modules_path(),
         stdio: None,
         kill_signal,

filename: tests/specs/task/dependencies/__test__.jsonc
patch: @@ -56,6 +56,11 @@
       "args": "task a",
       "output": "./cycle_2.out",
       "exitCode": 1
+    },
+    "arg_task_with_deps": {
+      "cwd": "arg_task_with_deps",
+      "args": "task a a",
+      "output": "./arg_task_with_deps.out"
     }
   }
 }

filename: tests/specs/task/dependencies/arg_task_with_deps.out
patch: @@ -0,0 +1,4 @@
+Task b echo 'b'
+b
+Task a echo "a"
+a

filename: tests/specs/task/dependencies/arg_task_with_deps/deno.json
patch: @@ -0,0 +1,9 @@
+{
+  "tasks": {
+    "a": {
+      "command": "echo",
+      "dependencies": ["b"]
+    },
+    "b": "echo 'b'"
+  }
+}