Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
de168a0
fix(core): validate batch task hashes against stale dependency outputs
FrozenPandaz Feb 13, 2026
2f72a5b
feat(core): add output fingerprinting for daemon-free batch validation
FrozenPandaz Feb 13, 2026
2c007ef
chore(testing): update cache assertions for output fingerprinting
FrozenPandaz Feb 13, 2026
f87b81b
chore(maven): remove reset because of this issu9e
FrozenPandaz Feb 20, 2026
a93ad74
fix(core): guard hashTaskOutput against undefined outputs
FrozenPandaz Feb 20, 2026
25dc3d8
chore(testing): update cache assertions for output fingerprinting
FrozenPandaz Feb 20, 2026
69a39bd
fix(core): make batch stale-dep detection resilient to errors
FrozenPandaz Feb 20, 2026
0b46e8a
fix(core): re-hash stale batch tasks on original objects, not copies
FrozenPandaz Feb 20, 2026
629550e
fix(maven): include test sources in testCompile task hash
FrozenPandaz Feb 20, 2026
d56cad9
fix(maven): fix inputs for maven:test
FrozenPandaz Feb 21, 2026
20068b5
fix(maven): use glob patterns for gitignored dependent task outputs
FrozenPandaz Feb 25, 2026
6945a43
fix(maven): propagate batch runner exit code failures
FrozenPandaz Feb 25, 2026
d2c783b
fix(core): propagate stale hash detection to transitive dependents
FrozenPandaz Feb 26, 2026
7940589
fix(maven): remove redundant explicit dependentTaskOutputs from Cache…
FrozenPandaz Feb 27, 2026
23f84c8
fix(core): remove stale hash detection in favor of topological hashing
FrozenPandaz Mar 4, 2026
0dfcfbb
fix(core): remove output fingerprinting from batch hashing PR
FrozenPandaz Mar 4, 2026
6186553
fix(core): implement topological hashing for batch tasks
FrozenPandaz Mar 4, 2026
3fe3a9d
fix(maven): remove unused DependentTaskOutput from CacheConfig
FrozenPandaz Mar 4, 2026
a1d731a
fix(core): simplify batch hashing with helper and parallel re-hash
FrozenPandaz Mar 4, 2026
0d1dc1c
fix(core): simplify batch hashing by deferring to post-execution
FrozenPandaz Mar 5, 2026
3fca7e4
fix(core): defer task history hash snapshot to endCommand
FrozenPandaz Mar 5, 2026
e7a9491
fix(core): continue topological cache walk past cache misses
FrozenPandaz Mar 6, 2026
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: 0 additions & 2 deletions e2e/maven/src/maven-batch-v4.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@ wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-w
});

it('should fail when unit test fails', () => {
// TODO: remove once batch mode dependentTaskOutputs is fixed
runCLI('reset');
// Add a failing unit test
updateFile(
'app/src/test/java/com/example/app/AppApplicationTests.java',
Expand Down
6 changes: 6 additions & 0 deletions e2e/maven/src/utils/create-maven-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
tmpProjPath,
readFile,
updateFile,
fileExists,
} from '@nx/e2e-utils';
import { execSync } from 'child_process';
import { writeFileSync } from 'fs-extra';
Expand Down Expand Up @@ -155,6 +156,11 @@ export async function createMavenProject(
'.mvn/wrapper/maven-wrapper.properties',
readFile('app/.mvn/wrapper/maven-wrapper.properties')
);
if (fileExists('.gitignore')) {
updateFile('.gitignore', readFile('.gitignore') + '\ntarget');
} else {
updateFile('.gitignore', 'target');
}

chmodSync(join(cwd, 'mvnw'), 0o755);
chmodSync(join(cwd, 'mvnw.cmd'), 0o755);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ data class Parameter(val name: String, val glob: String?)
data class MojoConfig(
val inputProperties: Set<String>? = null,
val inputParameters: Set<Parameter>? = null,
val outputParameters: Set<Parameter>? = null
val outputParameters: Set<Parameter>? = null,
)

/**
Expand Down Expand Up @@ -81,7 +81,7 @@ data class CacheConfig(
),
"maven-compiler-plugin:testCompile" to MojoConfig(
inputParameters = setOf(
Parameter("testCompileSourceRoots", "**/*.java"),
Parameter("compileSourceRoots", "**/*.java"),

This comment was marked as resolved.

),
outputParameters = setOf(
Parameter("outputDirectory", null),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -614,10 +614,10 @@ class NxTargetFactory(
val isIgnored = gitIgnoreClassifier.isIgnored(buildJsonFile)
if (isIgnored) {
log.warn("Input path is gitignored: ${buildJsonFile.path}")
val input = pathFormatter.toDependentTaskOutputs(buildJsonFile, project.basedir)
// Match the specific build state file in dependency outputs
val obj = JsonObject()
obj.addProperty("dependentTasksOutputFiles", input.path)
if (input.transitive) obj.addProperty("transitive", true)
obj.addProperty("dependentTasksOutputFiles", "nx-build-state.json")
obj.addProperty("transitive", true)
target.inputs?.add(obj)
} else {
val input = pathFormatter.formatInputPath(buildJsonFile, projectRoot = project.basedir)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,6 @@ class MavenExpressionResolver(
private val log: Logger = LoggerFactory.getLogger(MavenExpressionResolver::class.java)

fun resolveParameter(parameter: Parameter, project: MavenProject): List<String> {
// For compileSourceRoots and testCompileSourceRoots, always use collection handling
// to include ALL source roots (including generated sources)
if (parameter.name in setOf("compileSourceRoots", "testCompileSourceRoots")) {
return when (parameter.name) {
"compileSourceRoots" -> project.compileSourceRoots ?: emptyList()
"testCompileSourceRoots" -> project.testCompileSourceRoots ?: emptyList()
else -> emptyList()
}
}

return when (parameter.type) {
"java.io.File" -> listOfNotNull(
resolveStringParameterValue(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,15 +83,16 @@ class MojoAnalyzer(
val paths = expressionResolver.resolveParameter(parameter, project)

paths.forEach { path ->
val pathWithGlob = paramConfig.glob?.let { "$path/$it" } ?: path
val pathFile = File(pathWithGlob);
val pathFile = File(path)
val isIgnored = gitIgnoreClassifier.isIgnored(pathFile)
if (isIgnored) {
log.warn("Input path is gitignored: ${pathFile.path}")
val input = pathResolver.toDependentTaskOutputs(pathFile, project.basedir)
dependentTaskOutputInputs.add(input)
// Use the parameter's glob pattern if provided, otherwise use **/*
val globPattern = paramConfig.glob ?: "**/*"
dependentTaskOutputInputs.add(DependentTaskOutputs(globPattern, transitive = true))
} else {
val input = pathResolver.formatInputPath(pathFile, projectRoot = project.basedir)
val pathWithGlob = paramConfig.glob?.let { "$path/$it" } ?: path
val input = pathResolver.formatInputPath(File(pathWithGlob), projectRoot = project.basedir)

inputs.add(input)
}
Expand All @@ -106,8 +107,8 @@ class MojoAnalyzer(
val isIgnored = gitIgnoreClassifier.isIgnored(pathFile)
if (isIgnored) {
log.warn("Input path is gitignored: ${pathFile.path}")
val input = pathResolver.toDependentTaskOutputs(pathFile, project.basedir)
dependentTaskOutputInputs.add(input)
// For properties, always use **/* pattern
dependentTaskOutputInputs.add(DependentTaskOutputs("**/*", transitive = true))
} else {
val input = pathResolver.formatInputPath(pathFile, projectRoot = project.basedir)

Expand All @@ -122,8 +123,8 @@ class MojoAnalyzer(
val isIgnored = gitIgnoreClassifier.isIgnored(pathFile)
if (isIgnored) {
log.warn("Input path is gitignored: ${pathFile.path}")
val input = pathResolver.toDependentTaskOutputs(pathFile, project.basedir)
dependentTaskOutputInputs.add(input)
// For default inputs, always use **/* pattern
dependentTaskOutputInputs.add(DependentTaskOutputs("**/*", transitive = true))
} else {
val input = pathResolver.formatInputPath(pathFile, projectRoot = project.basedir)

Expand Down
7 changes: 6 additions & 1 deletion packages/maven/src/executors/maven/maven-batch.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,12 @@ export default async function* mavenBatchExecutor(
console.error(output);
}
}
resolve();
// Reject promise if batch runner exited with non-zero code
if (code !== 0) {
reject(new Error(`Maven batch runner exited with code ${code}`));
} else {
resolve();
}
});
child.on('error', reject);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,9 @@ import { LifeCycle, TaskResult } from '../life-cycle';

export class LegacyTaskHistoryLifeCycle implements LifeCycle {
private startTimings: Record<string, number> = {};
private taskRuns: TaskRun[] = [];
private pendingResults: TaskResult[] = [];
private flakyTasks: string[];

// hashes which could trigger flaky task detection.
// set here rather than on the TaskRun to avoid storing data on
// the fs
private cacheableHashes: Set<string> = new Set();

startTasks(tasks: Task[]): void {
for (let task of tasks) {
this.startTimings[task.id] = new Date().getTime();
Expand All @@ -29,11 +24,19 @@ export class LegacyTaskHistoryLifeCycle implements LifeCycle {

async endTasks(taskResults: TaskResult[]) {
for (const taskResult of taskResults) {
// Track cacheable tasks for flaky detection
this.pendingResults.push(taskResult);
}
}

async endCommand() {
// Build TaskRun objects now — task.hash is guaranteed to be set by this point
const taskRuns: TaskRun[] = [];
const cacheableHashes = new Set<string>();
for (const taskResult of this.pendingResults) {
if (taskResult.task.cache === true) {
this.cacheableHashes.add(taskResult.task.hash);
cacheableHashes.add(taskResult.task.hash);
}
this.taskRuns.push({
taskRuns.push({
project: taskResult.task.target.project,
target: taskResult.task.target.target,
configuration: taskResult.task.target.configuration,
Expand All @@ -46,13 +49,11 @@ export class LegacyTaskHistoryLifeCycle implements LifeCycle {
end: (taskResult.task.endTime ?? new Date().getTime()).toString(),
});
}
}

async endCommand() {
await writeTaskRunsToHistory(this.taskRuns);
await writeTaskRunsToHistory(taskRuns);
// Only check for flaky tasks among cacheable tasks
const cacheableTaskRuns = this.taskRuns.filter((t) =>
this.cacheableHashes.has(t.hash)
const cacheableTaskRuns = taskRuns.filter((t) =>
cacheableHashes.has(t.hash)
);
const history = await getHistoryForHashes(
cacheableTaskRuns.map((t) => t.hash)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export function getTasksHistoryLifeCycle():

export class TaskHistoryLifeCycle implements LifeCycle {
private startTimings: Record<string, number> = {};
private pendingResults = new Map<string, TaskResult>();
private taskRuns = new Map<string, TaskRun>();
private taskHistory: TaskHistory | null = getTaskHistory();
private flakyTasks: string[];
Expand All @@ -51,6 +52,17 @@ export class TaskHistoryLifeCycle implements LifeCycle {

async endTasks(taskResults: TaskResult[]) {
for (const taskResult of taskResults) {
this.pendingResults.set(taskResult.task.id, taskResult);
}
}

async endCommand() {
if (!this.taskHistory) {
return;
}

// Build TaskRun objects now — task.hash is guaranteed to be set by this point
for (const [, taskResult] of this.pendingResults) {
this.taskRuns.set(taskResult.task.hash, {
hash: taskResult.task.hash,
target: taskResult.task.target,
Expand All @@ -62,12 +74,7 @@ export class TaskHistoryLifeCycle implements LifeCycle {
cacheable: taskResult.task.cache === true,
});
}
}

async endCommand() {
if (!this.taskHistory) {
return;
}
const runs = [];

// Only check for flaky tasks among cacheable tasks
Expand Down
Loading
Loading