Skip to content

CI Workflow

Prerequisites

  • Consumer tests
  • Provider tests
  • Helm deployment
  • Pact Broker credentials
  • CircleCI token

Overview

One of the major benefits of Pact and contract testing is being able to ensure compatibility between applications without needing to deploy and run end-to-end tests. This requires setting up a proper CI workflow that both execute contract testing and verification of newly published pacts. Since publishing the pact happens on the consumer side and verification happens on the provider side, collaboration between the consumer and provider is key to having a good workflow.

The high level overview of the e2e CI workflow is:

  1. Consumer executes its contract tests and publishes the generated pact file. Consumer CI still continues with pipeline execution.
  2. Pact Broker detects that a new version of the pact has been published. This event triggers a webhook to kick off the contract tests on the provider side.
  3. Provider contract tests execute and publish results back to Pact Broker.
  4. NOTE: Contract tests should be executed against the versions of the provider running in each of the different environments. This is critical for us to know which versions of the provider the consumer is compatible with.
  5. Consumer CI workflow, which has still been running, fetches the verification results from Pact Broker just prior to deployment.

What's important to note is the collaboration between the consumer and provider and the concurrent CI workflows. The consumer may face a race condition where it attempts to fetch the verification results before the provider tests have finished executing. Pact Broker has ways to mitigate this.

Provider

1. Label the Kubernetes Deployment

You will need to update your Helm chart to add a git tag reference in the labels of the deployment resource. This is what is read during the provider test execution to know which version is deployed in each environment.

deployment/helm/templates/_helpers.tpl

git tag added to deployment labels
{{/*
Common labels
...
{{- if .Values.gitTag }}
tag: {{ .Values.gitTag | quote }}
{{- end }}
...
{{- end }}

2. Tests in the Pipeline

On the provider side, there are a few extra steps to executing the contract tests. We would like to be able to execute the tests against the deployed versions of the provider.

We first need to determine the version of the provider running in a particular environment by reading it from the label of the Kubernetes deployment.

After executing the tests and publishing the results, we must also tag the version in Pact Broker with the particular environment. This is done using the Pact Broker CLI.

job definition
  run-contract-tests:
    working_directory: ~/<PROVIDER APP NAME>
    executor: java-agent # $ECR_URL/client-di-circleci-java-11-agent:7 [update the client's path](liatrio-tag)
    parameters:
      environment:
        type: string
      team-name:
        description: team name (target k8s namespace prefix); exported to DEPLOY_TEAM env variable
        type: string
      cluster-name:
        description: target cluster for the deploy
        type: string
      aws-region:
        description: which aws region we want
        type: string
    steps:
      - checkout:
          path: ~/<PROVIDER APP NAME>
      - common-tasks/vault-login:
          <<: *circle-ci-service-account
      - common-tasks/setup-aws-credentials:
          aws-region: << parameters.aws-region >>
          team-name: << parameters.team-name >>
          environment: << parameters.environment >>
      - common-tasks/setup-eks-credentials:
          aws-region: << parameters.aws-region >>
          cluster-name: << parameters.cluster-name >>
      - run:
          name: Install Dependencies
          command: |
            apk --no-cache add ca-certificates wget bash ruby-json wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
            wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.29-r0/glibc-2.29-r0.apk
            apk add glibc-2.29-r0.apk
            gem install pact_broker-client
      - run:
          name: Run provider contract tests
          command: |
            export PACT_BROKER_URL="pact-broker.clientcloud.com" [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
            export PACT_BROKER_PORT="443"
            export PACT_BROKER_SCHEME="https"
            export PACT_BROKER_USERNAME=$(vault kv get -field username secret/<TEAM NAME>/pact)
            export PACT_BROKER_PASSWORD=$(vault kv get -field password secret/<TEAM NAME>/pact)
            export DEPLOYMENT_NAME=$(kubectl get deployments -n <TEAM NAME>-<< parameters.environment >> | grep <PROVIDER APP NAME> |  awk '{print $1}' | head -n 1)
            export PACT_PROVIDER_VERSION=$(kubectl get deployments -n <TEAM NAME>-<< parameters.environment >> $DEPLOYMENT_NAME -o json | jq -r .metadata.labels.tag)
            git checkout $PACT_PROVIDER_VERSION
            ./gradlew contractTest
      - run:
          name: Tag provider version
          command: |
            pact-broker create-version-tag --tag=<< parameters.environment >> --pacticipant=<PROVIDER PACTICIPANT NAME> --broker-base-url=https://$PACT_BROKER_URL --version=$PACT_PROVIDER_VERSION || true

3. Pipeline parameters

We will add a couple pipeline parameters to be able to filter which workflow is executed by the webhook. We would like the pact workflow to be disabled by default as to only execute from the webhook, and the dev-release workflow to be triggered normally by default.

pipeline parameters
parameters:
  run_workflow_pact:
    default: false
    type: boolean
  run_workflow_dev_release:
    default: true
    type: boolean

Then to use the pipeline parameters, update the dev-release workflow by adding the when key.

dev-release workflow
workflows:
  version: 2
  dev-release:
    when: << pipeline.parameters.run_workflow_dev_release >>

4. Add the Contract Testing Workflow

Add a new workflow for the contract testing. Here we use the same job definition for running the contract tests in 3 parallel executions- for dev, QA, and prod.

contract test workflow
  contract-tests:
    when: << pipeline.parameters.run_workflow_pact >>
    jobs:
      - run-contract-tests:
          name: Pact Contract Tests for Dev
          <<: *circle-ci-context
          environment: dev
          team-name: *team
          cluster-name: di-nonprod-cluster
          aws-region: *nonprod-aws-region
      - run-contract-tests:
          name: Pact Contract Tests for QA
          <<: *circle-ci-context
          environment: qa
          team-name: *team
          cluster-name: di-nonprod-cluster
          aws-region: *nonprod-aws-region
      - run-contract-tests:
          name: Pact Contract Tests for Prod
          <<: *circle-ci-context
          environment: prod
          team-name: *team
          cluster-name: di-prod-cluster
          aws-region: *prod-aws-region

Consumer

5. Tests in the Pipeline

You will need to add a new job definition for executing contract tests. For consumers, this job should handle both the exeuction of the tests as well as the publishing.

job definition
  run-contract-tests:
    working_directory: ~/<CONSUMER REPO NAME>
    executor: platform-agent # $ECR_URL/client-di-circleci-base-agent:9 [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
    steps:
      - checkout:
          path: ~/<CONSUMER REPO NAME>
      - common-pipeline-tasks/vault-login:
          <<: *circle-ci-service-account
      - setup-aws-credentials
      - run:
          name: Run contract tests
          command: |
            <npm run testContract || ./gradlew clean contractTest>
      - run:
          name: Publish pact
          command: |
            export PACT_BROKER_URL="https://pact-broker.clientcloud.com" [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
            export PACT_BROKER_USERNAME=$(vault kv get -field username secret/<TEAM NAME>/pact)
            export PACT_BROKER_PASSWORD=$(vault kv get -field password secret/<TEAM NAME>/pact)
            <npm run publish --workspace test/contract || ./gradlew pactPublish>

6. can-i-deploy

Another job definition will need to be added for checking the verification results. This job will use the Pact Broker CLI to run the can-i-deploy command to check the result.

Here we only have one parameter being passed in, the environment. The environment parameter is being used to determine which tagged version of the provider to check against.

Note also that there are retry flags being passed as well. This is due to the concurrency between the consumer and provider CI pipelines and the potential for the provider tests to still be executing by the time this stage is started. Here we are saying the retry 6 times with an interval of 30 seconds.

job definition
  can-i-deploy:
    working_directory: ~/<CONSUMER REPO NAME>
    executor: platform-agent # $ECR_URL/client-di-circleci-base-agent:9 [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
    parameters:
      environment:
        type: string
    steps:
      - common-pipeline-tasks/vault-login:
          <<: *circle-ci-service-account
      - run:
          name: Install Dependencies
          command: |
            gem install pact_broker-client
      - run:
          name: Check validation status
          command: |
            export PACT_BROKER_URL="https://pact-broker.clientcloud.com/" [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
            export PACT_BROKER_USERNAME=$(vault kv get -field username secret/<TEAM NAME>/pact)
            export PACT_BROKER_PASSWORD=$(vault kv get -field password secret/<TEAM NAME>/pact)
            pact-broker can-i-deploy -a "DKS UI" -b $PACT_BROKER_URL --latest --to=<< parameters.environment >> --retry-while-unknown=6 --retry-interval=30

7. Setting up the webhook

To setup the webhook in Pact Broker, you may configure it through the homepage in the UI, or another way to do so would be to use cURL.

NOTE: To setup the webhook, you must have an initial pact published. The reason for this is that Pact Broker needs to be aware of the consumer and provider this webhook corresponds to.

payload.json
{
  "consumer": {
    "name": "<CONSUMER NAME>"
  },
  "provider": {
    "name": "<PROVIDER NAME>"
  },
  "events": [
    {
      "name": "contract_content_changed"
    }
  ],
  "request": {
    "method": "POST",
    "url": "https://circleci.com/api/v2/project/bitbucket/client/<CIRCLECI PROVIDER PROJECT NAME>/pipeline", [if the client already has pact broker, update this link. If not, we can reference the generic pact broker link](liatrio-tag)
    "headers": {
      "Content-Type": "application/json",
      "Circle-Token": "<CIRCLECI TOKEN>"
    },
    "body": {
      "branch": "main",
      "parameters": {
        "run_workflow_pact": true,
        "run_workflow_dev_release": false
      }
    }
  }
}

cURL command

curl -H 'Content-Type: application/json' -u $PACT_BROKER_USERNAME:$PACT_BROKER_PASSWORD -d @payload.json https://pact-broker.clientcloud.com/webhooks
if the client already has pact broker, update this link. If not, we can reference the generic pact broker link