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
2 changes: 1 addition & 1 deletion TASK_CACHE_PARITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ outside this tracker.
- [x] Allow affected selection to feed existing task patterns, dependency expansion, scheduling, and
caching
- [x] Show why each project and task was considered affected
- [ ] Add machine-readable affected-project and affected-task output
- [x] Add machine-readable affected-project and affected-task output

## Local cache parity

Expand Down
4 changes: 4 additions & 0 deletions docs/cli/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Explain why projects and tasks were selected by --affected
Git head revision for --affected
Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD

### `--affected-json`

Output affected projects and tasks as JSON without running tasks

### `-c --continue-on-error`

Continue running tasks even if one fails
Expand Down
4 changes: 4 additions & 0 deletions docs/cli/tasks/run.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ Explain why projects and tasks were selected by --affected
Git head revision for --affected
Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD

### `--affected-json`

Output affected projects and tasks as JSON without running tasks

### `-c --continue-on-error`

Continue running tasks even if one fails
Expand Down
7 changes: 7 additions & 0 deletions docs/tasks/monorepo.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ mise run --affected build
# Inspect the selection while preserving normal dry-run behavior
mise run --affected --affected-explain --dry-run build

# Emit the selection as JSON without running tasks
mise run --affected --affected-json build

# Compare explicit revisions
mise run --affected --affected-base origin/main --affected-head HEAD test
```
Expand All @@ -384,6 +387,10 @@ It also lists the task-pattern matches associated with those projects. Normal ta
expanded afterward, so a selected task can still run a required prerequisite from an unchanged
project.

`--affected-json` emits the same pre-expansion selection without running tasks. Its stable JSON
object contains the base and head revisions, affected projects with their roots and reasons, and
task-pattern matches with their associated project IDs.

The revision defaults are `HEAD~1` and `HEAD` locally. `MISE_AFFECTED_BASE` and
`MISE_AFFECTED_HEAD` override them. GitHub Actions and GitLab merge-request metadata provide CI
defaults when those variables are not set; explicit CLI options take highest precedence.
Expand Down
37 changes: 37 additions & 0 deletions e2e/tasks/test_task_affected
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,43 @@ git commit -qm initial
echo changed >>crates/core/src/lib.rs
git add .
git commit -qm core-change
assert_json "mise run --affected --affected-json build" '{
"base": "HEAD~1",
"head": "HEAD",
"projects": [
{
"id": "cargo:app",
"root": "crates/app",
"reasons": [
{
"type": "dependent",
"dependency": "cargo:core"
}
]
},
{
"id": "cargo:core",
"root": "crates/core",
"reasons": [
{
"type": "changed-path",
"path": "crates/core/src/lib.rs"
}
]
}
],
"tasks": [
{
"name": "//crates/app:build",
"projects": ["cargo:app"]
},
{
"name": "//crates/core:build",
"projects": ["cargo:core"]
}
]
}'
assert "test ! -e affected.log"
output=$(mise run --affected --affected-explain --jobs 1 build)
assert_contains "echo \"$output\"" "Affected projects (HEAD~1...HEAD):"
assert_contains "echo \"$output\"" "changed path: crates/core/src/lib.rs"
Expand Down
6 changes: 6 additions & 0 deletions man/man1/mise.1
Original file line number Diff line number Diff line change
Expand Up @@ -3039,6 +3039,9 @@ Explain why projects and tasks were selected by \-\-affected
Git head revision for \-\-affected
Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD
.TP
\fB\-\-affected\-json\fR
Output affected projects and tasks as JSON without running tasks
.TP
\fB\-c, \-\-continue\-on\-error\fR
Continue running tasks even if one fails
.TP
Expand Down Expand Up @@ -3848,6 +3851,9 @@ Explain why projects and tasks were selected by \-\-affected
Git head revision for \-\-affected
Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD
.TP
\fB\-\-affected\-json\fR
Output affected projects and tasks as JSON without running tasks
.TP
\fB\-c, \-\-continue\-on\-error\fR
Continue running tasks even if one fails
.TP
Expand Down
2 changes: 2 additions & 0 deletions mise.usage.kdl
Original file line number Diff line number Diff line change
Expand Up @@ -3059,6 +3059,7 @@ Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD
"""# {
arg <REV>
}
flag --affected-json help="Output affected projects and tasks as JSON without running tasks"
flag "-c --continue-on-error" help="Continue running tasks even if one fails"
flag "-C --cd" help="Change to this directory before executing the command" {
arg <CD>
Expand Down Expand Up @@ -3908,6 +3909,7 @@ Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD
"""# {
arg <REV>
}
flag --affected-json help="Output affected projects and tasks as JSON without running tasks"
flag "-c --continue-on-error" help="Continue running tasks even if one fails"
flag "-C --cd" help="Change to this directory before executing the command" {
arg <CD>
Expand Down
1 change: 1 addition & 0 deletions src/cli/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -903,6 +903,7 @@ impl Bootstrap {
affected_base: None,
affected_head: None,
affected_explain: false,
affected_json: false,
cd: None,
continue_on_error: false,
dry_run: self.dry_run,
Expand Down
1 change: 1 addition & 0 deletions src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,7 @@ impl Cli {
affected_base: None,
affected_head: None,
affected_explain: false,
affected_json: false,
cd: self.cd,
continue_on_error: self.continue_on_error,
dry_run: self.dry_run,
Expand Down
96 changes: 94 additions & 2 deletions src/cli/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use clap::{CommandFactory, ValueHint};
use eyre::{Result, bail, eyre};
use futures_util::FutureExt;
use itertools::Itertools;
use serde::Serialize;
use std::panic::AssertUnwindSafe;
use tokio::sync::Mutex;

Expand Down Expand Up @@ -84,14 +85,28 @@ pub struct Run {
pub affected_base: Option<String>,

/// Explain why projects and tasks were selected by --affected
#[clap(long, requires = "affected", verbatim_doc_comment)]
#[clap(
long,
requires = "affected",
conflicts_with = "affected_json",
verbatim_doc_comment
)]
pub affected_explain: bool,

/// Git head revision for --affected
/// Defaults to MISE_AFFECTED_HEAD, CI metadata, or HEAD
#[clap(long, requires = "affected", value_name = "REV", verbatim_doc_comment)]
pub affected_head: Option<String>,

/// Output affected projects and tasks as JSON without running tasks
#[clap(
long,
requires = "affected",
conflicts_with = "affected_explain",
verbatim_doc_comment
)]
pub affected_json: bool,

/// Continue running tasks even if one fails
#[clap(long, short = 'c', verbatim_doc_comment)]
pub continue_on_error: bool,
Expand Down Expand Up @@ -293,6 +308,7 @@ async fn get_affected_task_list(
base: Option<&str>,
head: Option<&str>,
explain: bool,
json: bool,
) -> Result<Vec<Task>> {
Settings::get().ensure_experimental("affected tasks")?;
let workspace_root = config
Expand Down Expand Up @@ -365,12 +381,84 @@ async fn get_affected_task_list(
.map(crate::file::desymlink_path)
.is_some_and(|root| affected_roots.contains(&root))
});
if explain {
if json {
display_affected_json(&revisions, &workspace_root, &graph, &affected, &tasks)?;
} else if explain {
display_affected_explanation(&revisions, &workspace_root, &graph, &affected, &tasks)?;
}
Ok(tasks)
}

#[derive(Serialize)]
struct AffectedSelectionOutput<'a> {
base: &'a str,
head: &'a str,
projects: Vec<AffectedProjectOutput<'a>>,
tasks: Vec<AffectedTaskOutput<'a>>,
}

#[derive(Serialize)]
struct AffectedProjectOutput<'a> {
id: &'a crate::task::workspace::ProjectId,
root: &'a std::path::Path,
reasons: &'a BTreeSet<crate::task::workspace::AffectedProjectReason>,
}

#[derive(Serialize)]
struct AffectedTaskOutput<'a> {
name: &'a str,
projects: Vec<&'a crate::task::workspace::ProjectId>,
}

fn display_affected_json(
revisions: &crate::task::workspace::git::WorkspaceGitRevisions,
workspace_root: &std::path::Path,
graph: &crate::task::workspace::WorkspaceProjectGraph,
affected: &crate::task::workspace::AffectedProjects,
tasks: &[Task],
) -> Result<()> {
let mut projects_by_root = BTreeMap::<PathBuf, Vec<_>>::new();
let projects = affected
.projects()
.map(|(id, reasons)| {
let project = graph.get(id).expect("affected project exists in graph");
projects_by_root
.entry(crate::file::desymlink_path(
&workspace_root.join(&project.root),
))
.or_default()
.push(id);
AffectedProjectOutput {
id,
root: &project.root,
reasons,
}
})
.collect();
let mut tasks = tasks
.iter()
.map(|task| AffectedTaskOutput {
name: &task.display_name,
projects: task
.config_root
.as_deref()
.map(crate::file::desymlink_path)
.and_then(|root| projects_by_root.get(&root))
.cloned()
.unwrap_or_default(),
})
.collect::<Vec<_>>();
tasks.sort_by(|left, right| left.name.cmp(right.name));
let output = AffectedSelectionOutput {
base: &revisions.base,
head: &revisions.head,
projects,
tasks,
};
miseprintln!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}

fn display_affected_explanation(
revisions: &crate::task::workspace::git::WorkspaceGitRevisions,
workspace_root: &std::path::Path,
Expand Down Expand Up @@ -532,11 +620,15 @@ impl Run {
self.affected_base.as_deref(),
self.affected_head.as_deref(),
self.affected_explain,
self.affected_json,
)
.await?
} else {
get_task_lists(&config, &args, true, self.skip_deps).await?
};
if self.affected_json {
return Ok(());
}

// Args after -- go directly to tasks (no prefix). They are also
// recorded on `trailing_args` so the task renderer can detect
Expand Down