ben-manes/gradle-versions-plugin

Support for ignoring deliberately lagging dependencies

davidburstrom opened this issue · 5 comments

Using the version-compatibility-gradle-plugin it is possible to set up adapter layers that bridge API incompatibilities between different versions of a provided external dependency. See the following example.

When running the dependencyUpdates task, it will indicate that the adapter layers include dependencies with versions that are lagging behind the latest milestones. The problem is that they must lag behind by design.

What would be an idiomatic way to ensure that these dependencies aren't reported? I've had a look at excluding given configurations using filterConfigurations but that would ignore any other dependencies that actually should be up to date.

Please feel free to fork the example repo and play around with it!

Have you considered using a resolutionStrategy to force the version and reject later versions? This plugin honors your existing resolution rules or you can apply additional for its task. Then when it performs a dynamic version query (+), Gradle will force the resolution to the latest version matching your restrictions. If you want to pin it then that will be viewed as up-to-date.

I believe dependency constraints can be used similarly, but are usually better at enforcing minimum a version to avoid known CVEs so might not be appropriate here. I don't think we do a good job in the pinning approach and there may be a few tickets advocating for changes (but it's unclear as contradictory usages).

You can also modify the results prior to displaying them by using a custom output formatter. In this example I had a dependency whose maven metadata was incorrect due to a bug on Central, so I wanted to ignore it being flagged as the build exceeding known version. That was later fixed and removed, and in the meantime I rewrote the results being calling the plugin's reporter.

wdyt?

Thanks for the suggestion on resolutionStrategy. I've experimented with it, and the main drawback, especially for the version compatibility plugin, is that it becomes a bit clunky to set up the configurations properly. After the dependency has been added to the compat*CompileAndTestOnly configuration, there needs to be a resolutionStrategy block executed for every configuration that extends from it, e.g.

configurations.matching { it.name.contains("Lang3Dot0") }.configureEach {
    resolutionStrategy {
        force("org.apache.commons:commons-lang3:3.0")
    }
}

To make it less clunky, it could be possible to have a utility API in the version-compatibility-plugin to add a dependency with a corresponding resolution strategy, but I'm not too fond of that idea as it's less Gradle idiomatic.

Dependency constraints could also be an option, though it would lead to the same duplication as the resolutionStrategy solution. As a side note, in the linked example, the test runtime examples are set up with constraints, which seem to be ignored by the gradle-versions-plugin.

I think I would prefer strict dependencies. If I modify the example I linked to, it would read quite nicely if the dependency was specified as "compatLang3Dot0CompileAndTestOnly"("org.apache.commons:commons-lang3:3.0!!"), i.e. adding the !! for strict version. Is that information lost by the time the gradle-versions-plugin is running?

As another side note, could it be made more obvious in the report (XML, JSON, etc) which Gradle configuration(s) is lagging behind? This would be beneficial when more complex dependency setups are used, such as ones introduced by the version compatibility plugin.

the main drawback, especially for the version compatibility plugin, is that it becomes a bit clunky to set up the configurations properly.

I suspect that you could add the resolution strategy automatically. For example, you could add a resolution strategy for each matching configuration namespace and have a resolution rule that queries if the candidate is a direct dependency and rejects if not the current version. This way you don't need to inspect during the build's configuration phase where you might have to rely on afterEvaluate to ensure all of the state is ready.

Is that information lost by the time the gradle-versions-plugin is running?

It shouldn't be. We use Configuration.copyRecursive(), rewrite to dynamic dependency versions, add any of the plugin's additional configuration settings, and resolve. The intent is to try and honor your build configuration.

The rich version syntax is a little tricky because users have contradictory usages. Typically it is for transitive dependencies to pin or force a minimum version to avoid CVEs, so we have an option to query for new versions of dependency constraints (as the dependency is not explicitly declared). Others use it instead of a resolution strategy's forced dependencies, which is the older and (I think Gradle's preferred) approach. How we should report using constraints is a little unclear at the moment.

could it be made more obvious in the report (XML, JSON, etc) which Gradle configuration(s) is lagging behind?

I've long wanted this and and was surprised it wasn't ever asked for before. I didn't have a use-case to add it, but it seemed like a nice quality-of-life improvement that could be useful. PRs are welcome if you're so inclined.

Caveat lector: These two responses are independent and are attempts at addressing the problem from two different viewpoints.

How we should report using constraints is a little unclear at the moment.

How about a separate report category for constrained/pinned dependencies, where one would be informed that newer versions exist? E.g. "The following dependencies are pinned but have later milestone versions:"?
That category could even be filtered out completely in projects where pinning is deliberate to hold back versions.

Typically it is for transitive dependencies to pin or force a minimum version to avoid CVEs

Do users usually use strictly with fixed versions to avoid CVEs? If lib:2.0 is transitive and have a CVE (fixed in 2.1 and later), do users use lib:2.1 (implicit require), or lib:2.1!! (explicit strictly)? Because if the direct dependency ever starts using lib:2.2, the former would be overridden, whereas the latter would likely fail in resolve/compile/runtime. What I'm getting at is that I'd be perfectly happy if single version (x.y.z instead of a range [x.y,)) strictly pinning is ignored by the plugin. It could also be configurable.

How about a separate report category for constrained/pinned dependencies?

That would probably be better, so it would assume that constraints restrict the reported upgrades and then shows them in a separate category for those who want to upgrade as well. The resolver currently ignores constraints for direct dependencies under the assumption they were only for transitive restrictions, which is what I think users should do but not all will.

Do users usually use strictly with fixed versions to avoid CVEs?

I believe the claim is that newer versions might have CVEs and not be vetted by their security audits, so they want repeatable builds using the same versions. In those cases they likely would use dependency locking, but that only pins the current resolved versions so regenerating that noisy lock file could bring in updates. In those cases they might still use strictly to ensure consistency. In my projects I follow your practices, but I suppose those dealing with medical or other sensitive scenarios have different tradeoffs to consider.