From d1d4ec0de3cc6570143f7c860fc99ccead8a22e6 Mon Sep 17 00:00:00 2001 From: vedanthvasudev Date: Wed, 22 Apr 2026 14:17:26 +0100 Subject: [PATCH] chore/default-git-mode-to-committed-only: Default includeUncommitted/Staged to false MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The plugin's job is "what tests does *this commit* touch", and the plugin's answer should not depend on what happens to be sitting in the dev's working tree. Previously both includeUncommitted and includeStaged defaulted to true, which meant a local `./gradlew affectedTest` on a given HEAD would select a different test set than CI would on the same HEAD, and two back-to-back runs on the same commit could disagree with each other depending on what was saved or staged in between. The inclusion was also invisible in the summary log, so there was no trail. Flip both conventions to false so the default matches CI semantics exactly (the tree is already clean after checkout there, so the flags were always inert on CI) and two runs on the same commit are deterministic. Adopters who iterate on tests locally and want WIP to seed the diff boundary flip the two knobs back on explicitly in build.gradle — it is a one-line opt-in and the config resolver has always preferred the explicit value over the convention, so no migration is required for anyone who was already setting these. Flips apply at both layers so programmatic Java callers and Gradle callers land in the same place: AffectedTestsPlugin conventions are false, and the core AffectedTestsConfig.Builder initialises both fields to false. Javadoc on the extension and task properties, the README configuration section, and the CHANGELOG all carry the new default and the rationale so operators who only read the docs still find a committed-only story. A matching "Changed — behaviour flip" callout in the CHANGELOG will surface on the GitHub release notes automatically. --- CHANGELOG.md | 28 +++++++++++++++++++ README.md | 12 ++++++-- .../core/config/AffectedTestsConfig.java | 12 ++++++-- .../core/config/AffectedTestsConfigTest.java | 11 ++++++-- .../gradle/AffectedTestTask.java | 9 ++++-- .../gradle/AffectedTestsExtension.java | 13 +++++++-- .../gradle/AffectedTestsPlugin.java | 12 ++++++-- .../gradle/AffectedTestsPluginTest.java | 12 ++++++-- 8 files changed, 93 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9e9785..3d870cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,34 @@ adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] +### Changed — behaviour flip, read this + +- **`includeUncommitted` and `includeStaged` now default to `false` + (committed-only).** Previously both defaulted to `true`, meaning a + local `./gradlew affectedTest` would expand the diff boundary with + whatever happened to be sitting in the dev's working tree. That made + local and CI runs of the same HEAD pick different test sets, and the + inclusion was invisible in the summary log. + + The new default mirrors CI reality (where the tree is clean after + checkout anyway) and makes two runs on the same commit deterministic + regardless of workstation state. Adopters who iterate on tests + locally and want WIP to seed the diff flip the two knobs back on in + `build.gradle`: + + ```groovy + affectedTests { + includeUncommitted = true // opt back in for local WIP runs + includeStaged = true + } + ``` + + No migration is required for anyone who was already setting these + explicitly — the config resolver always preferred the explicit value + over the convention. Adopters who relied on the old `true` default + without setting anything will see a smaller local-run test set until + they commit or opt back in; CI selection is unchanged. + ### Added - `outOfScopeTestDirs` and `outOfScopeSourceDirs` now accept Ant-style diff --git a/README.md b/README.md index 0ae8468..c037374 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,15 @@ affectedTests { // Git base ref to diff against (default: "origin/master") baseRef = "origin/main" - // Include uncommitted/staged changes (default: true) - includeUncommitted = true - includeStaged = true + // Include uncommitted/staged changes (default: false — committed-only). + // The plugin ships COMMITTED-ONLY so a local run matches the MR diff + // CI will pick up on the same HEAD, and running the task twice in a + // row produces the same test selection regardless of workstation state. + // Flip to `true` locally when you are iterating on tests and want + // WIP to seed the diff. Never enable these in CI — the tree is + // already clean after checkout there, so they are pure noise. + includeUncommitted = false + includeStaged = false // v2 profile. "auto" is the recommended migration target. // See the "Mode profiles" table above. diff --git a/affected-tests-core/src/main/java/io/affectedtests/core/config/AffectedTestsConfig.java b/affected-tests-core/src/main/java/io/affectedtests/core/config/AffectedTestsConfig.java index c9f4cdd..707bf67 100644 --- a/affected-tests-core/src/main/java/io/affectedtests/core/config/AffectedTestsConfig.java +++ b/affected-tests-core/src/main/java/io/affectedtests/core/config/AffectedTestsConfig.java @@ -464,8 +464,16 @@ public static final class Builder { ); private String baseRef = "origin/master"; - private boolean includeUncommitted = true; - private boolean includeStaged = true; + // Committed-only by default: the plugin's question is "what + // tests does *this commit* touch?", not "what tests does this + // commit plus whatever is rattling around in your working + // tree touch?". Matching the default to that framing means a + // programmatic or Gradle invocation on the same HEAD picks the + // same tests every time, independent of dev workstation state, + // and lines up with how CI checks the tree out. Callers who + // want WIP to expand the diff opt in via {@code includeUncommitted(true)}. + private boolean includeUncommitted = false; + private boolean includeStaged = false; private Boolean runAllIfNoMatches; private Boolean runAllOnNonJavaChange; private Set strategies = Set.of(STRATEGY_NAMING, STRATEGY_USAGE, STRATEGY_IMPL, STRATEGY_TRANSITIVE); diff --git a/affected-tests-core/src/test/java/io/affectedtests/core/config/AffectedTestsConfigTest.java b/affected-tests-core/src/test/java/io/affectedtests/core/config/AffectedTestsConfigTest.java index 528300b..08b5da9 100644 --- a/affected-tests-core/src/test/java/io/affectedtests/core/config/AffectedTestsConfigTest.java +++ b/affected-tests-core/src/test/java/io/affectedtests/core/config/AffectedTestsConfigTest.java @@ -14,8 +14,15 @@ void defaultsAreApplied() { AffectedTestsConfig config = AffectedTestsConfig.builder().build(); assertEquals("origin/master", config.baseRef()); - assertTrue(config.includeUncommitted()); - assertTrue(config.includeStaged()); + // v1.10.x flip: the core builder now defaults to COMMITTED-ONLY + // (both flags false). Rationale lives in the release notes and + // AffectedTestsPlugin#apply — picking CI semantics as the + // default, with WIP inclusion as an explicit opt-in, keeps a + // programmatic run on the same HEAD deterministic. + assertFalse(config.includeUncommitted(), + "Core builder default must be COMMITTED-ONLY as of the v1.9.14 → next-release flip"); + assertFalse(config.includeStaged(), + "Core builder default must be COMMITTED-ONLY as of the v1.9.14 → next-release flip"); // Pre-v2 legacy defaults preserved 1:1 for zero-config callers — // the getters below read the raw configured value (or the // hardcoded pre-v2 default when unset), not the resolved diff --git a/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestTask.java b/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestTask.java index f941b63..4b4e59d 100644 --- a/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestTask.java +++ b/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestTask.java @@ -71,7 +71,11 @@ public AffectedTestTask() { /** * Whether to include uncommitted (unstaged) changes in the diff. - * Default: {@code true}. + * Default: {@code false} — committed-only, so a local run picks + * the same tests CI would pick on the same HEAD, and two runs on + * the same commit are deterministic. Flip to {@code true} in + * {@code build.gradle} if you iterate on tests locally and want + * WIP to seed the diff. * * @return the include uncommitted property */ @@ -80,7 +84,8 @@ public AffectedTestTask() { /** * Whether to include staged (added to index) changes in the diff. - * Default: {@code true}. + * Default: {@code false} — see {@link #getIncludeUncommitted()} for + * the rationale; both knobs move together on the CI-first defaults. * * @return the include staged property */ diff --git a/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsExtension.java b/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsExtension.java index c226f6a..0153bfc 100644 --- a/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsExtension.java +++ b/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsExtension.java @@ -10,7 +10,11 @@ *
{@code
  * affectedTests {
  *     baseRef = "origin/master"
- *     includeUncommitted = true
+ *     // Defaults are COMMITTED-ONLY (both flags default to false) so
+ *     // local runs match CI. Flip to true if you want WIP to seed the
+ *     // diff while iterating on tests locally.
+ *     includeUncommitted = false
+ *     includeStaged = false
  *     // v2: per-situation actions (replaces runAllIfNoMatches / runAllOnNonJavaChange).
  *     // See README.md "Migrating from v1 config" for the full table.
  *     mode = "ci"
@@ -42,7 +46,9 @@ public abstract class AffectedTestsExtension {
 
     /**
      * Include uncommitted (unstaged) changes.
-     * Default: {@code true}.
+     * Default: {@code false} — committed-only semantics match CI. Set
+     * to {@code true} if you iterate on WIP locally and want the
+     * unstaged edits to seed the diff boundary.
      *
      * @return the include uncommitted property
      */
@@ -50,7 +56,8 @@ public abstract class AffectedTestsExtension {
 
     /**
      * Include staged changes.
-     * Default: {@code true}.
+     * Default: {@code false}. See {@link #getIncludeUncommitted()} for
+     * the rationale.
      *
      * @return the include staged property
      */
diff --git a/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsPlugin.java b/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsPlugin.java
index ffb242e..ff68238 100644
--- a/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsPlugin.java
+++ b/affected-tests-gradle/src/main/java/io/affectedtests/gradle/AffectedTestsPlugin.java
@@ -26,8 +26,16 @@ public void apply(Project project) {
                 project.getProviders().gradleProperty("affectedTestsBaseRef")
                         .orElse("origin/master")
         );
-        extension.getIncludeUncommitted().convention(true);
-        extension.getIncludeStaged().convention(true);
+        // COMMITTED-ONLY by default: the plugin's whole job is "what
+        // tests does this MR touch?", and the MR is the committed diff
+        // — not the dev's WIP. Matching this default to the CI reality
+        // means a local `./gradlew affectedTest` run picks the same
+        // tests CI will run on the same HEAD, and two runs on the same
+        // commit are deterministic. Adopters who iterate on WIP tests
+        // flip these back to `true` in their build.gradle; the plugin
+        // never silently expands the diff boundary.
+        extension.getIncludeUncommitted().convention(false);
+        extension.getIncludeStaged().convention(false);
         // No conventions for the two legacy booleans — leaving them
         // unset is the v2 signal that the caller has not overridden the
         // defaults, which lets the core config resolver apply
diff --git a/affected-tests-gradle/src/test/java/io/affectedtests/gradle/AffectedTestsPluginTest.java b/affected-tests-gradle/src/test/java/io/affectedtests/gradle/AffectedTestsPluginTest.java
index f538ccc..8610c65 100644
--- a/affected-tests-gradle/src/test/java/io/affectedtests/gradle/AffectedTestsPluginTest.java
+++ b/affected-tests-gradle/src/test/java/io/affectedtests/gradle/AffectedTestsPluginTest.java
@@ -35,8 +35,16 @@ void extensionHasDefaults() {
                 .getByType(AffectedTestsExtension.class);
 
         assertEquals("origin/master", ext.getBaseRef().get());
-        assertTrue(ext.getIncludeUncommitted().get());
-        assertTrue(ext.getIncludeStaged().get());
+        // Default is now COMMITTED-ONLY. Matches CI semantics exactly
+        // (where the tree is already clean after checkout anyway) and
+        // gives local `./gradlew affectedTest` runs the same test
+        // selection the operator's MR will get in CI. Devs who want WIP
+        // to seed the diff opt in explicitly — see CHANGELOG for the
+        // v1.9.14 → next-release behaviour flip.
+        assertFalse(ext.getIncludeUncommitted().get(),
+                "Default must be COMMITTED-ONLY so local runs match CI — WIP inclusion is an explicit opt-in");
+        assertFalse(ext.getIncludeStaged().get(),
+                "Default must be COMMITTED-ONLY so local runs match CI — staged-index inclusion is an explicit opt-in");
         // v2: no convention for the legacy booleans — leaving them unset
         // is the signal the core builder uses to fall through to
         // mode-based defaults. Zero-config users still observe pre-v2