Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e07747a
Add approved file inventory for orphan detection
mkutz Feb 19, 2026
302e25b
Add .approvej to .gitignore
mkutz Feb 19, 2026
c2931e3
Add Gradle plugin
mkutz Feb 19, 2026
75de625
Add Gradle Plugin Portal publishing support
mkutz Feb 19, 2026
921ea08
Publish Gradle plugin to Plugin Portal in CI
mkutz Feb 19, 2026
04817a3
Add Maven plugin module
mkutz Feb 19, 2026
cd8f7a5
Fix version catalog ordering and CI test failure
mkutz Feb 19, 2026
3e22da2
Add ProjectBuilder unit tests for Gradle plugin
mkutz Feb 19, 2026
e5a1569
Add JaCoCo to Gradle and Maven plugin modules
mkutz Feb 19, 2026
7cb22d0
Improve test coverage for inventory and MojoHelper
mkutz Feb 19, 2026
d2104e1
Fix SonarCloud issues in inventory code
mkutz Feb 19, 2026
edb9c83
Move plugin modules to plugins/ directory
mkutz Feb 19, 2026
ad3835c
Use %n instead of \n
mkutz Feb 19, 2026
371dca7
Move test reference parsing into InventoryEntry
mkutz Feb 19, 2026
5ec74ed
Add documentation for inventory and cleanup plugins
mkutz Feb 19, 2026
8cc5f53
Fix plugin publishing configuration
mkutz Feb 19, 2026
932abda
Address review feedback
mkutz Feb 20, 2026
911f45e
Rename plugins, use leftover terminology, add clickable URIs
mkutz Feb 20, 2026
812ad57
Fix test method naming and ordering conventions
mkutz Feb 20, 2026
149befc
Fix outdated paths and project name in CONTRIBUTING.md
mkutz Feb 20, 2026
0a00566
Remove duplicate tests and parameterize buildCommand
mkutz Feb 20, 2026
9be849a
Harden ApprovedFileInventory
mkutz Feb 20, 2026
d10e2bf
Remove toAbsolutePath from path resolution
mkutz Feb 20, 2026
c2d58bf
Use formatted strings and List.of for clarity
mkutz Feb 20, 2026
5a3ba5e
Replace volatile with AtomicReference for inventoryFile
mkutz Feb 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/build-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,6 @@ jobs:
if: failure()
with:
name: modules-test-reports
path: modules/**/build/reports/tests/test/
path: |
modules/**/build/reports/tests/test/
plugins/**/build/reports/tests/test/
4 changes: 3 additions & 1 deletion .github/workflows/build-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ jobs:
if: failure()
with:
name: modules-test-reports
path: modules/**/build/reports/tests/test/
path: |
modules/**/build/reports/tests/test/
plugins/**/build/reports/tests/test/

- name: Enable auto merge for Dependabot PRs
run: |
Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ jobs:

- run: ./gradlew jreleaserConfig -Pversion=${{ github.event.inputs.version }} --git-root-search
- run: ./gradlew publish -Pversion=${{ github.event.inputs.version }}
- run: ./gradlew :plugins:approvej-gradle-plugin:publishPlugins -Pversion=${{ github.event.inputs.version }}
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
- run: ./gradlew jreleaserFullRelease -Pversion=${{ github.event.inputs.version }} --git-root-search --stacktrace
- uses: actions/upload-artifact@v6
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ docs/**
*.key
pages
banner-*.*
.approvej
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ It provides a fluent API to compare actual values against previously approved "g
- **modules/yaml-jackson3** - YAML support using Jackson 3.x
- **modules/http** - HTTP stub server for approving HTTP requests
- **modules/http-wiremock** - WireMock adapter for HTTP testing
- **plugins/approvej-gradle-plugin** - Gradle plugin for managing approved files
- **plugins/approvej-maven-plugin** - Maven plugin for managing approved files
- **bom** - Maven Bill of Materials
- **manual** - AsciiDoc documentation (code samples included from tests in `manual/src/test`)

Expand Down
18 changes: 12 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,20 +103,26 @@ All pipelines can be found in [.github/workflows](.github/workflows).

### Versioning

Shakespeare is using [SemVer 2.0](https://semver.org/spec/v2.0.0.html) but omits the patch digit in case it is `0`.
ApproveJ is using [SemVer 2.0](https://semver.org/spec/v2.0.0.html) but omits the patch digit in case it is `0`.

E.g. `1.0.0` is written as `1.0`, `1.0.1` is written as `1.0.1`, and `1.1.0` is written as `1.1`.


### Project Structure

It is structured in four main modules:
It is structured in four main directories:

- The [modules](modules) directory contains all the published code modules:
- The [modules](modules) directory contains all the published library modules:
- [core](modules/core) contains the code for the core framework and should not have any dependencies to other modules and only very few (if any) to external libraries,
- [json-jackson](modules/json-jackson) contains JSON-related code using Jackson,
- [yaml-jackson](modules/yaml-jackson) contains YAML-related code using Jackson
- [http](modules/http) contains code to create an HTTP server for approving requests
- [json-jackson](modules/json-jackson) contains JSON-related code using Jackson 2.x,
- [json-jackson3](modules/json-jackson3) contains JSON-related code using Jackson 3.x,
- [yaml-jackson](modules/yaml-jackson) contains YAML-related code using Jackson 2.x,
- [yaml-jackson3](modules/yaml-jackson3) contains YAML-related code using Jackson 3.x,
- [http](modules/http) contains code to create an HTTP server for approving requests,
- [http-wiremock](modules/http-wiremock) contains the WireMock adapter for HTTP testing
- The [plugins](plugins) directory contains build tool plugins:
- [approvej-gradle-plugin](plugins/approvej-gradle-plugin) contains the Gradle plugin for managing approved files
- [approvej-maven-plugin](plugins/approvej-maven-plugin) contains the Maven plugin for managing approved files
- the [bom](bom) directory contains the build file to generate a [Maven Bill of Material (BOM)](https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#bill-of-materials-bom-poms) for all the ApproveJ modules, and
- the [manual](manual) directory contains the projects documentation written in [AsciiDoc](https://docs.asciidoctor.org/asciidoc/latest/).

Expand Down
6 changes: 5 additions & 1 deletion bom/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ repositories { mavenCentral() }
dependencies {
constraints {
rootProject.subprojects
.filter { it != project && it.name != "manual" && it.subprojects.isEmpty() }
.filter {
it != project &&
it.name !in listOf("approvej-gradle-plugin", "approvej-maven-plugin", "manual") &&
it.subprojects.isEmpty()
}
.sortedBy { it.name }
.forEach { api(it) }

Expand Down
7 changes: 5 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ repositories { mavenCentral() }

subprojects {
afterEvaluate {
if (plugins.hasPlugin("maven-publish")) {
if (plugins.hasPlugin("maven-publish") && !plugins.hasPlugin("com.gradle.plugin-publish")) {
publishing {
publications {
create<MavenPublication>(name) {
Expand Down Expand Up @@ -78,7 +78,10 @@ gradle.projectsEvaluated {
mavenCentral {
named("sonatype") {
subprojects
.filter { it.plugins.hasPlugin("maven-publish") }
.filter {
it.plugins.hasPlugin("maven-publish") &&
!it.plugins.hasPlugin("com.gradle.plugin-publish")
}
.sortedBy { it.name }
.forEach {
stagingRepository("${it.projectDir.relativeTo(rootDir)}/build/staging-deploy")
Expand Down
6 changes: 6 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ junit = "6.0.3"
assertj = "3.27.7"
jackson2 = "2.21.0"
jackson3 = "3.0.4"
maven = "3.9.12"
wiremock = "3.13.2"

[libraries]
Expand All @@ -21,6 +22,9 @@ junit-jupiter-api = { module = "org.junit.jupiter:junit-jupiter-api", version.re
junit-jupiter-engine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-jupiter-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version = "6.0.3" }
maven-core = { module = "org.apache.maven:maven-core", version.ref = "maven" }
maven-plugin-annotations = { module = "org.apache.maven.plugin-tools:maven-plugin-annotations", version = "3.15.2" }
maven-plugin-api = { module = "org.apache.maven:maven-plugin-api", version.ref = "maven" }
spock = { module = "org.spockframework:spock-core", version = "2.4-M7-groovy-5.0" }
testng = { module = "org.testng:testng", version = "7.12.0" }
wiremock = { module = "org.wiremock:wiremock", version.ref = "wiremock" }
Expand All @@ -30,5 +34,7 @@ asciidoctor = { id = "org.asciidoctor.jvm.convert", version = "4.0.5" }
asciidoctor-pdf = { id = "org.asciidoctor.jvm.pdf", version = "4.0.5" }
jreleaser = { id = "org.jreleaser", version = "1.20.0" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version = "2.3.10" }
maven-plugin-development = { id = "org.gradlex.maven-plugin-development", version = "1.0.3" }
plugin-publish = { id = "com.gradle.plugin-publish", version = "2.0.0" }
sonar = { id = "org.sonarqube", version = "7.2.2.6593" }
spotless = { id = "com.diffplug.spotless", version = "7.2.1" }
151 changes: 151 additions & 0 deletions manual/src/docs/asciidoc/chapters/10-cleanup.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
[id=cleanup]
= Cleaning Up Leftover Approved Files


[id="cleanup_problem"]
== The Problem

When you rename or delete a test method, its approved file stays behind on disk.
Over time these leftover files accumulate and clutter the repository.

ApproveJ provides an inventory-based mechanism to detect and remove leftover approved files automatically.


[id="cleanup_inventory"]
== The Approved File Inventory

Every time a test calls `byFile()`, ApproveJ records the approved file path and the originating test method.
At the end of the test run, all entries are merged into a project-level inventory file:

----
.approvej/inventory.properties
----

The file uses Java Properties format.
Each entry maps a relative file path to the test reference that created it:

[source,properties]
----
src/test/resources/MyTest-myTest-approved.txt = com.example.MyTest#myTest
----

TIP: Commit `.approvej/inventory.properties` to version control so that the inventory is shared across the team.

The inventory is updated incrementally.
Only entries for test methods that ran in the current execution are refreshed.
Entries for tests that did not run are preserved.


[id="cleanup_configuration"]
== Enabling the Inventory

The inventory is controlled by the `inventoryEnabled` property.

|===
|Environment |Default

|Local development (no `CI` environment variable)
|`true`

|CI (the `CI` environment variable is set)
|`false`
|===

You can override this default in any <<configuration_sources,configuration source>>:

.`src/test/resources/approvej.properties`
[source,properties]
----
inventoryEnabled = true
----

Or via environment variable:

[source,bash]
----
export APPROVEJ_INVENTORY_ENABLED=false
----


[id="cleanup_finding_leftovers"]
== Finding and Removing Leftovers

After running your tests with the inventory enabled, use the build plugin to detect leftovers.
An approved file is considered a leftover when its originating test method no longer exists in the compiled test classes.


[id="cleanup_gradle"]
=== Gradle Plugin

Apply the plugin in your build file:

.Gradle
[source,groovy,subs=attributes+,role="primary"]
----
plugins {
id 'org.approvej' version '{revnumber}'
}
----
.Gradle.kts
[source,kotlin,subs=attributes+,role="secondary"]
----
plugins {
id("org.approvej") version "{revnumber}"
}
----

The plugin registers two tasks in the `verification` group:

[cols="1,2"]
|===
|Task |Description

|`./gradlew approvejFindLeftovers`
|Lists leftover approved files without deleting them

|`./gradlew approvejCleanup`
|Detects and removes leftover approved files
|===

NOTE: Both tasks require the Java plugin to be applied in the same project.
They use the test runtime classpath to resolve test classes.


[id="cleanup_maven"]
=== Maven Plugin

Add the plugin to your `pom.xml`:

[source,xml,subs=attributes+]
----
<plugin>
<groupId>org.approvej</groupId>
<artifactId>approvej-maven-plugin</artifactId>
<version>{revnumber}</version>
</plugin>
----

The plugin provides two goals:

[cols="1,2"]
|===
|Goal |Description

|`mvn approvej:find-leftovers`
|Lists leftover approved files without deleting them

|`mvn approvej:cleanup`
|Detects and removes leftover approved files
|===


[id="cleanup_workflow"]
== Typical Workflow

1. Run your tests locally so the inventory is populated.
2. Run the find-leftovers task/goal to see which approved files are leftovers.
3. Run the cleanup task/goal to delete the leftover files.
4. Commit the updated inventory and the removal of leftover files.

WARNING: Only approved files that have been recorded in the inventory can be detected as leftovers.
Make sure you have run all relevant tests with the inventory enabled at least once before cleaning up.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ Can be an alias (e.g., `none`, `automatic`), a fully-qualified class name, or a
|The script to execute for reviewing differences.
This implicitly sets the `defaultFileReviewer` to script
|_(none)_

|`inventoryEnabled`
|Whether the approved file inventory is enabled.
When enabled, ApproveJ records approved file paths in `.approvej/inventory.properties` during test runs.
|`true` locally, `false` in CI
|===


Expand Down Expand Up @@ -121,6 +126,9 @@ Environment variables use the `APPROVEJ_` prefix and convert camelCase property

|`defaultFileReviewer`
|`APPROVEJ_DEFAULT_FILE_REVIEWER`

|`inventoryEnabled`
|`APPROVEJ_INVENTORY_ENABLED`
|===

.Example: Set default print format via environment variable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,26 @@ endif::[]
|===


== Cleanup Tasks

[cols="2,3"]
|===
|Command |Description

|`./gradlew approvejFindLeftovers`
|List leftover approved files (Gradle)

|`./gradlew approvejCleanup`
|Remove leftover approved files (Gradle)

|`mvn approvej:find-leftovers`
|List leftover approved files (Maven)

|`mvn approvej:cleanup`
|Remove leftover approved files (Maven)
|===


== Configuration Properties

[cols="2,2,1"]
Expand All @@ -294,6 +314,10 @@ endif::[]
|`defaultFileReviewerScript`
|`APPROVEJ_DEFAULT_FILE_REVIEWER_SCRIPT`
|_(none)_

|`inventoryEnabled`
|`APPROVEJ_INVENTORY_ENABLED`
|`true` locally, `false` in CI
|===

Configuration is resolved in priority order: environment variables > project properties (`src/test/resources/approvej.properties`) > user home properties (`~/.config/approvej/approvej.properties`) > defaults.
5 changes: 3 additions & 2 deletions manual/src/docs/asciidoc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ include::chapters/06-reviewing.adoc[leveloffset=1]
include::chapters/07-json-jackson.adoc[leveloffset=1]
include::chapters/08-yaml-jackson.adoc[leveloffset=1]
include::chapters/09-http.adoc[leveloffset=1]
include::chapters/10-configuration.adoc[leveloffset=1]
include::chapters/11-cheat-sheet.adoc[leveloffset=1]
include::chapters/10-cleanup.adoc[leveloffset=1]
include::chapters/11-configuration.adoc[leveloffset=1]
include::chapters/12-cheat-sheet.adoc[leveloffset=1]
4 changes: 4 additions & 0 deletions modules/core/src/main/java/org/approvej/ApprovalBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import java.nio.file.Path;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import org.approvej.approve.ApprovedFileInventory;
import org.approvej.approve.Approver;
import org.approvej.approve.PathProvider;
import org.approvej.approve.PathProviders;
Expand Down Expand Up @@ -201,6 +202,9 @@ public void byValue(final String previouslyApproved) {
public void byFile(PathProvider pathProvider) {
PathProvider updatedPathProvider =
pathProvider.filenameAffix(name).filenameExtension(filenameExtension);
if (configuration.inventoryEnabled()) {
ApprovedFileInventory.registerApprovedFile(updatedPathProvider);
}
if (!(value instanceof String)) {
printed().byFile(updatedPathProvider);
return;
Expand Down
Loading