From 353045442435dcb8f5989dbe3e8f353f23fd2e07 Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Tue, 22 Jul 2025 13:32:04 +0100
Subject: [PATCH 01/12] messing

---
 .../org/revapi/gradle/GitVersionUtils.java    | 122 ++++++------------
 .../org/revapi/gradle/RevapiExtension.java    |  11 +-
 .../revapi/gradle/GitVersionUtilsSpec.groovy  |  11 +-
 .../org/revapi/gradle/RevapiSpec.groovy       |   6 +
 4 files changed, 59 insertions(+), 91 deletions(-)

diff --git a/src/main/java/org/revapi/gradle/GitVersionUtils.java b/src/main/java/org/revapi/gradle/GitVersionUtils.java
index f93c3ad6..465a5be2 100644
--- a/src/main/java/org/revapi/gradle/GitVersionUtils.java
+++ b/src/main/java/org/revapi/gradle/GitVersionUtils.java
@@ -16,55 +16,45 @@
 
 package org.revapi.gradle;
 
-import java.io.ByteArrayOutputStream;
-import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Optional;
-import java.util.Spliterator;
-import java.util.function.Consumer;
 import java.util.stream.Stream;
-import java.util.stream.StreamSupport;
-import org.gradle.api.Project;
+import javax.inject.Inject;
+import org.gradle.api.file.ProjectLayout;
+import org.gradle.api.provider.Provider;
+import org.gradle.api.provider.ProviderFactory;
+import org.gradle.process.ExecOutput;
 import org.gradle.process.ExecResult;
 import org.immutables.value.Value;
 
-final class GitVersionUtils {
-    private GitVersionUtils() {}
+public abstract class GitVersionUtils {
 
-    public static Stream<String> previousGitTags(Project project) {
-        return StreamSupport.stream(new PreviousGitTags(project), false)
-                .filter(tag -> !isInitial000Tag(project, tag))
-                .map(GitVersionUtils::stripVFromTag);
-    }
-
-    private static Optional<String> previousGitTagFromRef(Project project, String ref) {
-        String beforeLastRef = ref + "^";
-
-        GitResult beforeLastRefTypeResult = execute(project, "git", "cat-file", "-t", beforeLastRef);
+    @Inject
+    protected abstract ProviderFactory getProviderFactory();
 
-        boolean thereIsNoCommitBeforeTheRef = !beforeLastRefTypeResult.stdout().equals("commit");
-        if (thereIsNoCommitBeforeTheRef) {
-            return Optional.empty();
-        }
-
-        GitResult describeResult = execute(project, "git", "describe", "--tags", "--abbrev=0", beforeLastRef);
+    @Inject
+    protected abstract ProjectLayout getProjectLayout();
 
-        if (describeResult.stderr().contains("No tags can describe")
-                || describeResult.stderr().contains("No names found, cannot describe anything")) {
-            return Optional.empty();
-        }
+    public final Provider<Stream<String>> previousGitTags() {
+        return execute("git", "tag", "-l", "--merged", "HEAD^", "--sort=-committerdate")
+                .map(tagsResult -> {
+                    if (tagsResult.exitCode() != 0 || tagsResult.stdout().isEmpty()) {
+                        return Stream.<String>empty();
+                    }
 
-        return Optional.of(describeResult.stdoutOrThrowIfNonZero());
+                    return Arrays.stream(tagsResult.stdout().split("\n"))
+                            .filter(tag -> !isInitial000Tag(tag))
+                            .map(GitVersionUtils::stripVFromTag);
+                });
     }
 
-    private static boolean isInitial000Tag(Project project, String tag) {
+    private boolean isInitial000Tag(String tag) {
         if (!tag.equals("0.0.0")) {
             return false;
         }
 
-        GitResult foo = execute(project, "git", "rev-parse", "--verify", "--quiet", "0.0.0^");
-        boolean parentDoesNotExist = foo.exitCode() != 0;
+        GitResult result = execute("git", "rev-parse", "--verify", "--quiet", "0.0.0^").get();
+        boolean parentDoesNotExist = result.exitCode() != 0;
         return parentDoesNotExist;
     }
 
@@ -76,22 +66,23 @@ private static String stripVFromTag(String tag) {
         }
     }
 
-    private static GitResult execute(Project project, String... command) {
-        ByteArrayOutputStream stdout = new ByteArrayOutputStream();
-        ByteArrayOutputStream stderr = new ByteArrayOutputStream();
-
-        ExecResult execResult = project.exec(spec -> {
-            spec.setCommandLine(Arrays.asList(command));
-            spec.setStandardOutput(stdout);
-            spec.setErrorOutput(stderr);
-            spec.setIgnoreExitValue(true);
+    private Provider<GitResult> execute(String... command) {
+        ExecOutput output = getProviderFactory().exec(execSpec -> {
+            execSpec.commandLine((Object[]) command);
+            execSpec.setIgnoreExitValue(true);
+            execSpec.setWorkingDir(getProjectLayout().getProjectDirectory());
         });
 
-        return GitResult.builder()
-                .exitCode(execResult.getExitValue())
-                .stdout(new String(stdout.toByteArray(), StandardCharsets.UTF_8).trim())
-                .stderr(new String(stderr.toByteArray(), StandardCharsets.UTF_8).trim())
-                .build();
+        Provider<String> stdout = output.getStandardOutput().getAsText();
+        Provider<String> stderr = output.getStandardError().getAsText();
+        Provider<ExecResult> result = output.getResult();
+
+        return stdout.zip(stderr, (out, err) -> new Object[] {out, err})
+                .zip(result, (outErr, res) -> GitResult.builder()
+                        .exitCode(res.getExitValue())
+                        .stdout(((String) outErr[0]).trim())
+                        .stderr(((String) outErr[1]).trim())
+                        .build());
     }
 
     @Value.Immutable
@@ -122,41 +113,4 @@ static Builder builder() {
             return new Builder();
         }
     }
-
-    private static final class PreviousGitTags implements Spliterator<String> {
-        private final Project project;
-        private String lastSeenRef = "HEAD";
-
-        PreviousGitTags(Project project) {
-            this.project = project;
-        }
-
-        @Override
-        public boolean tryAdvance(Consumer<? super String> action) {
-            Optional<String> tag = previousGitTagFromRef(project, lastSeenRef);
-
-            if (!tag.isPresent()) {
-                return false;
-            }
-
-            lastSeenRef = tag.get();
-            action.accept(lastSeenRef);
-            return true;
-        }
-
-        @Override
-        public Spliterator<String> trySplit() {
-            return null;
-        }
-
-        @Override
-        public long estimateSize() {
-            return Long.MAX_VALUE;
-        }
-
-        @Override
-        public int characteristics() {
-            return 0;
-        }
-    }
 }
diff --git a/src/main/java/org/revapi/gradle/RevapiExtension.java b/src/main/java/org/revapi/gradle/RevapiExtension.java
index bd5e4405..402eae56 100644
--- a/src/main/java/org/revapi/gradle/RevapiExtension.java
+++ b/src/main/java/org/revapi/gradle/RevapiExtension.java
@@ -22,17 +22,21 @@
 import org.gradle.api.provider.ListProperty;
 import org.gradle.api.provider.Property;
 import org.gradle.api.provider.Provider;
+import org.gradle.api.tasks.Nested;
 import org.revapi.gradle.config.GroupAndName;
 import org.revapi.gradle.config.GroupNameVersion;
 import org.revapi.gradle.config.Version;
 
 @SuppressWarnings("DesignForExtension")
-public class RevapiExtension {
+public abstract class RevapiExtension {
     private final Property<String> oldGroup;
     private final Property<String> oldName;
     private final ListProperty<String> oldVersions;
     private final Provider<GroupAndName> oldGroupAndName;
 
+    @Nested
+    protected abstract GitVersionUtils getGitVersionUtils();
+
     public RevapiExtension(Project project) {
         this.oldGroup = project.getObjects().property(String.class);
         this.oldGroup.set(
@@ -42,9 +46,8 @@ public RevapiExtension(Project project) {
         this.oldName.set(project.getProviders().provider(project::getName));
 
         this.oldVersions = project.getObjects().listProperty(String.class);
-        this.oldVersions.set(project.getProviders()
-                .provider(
-                        () -> GitVersionUtils.previousGitTags(project).limit(3).collect(Collectors.toList())));
+        this.oldVersions.set(getGitVersionUtils().previousGitTags().map(versions -> versions.limit(3)
+                .collect(Collectors.toList())));
 
         this.oldGroupAndName = project.provider(() ->
                 GroupAndName.builder().group(oldGroup.get()).name(oldName.get()).build());
diff --git a/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy b/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
index 29b57373..05f90fc1 100644
--- a/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
@@ -68,17 +68,21 @@ class GitVersionUtilsSpec extends AbstractProjectSpec {
     def 'return a number of tags that are behind a tag'() {
         when:
         git.command 'git commit --allow-empty -m "First"'
-        git.command 'git tag 1'
+        git.command 'git tag 5'
+        // need to sleep for at least a second so each commit is in its own timestamp to ensure sort order is correct
+        sleep(1001)
         git.command 'git commit --allow-empty -m "Second"'
         git.command 'git tag 2'
+        sleep(1001)
         git.command 'git commit --allow-empty -m "Third"'
         git.command 'git tag 3'
+        sleep(1001)
 
         git.command 'git commit --allow-empty -m "Fourth"'
         git.command 'git tag 4'
 
         then:
-        assert previousGitTags() == ["3", "2", "1"]
+        assert previousGitTags() == ["3", "2", "5"]
     }
 
     def 'when the initial commit is 0.0.0, ignore it as its the first, unpublished release'() {
@@ -113,6 +117,7 @@ class GitVersionUtilsSpec extends AbstractProjectSpec {
     }
 
     private List<String> previousGitTags() {
-        GitVersionUtils.previousGitTags(getProject()).collect(Collectors.toList())
+        GitVersionUtils utils = getProject().objects.newInstance(GitVersionUtils.class)
+        utils.previousGitTags().get().collect(Collectors.toList())
     }
 }
diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index fff69f94..4b510008 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -293,14 +293,19 @@ class RevapiSpec extends IntegrationSpec {
         """.stripIndent()
 
         git.command 'git add .'
+        sleep(1001)
         git.command 'git commit -m 0.1.0'
+        sleep(1001)
         git.command 'git tag 0.1.0'
+        sleep(1001)
 
         runTasksSuccessfully('publish')
 
         and:
         git.command 'git commit --allow-empty -m publish-failed'
+        sleep(1001)
         git.command 'git tag 0.2.0'
+        sleep(1001)
 
         and:
         writeToFile javaFile, """
@@ -308,6 +313,7 @@ class RevapiSpec extends IntegrationSpec {
         """.stripIndent()
 
         git.command 'git commit -am new-work'
+        sleep(1001)
 
         then:
         def standardError = runTasksWithFailure('revapi').standardError

From 27af76ec68eb127824c68a0bf9c80c288366f8dc Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Wed, 23 Jul 2025 11:05:31 +0100
Subject: [PATCH 02/12] tidy

---
 .../org/revapi/gradle/GitVersionUtils.java    | 21 +++++--------------
 .../org/revapi/gradle/RevapiSpec.groovy       |  6 ------
 2 files changed, 5 insertions(+), 22 deletions(-)

diff --git a/src/main/java/org/revapi/gradle/GitVersionUtils.java b/src/main/java/org/revapi/gradle/GitVersionUtils.java
index 465a5be2..32054fee 100644
--- a/src/main/java/org/revapi/gradle/GitVersionUtils.java
+++ b/src/main/java/org/revapi/gradle/GitVersionUtils.java
@@ -39,7 +39,7 @@ public final Provider<Stream<String>> previousGitTags() {
         return execute("git", "tag", "-l", "--merged", "HEAD^", "--sort=-committerdate")
                 .map(tagsResult -> {
                     if (tagsResult.exitCode() != 0 || tagsResult.stdout().isEmpty()) {
-                        return Stream.<String>empty();
+                        return Stream.empty();
                     }
 
                     return Arrays.stream(tagsResult.stdout().split("\n"))
@@ -53,9 +53,10 @@ private boolean isInitial000Tag(String tag) {
             return false;
         }
 
-        GitResult result = execute("git", "rev-parse", "--verify", "--quiet", "0.0.0^").get();
-        boolean parentDoesNotExist = result.exitCode() != 0;
-        return parentDoesNotExist;
+        return execute("git", "rev-parse", "--verify", "--quiet", "0.0.0^")
+                        .get()
+                        .exitCode()
+                != 0;
     }
 
     private static String stripVFromTag(String tag) {
@@ -95,18 +96,6 @@ interface GitResult {
 
         List<String> command();
 
-        default String stdoutOrThrowIfNonZero() {
-            if (exitCode() == 0) {
-                return stdout();
-            }
-
-            throw new RuntimeException("Failed running command:\n"
-                    + "\tCommand:" + command() + "\n"
-                    + "\tExit code: " + exitCode() + "\n"
-                    + "\tStdout:" + stdout() + "\n"
-                    + "\tStderr:" + stderr() + "\n");
-        }
-
         class Builder extends ImmutableGitResult.Builder {}
 
         static Builder builder() {
diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index 4b510008..fff69f94 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -293,19 +293,14 @@ class RevapiSpec extends IntegrationSpec {
         """.stripIndent()
 
         git.command 'git add .'
-        sleep(1001)
         git.command 'git commit -m 0.1.0'
-        sleep(1001)
         git.command 'git tag 0.1.0'
-        sleep(1001)
 
         runTasksSuccessfully('publish')
 
         and:
         git.command 'git commit --allow-empty -m publish-failed'
-        sleep(1001)
         git.command 'git tag 0.2.0'
-        sleep(1001)
 
         and:
         writeToFile javaFile, """
@@ -313,7 +308,6 @@ class RevapiSpec extends IntegrationSpec {
         """.stripIndent()
 
         git.command 'git commit -am new-work'
-        sleep(1001)
 
         then:
         def standardError = runTasksWithFailure('revapi').standardError

From facccafd48bcf1ef2c99d8e2ee9f66ff16c96536 Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Wed, 23 Jul 2025 11:10:18 +0100
Subject: [PATCH 03/12] tidy

---
 src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy b/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
index 05f90fc1..f6161113 100644
--- a/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
@@ -68,7 +68,7 @@ class GitVersionUtilsSpec extends AbstractProjectSpec {
     def 'return a number of tags that are behind a tag'() {
         when:
         git.command 'git commit --allow-empty -m "First"'
-        git.command 'git tag 5'
+        git.command 'git tag 1'
         // need to sleep for at least a second so each commit is in its own timestamp to ensure sort order is correct
         sleep(1001)
         git.command 'git commit --allow-empty -m "Second"'
@@ -82,7 +82,7 @@ class GitVersionUtilsSpec extends AbstractProjectSpec {
         git.command 'git tag 4'
 
         then:
-        assert previousGitTags() == ["3", "2", "5"]
+        assert previousGitTags() == ["3", "2", "1"]
     }
 
     def 'when the initial commit is 0.0.0, ignore it as its the first, unpublished release'() {

From 80ef379dfa2362541fa7d58086fec3ba7dc0587f Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Wed, 23 Jul 2025 12:54:59 +0100
Subject: [PATCH 04/12] tests with cc

---
 build.gradle                                  |  11 +-
 .../org/revapi/gradle/RevapiSpec.groovy       | 126 ++++++++----------
 versions.lock                                 |  40 +++---
 versions.props                                |   4 +-
 4 files changed, 89 insertions(+), 92 deletions(-)

diff --git a/build.gradle b/build.gradle
index f5d6ce59..8b85d22c 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,12 +4,14 @@ buildscript {
     repositories {
         mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } }
         gradlePluginPortal() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } }
+        mavenLocal()
     }
 
     dependencies {
         classpath 'com.palantir.jakartapackagealignment:jakarta-package-alignment:0.6.0'
         classpath 'com.palantir.gradle.externalpublish:gradle-external-publish-plugin:1.11.0'
         classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.47.0'
+        classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:999'
     }
 }
 
@@ -23,6 +25,7 @@ plugins {
 
 repositories {
     mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } }
+    mavenLocal()
 }
 
 group 'org.revapi'
@@ -33,6 +36,7 @@ apply plugin: 'java-gradle-plugin'
 apply plugin: 'java-library'
 apply plugin: 'groovy'
 apply plugin: 'com.palantir.external-publish-jar'
+apply plugin: 'com.palantir.gradle-plugin-testing'
 
 sourceCompatibility = 1.8
 
@@ -48,10 +52,12 @@ dependencies {
 
     testImplementation platform('org.junit:junit-bom')
     testImplementation 'com.netflix.nebula:nebula-test'
-    testImplementation "org.junit.jupiter:junit-jupiter"
     testImplementation 'org.mockito:mockito-core'
     testImplementation 'org.assertj:assertj-core'
-    testRuntimeOnly "org.junit.vintage:junit-vintage-engine"
+    testImplementation 'org.junit.jupiter:junit-jupiter'
+    testRuntimeOnly 'org.junit.platform:junit-platform-engine'
+    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
+    testRuntimeOnly 'org.junit.vintage:junit-vintage-engine'
 
     annotationProcessor "org.immutables:value"
     annotationProcessor "org.immutables:serial"
@@ -68,6 +74,7 @@ test {
     environment = System.getenv().entrySet().stream()
             .filter { entry -> entry.key != 'CIRCLE_TEST_REPORTS' }
             .collect(Collectors.toMap({ it.key }, { it.value }))
+    systemProperty 'ignoreDeprecations', 'true'
 }
 
 gradlePlugin {
diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index fff69f94..ecdf22ad 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -16,19 +16,19 @@
 
 package org.revapi.gradle
 
+import com.palantir.gradle.plugintesting.ConfigurationCacheSpec
 import spock.lang.Ignore
 
 import java.util.regex.Pattern
-import nebula.test.IntegrationSpec
-import nebula.test.functional.ExecutionResult
+import org.gradle.testkit.runner.BuildResult
+import org.gradle.testkit.runner.TaskOutcome
 import spock.util.environment.RestoreSystemProperties
 
-class RevapiSpec extends IntegrationSpec {
+class RevapiSpec extends ConfigurationCacheSpec {
     private Git git
 
     def setup() {
         git = new Git(projectDir)
-        System.setProperty("ignoreDeprecations", "true")
     }
 
     def 'fails when comparing produced jar versus some random other jar'() {
@@ -118,17 +118,18 @@ class RevapiSpec extends IntegrationSpec {
             public class Foo extends org.junit.rules.ExternalResource { }
         '''.stripIndent()
 
-        println runTasksSuccessfully("publish").standardOutput
+        println runTasksWithConfigurationCache("publish").output
 
         and:
         buildFile.text = buildFile.text.replace('implementation', 'compileOnly')
 
         then:
 
-        def executionResult = runTasks('revapi')
-        println executionResult.standardOutput
-        println executionResult.standardError
-        executionResult.rethrowFailure()
+        def buildResult = runTasksWithConfigurationCacheAndCheck('revapi')
+        println buildResult.output
+        if (buildResult.task(':revapi').outcome != TaskOutcome.SUCCESS) {
+            throw new RuntimeException("Task failed: ${buildResult.output}")
+        }
     }
 
     def 'revapiAcceptAllBreaks succeeds when there are no breaking changes'() {
@@ -252,9 +253,9 @@ class RevapiSpec extends IntegrationSpec {
         """.stripIndent()
 
         then:
-        def executionResult = runTasksSuccessfully('revapi')
-        executionResult.wasSkipped(':revapiAnalyze')
-        executionResult.wasSkipped(':revapi')
+        def buildResult = runTasksWithConfigurationCacheAndCheck('revapi')
+        assert buildResult.task(':revapiAnalyze').outcome == TaskOutcome.SKIPPED
+        assert buildResult.task(':revapi').outcome == TaskOutcome.SKIPPED
     }
 
     def 'when the previous git tag has failed to publish, it will look back up to a further git tag'() {
@@ -310,8 +311,8 @@ class RevapiSpec extends IntegrationSpec {
         git.command 'git commit -am new-work'
 
         then:
-        def standardError = runTasksWithFailure('revapi').standardError
-        assert standardError.contains('willBeRemoved')
+        def buildResult = runTasksAndFail('revapi')
+        assert buildResult.output.contains('willBeRemoved')
     }
 
     def 'if there are no published versions of the library at all, ./gradlew revapi doesnt fail'() {
@@ -320,9 +321,9 @@ class RevapiSpec extends IntegrationSpec {
         writeHelloWorld()
 
         then:
-        def executionResult = runTasksSuccessfully('revapi')
-        executionResult.wasSkipped(':revapiAnalyze')
-        executionResult.wasSkipped(':revapi')
+        def buildResult = runTasksWithConfigurationCacheAndCheck('revapi')
+        assert buildResult.task(':revapiAnalyze').outcome == TaskOutcome.SKIPPED
+        assert buildResult.task(':revapi').outcome == TaskOutcome.SKIPPED
     }
 
     def 'if there are no published versions of the library at all, ./gradlew revapiAcceptAllBreaks is a no-op'() {
@@ -331,9 +332,9 @@ class RevapiSpec extends IntegrationSpec {
         writeHelloWorld()
 
         then:
-        def executionResult = runTasksSuccessfully('revapiAcceptAllBreaks')
-        executionResult.wasSkipped(':revapiAnalyze')
-        executionResult.wasSkipped(':revapiAcceptAllBreaks')
+        def buildResult = runTasksWithConfigurationCacheAndCheck('revapiAcceptAllBreaks')
+        assert buildResult.task(':revapiAnalyze').outcome == TaskOutcome.SKIPPED
+        assert buildResult.task(':revapiAcceptAllBreaks').outcome == TaskOutcome.SKIPPED
     }
 
     def 'if there are no published versions of the library at all, ./gradlew revapiAcceptBreak doesnt fail'() {
@@ -342,9 +343,9 @@ class RevapiSpec extends IntegrationSpec {
         writeHelloWorld()
 
         then:
-        def executionResult = runTasksSuccessfully('revapiAcceptBreak', '--justification', 'foo', '--code', 'bar',
+        def buildResult = runTasksWithConfigurationCacheAndCheck('revapiAcceptBreak', '--justification', 'foo', '--code', 'bar',
                 '--old', 'old', '--new', 'new')
-        executionResult.wasExecuted(':revapiAcceptBreak')
+        assert buildResult.task(':revapiAcceptBreak').outcome in [TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE]
     }
 
     private File setupUnpublishedLibrary() {
@@ -506,13 +507,13 @@ class RevapiSpec extends IntegrationSpec {
         '''.stripIndent()
 
         when:
-        println runTasksSuccessfully("publish").standardOutput
+        println runTasksSuccessfully("publish").output
 
         writeToFile two, 'src/main/java/foo/Foo.java', originalJavaFile.text
         originalJavaFile.delete()
 
         and:
-        println runTasksSuccessfully("revapi").standardOutput
+        println runTasksSuccessfully("revapi").output
 
         and:
         def oneBuildGradle = new File(one, 'build.gradle')
@@ -565,12 +566,12 @@ class RevapiSpec extends IntegrationSpec {
         '''.stripIndent()
 
         and:
-        println runTasksSuccessfully("publish").standardOutput
+        println runTasksSuccessfully("publish").output
 
         javaFileInDependentProject.text = javaFileInDependentProject.text.replace('}', 'void foo();\n}')
 
         then:
-        println runTasksSuccessfully("revapi").standardOutput
+        println runTasksSuccessfully("revapi").output
     }
 
     def 'should not say there are breaks in api dependencies when nothing has changed'() {
@@ -608,10 +609,10 @@ class RevapiSpec extends IntegrationSpec {
         '''
 
         and:
-        println runTasksSuccessfully("publish").standardOutput
+        println runTasksSuccessfully("publish").output
 
         then:
-        println runTasksSuccessfully("revapi").standardOutput
+        println runTasksSuccessfully("revapi").output
     }
 
     def 'ignores scala classes'() {
@@ -665,10 +666,10 @@ class RevapiSpec extends IntegrationSpec {
             class Foo {}
         '''.stripIndent()
 
-        println runTasksSuccessfully("publish").standardOutput
+        println runTasksSuccessfully("publish").output
 
         then:
-        println runTasksSuccessfully("revapi").standardOutput
+        println runTasksSuccessfully("revapi").output
     }
 
     def 'detects breaks in groovy code'() {
@@ -705,7 +706,7 @@ class RevapiSpec extends IntegrationSpec {
             }
         '''.stripIndent()
 
-        println runTasksSuccessfully("publish").standardOutput
+        println runTasksWithConfigurationCache("publish").output
 
         and:
         writeToFile groovyFile, '''
@@ -714,11 +715,11 @@ class RevapiSpec extends IntegrationSpec {
         '''.stripIndent()
 
         then:
-        def stderr = runRevapiExpectingFailure()
+        def output = runRevapiExpectingFailure()
 
-        assert stderr.contains('java.method.removed')
-        assert stderr.contains('method java.lang.String foo.Foo::getSomeProperty()')
-        assert stderr.contains('method void foo.Foo::setSomeProperty(java.lang.String)')
+        assert output.contains('java.method.removed')
+        assert output.contains('method java.lang.String foo.Foo::getSomeProperty()')
+        assert output.contains('method void foo.Foo::setSomeProperty(java.lang.String)')
     }
 
     def 'does not throw exception when baseline-circleci is applied before this plugin'() {
@@ -766,8 +767,8 @@ class RevapiSpec extends IntegrationSpec {
         """.stripIndent()
 
         then:
-        runTasksSuccessfully('revapi').wasExecuted('revapiAnalyze')
-        runTasksSuccessfully('revapi').wasUpToDate('revapiAnalyze')
+        assert runTasksSuccessfully('revapi').task(':revapiAnalyze').outcome == TaskOutcome.SUCCESS
+        assert runTasksSuccessfully('revapi').task(':revapiAnalyze').outcome == TaskOutcome.UP_TO_DATE
     }
 
     def 'is not up to date when public (not private) api has changed'() {
@@ -796,7 +797,7 @@ class RevapiSpec extends IntegrationSpec {
         '''.stripIndent()
 
         then:
-        runTasksSuccessfully('revapi').wasExecuted('revapiAnalyze')
+        assert runTasksSuccessfully('revapi').task(':revapiAnalyze').outcome == TaskOutcome.SUCCESS
 
         writeToFile javaFile, '''
             public class Foo {
@@ -805,7 +806,7 @@ class RevapiSpec extends IntegrationSpec {
             }
         '''.stripIndent()
 
-        runTasksSuccessfully('revapi').wasUpToDate('revapiAnalyze')
+        assert runTasksSuccessfully('revapi').task(':revapiAnalyze').outcome == TaskOutcome.UP_TO_DATE
 
         writeToFile javaFile, '''
             public class Foo {
@@ -814,7 +815,7 @@ class RevapiSpec extends IntegrationSpec {
             }
         '''.stripIndent()
 
-        runTasksSuccessfully('revapi').wasExecuted('revapiAnalyze')
+        assert runTasksSuccessfully('revapi').task(':revapiAnalyze').outcome == TaskOutcome.SUCCESS
     }
 
     def 'compatible with gradle-shadow-jar'() {
@@ -847,12 +848,12 @@ class RevapiSpec extends IntegrationSpec {
         '''.stripIndent()
 
         and:
-        println runTasksSuccessfully('publish').standardOutput
+        println runTasksSuccessfully('publish').output
 
         file(shadowedClass).delete()
 
         then:
-        println runTasksSuccessfully('revapi').standardOutput
+        println runTasksSuccessfully('revapi').output
     }
 
     def 'changing a protected method in an immutables class is not a break'() {
@@ -965,16 +966,15 @@ class RevapiSpec extends IntegrationSpec {
         writeOutImmutablesClass { it.oldText }
 
         and:
-        runTasksSuccessfully('publish')
+        runTasksWithConfigurationCache('publish')
 
         writeOutImmutablesClass { it.newText }
 
         then:
-        def executionResult = runTasks('revapi')
-        println executionResult.standardError
-        !executionResult.success
+        def buildResult = runTasksAndFail('revapi')
+        println buildResult.output
 
-        def errorMessage = executionResult.failure.cause.cause.message
+        def errorMessage = buildResult.output
         errorMessage.contains 'There were Java public API/ABI breaks reported by revapi:'
 
         for (MethodChange methodChange : methodChanges) {
@@ -1086,7 +1086,7 @@ class RevapiSpec extends IntegrationSpec {
         runTasksSuccessfully('compileConjure')
 
         then:
-        runTasksWithFailure(':api-jersey:revapi')
+        runTasksAndFail(':api-jersey:revapi')
         def jerseyJunit = new File(projectDir, 'api-jersey/build/junit-reports/revapi/revapi-api-jersey.xml').text
 
         assert jerseyJunit.contains('java.class.removed-interface services.RenamedService')
@@ -1097,7 +1097,7 @@ class RevapiSpec extends IntegrationSpec {
         assert !jerseyJunit.contains('services.TestService::renamedToSomethingElse()')
         assert !jerseyJunit.contains('java.annotation.attributeValueChanged')
 
-        runTasksWithFailure(':api-retrofit:revapi')
+        runTasksAndFail(':api-retrofit:revapi')
         def retrofitJunit = new File(projectDir, 'api-retrofit/build/junit-reports/revapi/revapi-api-retrofit.xml').text
 
         assert retrofitJunit.contains('java.class.removed-interface services.RenamedServiceRetrofit')
@@ -1184,10 +1184,9 @@ class RevapiSpec extends IntegrationSpec {
     }
 
     private String runRevapiExpectingFailure() {
-        ExecutionResult executionResult = runTasksWithFailure("revapi")
-        println executionResult.standardOutput
-        println executionResult.standardError
-        return executionResult.standardError
+        BuildResult buildResult = runTasksAndFail("revapi")
+        println buildResult.output
+        return buildResult.output
     }
 
     private void andJunitXmlToHaveBeenProduced(String projectName) {
@@ -1196,22 +1195,11 @@ class RevapiSpec extends IntegrationSpec {
         assert junitOutput.text.contains("java.class.removed")
     }
 
-    @Override
-    ExecutionResult runTasksSuccessfully(String... tasks) {
-        ExecutionResult result = runTasks(tasks)
-        if (result.failure) {
-            println result.standardOutput
-            result.rethrowFailure()
-        }
-        result
-    }
-
-    @Override
-    ExecutionResult runTasksWithFailure(String... tasks) {
-        ExecutionResult result = runTasks(tasks)
-        if (result.success) {
-            println result.standardOutput
-            assert false
+    private BuildResult runTasksSuccessfully(String... tasks) {
+        BuildResult result = runTasksWithConfigurationCacheAndCheck(tasks)
+        if (result.task(':' + tasks[0])?.outcome == TaskOutcome.FAILED) {
+            println result.output
+            throw new RuntimeException("Task failed: ${result.output}")
         }
         result
     }
diff --git a/versions.lock b/versions.lock
index 10342302..0936ba60 100644
--- a/versions.lock
+++ b/versions.lock
@@ -4,17 +4,16 @@ com.fasterxml.jackson.core:jackson-core:2.14.2 (3 constraints: 55402771)
 com.fasterxml.jackson.core:jackson-databind:2.14.2 (3 constraints: 0733c1e3)
 com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.14.2 (1 constraints: 3b053d3b)
 com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.2 (1 constraints: 3b053d3b)
-com.google.code.findbugs:jsr305:3.0.2 (1 constraints: 170aecb4)
-com.google.errorprone:error_prone_annotations:2.26.1 (1 constraints: 4d0a46bf)
-com.google.guava:failureaccess:1.0.2 (1 constraints: 150ae2b4)
-com.google.guava:guava:33.2.1-jre (1 constraints: a9067053)
+com.google.errorprone:error_prone_annotations:2.36.0 (1 constraints: 4d0a49bf)
+com.google.guava:failureaccess:1.0.3 (1 constraints: 160ae3b4)
+com.google.guava:guava:33.4.8-jre (2 constraints: da1e577d)
 com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava (1 constraints: bd17c918)
 com.google.j2objc:j2objc-annotations:3.0.0 (1 constraints: 150aeab4)
-org.checkerframework:checker-qual:3.42.0 (1 constraints: 4b0a47bf)
 org.freemarker:freemarker:2.3.33 (2 constraints: 5e13e559)
 org.immutables:serial:2.10.1 (1 constraints: 3605303b)
 org.immutables:value:2.10.1 (1 constraints: 3605303b)
 org.jboss:jboss-dmr:1.2.0.Final (1 constraints: 710a5ab9)
+org.jspecify:jspecify:1.0.0 (1 constraints: 130ae0b4)
 org.revapi:revapi:0.11.1 (4 constraints: 8532f166)
 org.revapi:revapi-basic-features:0.8.1 (1 constraints: 0b050436)
 org.revapi:revapi-java:0.19.1 (1 constraints: 3d053f3b)
@@ -25,25 +24,28 @@ org.yaml:snakeyaml:1.33 (1 constraints: 6f17f827)
 
 [Test dependencies]
 cglib:cglib-nodep:3.2.2 (1 constraints: 490ded24)
-com.netflix.nebula:nebula-test:10.0.0 (1 constraints: 3305273b)
-junit:junit:4.13.2 (2 constraints: 6a1ed87b)
+com.netflix.nebula:nebula-test:10.6.2 (2 constraints: d61de72d)
+com.palantir.gradle.plugintesting:configuration-cache-spec:999 (1 constraints: c104512c)
+com.palantir.gradle.plugintesting:plugin-testing-core:999 (1 constraints: c104512c)
+junit:junit:4.13.2 (2 constraints: ca1e3ab9)
 net.bytebuddy:byte-buddy:1.14.16 (2 constraints: f8167267)
 net.bytebuddy:byte-buddy-agent:1.14.15 (1 constraints: 760bb5e9)
-org.apiguardian:apiguardian-api:1.1.2 (6 constraints: 896455cc)
+org.apiguardian:apiguardian-api:1.1.2 (7 constraints: cc761de9)
 org.assertj:assertj-core:3.26.0 (1 constraints: 3d054b3b)
-org.codehaus.groovy:groovy:3.0.6 (2 constraints: 1e1b476d)
+org.codehaus.groovy:groovy:3.0.21 (3 constraints: 103436dc)
 org.hamcrest:hamcrest:2.2 (1 constraints: d20cdc04)
 org.hamcrest:hamcrest-core:1.3 (1 constraints: cc05fe3f)
-org.junit:junit-bom:5.10.2 (1 constraints: 3a05433b)
-org.junit.jupiter:junit-jupiter:5.10.2 (1 constraints: 3f09b79e)
-org.junit.jupiter:junit-jupiter-api:5.10.2 (4 constraints: 2f393ee2)
-org.junit.jupiter:junit-jupiter-engine:5.10.2 (2 constraints: 7317c263)
-org.junit.jupiter:junit-jupiter-params:5.10.2 (2 constraints: 7317c263)
-org.junit.platform:junit-platform-commons:1.10.2 (3 constraints: 692ae28e)
-org.junit.platform:junit-platform-engine:1.10.2 (4 constraints: ea379e38)
-org.junit.vintage:junit-vintage-engine:5.10.2 (1 constraints: 3f09b79e)
+org.junit:junit-bom:5.13.4 (1 constraints: 3f054e3b)
+org.junit.jupiter:junit-jupiter:5.13.4 (1 constraints: 4409c29e)
+org.junit.jupiter:junit-jupiter-api:5.13.4 (5 constraints: fe4f85b3)
+org.junit.jupiter:junit-jupiter-engine:5.13.4 (2 constraints: 7d17a564)
+org.junit.jupiter:junit-jupiter-params:5.13.4 (3 constraints: 382e9ca9)
+org.junit.platform:junit-platform-commons:1.13.4 (3 constraints: 782ad891)
+org.junit.platform:junit-platform-engine:1.13.4 (5 constraints: 734a41d4)
+org.junit.platform:junit-platform-launcher:1.13.4 (1 constraints: 4009aa9e)
+org.junit.vintage:junit-vintage-engine:5.13.4 (1 constraints: 4409c29e)
 org.mockito:mockito-core:5.12.0 (1 constraints: 3a05473b)
 org.objenesis:objenesis:3.3 (2 constraints: 9b17f557)
 org.opentest4j:opentest4j:1.3.0 (2 constraints: cf209249)
-org.spockframework:spock-core:2.0-M4-groovy-3.0 (2 constraints: e822d65a)
-org.spockframework:spock-junit4:2.0-M4-groovy-3.0 (1 constraints: 25115ddf)
+org.spockframework:spock-core:2.3-groovy-3.0 (3 constraints: 2c3deb38)
+org.spockframework:spock-junit4:2.3-groovy-3.0 (1 constraints: 7a1000b0)
diff --git a/versions.props b/versions.props
index 5936b298..b60c40bd 100644
--- a/versions.props
+++ b/versions.props
@@ -1,6 +1,6 @@
 com.fasterxml.jackson.core:jackson-databind = 2.14.2
-com.netflix.nebula:nebula-test = 10.0.0
-org.junit:junit-bom = 5.10.2
+com.netflix.nebula:nebula-test = 10.6.2
+org.junit:junit-bom = 5.13.4
 org.revapi:revapi-basic-features = 0.8.1
 org.revapi:revapi-java = 0.19.1
 org.revapi:revapi-reporter-text = 0.10.1

From abc936342046d5de48c7463e1f27572bad0ae61c Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Wed, 23 Jul 2025 14:13:26 +0100
Subject: [PATCH 05/12] fix all configuration cache issues and test

---
 .../revapi/gradle/ConjureProjectFilters.java  |  9 +--
 .../revapi/gradle/RevapiAcceptBreakTask.java  | 22 +++----
 .../org/revapi/gradle/RevapiAnalyzeTask.java  | 62 ++++++-------------
 .../java/org/revapi/gradle/RevapiPlugin.java  | 43 +++++++++----
 .../org/revapi/gradle/RevapiReportTask.java   | 40 +++++-------
 .../gradle/RevapiVersionOverrideTask.java     | 15 ++---
 .../org/revapi/gradle/RevapiSpec.groovy       | 26 +++++---
 7 files changed, 96 insertions(+), 121 deletions(-)

diff --git a/src/main/java/org/revapi/gradle/ConjureProjectFilters.java b/src/main/java/org/revapi/gradle/ConjureProjectFilters.java
index e8c9741e..ec7fa9f8 100644
--- a/src/main/java/org/revapi/gradle/ConjureProjectFilters.java
+++ b/src/main/java/org/revapi/gradle/ConjureProjectFilters.java
@@ -18,7 +18,6 @@
 
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import java.util.Optional;
-import org.gradle.api.Project;
 
 final class ConjureProjectFilters {
     private static final ArrayNode CHECKS_FOR_CLIENT_PROJECTS = RevapiConfig.createArrayNode()
@@ -29,16 +28,12 @@ final class ConjureProjectFilters {
 
     private ConjureProjectFilters() {}
 
-    public static RevapiConfig forProject(Project project) {
-        boolean isConjure = Optional.ofNullable(project.getParent())
-                .map(parentProject -> parentProject.getPluginManager().hasPlugin("com.palantir.conjure"))
-                .orElse(false);
-
+    public static RevapiConfig forProject(String projectName, boolean isConjure) {
         if (!isConjure) {
             return RevapiConfig.empty();
         }
 
-        return checksForProjectName(project.getName())
+        return checksForProjectName(projectName)
                 .map(checks -> RevapiConfig.empty().withExtension(CheckWhitelist.EXTENSION_ID, checks))
                 .orElseGet(RevapiConfig::empty);
     }
diff --git a/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java b/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java
index 46818cb7..590df6e9 100644
--- a/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java
@@ -20,6 +20,7 @@
 import java.util.Optional;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
 import org.gradle.api.tasks.Internal;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.api.tasks.options.Option;
@@ -27,28 +28,23 @@
 import org.revapi.gradle.config.GroupNameVersion;
 import org.revapi.gradle.config.Justification;
 
-public class RevapiAcceptBreakTask extends DefaultTask {
+public abstract class RevapiAcceptBreakTask extends DefaultTask {
     private static final String CODE_OPTION = "code";
     private static final String OLD_OPTION = "old";
     private static final String NEW_OPTION = "new";
     private static final String JUSTIFICATION_OPTION = "justification";
 
-    private final Property<ConfigManager> configManager =
-            getProject().getObjects().property(ConfigManager.class);
     private final Property<String> code = getProject().getObjects().property(String.class);
     private final Property<String> oldElement = getProject().getObjects().property(String.class);
     private final Property<String> newElement = getProject().getObjects().property(String.class);
     private final Property<Justification> justification =
             getProject().getObjects().property(Justification.class);
 
-    public RevapiAcceptBreakTask() {
-        getOutputs().upToDateWhen(_ignored -> false);
-    }
+    @Input
+    protected abstract Property<GroupNameVersion> getGroupNameVersion();
 
     @Internal
-    final Property<ConfigManager> getConfigManager() {
-        return configManager;
-    }
+    protected abstract Property<ConfigManager> getConfigManager();
 
     @Option(option = CODE_OPTION, description = "Revapi change code")
     public final void setCode(String codeString) {
@@ -75,10 +71,10 @@ public final void addVersionOverride() {
         ensurePresent(code, CODE_OPTION);
         ensurePresent(justification, JUSTIFICATION_OPTION);
 
-        configManager
+        getConfigManager()
                 .get()
                 .modifyConfigFile(revapiConfig -> revapiConfig.addAcceptedBreaks(
-                        oldGroupNameVersion(),
+                        getGroupNameVersion().get(),
                         Collections.singleton(AcceptedBreak.builder()
                                 .code(code.get())
                                 .oldElement(Optional.ofNullable(oldElement.getOrNull()))
@@ -92,8 +88,4 @@ private void ensurePresent(Property<?> prop, String option) {
             throw new IllegalArgumentException("Please supply the --" + option + " param to this task");
         }
     }
-
-    private GroupNameVersion oldGroupNameVersion() {
-        return getProject().getExtensions().getByType(RevapiExtension.class).oldGroupNameVersion();
-    }
 }
diff --git a/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java b/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java
index f9fd0da7..5ad28cfa 100644
--- a/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java
@@ -42,63 +42,40 @@
 import org.slf4j.LoggerFactory;
 
 @CacheableTask
-public class RevapiAnalyzeTask extends DefaultTask {
+public abstract class RevapiAnalyzeTask extends DefaultTask {
     private static final Logger log = LoggerFactory.getLogger(RevapiAnalyzeTask.class);
 
-    private final SetProperty<AcceptedBreak> acceptedBreaks =
-            getProject().getObjects().setProperty(AcceptedBreak.class);
-    private final Property<FileCollection> newApiJars =
-            getProject().getObjects().property(FileCollection.class);
-    private final Property<FileCollection> newApiDependencyJars =
-            getProject().getObjects().property(FileCollection.class);
-    private final Property<FileCollection> jarsToReportBreaks =
-            getProject().getObjects().property(FileCollection.class);
-    private final Property<FileCollection> oldApiJars =
-            getProject().getObjects().property(FileCollection.class);
-    private final Property<FileCollection> oldApiDependencyJars =
-            getProject().getObjects().property(FileCollection.class);
-    private final RegularFileProperty analysisResultsFile =
-            getProject().getObjects().fileProperty();
+    @Input
+    protected abstract SetProperty<AcceptedBreak> getAcceptedBreaks();
 
     @Input
-    public final SetProperty<AcceptedBreak> getAcceptedBreaks() {
-        return acceptedBreaks;
-    }
+    protected abstract Property<String> getProjectName();
+
+    @Input
+    protected abstract Property<Boolean> getIsConjure();
 
     @CompileClasspath
-    public final Property<FileCollection> getNewApiJars() {
-        return newApiJars;
-    }
+    protected abstract Property<FileCollection> getNewApiJars();
 
     @CompileClasspath
-    public final Property<FileCollection> getNewApiDependencyJars() {
-        return newApiDependencyJars;
-    }
+    protected abstract Property<FileCollection> getNewApiDependencyJars();
 
     @CompileClasspath
-    public final Property<FileCollection> getJarsToReportBreaks() {
-        return jarsToReportBreaks;
-    }
+    protected abstract Property<FileCollection> getJarsToReportBreaks();
 
     @CompileClasspath
-    public final Property<FileCollection> getOldApiJars() {
-        return oldApiJars;
-    }
+    protected abstract Property<FileCollection> getOldApiJars();
 
     @CompileClasspath
-    public final Property<FileCollection> getOldApiDependencyJars() {
-        return oldApiDependencyJars;
-    }
+    protected abstract Property<FileCollection> getOldApiDependencyJars();
 
     @OutputFile
-    public final RegularFileProperty getAnalysisResultsFile() {
-        return analysisResultsFile;
-    }
+    protected abstract RegularFileProperty getAnalysisResultsFile();
 
     @TaskAction
     protected final void runRevapi() throws Exception {
-        API oldApi = api(oldApiJars, oldApiDependencyJars);
-        API newApi = api(newApiJars, newApiDependencyJars);
+        API oldApi = api(getOldApiJars(), getOldApiDependencyJars());
+        API newApi = api(getNewApiJars(), getNewApiDependencyJars());
 
         log.info("Old API: {}", oldApi);
         log.info("New API: {}", newApi);
@@ -111,13 +88,14 @@ protected final void runRevapi() throws Exception {
                 .build();
 
         RevapiConfig revapiConfig = RevapiConfig.mergeAll(
-                RevapiConfig.defaults(jarsToReportBreaks.get()),
+                RevapiConfig.defaults(getJarsToReportBreaks().get()),
                 RevapiConfig.empty()
                         .withTextReporter(
                                 "gradle-revapi-results.ftl",
-                                analysisResultsFile.getAsFile().get()),
+                                getAnalysisResultsFile().getAsFile().get()),
                 revapiIgnores(),
-                ConjureProjectFilters.forProject(getProject()),
+                ConjureProjectFilters.forProject(
+                        getProjectName().get(), getIsConjure().get()),
                 ImmutablesFilter.CONFIG);
 
         log.info("revapi config:\n{}", revapiConfig.configAsString());
@@ -133,7 +111,7 @@ protected final void runRevapi() throws Exception {
     }
 
     private RevapiConfig revapiIgnores() {
-        return RevapiConfig.empty().withIgnoredBreaks(acceptedBreaks.get());
+        return RevapiConfig.empty().withIgnoredBreaks(getAcceptedBreaks().get());
     }
 
     private API api(Provider<FileCollection> apiJars, Provider<FileCollection> dependencyJars) {
diff --git a/src/main/java/org/revapi/gradle/RevapiPlugin.java b/src/main/java/org/revapi/gradle/RevapiPlugin.java
index 8e6feb3b..7081f15c 100644
--- a/src/main/java/org/revapi/gradle/RevapiPlugin.java
+++ b/src/main/java/org/revapi/gradle/RevapiPlugin.java
@@ -20,6 +20,7 @@
 import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
+import org.gradle.api.DefaultTask;
 import org.gradle.api.Plugin;
 import org.gradle.api.Project;
 import org.gradle.api.Task;
@@ -108,6 +109,30 @@ public void apply(Project project) {
 
                     task.getAnalysisResultsFile().set(new File(project.getBuildDir(), "revapi/revapi-results.json"));
 
+                    task.getProjectName().set(project.getName());
+                    task.getIsConjure()
+                            .set(Optional.ofNullable(project.getParent())
+                                    .map(parentProject ->
+                                            parentProject.getPluginManager().hasPlugin("com.palantir.conjure"))
+                                    .orElse(false));
+
+                    task.onlyIf(oldApiIsPresent);
+                });
+
+        TaskProvider<RevapiAcceptBreakTask> acceptBreakTaskTaskProvider = project.getTasks()
+                .register(ACCEPT_BREAK_TASK_NAME, RevapiAcceptBreakTask.class, task -> {
+                    task.getConfigManager().set(configManager);
+                    task.getGroupNameVersion().set(project.getProviders().provider(extension::oldGroupNameVersion));
+                    task.doNotTrackState("This task should always run.");
+                });
+
+        TaskProvider<RevapiAcceptAllBreaksTask> acceptAllBreaksProjectTaskProvider = project.getTasks()
+                .register(ACCEPT_ALL_BREAKS_TASK_NAME, RevapiAcceptAllBreaksTask.class, task -> {
+                    task.dependsOn(analyzeTask);
+
+                    task.getOldGroupNameVersion().set(project.getProviders().provider(extension::oldGroupNameVersion));
+                    task.getConfigManager().set(configManager);
+                    task.getAnalysisResultsFile().set(analyzeTask.flatMap(RevapiAnalyzeTask::getAnalysisResultsFile));
                     task.onlyIf(oldApiIsPresent);
                 });
 
@@ -116,27 +141,19 @@ public void apply(Project project) {
                     task.dependsOn(analyzeTask);
                     task.getAnalysisResultsFile().set(analyzeTask.flatMap(RevapiAnalyzeTask::getAnalysisResultsFile));
                     task.getJunitOutputFile().set(junitOutput(project));
+                    task.getAcceptAllBreaksTaskPath().set(acceptBreakTaskTaskProvider.map(DefaultTask::getPath));
+                    task.getAcceptAllBreaksProjectTaskPath()
+                            .set(acceptAllBreaksProjectTaskProvider.map(DefaultTask::getPath));
 
                     task.onlyIf(oldApiIsPresent);
                 });
 
         project.getTasks().findByName(LifecycleBasePlugin.CHECK_TASK_NAME).dependsOn(reportTask);
 
-        project.getTasks().register(ACCEPT_ALL_BREAKS_TASK_NAME, RevapiAcceptAllBreaksTask.class, task -> {
-            task.dependsOn(analyzeTask);
-
-            task.getOldGroupNameVersion().set(project.getProviders().provider(extension::oldGroupNameVersion));
-            task.getConfigManager().set(configManager);
-            task.getAnalysisResultsFile().set(analyzeTask.flatMap(RevapiAnalyzeTask::getAnalysisResultsFile));
-            task.onlyIf(oldApiIsPresent);
-        });
-
         project.getTasks().register(VERSION_OVERRIDE_TASK_NAME, RevapiVersionOverrideTask.class, task -> {
             task.getConfigManager().set(configManager);
-        });
-
-        project.getTasks().register(ACCEPT_BREAK_TASK_NAME, RevapiAcceptBreakTask.class, task -> {
-            task.getConfigManager().set(configManager);
+            task.getGroupNameVersion().set(project.getProviders().provider(extension::oldGroupNameVersion));
+            task.doNotTrackState("This task should always run.");
         });
     }
 
diff --git a/src/main/java/org/revapi/gradle/RevapiReportTask.java b/src/main/java/org/revapi/gradle/RevapiReportTask.java
index 315aef98..bae0de62 100644
--- a/src/main/java/org/revapi/gradle/RevapiReportTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiReportTask.java
@@ -27,56 +27,46 @@
 import java.util.Map;
 import org.gradle.api.DefaultTask;
 import org.gradle.api.file.RegularFileProperty;
+import org.gradle.api.provider.Property;
 import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.Internal;
 import org.gradle.api.tasks.OutputFile;
 import org.gradle.api.tasks.TaskAction;
 import org.revapi.gradle.config.Justification;
 
-public class RevapiReportTask extends DefaultTask {
-    private final RegularFileProperty analysisResultsFile =
-            getProject().getObjects().fileProperty();
-    private final RegularFileProperty junitOutputFile =
-            getProject().getObjects().fileProperty();
+public abstract class RevapiReportTask extends DefaultTask {
+
+    @Internal
+    protected abstract Property<String> getAcceptAllBreaksTaskPath();
+
+    @Internal
+    protected abstract Property<String> getAcceptAllBreaksProjectTaskPath();
 
     @InputFile
-    public final RegularFileProperty getAnalysisResultsFile() {
-        return analysisResultsFile;
-    }
+    protected abstract RegularFileProperty getAnalysisResultsFile();
 
     @OutputFile
-    public final RegularFileProperty getJunitOutputFile() {
-        return junitOutputFile;
-    }
+    protected abstract RegularFileProperty getJunitOutputFile();
 
     @TaskAction
     public final void reportBreaks() throws Exception {
         AnalysisResults results =
-                AnalysisResults.fromFile(analysisResultsFile.getAsFile().get());
+                AnalysisResults.fromFile(getAnalysisResultsFile().getAsFile().get());
 
         Configuration freeMarkerConfiguration = createFreeMarkerConfiguration();
         Map<String, Object> templateData = new HashMap<>();
         templateData.put("results", results);
-        templateData.put(
-                "acceptBreakTask",
-                getProject()
-                        .getTasks()
-                        .withType(RevapiAcceptBreakTask.class)
-                        .getByName(RevapiPlugin.ACCEPT_BREAK_TASK_NAME)
-                        .getPath());
+        templateData.put("acceptBreakTask", getAcceptAllBreaksTaskPath().get());
         templateData.put(
                 "acceptAllBreaksProjectTask",
-                getProject()
-                        .getTasks()
-                        .withType(RevapiAcceptAllBreaksTask.class)
-                        .getByName(RevapiPlugin.ACCEPT_ALL_BREAKS_TASK_NAME)
-                        .getPath());
+                getAcceptAllBreaksProjectTaskPath().get());
         templateData.put("acceptAllBreaksEverywhereTask", RevapiPlugin.ACCEPT_ALL_BREAKS_TASK_NAME);
         templateData.put("explainWhy", Justification.YOU_MUST_ENTER_JUSTIFICATION);
 
         Template junitTemplate = freeMarkerConfiguration.getTemplate("gradle-revapi-junit-template.ftl");
         junitTemplate.process(
                 templateData,
-                Files.newBufferedWriter(junitOutputFile.getAsFile().get().toPath(), StandardCharsets.UTF_8));
+                Files.newBufferedWriter(getJunitOutputFile().getAsFile().get().toPath(), StandardCharsets.UTF_8));
 
         Template textTemplate = freeMarkerConfiguration.getTemplate("gradle-revapi-text-template.ftl");
         StringWriter textOutputWriter = new StringWriter();
diff --git a/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java b/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java
index 8364481b..468865d0 100644
--- a/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java
@@ -18,12 +18,13 @@
 
 import org.gradle.api.DefaultTask;
 import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.Input;
 import org.gradle.api.tasks.Internal;
 import org.gradle.api.tasks.TaskAction;
 import org.gradle.api.tasks.options.Option;
 import org.revapi.gradle.config.GroupNameVersion;
 
-public class RevapiVersionOverrideTask extends DefaultTask {
+public abstract class RevapiVersionOverrideTask extends DefaultTask {
     public static final String REPLACEMENT_VERSION_OPTION = "replacement-version";
 
     private final Property<ConfigManager> configManager =
@@ -31,9 +32,8 @@ public class RevapiVersionOverrideTask extends DefaultTask {
     private final Property<String> replacementVersion =
             getProject().getObjects().property(String.class);
 
-    public RevapiVersionOverrideTask() {
-        getOutputs().upToDateWhen(_ignored -> false);
-    }
+    @Input
+    protected abstract Property<GroupNameVersion> getGroupNameVersion();
 
     @Internal
     final Property<ConfigManager> getConfigManager() {
@@ -53,10 +53,7 @@ public final void addVersionOverride() {
 
         configManager
                 .get()
-                .modifyConfigFile(config -> config.addVersionOverride(oldGroupNameVersion(), replacementVersion.get()));
-    }
-
-    private GroupNameVersion oldGroupNameVersion() {
-        return getProject().getExtensions().getByType(RevapiExtension.class).oldGroupNameVersion();
+                .modifyConfigFile(config ->
+                        config.addVersionOverride(getGroupNameVersion().get(), replacementVersion.get()));
     }
 }
diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index ecdf22ad..cbafc24e 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -125,7 +125,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
 
         then:
 
-        def buildResult = runTasksWithConfigurationCacheAndCheck('revapi')
+        def buildResult = runTasksSuccessfully('revapi')
         println buildResult.output
         if (buildResult.task(':revapi').outcome != TaskOutcome.SUCCESS) {
             throw new RuntimeException("Task failed: ${buildResult.output}")
@@ -150,7 +150,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         """.stripIndent()
 
         then:
-        runTasksSuccessfully("revapiAcceptAllBreaks", "--justification", "fight me")
+        runTasksWithConfigurationCache("revapiAcceptAllBreaks", "--justification", "fight me")
     }
 
     def 'does not error out when project has a version greater than the "old version"'() {
@@ -253,7 +253,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         """.stripIndent()
 
         then:
-        def buildResult = runTasksWithConfigurationCacheAndCheck('revapi')
+        def buildResult = runTasksSuccessfully('revapi')
         assert buildResult.task(':revapiAnalyze').outcome == TaskOutcome.SKIPPED
         assert buildResult.task(':revapi').outcome == TaskOutcome.SKIPPED
     }
@@ -272,7 +272,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
 
         buildFile << """
             plugins {
-                id 'com.palantir.git-version' version '3.1.0'
+                id 'com.palantir.git-version' version '4.0.0'
             }
 
             apply plugin: '${TestConstants.PLUGIN_NAME}'
@@ -297,7 +297,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         git.command 'git commit -m 0.1.0'
         git.command 'git tag 0.1.0'
 
-        runTasksSuccessfully('publish')
+        runTasksWithConfigurationCache('publish')
 
         and:
         git.command 'git commit --allow-empty -m publish-failed'
@@ -321,7 +321,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         writeHelloWorld()
 
         then:
-        def buildResult = runTasksWithConfigurationCacheAndCheck('revapi')
+        def buildResult = runTasksSuccessfully('revapi')
         assert buildResult.task(':revapiAnalyze').outcome == TaskOutcome.SKIPPED
         assert buildResult.task(':revapi').outcome == TaskOutcome.SKIPPED
     }
@@ -332,7 +332,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         writeHelloWorld()
 
         then:
-        def buildResult = runTasksWithConfigurationCacheAndCheck('revapiAcceptAllBreaks')
+        def buildResult = runTasksSuccessfully('revapiAcceptAllBreaks')
         assert buildResult.task(':revapiAnalyze').outcome == TaskOutcome.SKIPPED
         assert buildResult.task(':revapiAcceptAllBreaks').outcome == TaskOutcome.SKIPPED
     }
@@ -343,7 +343,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         writeHelloWorld()
 
         then:
-        def buildResult = runTasksWithConfigurationCacheAndCheck('revapiAcceptBreak', '--justification', 'foo', '--code', 'bar',
+        def buildResult = runTasksSuccessfully('revapiAcceptBreak', '--justification', 'foo', '--code', 'bar',
                 '--old', 'old', '--new', 'new')
         assert buildResult.task(':revapiAcceptBreak').outcome in [TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE]
     }
@@ -418,7 +418,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
 
         and:
         !revapiYml.exists()
-        runTasksSuccessfully("revapiAcceptAllBreaks", "--justification", "it's all good :)")
+        runTasksWithConfigurationCache("revapiAcceptAllBreaks", "--justification", "it's all good :)")
         revapiYml.text.contains('java.class.removed')
 
         then:
@@ -736,7 +736,13 @@ class RevapiSpec extends ConfigurationCacheSpec {
                 }
             
                 dependencies {
-                    classpath 'com.palantir.baseline:gradle-baseline-java:5.61.0'
+                    classpath 'com.palantir.baseline:gradle-baseline-java:6.48.0'
+                }
+            }
+            
+            allprojects {
+                repositories {
+                    mavenCentral()
                 }
             }
 

From 51fe379910717b92a241b7edf0630ccaecb70ecf Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Wed, 23 Jul 2025 15:02:51 +0100
Subject: [PATCH 06/12] fix

---
 .../org/revapi/gradle/GitVersionUtils.java    | 111 +++++++++++++++---
 .../org/revapi/gradle/RevapiExtension.java    |   4 +-
 .../revapi/gradle/GitVersionUtilsSpec.groovy  |   6 +-
 3 files changed, 97 insertions(+), 24 deletions(-)

diff --git a/src/main/java/org/revapi/gradle/GitVersionUtils.java b/src/main/java/org/revapi/gradle/GitVersionUtils.java
index 32054fee..d3c5640a 100644
--- a/src/main/java/org/revapi/gradle/GitVersionUtils.java
+++ b/src/main/java/org/revapi/gradle/GitVersionUtils.java
@@ -16,10 +16,14 @@
 
 package org.revapi.gradle;
 
-import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
+import java.util.Spliterator;
+import java.util.function.Consumer;
 import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 import javax.inject.Inject;
+import org.gradle.api.file.Directory;
 import org.gradle.api.file.ProjectLayout;
 import org.gradle.api.provider.Provider;
 import org.gradle.api.provider.ProviderFactory;
@@ -35,25 +39,46 @@ public abstract class GitVersionUtils {
     @Inject
     protected abstract ProjectLayout getProjectLayout();
 
-    public final Provider<Stream<String>> previousGitTags() {
-        return execute("git", "tag", "-l", "--merged", "HEAD^", "--sort=-committerdate")
-                .map(tagsResult -> {
-                    if (tagsResult.exitCode() != 0 || tagsResult.stdout().isEmpty()) {
-                        return Stream.empty();
-                    }
-
-                    return Arrays.stream(tagsResult.stdout().split("\n"))
-                            .filter(tag -> !isInitial000Tag(tag))
-                            .map(GitVersionUtils::stripVFromTag);
-                });
+    public final Stream<String> previousGitTags() {
+        return StreamSupport.stream(
+                        new PreviousGitTags(
+                                getProviderFactory(), getProjectLayout().getProjectDirectory()),
+                        false)
+                .filter(tag -> !isInitial000Tag(
+                        getProviderFactory(), getProjectLayout().getProjectDirectory(), tag))
+                .map(GitVersionUtils::stripVFromTag);
     }
 
-    private boolean isInitial000Tag(String tag) {
+    private static Optional<String> previousGitTagFromRef(
+            ProviderFactory providerFactory, Directory directory, String ref) {
+        String beforeLastRef = ref + "^";
+
+        GitResult beforeLastRefTypeResult = execute(providerFactory, directory, "git", "cat-file", "-t", beforeLastRef)
+                .get();
+
+        boolean thereIsNoCommitBeforeTheRef = !beforeLastRefTypeResult.stdout().equals("commit");
+        if (thereIsNoCommitBeforeTheRef) {
+            return Optional.empty();
+        }
+
+        GitResult describeResult = execute(
+                        providerFactory, directory, "git", "describe", "--tags", "--abbrev=0", beforeLastRef)
+                .get();
+
+        if (describeResult.stderr().contains("No tags can describe")
+                || describeResult.stderr().contains("No names found, cannot describe anything")) {
+            return Optional.empty();
+        }
+
+        return Optional.of(describeResult.stdoutOrThrowIfNonZero());
+    }
+
+    private static boolean isInitial000Tag(ProviderFactory providerFactory, Directory directory, String tag) {
         if (!tag.equals("0.0.0")) {
             return false;
         }
 
-        return execute("git", "rev-parse", "--verify", "--quiet", "0.0.0^")
+        return execute(providerFactory, directory, "git", "rev-parse", "--verify", "--quiet", "0.0.0^")
                         .get()
                         .exitCode()
                 != 0;
@@ -67,11 +92,12 @@ private static String stripVFromTag(String tag) {
         }
     }
 
-    private Provider<GitResult> execute(String... command) {
-        ExecOutput output = getProviderFactory().exec(execSpec -> {
+    private static Provider<GitResult> execute(
+            ProviderFactory providerFactory, Directory directory, String... command) {
+        ExecOutput output = providerFactory.exec(execSpec -> {
             execSpec.commandLine((Object[]) command);
             execSpec.setIgnoreExitValue(true);
-            execSpec.setWorkingDir(getProjectLayout().getProjectDirectory());
+            execSpec.setWorkingDir(directory);
         });
 
         Provider<String> stdout = output.getStandardOutput().getAsText();
@@ -96,10 +122,61 @@ interface GitResult {
 
         List<String> command();
 
+        default String stdoutOrThrowIfNonZero() {
+            if (exitCode() == 0) {
+                return stdout();
+            }
+
+            throw new RuntimeException("Failed running command:\n"
+                    + "\tCommand:" + command() + "\n"
+                    + "\tExit code: " + exitCode() + "\n"
+                    + "\tStdout:" + stdout() + "\n"
+                    + "\tStderr:" + stderr() + "\n");
+        }
+
         class Builder extends ImmutableGitResult.Builder {}
 
         static Builder builder() {
             return new Builder();
         }
     }
+
+    private static final class PreviousGitTags implements Spliterator<String> {
+        private final ProviderFactory providerFactory;
+        private final Directory directory;
+        private String lastSeenRef = "HEAD";
+
+        PreviousGitTags(ProviderFactory providerFactory, Directory directory) {
+            this.providerFactory = providerFactory;
+            this.directory = directory;
+        }
+
+        @Override
+        public boolean tryAdvance(Consumer<? super String> action) {
+            Optional<String> tag = previousGitTagFromRef(providerFactory, directory, lastSeenRef);
+
+            if (!tag.isPresent()) {
+                return false;
+            }
+
+            lastSeenRef = tag.get();
+            action.accept(lastSeenRef);
+            return true;
+        }
+
+        @Override
+        public Spliterator<String> trySplit() {
+            return null;
+        }
+
+        @Override
+        public long estimateSize() {
+            return Long.MAX_VALUE;
+        }
+
+        @Override
+        public int characteristics() {
+            return 0;
+        }
+    }
 }
diff --git a/src/main/java/org/revapi/gradle/RevapiExtension.java b/src/main/java/org/revapi/gradle/RevapiExtension.java
index 402eae56..bd7b5340 100644
--- a/src/main/java/org/revapi/gradle/RevapiExtension.java
+++ b/src/main/java/org/revapi/gradle/RevapiExtension.java
@@ -46,8 +46,8 @@ public RevapiExtension(Project project) {
         this.oldName.set(project.getProviders().provider(project::getName));
 
         this.oldVersions = project.getObjects().listProperty(String.class);
-        this.oldVersions.set(getGitVersionUtils().previousGitTags().map(versions -> versions.limit(3)
-                .collect(Collectors.toList())));
+        this.oldVersions.set(project.getProviders()
+                .provider(() -> getGitVersionUtils().previousGitTags().limit(3).collect(Collectors.toList())));
 
         this.oldGroupAndName = project.provider(() ->
                 GroupAndName.builder().group(oldGroup.get()).name(oldName.get()).build());
diff --git a/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy b/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
index f6161113..fe4ccdab 100644
--- a/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/GitVersionUtilsSpec.groovy
@@ -69,14 +69,10 @@ class GitVersionUtilsSpec extends AbstractProjectSpec {
         when:
         git.command 'git commit --allow-empty -m "First"'
         git.command 'git tag 1'
-        // need to sleep for at least a second so each commit is in its own timestamp to ensure sort order is correct
-        sleep(1001)
         git.command 'git commit --allow-empty -m "Second"'
         git.command 'git tag 2'
-        sleep(1001)
         git.command 'git commit --allow-empty -m "Third"'
         git.command 'git tag 3'
-        sleep(1001)
 
         git.command 'git commit --allow-empty -m "Fourth"'
         git.command 'git tag 4'
@@ -118,6 +114,6 @@ class GitVersionUtilsSpec extends AbstractProjectSpec {
 
     private List<String> previousGitTags() {
         GitVersionUtils utils = getProject().objects.newInstance(GitVersionUtils.class)
-        utils.previousGitTags().get().collect(Collectors.toList())
+        utils.previousGitTags().collect(Collectors.toList())
     }
 }

From f7d769541e16599a80e95cb0850b0b151f523820 Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Wed, 23 Jul 2025 16:26:06 +0100
Subject: [PATCH 07/12] more abstract

---
 .../revapi/gradle/RevapiAcceptBreakTask.java  | 49 ++++++++-----------
 .../gradle/RevapiVersionOverrideTask.java     | 24 +++------
 2 files changed, 28 insertions(+), 45 deletions(-)

diff --git a/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java b/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java
index 590df6e9..0d0b4b87 100644
--- a/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiAcceptBreakTask.java
@@ -34,52 +34,43 @@ public abstract class RevapiAcceptBreakTask extends DefaultTask {
     private static final String NEW_OPTION = "new";
     private static final String JUSTIFICATION_OPTION = "justification";
 
-    private final Property<String> code = getProject().getObjects().property(String.class);
-    private final Property<String> oldElement = getProject().getObjects().property(String.class);
-    private final Property<String> newElement = getProject().getObjects().property(String.class);
-    private final Property<Justification> justification =
-            getProject().getObjects().property(Justification.class);
-
     @Input
-    protected abstract Property<GroupNameVersion> getGroupNameVersion();
-
-    @Internal
-    protected abstract Property<ConfigManager> getConfigManager();
-
     @Option(option = CODE_OPTION, description = "Revapi change code")
-    public final void setCode(String codeString) {
-        this.code.set(codeString);
-    }
+    public abstract Property<String> getCode();
 
+    @Input
     @Option(option = OLD_OPTION, description = "Old API element")
-    public final void setOldElement(String oldElementString) {
-        this.oldElement.set(oldElementString);
-    }
+    public abstract Property<String> getOldElement();
 
+    @Input
     @Option(option = NEW_OPTION, description = "New API element")
-    public final void setNewElement(String newElementString) {
-        this.newElement.set(newElementString);
-    }
+    public abstract Property<String> getNewElement();
 
+    @Input
     @Option(option = JUSTIFICATION_OPTION, description = "Justification for why these breaks are ok")
-    public final void setJustification(String justificationString) {
-        this.justification.set(Justification.fromString(justificationString));
-    }
+    public abstract Property<String> getJustificationString();
+
+    @Input
+    protected abstract Property<GroupNameVersion> getGroupNameVersion();
+
+    @Internal
+    protected abstract Property<ConfigManager> getConfigManager();
 
     @TaskAction
     public final void addVersionOverride() {
-        ensurePresent(code, CODE_OPTION);
-        ensurePresent(justification, JUSTIFICATION_OPTION);
+        ensurePresent(getCode(), CODE_OPTION);
+        ensurePresent(getJustificationString(), JUSTIFICATION_OPTION);
 
         getConfigManager()
                 .get()
                 .modifyConfigFile(revapiConfig -> revapiConfig.addAcceptedBreaks(
                         getGroupNameVersion().get(),
                         Collections.singleton(AcceptedBreak.builder()
-                                .code(code.get())
-                                .oldElement(Optional.ofNullable(oldElement.getOrNull()))
-                                .newElement(Optional.ofNullable(newElement.getOrNull()))
-                                .justification(justification.get())
+                                .code(getCode().get())
+                                .oldElement(Optional.ofNullable(getOldElement().getOrNull()))
+                                .newElement(Optional.ofNullable(getNewElement().getOrNull()))
+                                .justification(Justification.fromString(
+                                        getJustificationString().get()))
                                 .build())));
     }
 
diff --git a/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java b/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java
index 468865d0..4be4bd61 100644
--- a/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiVersionOverrideTask.java
@@ -27,33 +27,25 @@
 public abstract class RevapiVersionOverrideTask extends DefaultTask {
     public static final String REPLACEMENT_VERSION_OPTION = "replacement-version";
 
-    private final Property<ConfigManager> configManager =
-            getProject().getObjects().property(ConfigManager.class);
-    private final Property<String> replacementVersion =
-            getProject().getObjects().property(String.class);
-
     @Input
     protected abstract Property<GroupNameVersion> getGroupNameVersion();
 
     @Internal
-    final Property<ConfigManager> getConfigManager() {
-        return configManager;
-    }
+    protected abstract Property<ConfigManager> getConfigManager();
 
+    @Input
     @Option(option = REPLACEMENT_VERSION_OPTION, description = "The version to use instead of the default oldVersion")
-    public final void setReplacementVersion(String replacementVersionValue) {
-        replacementVersion.set(replacementVersionValue);
-    }
+    public abstract Property<String> getReplacementVersion();
 
     @TaskAction
     public final void addVersionOverride() {
-        if (!replacementVersion.isPresent()) {
-            throw new RuntimeException("Please supply the --" + REPLACEMENT_VERSION_OPTION + " param this task");
+        if (!getReplacementVersion().isPresent()) {
+            throw new RuntimeException("Please supply the --" + REPLACEMENT_VERSION_OPTION + " param to this task");
         }
 
-        configManager
+        getConfigManager()
                 .get()
-                .modifyConfigFile(config ->
-                        config.addVersionOverride(getGroupNameVersion().get(), replacementVersion.get()));
+                .modifyConfigFile(config -> config.addVersionOverride(
+                        getGroupNameVersion().get(), getReplacementVersion().get()));
     }
 }

From c1d21e407d38559a62ddcd76465f19c0b7f17c77 Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Thu, 24 Jul 2025 09:31:26 +0100
Subject: [PATCH 08/12] change function name

---
 src/main/java/org/revapi/gradle/ConjureProjectFilters.java | 2 +-
 src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java     | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/java/org/revapi/gradle/ConjureProjectFilters.java b/src/main/java/org/revapi/gradle/ConjureProjectFilters.java
index ec7fa9f8..ce6ce938 100644
--- a/src/main/java/org/revapi/gradle/ConjureProjectFilters.java
+++ b/src/main/java/org/revapi/gradle/ConjureProjectFilters.java
@@ -28,7 +28,7 @@ final class ConjureProjectFilters {
 
     private ConjureProjectFilters() {}
 
-    public static RevapiConfig forProject(String projectName, boolean isConjure) {
+    public static RevapiConfig from(String projectName, boolean isConjure) {
         if (!isConjure) {
             return RevapiConfig.empty();
         }
diff --git a/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java b/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java
index 5ad28cfa..49d8ad31 100644
--- a/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java
+++ b/src/main/java/org/revapi/gradle/RevapiAnalyzeTask.java
@@ -94,7 +94,7 @@ protected final void runRevapi() throws Exception {
                                 "gradle-revapi-results.ftl",
                                 getAnalysisResultsFile().getAsFile().get()),
                 revapiIgnores(),
-                ConjureProjectFilters.forProject(
+                ConjureProjectFilters.from(
                         getProjectName().get(), getIsConjure().get()),
                 ImmutablesFilter.CONFIG);
 

From 6e40d31990c4163cb00cb4a63b7c4bb13ad18349 Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Thu, 24 Jul 2025 09:50:02 +0100
Subject: [PATCH 09/12] be more accurate

---
 src/test/groovy/org/revapi/gradle/RevapiSpec.groovy | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index cbafc24e..491d9675 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -345,7 +345,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         then:
         def buildResult = runTasksSuccessfully('revapiAcceptBreak', '--justification', 'foo', '--code', 'bar',
                 '--old', 'old', '--new', 'new')
-        assert buildResult.task(':revapiAcceptBreak').outcome in [TaskOutcome.SUCCESS, TaskOutcome.UP_TO_DATE]
+        assert buildResult.task(':revapiAcceptBreak').outcome == TaskOutcome.SUCCESS
     }
 
     private File setupUnpublishedLibrary() {

From 2964e7895a5b759755eb1061fd32899598478fa0 Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Thu, 24 Jul 2025 10:11:56 +0100
Subject: [PATCH 10/12] keep with failure

---
 .../org/revapi/gradle/RevapiSpec.groovy       | 19 ++++++++++++++-----
 1 file changed, 14 insertions(+), 5 deletions(-)

diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index 491d9675..210e8c14 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -311,7 +311,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         git.command 'git commit -am new-work'
 
         then:
-        def buildResult = runTasksAndFail('revapi')
+        def buildResult = runTasksWithFailure('revapi')
         assert buildResult.output.contains('willBeRemoved')
     }
 
@@ -977,7 +977,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         writeOutImmutablesClass { it.newText }
 
         then:
-        def buildResult = runTasksAndFail('revapi')
+        def buildResult = runTasksWithFailure('revapi')
         println buildResult.output
 
         def errorMessage = buildResult.output
@@ -1092,7 +1092,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         runTasksSuccessfully('compileConjure')
 
         then:
-        runTasksAndFail(':api-jersey:revapi')
+        runTasksWithFailure(':api-jersey:revapi')
         def jerseyJunit = new File(projectDir, 'api-jersey/build/junit-reports/revapi/revapi-api-jersey.xml').text
 
         assert jerseyJunit.contains('java.class.removed-interface services.RenamedService')
@@ -1103,7 +1103,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         assert !jerseyJunit.contains('services.TestService::renamedToSomethingElse()')
         assert !jerseyJunit.contains('java.annotation.attributeValueChanged')
 
-        runTasksAndFail(':api-retrofit:revapi')
+        runTasksWithFailure(':api-retrofit:revapi')
         def retrofitJunit = new File(projectDir, 'api-retrofit/build/junit-reports/revapi/revapi-api-retrofit.xml').text
 
         assert retrofitJunit.contains('java.class.removed-interface services.RenamedServiceRetrofit')
@@ -1190,7 +1190,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
     }
 
     private String runRevapiExpectingFailure() {
-        BuildResult buildResult = runTasksAndFail("revapi")
+        BuildResult buildResult = runTasksWithFailure("revapi")
         println buildResult.output
         return buildResult.output
     }
@@ -1209,4 +1209,13 @@ class RevapiSpec extends ConfigurationCacheSpec {
         }
         result
     }
+
+    private BuildResult runTasksWithFailure(String... tasks) {
+        BuildResult result = runTasksAndFailWithConfigurationCache(tasks)
+        if (result.task(':' + tasks[0])?.outcome == TaskOutcome.SUCCESS) {
+            println result.output
+            throw new RuntimeException("Task succeded: ${result.output}")
+        }
+        result
+    }
 }

From 3188085578c79843b7a7ceedebbb69c2b9e9c20b Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Thu, 24 Jul 2025 10:23:02 +0100
Subject: [PATCH 11/12] final touches

---
 src/test/groovy/org/revapi/gradle/RevapiSpec.groovy | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
index 210e8c14..ca77808b 100644
--- a/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
+++ b/src/test/groovy/org/revapi/gradle/RevapiSpec.groovy
@@ -507,7 +507,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         '''.stripIndent()
 
         when:
-        println runTasksSuccessfully("publish").output
+        println runTasksWithConfigurationCache("publish").output
 
         writeToFile two, 'src/main/java/foo/Foo.java', originalJavaFile.text
         originalJavaFile.delete()
@@ -566,7 +566,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         '''.stripIndent()
 
         and:
-        println runTasksSuccessfully("publish").output
+        println runTasksWithConfigurationCache("publish").output
 
         javaFileInDependentProject.text = javaFileInDependentProject.text.replace('}', 'void foo();\n}')
 
@@ -609,7 +609,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
         '''
 
         and:
-        println runTasksSuccessfully("publish").output
+        println runTasksWithConfigurationCache("publish").output
 
         then:
         println runTasksSuccessfully("revapi").output
@@ -666,7 +666,7 @@ class RevapiSpec extends ConfigurationCacheSpec {
             class Foo {}
         '''.stripIndent()
 
-        println runTasksSuccessfully("publish").output
+        println runTasksWithConfigurationCache("publish").output
 
         then:
         println runTasksSuccessfully("revapi").output

From 126924f4c5f8a50b14a2e04357009e51e95ab51b Mon Sep 17 00:00:00 2001
From: Finlay Williams <finlayw@palantir.com>
Date: Mon, 28 Jul 2025 09:17:03 +0100
Subject: [PATCH 12/12] use released version of plugin testing

---
 build.gradle  |  4 +---
 versions.lock | 12 ++++++------
 2 files changed, 7 insertions(+), 9 deletions(-)

diff --git a/build.gradle b/build.gradle
index 8b85d22c..af4e124d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -4,14 +4,13 @@ buildscript {
     repositories {
         mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } }
         gradlePluginPortal() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } }
-        mavenLocal()
     }
 
     dependencies {
         classpath 'com.palantir.jakartapackagealignment:jakarta-package-alignment:0.6.0'
         classpath 'com.palantir.gradle.externalpublish:gradle-external-publish-plugin:1.11.0'
         classpath 'com.palantir.javaformat:gradle-palantir-java-format:2.47.0'
-        classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:999'
+        classpath 'com.palantir.gradle.plugintesting:gradle-plugin-testing:0.10.0'
     }
 }
 
@@ -25,7 +24,6 @@ plugins {
 
 repositories {
     mavenCentral() { metadataSources { mavenPom(); ignoreGradleMetadataRedirection() } }
-    mavenLocal()
 }
 
 group 'org.revapi'
diff --git a/versions.lock b/versions.lock
index 0936ba60..3e515828 100644
--- a/versions.lock
+++ b/versions.lock
@@ -25,21 +25,21 @@ org.yaml:snakeyaml:1.33 (1 constraints: 6f17f827)
 [Test dependencies]
 cglib:cglib-nodep:3.2.2 (1 constraints: 490ded24)
 com.netflix.nebula:nebula-test:10.6.2 (2 constraints: d61de72d)
-com.palantir.gradle.plugintesting:configuration-cache-spec:999 (1 constraints: c104512c)
-com.palantir.gradle.plugintesting:plugin-testing-core:999 (1 constraints: c104512c)
+com.palantir.gradle.plugintesting:configuration-cache-spec:0.10.0 (1 constraints: 3305233b)
+com.palantir.gradle.plugintesting:plugin-testing-core:0.10.0 (1 constraints: 3305233b)
 junit:junit:4.13.2 (2 constraints: ca1e3ab9)
 net.bytebuddy:byte-buddy:1.14.16 (2 constraints: f8167267)
 net.bytebuddy:byte-buddy-agent:1.14.15 (1 constraints: 760bb5e9)
 org.apiguardian:apiguardian-api:1.1.2 (7 constraints: cc761de9)
 org.assertj:assertj-core:3.26.0 (1 constraints: 3d054b3b)
-org.codehaus.groovy:groovy:3.0.21 (3 constraints: 103436dc)
+org.codehaus.groovy:groovy:3.0.25 (3 constraints: 14347add)
 org.hamcrest:hamcrest:2.2 (1 constraints: d20cdc04)
 org.hamcrest:hamcrest-core:1.3 (1 constraints: cc05fe3f)
 org.junit:junit-bom:5.13.4 (1 constraints: 3f054e3b)
-org.junit.jupiter:junit-jupiter:5.13.4 (1 constraints: 4409c29e)
-org.junit.jupiter:junit-jupiter-api:5.13.4 (5 constraints: fe4f85b3)
+org.junit.jupiter:junit-jupiter:5.13.4 (2 constraints: 540ebf67)
+org.junit.jupiter:junit-jupiter-api:5.13.4 (5 constraints: 01507ab5)
 org.junit.jupiter:junit-jupiter-engine:5.13.4 (2 constraints: 7d17a564)
-org.junit.jupiter:junit-jupiter-params:5.13.4 (3 constraints: 382e9ca9)
+org.junit.jupiter:junit-jupiter-params:5.13.4 (3 constraints: 3b2e71aa)
 org.junit.platform:junit-platform-commons:1.13.4 (3 constraints: 782ad891)
 org.junit.platform:junit-platform-engine:1.13.4 (5 constraints: 734a41d4)
 org.junit.platform:junit-platform-launcher:1.13.4 (1 constraints: 4009aa9e)
