A Gradle plugin that guards against unintentional dependency changes.

Last update: May 19, 2022

🛡️ Dependency Guard

A Gradle plugin that helps you guard against unintentional dependency changes.

Surface Transitive Dependency Changes

Comparison to a baseline occurs whenever you run the dependencyGuard Gradle task.

A small single version bump of androidx.activity from 1.3.1 -> 1.4.0 causes many libraries to transitively update.

Provide Custom Rules for Allowed Dependencies

For a given configuration, you may never want junit to be shipped. You can prevent this by modifying the allowRule rule to return !it.contains("junit") for your situation.

Why Was Dependency Guard Built?

As platform engineers, we do a lot of library upgrades, and needed insight into how dependencies were changing over time. Any small change can have a large impact. On large teams, it's not possible to track all dependency changes in a practical way, so we needed tooling to help. This is a tool that surfaces these changes to any engineer making dependency changes, and allows them to re-baseline if it's intentional. This provides us with historical reference on when dependencies (including transitive) are changed for a given configuration.

Goals:

  • Surface transitive and non-transitive dependencies changes for our production build configurations. This helps during version bumps and when new libraries are added.
  • Provide the developer with the ability to easily accept changes as needed.
  • Add deny-listing for production build configurations to explicitly block some dependencies from production.
  • Include a record of the change in Git when their code is merged to easily diagnose/debug crashes in the future.
  • Be deterministic. De-duplicate entries, and order alphabetically.

Real World Issues Which Dependency Guard Addresses

  • Accidentally shipping a testing dependency to production because implementation was used instead of testImplementation - @handstandsam
    • Dependency Guard has List and Tree baseline formats that can compared against to identify changes. If changes occur, and they are expected, you can re-baseline.
  • Dependency versions were transitively upgraded which worked fine typically, but caused a runtime crash later that was hard to figure out why it happened.
    • You would see the diff off dependencies in Git since the last working version and be able to have quickly track down the issue.
  • After adding Ktor 2.0.0, it transitively upgraded to Kotlin 1.6.20 which caused incompatibilities with Jetpack Compose 1.1.1 which requires Kotlin 1.6.10 - @joreilly
    • During large upgrades it is important to see how a single version bump will impact the rest of your application.
  • Upgrading to the Android Gradle Plugin transitively downgraded protobuf plugin which caused issues - @AutonomousApps

Setup and Configuration

Step 1: Adding The Dependency Guard Gradle Plugin and Baselining

// sample/app/build.gradle.kts
plugins {
  id("com.dropbox.dependency-guard")
}

Step 2: Run ./gradle dependencyGuard and Configure

This will show a list of available configuration(s) for this Gradle module.

You can choose the configurations you want to monitor.

We suggest monitoring your release configuration to know what is included in production builds. You can choose to monitor any classpath configuration with Dependency Guard.

// sample/app/build.gradle.kts
dependencyGuard {
    // All dependencies included in Production Release APK
    configuration("releaseRuntimeClasspath") 
}

NOTE: Checkout the "Adding to the Buildscript Classpath" section below if the plugin can't be resolved.

Step 3: Run ./gradlew dependencyGuard to Detect Changes

It's suggested to run in your CI or pre-commit hooks to detect these changes. If this task is not run, then you aren't getting the full benefit of this plugin.

Bump Version and Detect Change

Step 4: Rebaselining to Accept Changes

If any dependencies have changed, you will be provided with the ./gradlew dependencyGuardBaseline task to update the baseline and intentionally accept these new changes.

Rebaseline to Accept Change

Additional Configuration Options

Allow Rules for Dependencies (Optional)

If you have explicit test or debugging dependencies you never want to ship, you can create rules for them here.

dependencyGuard {
    configuration("releaseRuntimeClasspath") {
        allowRule = {
            // Disallow dependencies with a name containing "junit"
            !it.contains("junit")
        }
    }
}

Configuring Your Dependency Baseline (Optional)

By default, Dependency Guard tracks modules and artifacts in a list format is generated at dependencies/${configurationName}.txt.

:sample:module1
:sample:module2
org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10
org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10
org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10
org.jetbrains.kotlin:kotlin-stdlib:1.6.10
org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
org.jetbrains:annotations:13.0

You can customize choose to not track modules or artifacts:

dependencyGuard {
  configuration("releaseRuntimeClasspath") {
    // What is included in the list report
    modules = true // Defaults to true
    artifacts = true // Defaults to true
  }
}

Tree Format (Optional)

The builtin Dependencies task from Gradle is :dependencies. The tree format support for Dependency Guard leverages this under the hood and targets it for a specific configuration.

The dependency-tree-diff library in extremely helpful tool, but is impractical for many projects in Continuous Integration since it has a lot of custom setup to enable it. (related discussion). dependency-tree-diff's diffing logic is actually used by Dependency Guard to highlight changes in trees.

The tree format proved to be very helpful, but as we looked at it from a daily usage standpoint, we found that the tree format created more noise than signal. It's super helpful to use for development, but we wouldn't recommend storing the tree format in Git, especially in large projects as it gets noisy, and it becomes ignored. In brainstorming with @joshfein, it seemed like a simple orded, de-duplicated list of dependencies were better for storing in CI.

dependencies/releaseRuntimeClasspath.tree.txt


------------------------------------------------------------
Project ':sample:app'
------------------------------------------------------------

releaseRuntimeClasspath - Resolved configuration for runtime for variant: release
\--- project :sample:module1
     +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10
     |    +--- org.jetbrains.kotlin:kotlin-stdlib:1.6.10
     |    |    +--- org.jetbrains:annotations:13.0
     |    |    \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.6.10
     |    \--- org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.6.10
     |         \--- org.jetbrains.kotlin:kotlin-stdlib:1.6.10 (*)
     \--- project :sample:module2
          +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.6.10 (*)
          \--- org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.2
               \--- org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.5.2
                    +--- org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.30 -> 1.6.10 (*)
                    \--- org.jetbrains.kotlin:kotlin-stdlib-common:1.5.30 -> 1.6.10

(*) - dependencies omitted (listed previously)

A web-based, searchable dependency report is available by adding the --scan option.

Enabling Tree Format

Enable the tree format for a configuration with the following option:

dependencyGuard {
  configuration("releaseRuntimeClasspath") {
    tree = true // Enable Tree Format
  }
}

Creating a Tree Format Baseline

Detecting Changes in Tree Format

Rebaselining Tree Format

Additional Information

Which Configurations Should be Guarded?

  • Production App Configurations (what you ship)
  • Production Build Configurations (what you use to build your app)

Changes to either one of these can change your resulting application.

How Does Dependency Guard Work?

Dependency Guard writes a baseline file containing all your transtitive dependencies for a given configuration that should be checked into git.

Under the hood, we're leveraging the same logic that the Gradle dependencies task uses and displays in build scans, but creating a process which can guard against changes.

A baseline file is created for each build configuration you want to guard.

If the dependencies do change, you'll get easy to read warnings to help you visualize the differences, and accept them by re-baselining. This new baseline will be visible in your Git history to help understand when dependencies changed (including transitive).

Full Configuration Options

return true // Defaults to true } } }">
dependencyGuard {
  configuration("releaseRuntimeClasspath") {
    // What is included in the list report
    modules = true // Defaults to true
    artifacts = true // Defaults to true

    // Tree Report
    tree = false // Defaults to false

    // Filter through dependencies
    allowRule = {dependencyName: String ->
        return true // Defaults to true
    }
  }
}

Suggested Workflows

The Dependency Guard plugin adds a few tasks for you to use in your Gradle Builds. Your continuous integration environment would run the dependencyGuard task to ensure things did not change, and require developers to re-baseline using the dependencyGuardBaseline tasks when changes were intentional.

Gradle Tasks added by Dependency Guard

dependencyGuard

Compare against the configured transitive dependency report baselines, or generate them if they don't exist.

This task is added to any project that you apply the plugin to. The plugin should only be applied to project you are interested in tracking transtive dependencies for.

dependencyGuardBaseline

This task overwrites the dependencies in the dependencies directory in your module.

Baseline Files

Baseline files are created in the "dependency-guard" folder in your module. The following reports are created for the :sample:app module by running ./gradlew :sample:app:dependencyGuardBaseline

Adding to the Buildscript Classpath

Snapshot versions are under development and change, but can be used by adding in the snapshot repository

// Root build.gradle
buildscript {
    repositories {
        mavenCentral()
        google()
        gradlePluginPortal()
        // SNAPSHOT Versions of Dependency Guard
        maven { url "https://s01.oss.sonatype.org/content/repositories/snapshots" }
    }
    dependencies {
        classpath("com.dropbox.dependency-guard:dependency-guard:0.1.0")
    }
}

License

Copyright (c) 2022 Dropbox, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

GitHub

https://github.com/dropbox/dependency-guard
Comments
  • 1. Tasks not compatible with the configuration cache

    First of all, this is not surprising, as dependency analysis in general is not well-supported by the configuration cache (CC). However, since I'm adding this to my project, I want an issue to refer to explain why CC has been disabled for these tasks.

    I'm using the runtime API for this, available since Gradle 7.4. This looks like:

    tasks.named('dependencyGuard') {
      notCompatibleWithConfigurationCache('https://github.com/dropbox/dependency-guard/issues/4')
    }
    

    I'm not sure which versions of Gradle you want to support, but you might bake this in with something like

    tasks.register(...) {
      if (GradleVersion.current() > GradleVersion.version("7.4")) {
        notCompatibleWithConfigurationCache('https://github.com/dropbox/dependency-guard/issues/4')
      }
    }
    

    And of course, ideally, one day, this could be compatible out of the box.

    Reviewed by autonomousapps at 2022-04-21 22:08
  • 2. Internal Gradle API Usage: For `buildEnvironment` Dependency Trees, an internal Gradle API is used

    The following internal class is being used as there is no other public API available like it is for normal dependency trees. org.gradle.api.tasks.diagnostics.internal.ReportGenerator

    Reviewed by handstandsam at 2022-05-12 15:13
  • 3. Use domain object container to avoid afterEvaluate.

    This draft PR is based on the test PR I made earlier (so you can ignore the first commit). I'm sharing this in its draft state to get your feedback on the general idea. In the end, the ultimate idea would be to remove the mutable list entirely from the extension.

    One thing worth noting is that there's a subtle bug in the current implementation of the plugin. You use this function in your task configuration:

    requirePluginConfig(target, extension)
    

    That will fail if extension.configurations is empty. You are implicitly assuming that the dependencyGuard DSL block will be evaluated before tasks get configured. I can imagine a scenario that triggers task configuration early, causing a failure even though the build script is correctly configured. It would be a complex scenario, but people do crazy things in their builds and plugins. I think you'd be better off delaying that check to task execution.

    Reviewed by autonomousapps at 2022-04-25 07:34
  • 4. Improve test coverage.

    Move Path extension functions to util package. Delete build scan stuff from test fixture.

    My main goal here was to ensure there was actual coverage for use of the Action-enhanced DSL (tree = true), and in the process decided to move the Path functions as well.

    Reviewed by autonomousapps at 2022-04-25 04:19
  • 5. Cleanup of Diffing Code

    Cleaned up Diffing code to use Sealed Classes, and Data Classes. It just is a lot easier to read and GROK now.

    Also Fixes #19

    Also fixes #23 in another spot

    Reviewed by handstandsam at 2022-05-12 02:14
  • 6. Opt all tasks out of tracking and configuration caching.

    Resolves https://github.com/dropbox/dependency-guard/issues/4.

    See https://docs.gradle.org/current/userguide/configuration_cache.html for more information.

    The point of notCompatibleWithConfigurationCache("Uses Project at execution time") is to tell Gradle that, if a user tries to run this task with the flag org.gradle.unsafe.configuration-cache=true set, then that flag should be ignored. Without that method call, then Gradle would instead fail the build. This lets users say they want CC on by default, while gracefully falling back to having it off for tasks that aren't compatible. Otherwise, they'd have to remember it doesn't work for tasks X, Y, and Z, and manually use --no-configuration-cache, which is annoying.

    As an aside, something weird is happening with this project's .gitignore files. I had to manually add both files in the utils package.

    git add dependency-guard/src/main/kotlin/com/dropbox/gradle/plugins/dependencyguard/internal/utils/GradleVersion.kt
    The following paths are ignored by one of your .gitignore files:
    dependency-guard/src/main/kotlin/com/dropbox/gradle
    hint: Use -f if you really want to add them.
    hint: Turn this message off by running
    hint: "git config advice.addIgnoredFile false"
    

    I think it's the gitignore rule to ignore simply gradle.

    Reviewed by autonomousapps at 2022-05-11 22:49
  • 7. Set `artifacts=true` and `modules=false` as the default config.

    Current in the configuration, both modules and artifacts default to true.

    I've seen in a few projects modules being set to false. Which value makes the most sense as a default?

    Reviewed by handstandsam at 2022-05-05 19:06
  • 8. `baselineFilter` - Filter out certain dependencies from baseline (allowed, but ignored for baseline purposes)

    In our build, we have a lot of convention plugins that we publish to an internal Artifactory instance, and which we update regularly. Their coordinates look like this:

    com.squareup.register:plugins:1.40.0
    

    Since we run ./gradlew :dependencyGuard on every build, we get a failure every time we publish a new version. (The baseline will contain ...1.39.0, but the build is running on 1.40.0.) One way to avoid this is to filter these buildscript dependencies out of the classpath.txt baseline file: basically ignore them for purposes of the baseline.

    Alternatively, say that we don't care if the version changes (only care about the identifer, or com.squareup.register:plugins in this case).

    I'm not actually convinced this is a great idea, but I wanted to run it by you to get your thoughts.

    Reviewed by autonomousapps at 2022-05-04 00:21
  • 9. Moving `dependency-guard-gradle-plugin` -> `dependency-guard` to save keystrokes.

    Moving dependency-guard-gradle-plugin -> dependency-guard to save keystrokes.

    No real reason here other than there is no need to have the module name be that long.

    Originally I used a folder called dependency-guard to store the dependency baselines, but now we use dependencies, the decision made in #1

    Reviewed by handstandsam at 2022-04-25 02:11
  • 10. dependencyGuard failure output contains a double-colon (::) when run on root project. Should be a single colon.

    When run on the root project, the output will include this line

    If this is a desired change, you can re-baseline using ./gradlew ::dependencyGuardBaseline
    

    (note the ::)

    and it should be

    If this is a desired change, you can re-baseline using ./gradlew :dependencyGuardBaseline
    
    Reviewed by autonomousapps at 2022-04-21 21:24
  • 11. Support analysis of source-containing root projects

    This plugin currently assumes that, if it is applied to the root project, the only possible relevant classpath is the buildscript classpath. However, it is possible and quite common for (generally simple, uni-modular) projects to have source in the root. An example is the Dependency Analysis Plugin.

    Reviewed by autonomousapps at 2022-04-21 21:33
Gradle Plugin to automatically upgrade your gradle project dependencies and send a GitHub pull request with the changes
Gradle Plugin to automatically upgrade your gradle project dependencies and send a GitHub pull request with the changes

Gradle Plugin to automatically upgrade your gradle project dependencies and send a GitHub pull request with the changes

May 16, 2022
Gradle plugin to ease Kotlin IR plugin development and usage in multimodule gradle projects

Gradle plugin to ease Kotlin IR plugin development and usage in multimodule gradle projects. Former: kotlin-ir-plugin-adapter-gradle

Mar 8, 2022
Gradle plugin which validates the licenses of your dependency graph match what you expect

Gradle plugin which validates the licenses of your dependency graph match what you expect

May 2, 2022
Graphfity is a Gradle Plugin which creates a dependency node diagram graph about your internal modules dependencies, specially useful if you are developing a multi-module application
Graphfity is a Gradle Plugin which creates a dependency node diagram graph about your internal modules dependencies, specially useful if you are developing a multi-module application

Graphfity creates a dependency nodes diagram graph about your internal modules dependencies, specially useful if you are developing a multi-module app

May 21, 2022
Gradle Plugin that allows you to decompile bytecode compiled with Jetpack Compose Compiler Plugin into Java and check it

decomposer Gradle Plugin that allows you to decompile bytecode compiled with Jetpack Compose Compiler Plugin into Java and check it How to use Run bui

May 10, 2022
Gradle Plugin to enable auto-completion and symbol resolution for all Kotlin/Native platforms.
Gradle Plugin to enable auto-completion and symbol resolution for all Kotlin/Native platforms.

CompleteKotlin Gradle Plugin to enable auto-completion and symbol resolution for all Kotlin/Native platforms. What this plugin provides This zero-conf

May 12, 2022
A Gradle plugin that improves the experience when developing Android apps, especially system tools, that use hidden APIs.

A Gradle plugin that improves the experience when developing Android apps, especially system tools, that use hidden APIs.

May 9, 2022
Grazel is a Gradle plugin to automate generation of valid Bazel files for a given Android/Kotlin/Java project.

Grazel Grazel stands for Gradle to Bazel. It is a Gradle plugin that enables you to migrate Android projects to Bazel build system in an incremental a

May 20, 2022
Gradle Plugin for Continuous Integration of AppSweep App Testing.
 Gradle Plugin for Continuous Integration of AppSweep App Testing.

This Gradle plugin can be used to continuously integrate app scanning using AppSweep into your Android app build process

Feb 16, 2022
Artifactory is a gradle plugin to assist in developing Minecraft mods that can target different modloaders.

Artifactory Artifactory is a gradle plugin to assist in developing Minecraft mods that can target different modloaders. Currently, Fabric and Forge ar

Dec 29, 2021
A Gradle Plugin to determine which modules were affected by a set of files in a commit.
A Gradle Plugin to determine which modules were affected by a set of files in a commit.

A Gradle Plugin to determine which modules were affected by a set of files in a commit. One use case for this plugin is for developers who would like to only run tests in modules which have changed in a given commit.

May 23, 2022
EasyVersion is a Gradle plugin that manage your app or library version.

EasyVersion EasyVersion is a Gradle plugin that manage your app or library version. Before Downloading Create easy_version.json in your root project d

Jan 18, 2022
Android Gradle Plugin -- Auto Check big image and compress image in building.

McImage I will continue to update, please rest assured to use 中文文档 Android优雅的打包时自动化获取全部res资源 McImage is a Non-invasive plugin for compress all res in

May 18, 2022
Gradle plugin that generates a Swift Package Manager manifest and an XCFramework to distribute a Kotlin Multiplatform library for Apple platforms.

Multiplatform Swift Package This is a Gradle plugin for Kotlin Multiplatform projects that generates an XCFramework for your native Apple targets and

May 14, 2022
A Gradle plugin helps to proxy all the BuildConfig fields in the Android project.
A Gradle plugin helps to proxy all the BuildConfig fields in the Android project.

Readme A Gradle plugin helps to proxy all the BuildConfig fields in the Android project. Background In android BuildConfig, We might have different co

Oct 11, 2021
Gradle Plugin for publishing artifacts to Sonatype and Nexus

Introduction Due to Sonatype's strict validation rules, the publishing requirement must be satisfied by every artifact which wants to be published to

May 18, 2022
Gradle-i18n-plugin is meant for generating a JSON file with internationalized texts, based on a Google Sheet.
Gradle-i18n-plugin is meant for generating a JSON file with internationalized texts, based on a Google Sheet.

Gradle-i18n-plugin Gradle-i18n-plugin is meant for generating a JSON file with internationalized texts, based on a Google Sheet. The plugin assumes th

Oct 11, 2021
Gradle plugin that parses version updates and assigns them to groups of people.

Notifier Gradle Plugin This gradle plugin serves the need of automating how dependencies are handles in a project. More specifically, it functions usi

Oct 15, 2021
Gradle plugin for running Android tests with emulator.wtf

emulator.wtf Gradle Plugin Emulator.wtf is an Android cloud emulator laser-focused on performance to deliver quick feedback to your PRs. With this Gra

May 15, 2022