We have a consumer that added new contracts with c...
# general
é
We have a consumer that added new contracts with consulting its provider, and that broke their pipeline. I know that communication is paramount with Pact and you encourage CDCT, but how do you tackle the case where the provider is already verifying contracts from other consumers? Does the provider need to handle all the states declared by its consumers? Is there a way to tell the consumer is experimenting?
j
If it helps, I can tell you that the way I've been handling it is that the consumer feature branch which creates the contract must verify the contract before it can be merged into the master branch of that consumer, and the provider feature branches only have to verify against the master branch of its consumer. This way you can't merge a new contract if you haven't proven it works at least once already, and the provider will only be trying to verify pacts that have been proven to work at least once already.
é
Thanks, I believe that what we need to do next! When you say:
consumer feature branch which creates the contract must verify the contract before it can be merged into the master branch of that consumer
I tend to only use "_verifiaction"_ to talk about the provider to avoid confusion, so I reckon you mean consumer test pass and generate the pact file, right?
j
No, actually! I mean that as part of the consumer's feature branch pipeline which must pass before it can be merged to master, it remotely triggers a provider verification job (using the contract_requiring_verification_published webhook) and then runs the can-i-deploy script to ask of the current version of the consumer is compatible with the master version of the provider.
1
This means that both the consumer and the provider have to agree on a change to the contract before it can be merged to master
é
Do you have an example of config to share?
j
I suppose I can share some of our pipeline
We use Jenkins, so this is all written in Jenkinsfile groovy
é
Jenkinsfile will bring back some memory 👴 But you do target master branch using your pipeline? I thought it was a pact config
j
Skipping right to the pact-related stuff, the first relevant stage runs the unit tests:
Copy code
stage("RUN UNIT TESTS") {
                steps {
                    script {
                        sh "$POETRY run pytest tests --cov=${job.sourceName}"
                        IS_PACT_CONSUMER = library.exists('./tests/*.json')
                        IS_PACT_PROVIDER = library.exists('./tests/pact_provider')
                    }
                }
            }
We have a library method which is just calling a shell script to check for the existence of a file or directory and convert it to a boolean. Since the
Pact
fixture in pytest automatically produces a pact json file for each provider this service consumes when it's done, we can check if this is a consumer if it has any pact files, and we're using a convention that if this service is a provider, it has its provider state setup logic in
./tests/pact_provider
so we can check for the existence of that directory to see if this is a provider (it can also be both or neither, since this is a general-use pipeline script)
Copy code
stage("UPLOAD PACT FILE") {
                when {
                    expression { return IS_PACT_CONSUMER }
                }
                steps {
                    script {
                        sh "docker run --network host -v \$(pwd)/tests:/tests " +
                            "pactfoundation/pact-cli broker publish " +
                            "/tests/ " +
                            "--broker-base-url=\"${PACT_BROKER_URL}\" " +
                            "--consumer-app-version=\"${job.abbrevCommitHash}\" "
                    }
                }
            }
Next, if the current project is a consumer, it uploads the pact file using the pact CLI (it's running it through the published docker container so we don't have to worry about installing it on our jenkins agents). We're creating our pact contract files into the
tests
directory, so we pass that directory into this script and it is smart enough to find all the pact json files. It uploads them to our broker (at
PACT_BROKER_URL
) and tags the contracts with the consumer version provided by
job.abbrevCommitHash
.
The next thing that happens is actually not part of the Jenkinsfile, but rather is a webhook in our pact broker which triggers automatically when it detects a new pact contract which requires verification:
Copy code
{
  "uuid": "a0beeef6-b69e-421a-8033-d9212c274426",
  "description": "Automatically trigger pact verification on contract change.",
  "enabled": true,
  "request": {
    "method": "POST",
    "url": "http://<jenkins-url>.com/job/Pact%20Verify%20Provider/buildWithParameters",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "project": "${pactbroker.providerName}",
      "version": "${pactbroker.providerVersionNumber}",
      "pactfile_url": "${pactbroker.pactUrl}"
    },
    "username": "**********",
    "password": "**********"
  },
  "events": [
    {
      "name": "contract_requiring_verification_published"
    }
  ],
  "createdAt": "2022-06-01T20:32:58+00:00"
}
This webhook is configured to call another jenkins job, passing it the name of the provider that needs to be verified, the version of that provider that needs to be verified (the git commit hash) and the URL of the pactfile that it needs to verify.
The jenkins job that it calls checks out the matching project and git hash, starts the service, and then runs the pact verification via the v3 CLI docker image:
Copy code
stage("VERIFY") {
            steps {
                script {
                    cmd = "docker run " +
                        "--network host pactfoundation/pact-ref-verifier " +
                        "--broker-url \"${PACT_BROKER_URL}\" " +
                        "--provider-name \"${project}\" " +
                        "--provider-version ${version} " +
                        "--publish " +
                        "--hostname \"localhost\" " +
                        "--port \"$PROVIDER_PORT\" " +
                        "--state-change-url \"<http://localhost>:$PROVIDER_PORT/_pact/provider_states\" "
                    if (pactfile_url != "false") {
                        cmd += "--url \"${pactfile_url}\" "
                    } else {
                        cmd += "--consumer-version-selectors \"{ 'environment': '${environment}' }\" "
                    }
                    sh cmd
                }
            }
        }
(the "else" clause in there is because this job can alternatively be supplied with an environment name instead of a pact url, and it will verify the provider against all pacts tagged with that environment)
Looking back at the project jenkinsfile, the next thing that it does is check if the current project is a pact provider, and if it is, it triggers that same verification jenkins job, but with the environment "master" instead of a pact file url:
Copy code
stage ("VERIFY CONTRACTS") {
                when {
                    expression { return IS_PACT_PROVIDER }
                }
                steps{
                    script {
                        echo 'Verify pacts where self is provider...'
                        build job: "Pact Verify Provider", 
                            parameters: [
                                string(name: 'project', value: job.projectName),
                                string(name: 'version', value: job.abbrevCommitHash),
                                string(name: 'environment', value: 'master')
                            ],
                            wait: true
                    }
                }
            }
We are treating "master" like an environment since we are on a trunk-based development model and simply want to verify all new changes against the head of master before we allow them to merge.
Next, as long as the current project is a pact provider or a pact consumer (or both) it will run the can-i-deploy script against the master "environment", blocking the rest of the build if the contracts were not verified:
Copy code
stage("CHECK PACT VERIFICATIONS") {
                when {
                    expression { return IS_PACT_PROVIDER || IS_PACT_CONSUMER }
                }
                steps {
                    script {
                        sh "docker run --network host pactfoundation/pact-cli broker " +
                            "can-i-deploy " +
                            "--broker-base-url=\"${PACT_BROKER_URL}\" " +
                            "--retry-while-unknown=30 " +
                            "--retry-interval=10 " +
                            "--pacticipant=\"${job.projectName}\" " +
                            "--version=\"${job.abbrevCommitHash}\" " +
                            "--to-environment=master "
                    }
                }
            }
And then finally, assuming this was all successful, we check if the current branch is master, and if it is, we tag it in the pact broker as such (along with also tagging it with the sem ver tag, for easy readability in the pact broker matrix):
Copy code
stage("TAG PACT BROKER") {
                when {
                    branch 'master'
                    expression { return IS_PACT_PROVIDER || IS_PACT_CONSUMER }
                }
                steps {
                    script {
                        sh "docker run --network host pactfoundation/pact-cli broker " +
                            "create-version-tag " +
                            "--broker-base-url=\"${PACT_BROKER_URL}\" " +
                            "--pacticipant=\"${job.projectName}\" " +
                            "--version=\"${job.abbrevCommitHash}\" " +
                            "--tag=\"${job.publishedVersion}\" "

                        sh "docker run --network host pactfoundation/pact-cli broker " +
                            "record-deployment " +
                            "--broker-base-url=\"${PACT_BROKER_URL}\" " +
                            "--pacticipant=\"${job.projectName}\" " +
                            "--version=\"${job.abbrevCommitHash}\" " +
                            "--environment=\"master\" "
                    }
                }
            }
Does that help clear things up a bit?