How Can I Use Git as the Storage for Contracts and Stubs?

In the polyglot world, there are languages that do not use binary storage, as Artifactory and Nexus do. Starting from Spring Cloud Contract version 2.0.0, we provide mechanisms to store contracts and stubs in a SCM (Source Control Management) repository. Currently, the only supported SCM is Git.

The repository would have to have the following setup (which you can checkout from here):

.
└── META-INF
    └── com.example
        └── beer-api-producer-git
            └── 0.0.1-SNAPSHOT
                ├── contracts
                │   └── beer-api-consumer
                │       ├── messaging
                │       │   ├── shouldSendAcceptedVerification.groovy
                │       │   └── shouldSendRejectedVerification.groovy
                │       └── rest
                │           ├── shouldGrantABeerIfOldEnough.groovy
                │           └── shouldRejectABeerIfTooYoung.groovy
                └── mappings
                    └── beer-api-consumer
                        └── rest
                            ├── shouldGrantABeerIfOldEnough.json
                            └── shouldRejectABeerIfTooYoung.json

Under the META-INF folder:

  • We group applications by groupId (such as com.example).

  • Each application is represented by its artifactId (for example, beer-api-producer-git).

  • Next, each application is organized by its version (such as 0.0.1-SNAPSHOT). Starting from Spring Cloud Contract version 2.1.0, you can specify the versions as follows (assuming that your versions follow semantic versioning):

    • + or latest: To find the latest version of your stubs (assuming that the snapshots are always the latest artifact for a given revision number). That means:

      • If you have 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT, and 2.0.0.RELEASE, we assume that the latest is 2.0.0.BUILD-SNAPSHOT.

      • If you have 1.0.0.RELEASE and 2.0.0.RELEASE, we assume that the latest is 2.0.0.RELEASE.

      • If you have a version called latest or +, we will pick that folder.

    • release: To find the latest release version of your stubs. That means:

      • If you have 1.0.0.RELEASE, 2.0.0.BUILD-SNAPSHOT, and 2.0.0.RELEASE we assume that the latest is 2.0.0.RELEASE.

      • If you have a version called release, we pick that folder.

Finally, there are two folders:

  • contracts: The good practice is to store the contracts required by each consumer in the folder with the consumer name (such as beer-api-consumer). That way, you can use the stubs-per-consumer feature. Further directory structure is arbitrary.

  • mappings: The Maven or Gradle Spring Cloud Contract plugins push the stub server mappings in this folder. On the consumer side, Stub Runner scans this folder to start stub servers with stub definitions. The folder structure is a copy of the one created in the contracts subfolder.

Protocol Convention

To control the type and location of the source of contracts (whether binary storage or an SCM repository), you can use the protocol in the URL of the repository. Spring Cloud Contract iterates over registered protocol resolvers and tries to fetch the contracts (by using a plugin) or stubs (from Stub Runner).

For the SCM functionality, currently, we support the Git repository. To use it, in the property where the repository URL needs to be placed, you have to prefix the connection URL with git://. The following listing shows some examples:

git://file:///foo/bar
git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git
git://[email protected]:spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git

Producer

For the producer, to use the SCM (Source Control Management) approach, we can reuse the same mechanism we use for external contracts. We route Spring Cloud Contract to use the SCM implementation from the URL that starts with the git:// protocol.

You have to manually add the pushStubsToScm goal in Maven or use (bind) the pushStubsToScm task in Gradle. We do not push stubs to the origin of your git repository.

The following listing includes the relevant parts both Maven and Gradle build files:

  • Maven

  • Gradle

<plugin>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-contract-maven-plugin</artifactId>
    <version>${spring-cloud-contract.version}</version>
    <extensions>true</extensions>
    <configuration>
        <!-- Base class mappings etc. -->

        <!-- We want to pick contracts from a Git repository -->
        <contractsRepositoryUrl>git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git</contractsRepositoryUrl>

        <!-- We reuse the contract dependency section to set up the path
        to the folder that contains the contract definitions. In our case the
        path will be /groupId/artifactId/version/contracts -->
        <contractDependency>
            <groupId>${project.groupId}</groupId>
            <artifactId>${project.artifactId}</artifactId>
            <version>${project.version}</version>
        </contractDependency>

        <!-- The contracts mode can't be classpath -->
        <contractsMode>REMOTE</contractsMode>
    </configuration>
    <executions>
        <execution>
            <phase>package</phase>
            <goals>
                <!-- By default we will not push the stubs back to SCM,
                you have to explicitly add it as a goal -->
                <goal>pushStubsToScm</goal>
            </goals>
        </execution>
    </executions>
</plugin>
contracts {
	// We want to pick contracts from a Git repository
	contractDependency {
		stringNotation = "${project.group}:${project.name}:${project.version}"
	}
	/*
	We reuse the contract dependency section to set up the path
	to the folder that contains the contract definitions. In our case the
	path will be /groupId/artifactId/version/contracts
	 */
	contractRepository {
		repositoryUrl = "git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git"
	}
	// The mode can't be classpath
	contractsMode = "REMOTE"
	// Base class mappings etc.
}

/*
In this scenario we want to publish stubs to SCM whenever
the `publish` task is invoked
*/
publish.dependsOn("publishStubsToScm")

You can also further customize the publishStubsToScm gradle task. In the following example, the task is customized to pick contracts from a local git repository:

gradle
publishStubsToScm {
	// We want to modify the default set up of the plugin when publish stubs to scm is called
	// We want to pick contracts from a Git repository
	contractDependency {
		stringNotation = "${project.group}:${project.name}:${project.version}"
	}
	/*
	We reuse the contract dependency section to set up the path
	to the folder that contains the contract definitions. In our case the
	path will be /groupId/artifactId/version/contracts
	 */
	contractRepository {
		repositoryUrl = "git://file://${new File(project.rootDir, "../target")}/contract_empty_git/"
	}
	// We set the contracts mode to `LOCAL`
	contractsMode = "LOCAL"
	}
IMPORTANT

Starting with the 2.3.0.RELEASE, the customize{} closure previously used for the publishStubsToScm customization is no longer available. The settings should be applied directly within the publishStubsToScm closure, as in the preceding example.

With such a setup:

  • A git project is cloned to a temporary directory

  • The SCM stub downloader goes to the META-INF/groupId/artifactId/version/contracts folder to find contracts. For example, for com.example:foo:1.0.0, the path would be META-INF/com.example/foo/1.0.0/contracts.

  • Tests are generated from the contracts.

  • Stubs are created from the contracts.

  • Once the tests pass, the stubs are committed in the cloned repository.

  • Finally, a push is sent to that repo’s origin.

Producer with Contracts Stored Locally

Another option to use the SCM as the destination for stubs and contracts is to store the contracts locally, with the producer, and only push the contracts and the stubs to SCM. The following project shows the setup required to achieve this with Maven and Gradle.

With such a setup:

  • Contracts from the default src/test/resources/contracts directory are picked.

  • Tests are generated from the contracts.

  • Stubs are created from the contracts.

  • Once the tests pass:

    • The git project is cloned to a temporary directory.

    • The stubs and contracts are committed in the cloned repository.

  • Finally, a push is done to that repository’s origin.

Keeping Contracts with the Producer and Stubs in an External Repository

You can also keep the contracts in the producer repository but keep the stubs in an external git repository. This is most useful when you want to use the base consumer-producer collaboration flow but cannot use an artifact repository to store the stubs.

To do so, use the usual producer setup and then add the pushStubsToScm goal and set contractsRepositoryUrl to the repository where you want to keep the stubs.

Consumer

On the consumer side, when passing the repositoryRoot parameter, either from the @AutoConfigureStubRunner annotation, the JUnit 4 rule, JUnit 5 extension, or properties, you can pass the URL of the SCM repository, prefixed with the git:// protocol. The following example shows how to do so:

@AutoConfigureStubRunner(
    stubsMode="REMOTE",
    repositoryRoot="git://https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs-contracts-git.git",
    ids="com.example:bookstore:0.0.1.RELEASE"
)

With such a setup:

  • The git project is cloned to a temporary directory.

  • The SCM stub downloader goes to the META-INF/groupId/artifactId/version/ folder to find stub definitions and contracts. For example, for com.example:foo:1.0.0, the path would be META-INF/com.example/foo/1.0.0/.

  • Stub servers are started and fed with mappings.

  • Messaging definitions are read and used in the messaging tests.