From b8a3c7cd3793e4710fe7c6b5b980e74695628b3d Mon Sep 17 00:00:00 2001
From: Miles Johnson <milesj@users.noreply.github.com>
Date: Tue, 19 Sep 2023 15:16:02 -0700
Subject: [PATCH] new: Allow targets to be passed to `moon ci`. (#1056)

* Update ci.

* Update targets.

* Remove job.
---
 crates/cli/src/commands/ci.rs          | 57 ++++++++++++++++----------
 crates/core/dep-graph/src/dep_graph.rs |  4 ++
 packages/cli/CHANGELOG.md              |  1 +
 website/docs/commands/ci.mdx           | 13 ++++++
 website/docs/guides/ci.mdx             | 39 +++++++++++-------
 5 files changed, 78 insertions(+), 36 deletions(-)

diff --git a/crates/cli/src/commands/ci.rs b/crates/cli/src/commands/ci.rs
index 1e92c190c44..05ddd85a683 100644
--- a/crates/cli/src/commands/ci.rs
+++ b/crates/cli/src/commands/ci.rs
@@ -23,6 +23,9 @@ const HEADING_PARALLELISM: &str = "Parallelism and distribution";
 
 #[derive(Args, Clone, Debug)]
 pub struct CiArgs {
+    #[arg(help = "List of targets (scope:task) to run")]
+    targets: Vec<Target>,
+
     #[arg(long, help = "Base branch, commit, or revision to compare against")]
     base: Option<String>,
 
@@ -88,37 +91,35 @@ async fn gather_touched_files(
 fn gather_runnable_targets(
     provider: &CiOutput,
     project_graph: &ProjectGraph,
-    touched_files: &FxHashSet<WorkspaceRelativePathBuf>,
+    args: &CiArgs,
 ) -> AppResult<TargetList> {
     print_header(provider, "Gathering runnable targets");
 
     let mut targets = vec![];
 
     // Required for dependents
-    for project in project_graph.get_all()? {
-        for task in project.get_tasks()? {
-            if task.should_run_in_ci() {
-                if task.is_affected(touched_files)? {
+    let projects = project_graph.get_all()?;
+
+    if args.targets.is_empty() {
+        for project in projects {
+            for task in project.get_tasks()? {
+                if task.should_run_in_ci() {
                     targets.push(task.target.clone());
+                } else {
+                    debug!(
+                        "Not running target {} because it either has no {} or {} is false",
+                        color::label(&task.target.id),
+                        color::property("outputs"),
+                        color::property("runInCI"),
+                    );
                 }
-            } else {
-                debug!(
-                    "Not running target {} because it either has no `outputs` or `runInCI` is false",
-                    color::label(&task.target),
-                );
             }
         }
-    }
-
-    if targets.is_empty() {
-        println!(
-            "{}",
-            color::invalid("No targets to run based on touched files")
-        );
     } else {
-        print_targets(&targets);
+        targets.extend(args.targets.clone());
     }
 
+    print_targets(&targets);
     print_footer(provider);
 
     Ok(targets)
@@ -165,6 +166,7 @@ fn generate_dep_graph(
     provider: &CiOutput,
     project_graph: &ProjectGraph,
     targets: &TargetList,
+    touched_files: &FxHashSet<WorkspaceRelativePathBuf>,
 ) -> AppResult<DepGraph> {
     print_header(provider, "Generating dependency graph");
 
@@ -172,7 +174,7 @@ fn generate_dep_graph(
 
     for target in targets {
         // Run the target and its dependencies
-        dep_builder.run_target(target, None)?;
+        dep_builder.run_target(target, Some(touched_files))?;
 
         // And also run its dependents to ensure consumers still work correctly
         dep_builder.run_dependents_for_target(target)?;
@@ -199,17 +201,28 @@ pub async fn ci(
     });
     let project_graph = generate_project_graph(workspace).await?;
     let touched_files = gather_touched_files(&ci_provider, workspace, args).await?;
-    let targets = gather_runnable_targets(&ci_provider, &project_graph, &touched_files)?;
+    let targets = gather_runnable_targets(&ci_provider, &project_graph, args)?;
 
     if targets.is_empty() {
+        println!("{}", color::invalid("No targets to run"));
+
         return Ok(());
     }
 
     let targets = distribute_targets_across_jobs(&ci_provider, args, targets);
-    let dep_graph = generate_dep_graph(&ci_provider, &project_graph, &targets)?;
+    let dep_graph = generate_dep_graph(&ci_provider, &project_graph, &targets, &touched_files)?;
+
+    if dep_graph.is_empty() {
+        println!(
+            "{}",
+            color::invalid("No targets to run based on touched files")
+        );
+
+        return Ok(());
+    }
 
     // Process all tasks in the graph
-    print_header(&ci_provider, "Running all targets");
+    print_header(&ci_provider, "Running targets");
 
     let context = ActionContext {
         primary_targets: FxHashSet::from_iter(targets),
diff --git a/crates/core/dep-graph/src/dep_graph.rs b/crates/core/dep-graph/src/dep_graph.rs
index a4997a2f041..bc48cec9b28 100644
--- a/crates/core/dep-graph/src/dep_graph.rs
+++ b/crates/core/dep-graph/src/dep_graph.rs
@@ -25,6 +25,10 @@ impl DepGraph {
         DepGraph { graph, indices }
     }
 
+    pub fn is_empty(&self) -> bool {
+        self.get_node_count() == 0
+    }
+
     pub fn get_index_from_node(&self, node: &ActionNode) -> Option<&NodeIndex> {
         self.indices.get(node)
     }
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 5b447062e7f..95769c430ce 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -9,6 +9,7 @@
   - Better concurrency handling and scheduling.
   - More accurately monitors signals (ctrl+c) and shutdowns.
   - Tasks can now be configured with a timeout.
+- Updated `moon ci` to support running a list of targets, instead of running everything.
 
 #### ⚙️ Internal
 
diff --git a/website/docs/commands/ci.mdx b/website/docs/commands/ci.mdx
index bff807c5aa4..5bdcdcb8041 100644
--- a/website/docs/commands/ci.mdx
+++ b/website/docs/commands/ci.mdx
@@ -5,13 +5,26 @@ title: ci
 The `moon ci` command is a special command that should be ran in a continuous integration (CI)
 environment, as it does all the heavy lifting necessary for effectively running tasks.
 
+By default this will run all tasks that are affected by touched files and have the
+[`runInCI`](../config/project#runinci) task option enabled.
+
 ```shell
 $ moon ci
 ```
 
+However, you can provide a list of targets to run, instead of relying on `runInCI`.
+
+```shell
+$ moon ci :build :lint
+```
+
 > View the official [continuous integration guide](../guides/ci) for a more in-depth example of how
 > to utilize this command.
 
+### Arguments
+
+- `...[target]` - [Targets](../concepts/target) to run.
+
 ### Options
 
 - `--base <rev>` - Base branch, commit, or revision to compare against. Can be set with `MOON_BASE`.
diff --git a/website/docs/guides/ci.mdx b/website/docs/guides/ci.mdx
index 956f19857fe..2432a7ddfee 100644
--- a/website/docs/guides/ci.mdx
+++ b/website/docs/guides/ci.mdx
@@ -5,6 +5,7 @@ title: Continuous integration (CI)
 import Tabs from '@theme/Tabs';
 import TabItem from '@theme/TabItem';
 import Image from '@site/src/components/Image';
+import VersionLabel from '@site/src/components/Docs/VersionLabel';
 
 All companies and projects rely on continuous integration (CI) to ensure high quality code and to
 avoid regressions. Because this is such a critical piece of every developer's workflow, we wanted to
@@ -50,15 +51,10 @@ by default. This can be easily controlled with the [`local`](../config/project#l
 
 ## Integrating
 
-Although moon has an [integrated toolchain](../concepts/toolchain), we still require Node.js and
-dependencies to be installed _beforehand_, as moon is currently shipped as an
-[npm package](https://www.npmjs.com/package/@moonrepo/cli). This is unfortunate and we're looking
-into other distribution channels.
-
-With that being said, the following examples can be referenced for setting up moon and its CI
-workflow in popular services. The examples assume a
-[package script named `moon`](../install#adding-a-package-script) and are using Yarn 3, but feel
-free to replace with your chosen setup.
+The following examples can be referenced for setting up moon and its CI workflow in popular
+providers. For GitHub, we're using our
+[`setup-moon` action](https://github.com/moonrepo/setup-moon-action) to install moon. For other
+providers, we assume moon is an npm dependency and must be installed with Node.js.
 
 <Tabs groupId="ci-env">
 <TabItem value="github" label="GitHub">
@@ -78,11 +74,8 @@ jobs:
       - uses: 'actions/checkout@v4'
         with:
           fetch-depth: 0
-      - uses: 'actions/setup-node@v3'
-        with:
-          cache: 'yarn'
-      - run: 'yarn install --immutable'
-      - run: 'yarn moon ci'
+      - uses: 'moonrepo/setup-moon-action@v1'
+      - run: 'moon ci'
 ```
 
 </TabItem>
@@ -136,6 +129,24 @@ script: 'moon ci'
 </TabItem>
 </Tabs>
 
+## Choosing targets<VersionLabel version="1.14.0" />
+
+By default `moon ci` will run _all_ tasks from _all_ projects that are affected by touched files and
+have the [`runInCI`](../config/project#runinci) task option enabled. This is a great catch-all
+solution, but may not vibe with your workflow or requirements.
+
+If you'd prefer more control, you can pass a list of targets to `moon ci`, instead of moon
+attempting to detect them. When providing targets, `moon ci` will still only run them if affected by
+touched files, but will ignore the `runInCI` option.
+
+```shell
+# Run all builds
+$ moon ci :build
+
+# In another job, run tests
+$ moon ci :test :lint
+```
+
 ## Comparing revisions
 
 By default the command will compare the current HEAD against a base revision, which is typically the